GitHub で見る

MAMACD戦略

概要

この戦略は、MetaTrader 5のエキスパートアドバイザー**MAMACD(barabashkakvn版)**をMQL/19334フォルダからStockSharpの高レベルAPIに直接変換したものです。このアプローチは、2つの線形加重移動平均(LWMA)による安値でのトレンド検出と、高速指数移動平均(EMA)のトリガー、そしてMACD主線による確認を組み合わせています。取引は完成したキャンドルごとに1回実行され、元のEAのロジックを維持しています。これには、新規エントリーを許可する前に高速EMAがLWMAチャネルを離れる必要があるリセットフラグが含まれます。

インジケーター

  • LWMA #1(安値、デフォルト85) – キャンドルの安値に適用される低速ベースラインフィルター。
  • LWMA #2(安値、デフォルト75) – チャネル確認のためのキャンドル安値上のわずかに高速なフィルター。
  • EMAトリガー(終値、デフォルト5) – 取引を有効にするために両方のLWMAを上下にクロスする必要があるモメンタムトリガー。
  • MACD主線(高速15、低速26) – 確認フィルター;ロングにはMACDがゼロ以上または前の値より大きいこと、ショートにはMACDがゼロ以下または前の値より小さいことが必要。

エントリー条件

  1. 戦略は完成したキャンドルのみを待ちます(CandleStates.Finished)。
  2. トリガーEMAが両方のLWMAを下回ると、ロング準備フラグが設定されます。EMAが両方のLWMAを上回って戻り、かつMACDがゼロより上または前の値より大きい場合に、ロングポジションを開くことができます。同時に開けるロングポジションは1つだけです。
  3. トリガーEMAが両方のLWMAを上回ると、ショート準備フラグが設定されます。EMAが両方のLWMAを下回って戻り、MACDがゼロより下または前の値より小さい場合に、ショートポジションを開くことができます。同時に有効なショートポジションは1つだけです。
  4. ポジションサイジングには戦略のVolumeプロパティが使用されます。方向を切り替える際、アルゴリズムはまず反対のポジションを決済します。

エグジット条件

  • 元のEAには裁量的なエグジットロジックはコーディングされていません。保護注文はStockSharpのStartProtectionによって処理され、オプションのストップロスとテイクプロフィットの距離はpipsで測定されます。いずれかの保護に達すると、ポジションが自動的に決済されます。

パラメーター

名前 説明
FirstLowMaLength 安値に適用される最初のLWMAの期間(デフォルト85)。
SecondLowMaLength 安値に適用される2番目のLWMAの期間(デフォルト75)。
TriggerEmaLength 終値での高速EMAトリガーの期間(デフォルト5)。
MacdFastLength MACD主線の高速EMAの長さ(デフォルト15)。
MacdSlowLength MACD主線の低速EMAの長さ(デフォルト26)。
StopLossPips pipsでのストップロス距離;無効にするにはゼロに設定(デフォルト15)。
TakeProfitPips pipsでのテイクプロフィット距離;無効にするにはゼロに設定(デフォルト15)。
CandleType 戦略が処理するキャンドルの時間軸(デフォルト1時間)。

実装上の注意

  • PipサイズはSecurity.PriceStepから導出されます。3桁および5桁のシンボルでは、コードは自動的にステップに10を掛けてMT5のpipの定義を模倣します。
  • MACDの履歴バッファはEAと一致します:最初の有効なMACD値が格納され、シグナルが評価される前の次のバーの参照として使用されます。
  • _readyForLong_readyForShortのフラグは元のstartb/startsステートマシンを複製し、新しい取引が行われる前に価格がLWMAチャネルを離れる必要があることを保証します。
  • チャートエリアは移動平均とともに価格系列を視覚化し、変換の確認を容易にするための別個のMACDパネルを表示します。

変換マッピング

