GitHub で見る

ロング/ショート Expert MACD 戦略

概要

ロング/ショート Expert MACD 戦略は、MetaTraderエキスパート「LongShortExpertMACD」のStockSharp変換版です。標準的な移動平均収束拡散(MACD)クロスオーバーロジックと固定距離リスク管理を組み合わせています。戦略はMACD線とシグナル線のクロスオーバーに反応し、ロングのみ、ショートのみ、または双方向モードで運用でき、価格ポイントで表されたテイクプロフィットとストップロスレベルを自動的に適用します。

実装はローソク足サブスクリプションとインジケーターバインディングを持つStockSharpの高レベルAPIを使用しています。注文は成行注文として登録されるため、リアルタイムと歴史的なデータソースの両方に簡単に接続できます。

インジケーターと市場データ

  • ローソク足CandleTypeパラメーターが提供する単一の時間軸(デフォルト:1分時間軸)。戦略はSubscribeCandles経由でこのローソク足シリーズをサブスクライブします。
  • MovingAverageConvergenceDivergenceSignal – 設定可能な高速EMA、低速EMA、シグナルEMAの長さを持つStockSharpのMACDインジケーター。ヒストグラム値はMACDとシグナル出力の差から暗黙的に導出されます。

取引ロジック

  1. シグナル準備

    • 完了した各ローソク足でMACD値とシグナル値がインジケーターバインディングを通じて取得されます。
    • 履歴状態_prevIsMacdAboveSignalは、前のローソク足でMACDがシグナル線より上にあったかどうかを追跡します。
  2. エントリー条件

    • 強気クロスオーバー: MACDがシグナル線を上に越えると、設定された取引方向がロングエントリーを許可していればロングポジションを建てます。
      • ショートポジションがすでに有効で反転モードが有効(AllowedPosition = Both)の場合、注文サイズには現在のショート量が含まれ、単一の成行注文でポジションを閉じてロングに切り替えます。
      • ロングのみモードでは、既存のショートポジションはすぐに閉じられますが、次のシグナルまで新しいロングトレードは開かれません。
    • 弱気クロスオーバー: ショートエントリーのための対称的なアクション。
  3. エグジット条件

    • リスク管理: ストップロスとテイクプロフィットの両レベルは、ポジションが検出されるたびに現在の平均エントリー価格から再計算されます。距離は価格ポイントで設定されます(つまりSecurity.PriceStep * パラメーター)、これにより複数の銘柄間で動作が一貫します。
      • ロングポジションは、ローソク足の安値がストップロスレベルに達したとき、または高値がテイクプロフィットレベルに達したときに決済されます。
      • ショートポジションは、ローソク足の高値がストップロスレベルに達したとき、または安値がテイクプロフィットレベルに触れたときに決済されます。
    • 逆方向クロスオーバー: 取引方向が反対側を許可していれば、インジケーターの関係が反転するたびにポジションが平坦化されます(オプションで反転)。
  4. 運用上の保護

    • 取引ロジックは戦略が形成され、オンラインで取引が許可されているとき(IsFormedAndOnlineAndAllowTrading)にのみ実行されます。
    • ポジションが保持されていないときは古いしきい値を避けるために保護レベルがリセットされます。

パラメーター

名前 デフォルト 説明
AllowedPosition Both 戦略をロングのみ、ショートのみ、または双方向取引に制限します。
FastLength 12 MACD計算内の高速EMAの期間。
SlowLength 24 MACD計算内の低速EMAの期間。
SignalLength 9 クロスオーバー検出に使用するシグナルEMAの期間。
TakeProfitPoints 50 価格ポイントで測定したテイクプロフィットレベルまでの距離(PriceStep * ポイント)。0に設定して無効化。
StopLossPoints 20 価格ポイントで測定したストップロスレベルまでの距離。0に設定して無効化。
CandleType TimeFrame(1 minute) シグナル生成に使用するローソク足シリーズ。
Volume 1 各成行注文で送信するロット/契約数。

すべての数値パラメーターは、StockSharp DesignerまたはRunnerでのウォークフォワードテストを簡素化するための最適化範囲を公開しています。

