GitHub で見る

MACD自動化戦略の例

概要

この戦略はStockSharpの高レベルAPIを使用して、MetaTrader 4のエキスパートアドバイザー「Example of MACD Automated」を複製します。2つの時間軸でMACDのメインラインを監視し、両方のトレンドフィルターが一致した場合にのみ単一のポジションを開きます。保護的なストップロスとテイクプロフィットの距離は価格ステップで適用され、ポジションサイズは直近の損失トレードのボリュームを累積するオリジナルのAdvancedMMロジックに従います。

取引ロジック

  1. 上位時間軸フィルター – 上位時間軸(デフォルト: 日足ローソク足)で計算されたMACD (12, 26, 9)は、ロングシグナルには正のメインライン、ショートシグナルには負のメインラインが必要です。
  2. エントリー時間軸の確認 – エントリー時間軸(デフォルト: 15分ローソク足)での同じMACD設定が、上位時間軸フィルターと同じ方向を示す必要があります。
  3. 単一ポジション – 戦略は同時に1つのポジションを取引します。既存のポジションが保護レベルによってクローズされるまで、新しいエントリーはスキップされます。
  4. 保護注文 – ストップロスとテイクプロフィットレベルは銘柄の価格ステップの倍数で測定され、元のMT4の StopLossTakeProfit 入力を反映します。0 の値は対応する保護を無効化します。
  5. 高度なマネーマネジメント – 連続する損失トレードの後、トレードボリュームは損失のロットサイズを合計することで増加し、利益トレードの後はベースボリュームに戻ります。これはソースEAの AdvancedMM() 関数をエミュレートします。

パラメーター

名前 説明 デフォルト値
BaseVolume AdvancedMMロジックで使用するベース注文ボリューム。 0.01
StopLossPoints 価格ステップで表したストップロス距離。0 でストップを無効化。 50
TakeProfitPoints 価格ステップで表したテイクプロフィット距離。0 で目標を無効化。 30
MacdFastLength 両方の時間軸のMACDの高速EMA期間。 12
MacdSlowLength MACDの低速EMA期間。 26
MacdSignalLength シグナルラインのEMA期間。 9
EntryCandleType トレード実行の時間軸。 15m ローソク足
FilterCandleType トレンドフィルターとして使用する上位時間軸。 1d ローソク足

ポジション管理

  • ストップロスとテイクプロフィットの価格は、銘柄の価格ステップに基づいて新しいポジションごとに再計算されます。
  • バー内で保護レベルが触れられると、戦略はそのレベルで注文が執行されたと仮定し、実現損益を記録します。
  • 各クローズトレードの後、AdvancedMMロジックが次の注文サイズを更新します:
    • 歴史的トレードが2件未満 → ベースボリュームを使用。
    • 直近のトレードが損失だった → そのボリュームを繰り返す。
    • 最後の勝ちの前に連続損失があった → それらのボリュームを合計して回収。
    • それ以外 → ベースボリュームに戻る。

