GitHub で見る

MACD EA 戦略

この戦略は、MQL/20010フォルダーにある MetaTrader 5 エキスパートアドバイザー MACD EA (barabashkakvn's edition).mq5 の StockSharp ポートです。StockSharp の高レベル API を使用しながら、同じ MACD クロスオーバーロジック、部分的な利益確定、および資金管理機能を再現しています。

トレードロジック

  • シグナルソース – 設定可能な高速、低速、シグナル期間を使ってクラシックな MACD インジケーターを計算します。戦略は、2 本および 4 本前の完成したローソク足における MACD ラインとシグナルラインの差を調べます。強気のクロスオーバー(差がマイナスからプラスに転換)でロングトレードを開き、逆の条件でショートトレードを開きます。
  • ポジション管理 – 各注文は、pips 単位で測定された設定可能なストップロスとテイクプロフィットのオフセットで保護されます。オフセットは、銘柄の価格ステップを使って価格に変換され、銘柄が 3 または 5 桁の小数点を持つ場合は 10 倍にして、元の EA のポイント調整を模倣します。
  • 部分利確 – 有効にすると、価格がトレード方向に PartialProfitPips 進んだ時点で、オープンポジションの半分が決済されます。残りの部分は継続します。
  • ブレイクイーブン – 価格が BreakevenPips 有利方向に進んだ後、戦略はブレイクイーブンガードを有効にします。価格が元のエントリーレベルに戻ると、EA がストップをブレイクイーブンに移動させるのと同様に、エントリー価格でポジションが決済されます。
  • 逆 MACD シグナル – 逆の MACD クロスオーバーが残りのエクスポージャーを即座に決済し、戦略がインジケーターのトレンドに逆らったポジションを保持しないことを保証します。

資金管理

UseMoneyManagement が有効な場合、連続する負けトレードの後にポジションサイズが増加します。次のトレードは連続損失数に基づくマルチプライヤーを使用します(1 回の損失後は x2、2 回の損失後は x3、6 回以上の損失では x7 まで)。マルチプライヤーは RiskMultiplier パラメーターと組み合わされ、元のコードのマーチンゲールスタイルのサイジングを再現します。勝ちトレードは損失カウンターをゼロにリセットします。

パラメーター

パラメーター 説明
FastPeriod / SlowPeriod / SignalPeriod MACD 計算の長さ。
StopLossPips pips 単位の保護ストップまでの距離(0 で無効化)。
TakeProfitPips pips 単位の利益目標までの距離(0 で無効化)。
PartialProfitPips ポジションの半分を決済するために必要な pips(0 で部分決済を無効化)。
BreakevenPips ブレイクイーブンモードが有効になる前に必要な pips(0 でブレイクイーブンを無効化)。
UseMoneyManagement 損失連続に基づく動的なポジションサイジングを有効化。
RiskMultiplier 資金管理がアクティブな場合に適用される追加マルチプライヤー。
BaseVolume スケーリング前の基本トレードボリューム。
CandleType インジケーター計算に使用するローソク足シリーズ。

注記

  • 戦略は SubscribeCandles とインジケーターバインディングを使用し、推奨される高レベル API パターンに従っています。
  • Python バージョンはまだ利用できません。CS フォルダーの C# 実装のみが提供されています。
  • リクエストに従い、テストは追加または変更されていません。
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>
/// MACD crossover strategy converted from the "MACD EA" MetaTrader expert advisor.
/// Implements partial profit taking, breakeven logic, and optional money management scaling.
/// </summary>
public class MacdEaStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _signalPeriod;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _partialProfitPips;
	private readonly StrategyParam<int> _breakevenPips;
	private readonly StrategyParam<bool> _useMoneyManagement;
	private readonly StrategyParam<decimal> _riskMultiplier;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd;
	private readonly List<decimal> _macdDiffs = new();

	private decimal? _entryPrice;
	private decimal _currentPositionVolume;
	private int _entryDirection;
	private bool _partialTaken;
	private bool _breakevenActive;
	private decimal _tradePnl;
	private int _consecutiveLosses;

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Signal moving average period.
	/// </summary>
	public int SignalPeriod
	{
		get => _signalPeriod.Value;
		set => _signalPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Profit target for closing half of the position in pips.
	/// </summary>
	public int PartialProfitPips
	{
		get => _partialProfitPips.Value;
		set => _partialProfitPips.Value = value;
	}

	/// <summary>
	/// Breakeven activation distance in pips.
	/// </summary>
	public int BreakevenPips
	{
		get => _breakevenPips.Value;
		set => _breakevenPips.Value = value;
	}

	/// <summary>
	/// Enables money management scaling when true.
	/// </summary>
	public bool UseMoneyManagement
	{
		get => _useMoneyManagement.Value;
		set => _useMoneyManagement.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the base volume when money management is enabled.
	/// </summary>
	public decimal RiskMultiplier
	{
		get => _riskMultiplier.Value;
		set => _riskMultiplier.Value = value;
	}

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

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

	/// <summary>
	/// Initializes a new instance of <see cref="MacdEaStrategy"/>.
	/// </summary>
	public MacdEaStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 55)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
			
			.SetOptimize(10, 120, 5);

		_slowPeriod = Param(nameof(SlowPeriod), 69)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
			
			.SetOptimize(20, 200, 5);

		_signalPeriod = Param(nameof(SignalPeriod), 90)
			.SetGreaterThanZero()
			.SetDisplay("Signal MA", "Signal moving average period", "Indicators")
			
			.SetOptimize(10, 150, 5);

		_stopLossPips = Param(nameof(StopLossPips), 80)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk")
			
			.SetOptimize(0, 200, 10);

		_takeProfitPips = Param(nameof(TakeProfitPips), 500)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk")
			
			.SetOptimize(0, 800, 20);

		_partialProfitPips = Param(nameof(PartialProfitPips), 70)
			.SetNotNegative()
			.SetDisplay("Partial Profit", "Pips to close half the position", "Risk")
			
			.SetOptimize(0, 200, 10);

		_breakevenPips = Param(nameof(BreakevenPips), 0)
			.SetNotNegative()
			.SetDisplay("Breakeven", "Distance to activate breakeven", "Risk")
			
			.SetOptimize(0, 200, 10);

		_useMoneyManagement = Param(nameof(UseMoneyManagement), false)
			.SetDisplay("Use MM", "Enable money management scaling", "Money Management");

		_riskMultiplier = Param(nameof(RiskMultiplier), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Multiplier", "Multiplier applied to base volume", "Money Management")
			
			.SetOptimize(0.5m, 5m, 0.5m);

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Default order size", "General")
			
			.SetOptimize(0.1m, 5m, 0.1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");
	}

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

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

		_macdDiffs.Clear();
		_entryPrice = null;
		_currentPositionVolume = 0m;
		_entryDirection = 0;
		_partialTaken = false;
		_breakevenActive = false;
		_tradePnl = 0m;
		_consecutiveLosses = 0;
	}

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

		Volume = BaseVolume;

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd = { ShortMa = { Length = FastPeriod }, LongMa = { Length = SlowPeriod } },
			SignalMa = { Length = SignalPeriod }
		};

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

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

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

		var result = _macd.Process(candle);
		if (!_macd.IsFormed)
			return;

		var macdValue = result as MovingAverageConvergenceDivergenceSignalValue;
		if (macdValue == null)
			return;

		var macdLine = macdValue.Macd ?? 0m;
		var signalLine = macdValue.Signal ?? 0m;

		var diff = macdLine - signalLine;
		_macdDiffs.Add(diff);
		if (_macdDiffs.Count > 50)
		_macdDiffs.RemoveRange(0, _macdDiffs.Count - 50);

		if (_macdDiffs.Count < 5)
		return;

		var diffTwo = _macdDiffs[^3];
		var diffFour = _macdDiffs[^5];

		var bullish = diffTwo > 0m && diffFour < 0m;
		var bearish = diffTwo < 0m && diffFour > 0m;

		var pip = GetPipSize();

		if (Position > 0m)
		{
		if (HandleLongPosition(candle, bearish, pip))
		return;
		}
		else if (Position < 0m)
		{
		if (HandleShortPosition(candle, bullish, pip))
		return;
		}

		if (Position != 0m)
		return;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
		return;

		if (bullish)
		{
		BuyMarket(volume);
		InitializeTradeState(candle.ClosePrice, volume, 1);
		}
		else if (bearish)
		{
		SellMarket(volume);
		InitializeTradeState(candle.ClosePrice, volume, -1);
		}
	}

	private bool HandleLongPosition(ICandleMessage candle, bool bearishSignal, decimal pip)
	{
		if (_entryPrice is not decimal entry)
		return false;

		var remainingVolume = _currentPositionVolume > 0m ? _currentPositionVolume : Math.Abs(Position);
		remainingVolume = NormalizeVolume(remainingVolume);
		if (remainingVolume <= 0m)
		return false;

		var stop = StopLossPips > 0 ? entry - StopLossPips * pip : (decimal?)null;
		var take = TakeProfitPips > 0 ? entry + TakeProfitPips * pip : (decimal?)null;
		var partial = PartialProfitPips > 0 ? entry + PartialProfitPips * pip : (decimal?)null;
		var breakeven = BreakevenPips > 0 ? entry + BreakevenPips * pip : (decimal?)null;

		if (stop is decimal stopPrice && candle.LowPrice <= stopPrice)
		{
		CloseLong(remainingVolume, stopPrice);
		return true;
		}

		if (take is decimal takePrice && candle.HighPrice >= takePrice)
		{
		CloseLong(remainingVolume, takePrice);
		return true;
		}

		if (!_partialTaken && partial is decimal partialPrice && candle.HighPrice >= partialPrice)
		{
		var halfVolume = NormalizeVolume(remainingVolume / 2m);
		if (halfVolume > 0m)
		{
		SellMarket(halfVolume);
		RegisterPnl(partialPrice, halfVolume);
		_currentPositionVolume = Math.Max(0m, _currentPositionVolume - halfVolume);
		_partialTaken = true;
		return true;
		}
		}

		if (breakeven is decimal breakevenPrice && !_breakevenActive && candle.HighPrice >= breakevenPrice)
		_breakevenActive = true;

		if (_breakevenActive && candle.LowPrice <= entry)
		{
		CloseLong(remainingVolume, entry);
		return true;
		}

		if (bearishSignal)
		{
		CloseLong(remainingVolume, candle.ClosePrice);
		return true;
		}

		return false;
	}

	private bool HandleShortPosition(ICandleMessage candle, bool bullishSignal, decimal pip)
	{
		if (_entryPrice is not decimal entry)
		return false;

		var remainingVolume = _currentPositionVolume > 0m ? _currentPositionVolume : Math.Abs(Position);
		remainingVolume = NormalizeVolume(remainingVolume);
		if (remainingVolume <= 0m)
		return false;

		var stop = StopLossPips > 0 ? entry + StopLossPips * pip : (decimal?)null;
		var take = TakeProfitPips > 0 ? entry - TakeProfitPips * pip : (decimal?)null;
		var partial = PartialProfitPips > 0 ? entry - PartialProfitPips * pip : (decimal?)null;
		var breakeven = BreakevenPips > 0 ? entry - BreakevenPips * pip : (decimal?)null;

		if (stop is decimal stopPrice && candle.HighPrice >= stopPrice)
		{
		CloseShort(remainingVolume, stopPrice);
		return true;
		}

		if (take is decimal takePrice && candle.LowPrice <= takePrice)
		{
		CloseShort(remainingVolume, takePrice);
		return true;
		}

		if (!_partialTaken && partial is decimal partialPrice && candle.LowPrice <= partialPrice)
		{
		var halfVolume = NormalizeVolume(remainingVolume / 2m);
		if (halfVolume > 0m)
		{
		BuyMarket(halfVolume);
		RegisterPnl(partialPrice, halfVolume);
		_currentPositionVolume = Math.Max(0m, _currentPositionVolume - halfVolume);
		_partialTaken = true;
		return true;
		}
		}

		if (breakeven is decimal breakevenPrice && !_breakevenActive && candle.LowPrice <= breakevenPrice)
		_breakevenActive = true;

		if (_breakevenActive && candle.HighPrice >= entry)
		{
		CloseShort(remainingVolume, entry);
		return true;
		}

		if (bullishSignal)
		{
		CloseShort(remainingVolume, candle.ClosePrice);
		return true;
		}

		return false;
	}

	private void CloseLong(decimal volume, decimal exitPrice)
	{
		volume = NormalizeVolume(volume);
		if (volume <= 0m)
		return;

		SellMarket(volume);
		RegisterPnl(exitPrice, volume);
		_currentPositionVolume = Math.Max(0m, _currentPositionVolume - volume);
		FinalizeTradeIfClosed();
	}

	private void CloseShort(decimal volume, decimal exitPrice)
	{
		volume = NormalizeVolume(volume);
		if (volume <= 0m)
		return;

		BuyMarket(volume);
		RegisterPnl(exitPrice, volume);
		_currentPositionVolume = Math.Max(0m, _currentPositionVolume - volume);
		FinalizeTradeIfClosed();
	}

	private void InitializeTradeState(decimal entryPrice, decimal volume, int direction)
	{
		_entryPrice = entryPrice;
		_currentPositionVolume = NormalizeVolume(Math.Abs(volume));
		_entryDirection = direction;
		_partialTaken = false;
		_breakevenActive = false;
		_tradePnl = 0m;
	}

	private decimal CalculateOrderVolume()
	{
		var volume = BaseVolume;

		if (UseMoneyManagement)
		{
		var multiplier = _consecutiveLosses switch
		{
		0 => 1m,
		1 => 2m,
		2 => 3m,
		3 => 4m,
		4 => 5m,
		5 => 6m,
		_ => 7m,
		};

		volume *= multiplier * RiskMultiplier;
		}

		return NormalizeVolume(volume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
		return 0m;

		var sec = Security;
		if (sec == null)
		return volume;

		var step = sec.VolumeStep ?? 1m;
		if (step <= 0m)
		step = 1m;

		var steps = Math.Floor(volume / step);
		volume = steps * step;

		if (volume < step)
		return 0m;

		return volume;
	}

	private decimal GetPipSize()
	{
		var sec = Security;
		var step = sec?.PriceStep ?? 1m;
		if (step <= 0m)
		return 1m;

		var tmp = step;
		var decimals = 0;

		while (decimals < 10 && decimal.Truncate(tmp) != tmp)
		{
		tmp *= 10m;
		decimals++;
		}

		if (decimals == 3 || decimals == 5)
		return step * 10m;

		return step;
	}

	private void RegisterPnl(decimal exitPrice, decimal volume)
	{
		if (_entryPrice is not decimal entry || _entryDirection == 0)
		return;

		var pnl = (exitPrice - entry) * volume * _entryDirection;
		_tradePnl += pnl;
	}

	private void FinalizeTradeIfClosed()
	{
		if (_currentPositionVolume > 0m)
		return;

		if (_tradePnl > 0m)
		_consecutiveLosses = 0;
		else if (_tradePnl < 0m)
		_consecutiveLosses++;
		else
		_consecutiveLosses = 0;

		ResetTradeState();
	}

	private void ResetTradeState()
	{
		_entryPrice = null;
		_currentPositionVolume = 0m;
		_entryDirection = 0;
		_partialTaken = false;
		_breakevenActive = false;
		_tradePnl = 0m;
	}
}