ポジション管理

  • 反転ロジック: 双方向取引が許可されている場合、戦略は単一の成行注文でポジションを反転するために組み合わせた注文サイズを使用し、元のMetaTraderエキスパートの動作を反映します。
  • ロングのみ / ショートのみモード: 許可されていない側の既存ポジションはすぐに閉じられますが、許可された方向に沿ったシグナルが発生するまで新しいエクスポージャーは確立されません。
  • ストップ/テイクの再計算: 戦略は最新のPositionAvgPriceを使用して各ローソク足で保護レベルを再計算し、部分的なフィルやスケーリングエントリー後でも正確な距離を保証します。

使用上の注意

  • 銘柄が有効なPriceStepを提供していることを確認してください; 値が欠けている場合、戦略は1.0価格単位にフォールバックします。これは株式型商品には適していますが、Forex銘柄には調整が必要な場合があります。
  • 戦略は完成したローソク足に依存しています。レイテンシに敏感なシナリオでは、遅延を避けるために適切な粒度のローソク足を提供する必要があります。
  • 注文はスリッページ制御なしの成行注文であるため、特に非流動的な資産では潜在的なフィルの違いを考慮したリスク管理が必要です。
  • ホストアプリケーションがチャートエリアをサポートする場合、視覚化が自動的に作成されます; MACD、ローソク足、自己取引が迅速な監視のために描画されます。

変換メモ

  • StockSharp実装は、MQL5エキスパートからの設定可能なMACDパラメーター、テイクプロフィットとストップロスの距離、ポジション使用可能スイッチを保持しています。
  • MetaTraderで使用されるトレーリングストップとマネーマネジメントモジュールは、元のエキスパートに含まれる「なし」バリアントと同等であるため、意図的に省略されています。
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Long/short MACD expert strategy converted from the MetaTrader example.
/// The strategy opens positions on MACD crossovers and applies fixed stop-loss and take-profit distances.
/// Allowed trade direction can be restricted to long only, short only, or both sides.
/// </summary>
public class LongShortExpertMacdStrategy : Strategy
{
	/// <summary>
	/// Trade directions supported by the strategy.
	/// </summary>
	public enum AllowedPositionTypes
	{
		/// <summary>
		/// Long trades only.
		/// </summary>
		Long,

		/// <summary>
		/// Short trades only.
		/// </summary>
		Short,

		/// <summary>
		/// Long and short trades are allowed.
		/// </summary>
		Both
	}

	private readonly StrategyParam<AllowedPositionTypes> _allowedPosition;
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _signalLength;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd = null!;

	private bool? _prevIsMacdAboveSignal;
	private decimal _longStopPrice;
	private decimal _longTakePrice;
	private decimal _shortStopPrice;
	private decimal _shortTakePrice;
	private decimal? _entryPrice;

