GitHub で見る

MAMy Expert 戦略

概要

  • Victor ChebotariovによるMetaTrader 5の"MAMy Expert"アドバイザーをStockSharpのハイレベル戦略APIにポーティングしたもの。
  • 異なる価格ソース(始値、終値、加重価格)の3つの移動平均を比較する元のカスタムインジケーターを再現します。
  • 完了したローソク足でのみ動作し、一度に最大1つのネットポジションを管理し、MQLエキスパートの動作を反映します。

インジケーターの基礎

  • 戦略は同じ長さと平滑化アルゴリズムを使用して3つの移動平均を構築します:
    • MA(close) – ローソク足の終値で計算されます。
    • MA(open) – ローソク足の始値で計算されます。
    • MA(weighted) – 加重価格 (High + Low + 2 × Close) / 4 で計算されます。
  • MaType パラメーターは3つのシリーズすべての平均化アルゴリズム(Simple、Exponential、Smoothed、またはWeighted LWMA)を選択し、MetaTraderの MODE_* オプションに対応します。
  • "クローズバッファー"は差分 MA(close) − MA(weighted) として計算されます。
  • "オープンバッファー"は移動平均がトレンド構成で整列した場合にのみ生成されます:
    • 下降トレンドセットアップMA(close)MA(weighted) の両方が下落し、終値MAが加重MAを下回り、両方が始値MAを下回り、クローズバッファーが減少します。
    • 上昇トレンドセットアップMA(close)MA(weighted) の両方が上昇し、終値MAが加重MAを上回り、両方が始値MAを上回り、クローズバッファーが増加します。
    • どちらかのセットアップが真の場合、オープンバッファーは (MA(weighted) − MA(open)) + (MA(close) − MA(weighted)) になります;そうでなければゼロにリセットされます。
  • 新しい正のオープンバッファーがクローズバッファーのネガティブクロスを伴う場合、元のインジケーターコードと同様にクローズバッファーはゼロに強制されます。

シグナルロジック

  • エントリー条件
    • 買い:オープンバッファーがゼロを上向きにクロスした場合(前回 ≤ 0現在 > 0)。
    • 売り:オープンバッファーがゼロを下向きにクロスした場合(前回 ≥ 0現在 < 0)。
    • エントリーは既存のポジションがない場合にのみ考慮されます。
  • エグジット条件
    • ロング決済:クローズバッファーがゼロを下向きにクロスした場合(前回 ≥ 0現在 < 0)。
    • ショート決済:クローズバッファーがゼロを上向きにクロスした場合(前回 ≤ 0現在 > 0)。
    • エグジットは新しいエントリーの前に評価されるため、戦略はロングとショートのエクスポージャーを同時に保持することはありません。
  • 注文は設定された TradeVolume を使用して市場で発行されます。StartProtection() による保護自動化はStockSharpのサンプルの安全呼び出しを反映します。

チャートとデータフロー

  • CandleType で定義された時間軸にサブスクライブし、完了したローソク足のみを処理します。
  • 3つの移動平均とともに価格ローソク足を描画し、実行された注文を注釈して、MetaTraderで元のインジケーターが提供していたのと同じビジュアルキューを提供します。

パラメーター

名前 タイプ デフォルト 説明
CandleType DataType TimeSpan.FromHours(1).TimeFrame() インジケーターとシグナルにローソク足を提供するプライマリ時間軸。
MaPeriod int 3 3つの移動平均すべてに適用される長さ。
MaType MaCalculationType Weighted 平均化アルゴリズム(Simple、Exponential、Smoothed、Weighted)。
TradeVolume decimal 1 各市場注文エントリーに使用されるボリューム。

実装上の注意

  • StockSharpのハイレベル SubscribeCandles().Bind(...) ワークフローと組み込み移動平均インジケーターを使用します;シグナル検出に必要な最新の値を超えたカスタムバッファーは保存されません。
  • すべてのインジケーターが完全に形成され、戦略がライブトレーディングの準備ができた後にのみシグナルが評価されます(IsFormedAndOnlineAndAllowTrading())。
  • 戦略はポジションが開いている間の同時エントリーを意図的に無視し、ソースエキスパートアドバイザーのロジックを忠実に一致させます。

namespace StockSharp.Samples.Strategies;

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using StockSharp.Algo;