備考

  • 変換は保護レベルに達するまでポジションを保持するオリジナルの動作を維持します。MACDのクロスオーバーでのエグジットはありません。
  • ポイントベースのストップとターゲット距離が正しく計算されるよう、銘柄に有効な PriceStep 情報があることを確認してください。
  • 戦略は完了したローソク足に依存しており、完成したローソク足の更新を提供する過去データまたはライブフィードで使用する必要があります。
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>
/// Conversion of the "Example of MACD Automated" MQL4 expert advisor.
/// The strategy waits for MACD agreement on two timeframes and uses AdvancedMM sizing.
/// </summary>
public class ExampleOfMacdAutomatedStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _macdSignalLength;
	private readonly StrategyParam<DataType> _entryCandleType;
	private readonly StrategyParam<DataType> _filterCandleType;

	private MovingAverageConvergenceDivergenceSignal _entryMacd = null!;
	private MovingAverageConvergenceDivergenceSignal _filterMacd = null!;

	private decimal? _lastEntryMacd;
	private decimal? _lastFilterMacd;

	private readonly List<TradeInfo> _tradeHistory = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryVolume;
	private int _entryDirection;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExampleOfMacdAutomatedStrategy"/> class.
	/// </summary>
	public ExampleOfMacdAutomatedStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Starting order volume for AdvancedMM", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (steps)", "Stop-loss distance in price steps", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Take Profit (steps)", "Take-profit distance in price steps", "Risk")
			;

		_macdFastLength = Param(nameof(MacdFastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast", "Fast EMA length", "Indicators")
			;

		_macdSlowLength = Param(nameof(MacdSlowLength), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow", "Slow EMA length", "Indicators")
			;

		_macdSignalLength = Param(nameof(MacdSignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
			;

		_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Entry Timeframe", "Working timeframe for entries", "General");

		_filterCandleType = Param(nameof(FilterCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Filter Timeframe", "Higher timeframe used as trend filter", "General");
	}

	/// <summary>
	/// Base volume parameter.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

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

	/// <summary>
	/// MACD fast EMA length.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// MACD slow EMA length.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// MACD signal EMA length.
	/// </summary>
	public int MacdSignalLength
	{
		get => _macdSignalLength.Value;
		set => _macdSignalLength.Value = value;
	}

	/// <summary>
	/// Timeframe used for entries.
	/// </summary>
	public DataType EntryCandleType
	{
		get => _entryCandleType.Value;
		set => _entryCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe used as a trend filter.
	/// </summary>
	public DataType FilterCandleType
	{
		get => _filterCandleType.Value;
		set => _filterCandleType.Value = value;
	}

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

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

		_lastEntryMacd = null;
		_lastFilterMacd = null;
		_tradeHistory.Clear();
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;

		_entryMacd?.Reset();
		_filterMacd?.Reset();
	}

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

		// Create MACD indicators for entry and filter timeframes.
		_entryMacd = CreateMacd();
		_filterMacd = CreateMacd();

		var entrySubscription = SubscribeCandles(EntryCandleType);
		entrySubscription
			.BindEx(_entryMacd, ProcessEntryCandle)
			.Start();

		var filterSubscription = SubscribeCandles(FilterCandleType);
		filterSubscription
			.BindEx(_filterMacd, ProcessFilterCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, entrySubscription);
			DrawIndicator(area, _entryMacd);
			DrawIndicator(area, _filterMacd);
			DrawOwnTrades(area);
		}
	}

	private MovingAverageConvergenceDivergenceSignal CreateMacd()
	{
		// Instantiate MACD with shared parameters for both timeframes.
		return new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFastLength },
				LongMa = { Length = MacdSlowLength },
			},
			SignalMa = { Length = MacdSignalLength }
		};
	}

	private void ProcessFilterCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Process only completed candles on the filter timeframe.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		_lastFilterMacd = macd.Macd;
	}

	private void ProcessEntryCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Ensure that we operate on final candle values only.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		var currentEntryMacd = macd.Macd;

		// Manage protective exits before searching for new entries.
		if (HandleProtection(candle))
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Skip further processing if there is still an open position.
		if (Position != 0)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!_entryMacd.IsFormed || !_filterMacd.IsFormed)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (_lastEntryMacd is not decimal previousEntryMacd || _lastFilterMacd is not decimal filterMacdValue)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Enter only on a zero-line crossover aligned with the higher timeframe filter.
		if (previousEntryMacd <= 0m && currentEntryMacd > 0m && filterMacdValue > 0m)
		{
			EnterPosition(candle.ClosePrice, true);
		}
		else if (previousEntryMacd >= 0m && currentEntryMacd < 0m && filterMacdValue < 0m)
		{
			EnterPosition(candle.ClosePrice, false);
		}

		_lastEntryMacd = currentEntryMacd;
	}

	private void EnterPosition(decimal price, bool isLong)
	{
		var volume = CalculateTradeVolume();
		if (volume <= 0m)
		return;

		if (isLong)
		{
			BuyMarket(volume);
			RegisterEntry(price, volume, 1);
		}
		else
		{
			SellMarket(volume);
			RegisterEntry(price, volume, -1);
		}
	}

	private void RegisterEntry(decimal price, decimal volume, int direction)
	{
		// Store entry information for later profit calculation.
		_entryPrice = price;
		_entryVolume = volume;
		_entryDirection = direction;

		UpdateProtectionLevels(price, direction > 0);
	}

	private void UpdateProtectionLevels(decimal price, bool isLong)
	{
		var point = GetPointValue();

		if (point <= 0m)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		if (isLong)
		{
			_stopPrice = StopLossPoints > 0m ? price - StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price + TakeProfitPoints * point : null;
		}
		else
		{
			_stopPrice = StopLossPoints > 0m ? price + StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price - TakeProfitPoints * point : null;
		}
	}

	private bool HandleProtection(ICandleMessage candle)
	{
		if (Position == 0 || _entryDirection == 0)
		return false;

		if (_entryDirection > 0)
		{
			if (TryGetLongExitPrice(candle, out var exitPrice))
			{
				SellMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}
		else
		{
			if (TryGetShortExitPrice(candle, out var exitPrice))
			{
				BuyMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}

		return false;
	}

	private bool TryGetLongExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private bool TryGetShortExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private void RegisterClosedTrade(decimal exitPrice)
	{
		if (!_entryPrice.HasValue || _entryVolume <= 0m || _entryDirection == 0)
		return;

		var entryPrice = _entryPrice.Value;
		var volume = _entryVolume;
		var direction = _entryDirection;

		var profit = (exitPrice - entryPrice) * direction * volume;

		_tradeHistory.Add(new TradeInfo(volume, profit));
		if (_tradeHistory.Count > 200)
		_tradeHistory.RemoveAt(0);

		_entryPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private decimal CalculateTradeVolume()
	{
		var baseVolume = BaseVolume;
		if (baseVolume <= 0m)
		return 0m;

		if (_tradeHistory.Count < 2)
		return baseVolume;

		var advancedLots = 0m;
		var profit1 = false;
		var profit2 = false;
		var firstIteration = true;

		for (var i = _tradeHistory.Count - 1; i >= 0; i--)
		{
			var trade = _tradeHistory[i];
			var isProfit = trade.Profit >= 0m;

			if (isProfit && profit1)
			return baseVolume;

			if (firstIteration)
			{
				if (isProfit)
				{
					profit1 = true;
				}
				else
				{
					return trade.Volume;
				}

				firstIteration = false;
			}

			if (isProfit && profit2)
			return advancedLots > 0m ? advancedLots : baseVolume;

			if (isProfit)
			{
				profit2 = true;
			}
			else
			{
				profit1 = false;
				profit2 = false;
				advancedLots += trade.Volume;
			}
		}

		return advancedLots > 0m ? advancedLots : baseVolume;
	}

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

	private readonly struct TradeInfo
	{
		public TradeInfo(decimal volume, decimal profit)
		{
			Volume = volume;
			Profit = profit;
		}

		public decimal Volume { get; }
		public decimal Profit { get; }
	}
}