	/// <summary>
	/// Initializes a new instance of <see cref="LongShortExpertMacdStrategy"/>.
	/// </summary>
	public LongShortExpertMacdStrategy()
	{
		_allowedPosition = Param(nameof(AllowedPosition), AllowedPositionTypes.Both)
			.SetDisplay("Allowed Positions", "Permitted trade direction", "General");

		_fastLength = Param(nameof(FastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Fast MACD EMA length", "MACD")

			.SetOptimize(8, 16, 2);

		_slowLength = Param(nameof(SlowLength), 24)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Slow MACD EMA length", "MACD")

			.SetOptimize(20, 40, 2);

		_signalLength = Param(nameof(SignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("Signal EMA", "MACD signal EMA length", "MACD")

			.SetOptimize(5, 15, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit distance in price points", "Risk")

			.SetOptimize(0, 150, 10);

		_stopLossPoints = Param(nameof(StopLossPoints), 20)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop loss distance in price points", "Risk")

			.SetOptimize(0, 100, 10);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to process", "General");

		Volume = 1;
	}

	/// <summary>
	/// Allowed trade direction.
	/// </summary>
	public AllowedPositionTypes AllowedPosition
	{
		get => _allowedPosition.Value;
		set => _allowedPosition.Value = value;
	}

	/// <summary>
	/// Fast EMA length used by MACD.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length used by MACD.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	/// <summary>
	/// Signal EMA length used by MACD.
	/// </summary>
	public int SignalLength
	{
		get => _signalLength.Value;
		set => _signalLength.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	private bool CanEnterLong => AllowedPosition != AllowedPositionTypes.Short;
	private bool CanEnterShort => AllowedPosition != AllowedPositionTypes.Long;
	private bool AllowReverse => AllowedPosition == AllowedPositionTypes.Both;

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

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

		_prevIsMacdAboveSignal = null;
		_entryPrice = null;
		ResetProtection();
	}

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

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = FastLength },
				LongMa = { Length = SlowLength },
			},
			SignalMa = { Length = SignalLength }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_macd, ProcessCandle)
			.Start();

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

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

		var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;

		if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
			return;

		UpdateProtectionLevels();

		var isMacdAboveSignal = macd > signal;

		if (!_macd.IsFormed)
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		if (TryExitWithProtection(candle))
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		if (_prevIsMacdAboveSignal is null)
		{
			_prevIsMacdAboveSignal = isMacdAboveSignal;
			return;
		}

		var crossUp = isMacdAboveSignal && _prevIsMacdAboveSignal == false;
		var crossDown = !isMacdAboveSignal && _prevIsMacdAboveSignal == true;

		if (crossUp)
		{
			if (CanEnterLong)
			{
				if (Position < 0)
				{
					if (AllowReverse)
					{
						var volume = Volume + Math.Abs(Position);

						if (volume > 0)
						{
							ResetProtection();
							BuyMarket();
							_entryPrice = candle.ClosePrice;
						}
					}
					else
					{
						var volume = Math.Abs(Position);
						if (volume > 0)
						{
							BuyMarket();
							ResetProtection();
							_entryPrice = null;
						}
					}
				}
				else if (Position == 0)
				{
					if (Volume > 0)
					{
						ResetProtection();
						BuyMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
			else if (Position < 0)
			{
				var volume = Math.Abs(Position);
				if (volume > 0)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
				}
			}
		}
		else if (crossDown)
		{
			if (CanEnterShort)
			{
				if (Position > 0)
				{
					if (AllowReverse)
					{
						var volume = Volume + Math.Abs(Position);

						if (volume > 0)
						{
							ResetProtection();
							SellMarket();
							_entryPrice = candle.ClosePrice;
						}
					}
					else
					{
						var volume = Math.Abs(Position);
						if (volume > 0)
						{
							SellMarket();
							ResetProtection();
							_entryPrice = null;
						}
					}
				}
				else if (Position == 0)
				{
					if (Volume > 0)
					{
						ResetProtection();
						SellMarket();
						_entryPrice = candle.ClosePrice;
					}
				}
			}
			else if (Position > 0)
			{
				var volume = Math.Abs(Position);
				if (volume > 0)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
				}
			}
		}

		_prevIsMacdAboveSignal = isMacdAboveSignal;
	}

	private void UpdateProtectionLevels()
	{
		if (_entryPrice is not decimal entry)
		{
			ResetProtection();
			return;
		}

		if (Position > 0)
		{
			var step = GetPriceStep();
			_longStopPrice = StopLossPoints > 0 ? entry - StopLossPoints * step : 0m;
			_longTakePrice = TakeProfitPoints > 0 ? entry + TakeProfitPoints * step : 0m;
			_shortStopPrice = 0m;
			_shortTakePrice = 0m;
		}
		else if (Position < 0)
		{
			var step = GetPriceStep();
			_shortStopPrice = StopLossPoints > 0 ? entry + StopLossPoints * step : 0m;
			_shortTakePrice = TakeProfitPoints > 0 ? entry - TakeProfitPoints * step : 0m;
			_longStopPrice = 0m;
			_longTakePrice = 0m;
		}
		else
		{
			ResetProtection();
		}
	}

	private bool TryExitWithProtection(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var volume = Math.Abs(Position);

			if (volume > 0)
			{
				if (StopLossPoints > 0 && _longStopPrice > 0m && candle.LowPrice <= _longStopPrice)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}

				if (TakeProfitPoints > 0 && _longTakePrice > 0m && candle.HighPrice >= _longTakePrice)
				{
					SellMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);

			if (volume > 0)
			{
				if (StopLossPoints > 0 && _shortStopPrice > 0m && candle.HighPrice >= _shortStopPrice)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}

				if (TakeProfitPoints > 0 && _shortTakePrice > 0m && candle.LowPrice <= _shortTakePrice)
				{
					BuyMarket();
					ResetProtection();
					_entryPrice = null;
					return true;
				}
			}
		}
		return false;
	}

	private void ResetProtection()
	{
		_longStopPrice = 0m;
		_longTakePrice = 0m;
		_shortStopPrice = 0m;
		_shortTakePrice = 0m;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}