public class MamyExpertStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<MaCalculationTypes> _maType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private DecimalLengthIndicator _closeMa;
	private DecimalLengthIndicator _openMa;
	private DecimalLengthIndicator _weightedPriceMa;

	private decimal? _previousCloseMa;
	private decimal? _previousOpenMa;
	private decimal? _previousWeightedMa;
	private decimal? _previousOpenSignal;
	private decimal? _previousCloseSignal;

	public MamyExpertStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe used for price aggregation.", "General");

		_maPeriod = Param(nameof(MaPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("MA period", "Length applied to all moving averages.", "Indicator");

		_maType = Param(nameof(MaType), MaCalculationTypes.Weighted)
			.SetDisplay("MA method", "Averaging algorithm applied to open/close/weighted prices.", "Indicator");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume submitted for entries.", "Trading");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public MaCalculationTypes MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousCloseMa = null;
		_previousOpenMa = null;
		_previousWeightedMa = null;
		_previousOpenSignal = null;
		_previousCloseSignal = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = TradeVolume;
		StartProtection(null, null);

		_closeMa = CreateMovingAverage(MaType, MaPeriod);
		_openMa = CreateMovingAverage(MaType, MaPeriod);
		_weightedPriceMa = CreateMovingAverage(MaType, MaPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _closeMa);
			DrawIndicator(area, _openMa);
			DrawIndicator(area, _weightedPriceMa);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_closeMa == null || _openMa == null || _weightedPriceMa == null)
			return;

		var closeMaResult = _closeMa.Process(new DecimalIndicatorValue(_closeMa, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var openMaResult = _openMa.Process(new DecimalIndicatorValue(_openMa, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
		var weightedPrice = CalculateWeightedPrice(candle);
		var weightedMaResult = _weightedPriceMa.Process(new DecimalIndicatorValue(_weightedPriceMa, weightedPrice, candle.OpenTime) { IsFinal = true });

		if (closeMaResult.IsEmpty || openMaResult.IsEmpty || weightedMaResult.IsEmpty)
			return;

		var closeMaValue = closeMaResult.ToDecimal();
		var openMaValue = openMaResult.ToDecimal();
		var weightedMaValue = weightedMaResult.ToDecimal();

		var previousCloseMa = _previousCloseMa;
		var previousOpenMa = _previousOpenMa;
		var previousWeightedMa = _previousWeightedMa;
		var previousOpenSignal = _previousOpenSignal;
		var previousCloseSignal = _previousCloseSignal;

		_previousCloseMa = closeMaValue;
		_previousOpenMa = openMaValue;
		_previousWeightedMa = weightedMaValue;

		if (!_closeMa.IsFormed || !_openMa.IsFormed || !_weightedPriceMa.IsFormed)
		{
			_previousOpenSignal = null;
			_previousCloseSignal = null;
			return;
		}

		var closeSignal = closeMaValue - weightedMaValue;
		var openSignal = 0m;

		if (previousCloseMa.HasValue && previousOpenMa.HasValue && previousWeightedMa.HasValue && previousCloseSignal.HasValue)
		{
			var closeDecreasing = closeMaValue < previousCloseMa.Value &&
				weightedMaValue < previousWeightedMa.Value &&
				closeMaValue < weightedMaValue &&
				weightedMaValue < openMaValue &&
				previousWeightedMa.Value < previousOpenMa.Value &&
				closeSignal <= previousCloseSignal.Value;

			var closeIncreasing = closeMaValue > previousCloseMa.Value &&
				weightedMaValue > previousWeightedMa.Value &&
				closeMaValue > weightedMaValue &&
				weightedMaValue > openMaValue &&
				previousWeightedMa.Value > previousOpenMa.Value &&
				closeSignal >= previousCloseSignal.Value;

			if (closeDecreasing || closeIncreasing)
				openSignal = (weightedMaValue - openMaValue) + (closeMaValue - weightedMaValue);
		}

		if (previousOpenSignal.HasValue && previousCloseSignal.HasValue &&
			openSignal >= 0m &&
			openSignal > previousOpenSignal.Value &&
			closeSignal < 0m &&
			previousCloseSignal.Value >= 0m)
		{
			closeSignal = 0m;
		}

		var hasPreviousOpenSignal = previousOpenSignal.HasValue;
		var hasPreviousCloseSignal = previousCloseSignal.HasValue;

		var openBuy = hasPreviousOpenSignal && openSignal > 0m && previousOpenSignal.Value <= 0m;
		var openSell = hasPreviousOpenSignal && openSignal < 0m && previousOpenSignal.Value >= 0m;
		var closeBuy = hasPreviousCloseSignal && closeSignal < 0m && previousCloseSignal.Value >= 0m;
		var closeSell = hasPreviousCloseSignal && closeSignal > 0m && previousCloseSignal.Value <= 0m;

		_previousOpenSignal = openSignal;
		_previousCloseSignal = closeSignal;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (TradeVolume <= 0m)
			return;

		var longPosition = Position > 0m ? Position : 0m;
		var shortPosition = Position < 0m ? -Position : 0m;

		if (longPosition > 0m)
		{
			if (closeBuy)
				SellMarket(longPosition);
		}
		else if (shortPosition > 0m)
		{
			if (closeSell)
				BuyMarket(shortPosition);
		}
		else
		{
			if (openBuy)
				BuyMarket(TradeVolume);
			else if (openSell)
				SellMarket(TradeVolume);
		}
	}

	private static decimal CalculateWeightedPrice(ICandleMessage candle)
	{
		return (candle.HighPrice + candle.LowPrice + candle.ClosePrice * 2m) / 4m;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaCalculationTypes type, int length)
	{
		return type switch
		{
			MaCalculationTypes.Simple => new SMA { Length = length },
			MaCalculationTypes.Exponential => new EMA { Length = length },
			MaCalculationTypes.Smoothed => new SmoothedMovingAverage { Length = length },
			_ => new WeightedMovingAverage { Length = length },
		};
	}

	public enum MaCalculationTypes
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted
	}
}