MT5要素 StockSharp相当
安値/終値のiMA WeightedMovingAverage(安値フィード)とExponentialMovingAverage(終値フィード)
iMACD主線 MovingAverageConvergenceDivergenceのメイン出力
ポジション確認(buysell Positionの符号とBuyMarket / SellMarketによるボリューム処理
マジックナンバーとスリッページ StockSharpの高レベルAPIでは不要
ストップロス / テイクプロフィット(pips) Pipサイズから計算された絶対価格オフセットを使用したStartProtection

結果として得られる動作はMT5バージョンを反映しながら、StockSharpの戦略ライフサイクル、インジケーターバインディング、リスク管理ヘルパーを活用しています。

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>
/// MAMACD trend-following strategy converted from the original MetaTrader 5 expert advisor.
/// Combines two low-price LWMA filters, a fast EMA trigger, and a MACD confirmation filter.
/// </summary>
public class MamAcdStrategy : Strategy
{
	private readonly StrategyParam<int> _firstLowMaLength;
	private readonly StrategyParam<int> _secondLowMaLength;
	private readonly StrategyParam<int> _triggerEmaLength;
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private WeightedMovingAverage _firstLowMa = null!;
	private WeightedMovingAverage _secondLowMa = null!;
	private ExponentialMovingAverage _triggerEma = null!;
	private MovingAverageConvergenceDivergence _macd = null!;

	private decimal? _previousMacd;
	private bool _readyForLong;
	private bool _readyForShort;
	private decimal _pipSize;

	/// <summary>
	/// Period of the first LWMA calculated on low prices.
	/// </summary>
	public int FirstLowMaLength
	{
		get => _firstLowMaLength.Value;
		set => _firstLowMaLength.Value = value;
	}

	/// <summary>
	/// Period of the second LWMA calculated on low prices.
	/// </summary>
	public int SecondLowMaLength
	{
		get => _secondLowMaLength.Value;
		set => _secondLowMaLength.Value = value;
	}

	/// <summary>
	/// Period of the fast EMA calculated on close prices.
	/// </summary>
	public int TriggerEmaLength
	{
		get => _triggerEmaLength.Value;
		set => _triggerEmaLength.Value = value;
	}

	/// <summary>
	/// Fast EMA period of the MACD filter.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA period of the MACD filter.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips. Set to zero to disable protective stop.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

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

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="MamAcdStrategy"/> with default parameters.
	/// </summary>
	public MamAcdStrategy()
	{
		_firstLowMaLength = Param(nameof(FirstLowMaLength), 85)
		.SetGreaterThanZero()
		.SetDisplay("LWMA #1", "Length of the first LWMA on lows", "Indicators")
		;

		_secondLowMaLength = Param(nameof(SecondLowMaLength), 75)
		.SetGreaterThanZero()
		.SetDisplay("LWMA #2", "Length of the second LWMA on lows", "Indicators")
		;

		_triggerEmaLength = Param(nameof(TriggerEmaLength), 5)
		.SetGreaterThanZero()
		.SetDisplay("Trigger EMA", "Length of the EMA on closes", "Indicators")
		;

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

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

		_stopLossPips = Param(nameof(StopLossPips), 500)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk management");

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

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

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

		_previousMacd = null;
		_readyForLong = false;
		_readyForShort = false;
		_pipSize = 0m;
	}

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

		_firstLowMa = new WeightedMovingAverage { Length = FirstLowMaLength };
		_secondLowMa = new WeightedMovingAverage { Length = SecondLowMaLength };
		_triggerEma = new EMA { Length = TriggerEmaLength };
		_macd = new MovingAverageConvergenceDivergence
		{
			ShortMa = { Length = MacdFastLength },
			LongMa = { Length = MacdSlowLength }
		};

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

		_pipSize = CalculatePipSize();

		var takeProfit = TakeProfitPips > 0 ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : new Unit();
		var stopLoss = StopLossPips > 0 ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : new Unit();
		StartProtection(takeProfit, stopLoss);

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, subscription);
			DrawIndicator(priceArea, _firstLowMa);
			DrawIndicator(priceArea, _secondLowMa);
			DrawIndicator(priceArea, _triggerEma);
			DrawOwnTrades(priceArea);

			var macdArea = CreateChartArea();
			if (macdArea != null)
			{
				macdArea.Title = "MACD";
				DrawIndicator(macdArea, _macd);
			}
		}
	}

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

		// Feed indicator chain: LWMAs work on low prices, EMA and MACD on closes.
		var firstLowValue = _firstLowMa.Process(new DecimalIndicatorValue(_firstLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
		var secondLowValue = _secondLowMa.Process(new DecimalIndicatorValue(_secondLowMa, candle.LowPrice, candle.OpenTime) { IsFinal = true });
		var triggerValue = _triggerEma.Process(new DecimalIndicatorValue(_triggerEma, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var macdValue = _macd.Process(new DecimalIndicatorValue(_macd, candle.ClosePrice, candle.OpenTime) { IsFinal = true });

		// Wait for all indicators to collect enough history.
		if (!_firstLowMa.IsFormed || !_secondLowMa.IsFormed || !_triggerEma.IsFormed || !_macd.IsFormed)
		{
			if (_macd.IsFormed)
			_previousMacd = macdValue.ToDecimal();
			return;
		}

		// indicators already checked above

		var ma1 = firstLowValue.ToDecimal();
		var ma2 = secondLowValue.ToDecimal();
		var ma3 = triggerValue.ToDecimal();
		var macd = macdValue.ToDecimal();

		// Store the first complete MACD observation before evaluating signals.
		if (_previousMacd is null)
		{
			_previousMacd = macd;
			return;
		}

		// Skip calculations when MACD lacks momentum confirmation just like the original EA.
		if (macd == 0m || _previousMacd.Value == 0m)
		{
			_previousMacd = macd;
			return;
		}

		// Track reset flags: EMA must dip below both LWMAs to prepare for a new long, and rise above them for shorts.
		if (ma3 < ma1 && ma3 < ma2)
		_readyForLong = true;

		if (ma3 > ma1 && ma3 > ma2)
		_readyForShort = true;

		var macdImproving = macd > _previousMacd.Value;
		var longSignal = ma3 > ma1 && ma3 > ma2 && _readyForLong && (macd > 0m || macdImproving);
		var shortSignal = ma3 < ma1 && ma3 < ma2 && _readyForShort && (macd < 0m || !macdImproving);

		if (longSignal && Position <= 0)
		{
			var volume = Volume + (Position < 0 ? -Position : 0m);
			if (volume > 0)
			{
				BuyMarket();
				_readyForLong = false;
			}
		}
		else if (shortSignal && Position >= 0)
		{
			var volume = Volume + (Position > 0 ? Position : 0m);
			if (volume > 0)
			{
				SellMarket();
				_readyForShort = false;
			}
		}

		_previousMacd = macd;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
		return 1m;

		var decimals = CountDecimalPlaces(step);

		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}

	private static int CountDecimalPlaces(decimal value)
	{
		value = Math.Abs(value);

		var count = 0;
		while (value != Math.Truncate(value) && count < 10)
		{
			value *= 10m;
			count++;
		}

		return count;
	}
}