GitHub で見る

Macd Pattern Trader v03 (StockSharp ポート)

概要

Macd Pattern Trader v03 は、MetaTrader 4 エキスパート アドバイザー MacdPatternTraderv03 から変換された高レベルの StockSharp 戦略です。オリジナルのロボットは、MACD メインラインで 3 つのピークの反転形成を検索し、移動平均に基づいて部分的な利益確定ルールを適用します。この C# ポートは、StockSharp サブスクリプション、インジケーター、順序ヘルパーを使用する際にパターン ロジックを保持します。

この戦略は、流動的な FX ペアのトレンド枯渇セットアップ向けに設計されていますが、滑らかな MACD 曲線を示すあらゆる商品に適用できます。デフォルトの時間枠は元のアドバイザーと一致する 30 分のローソク足で、デフォルトの取引サイズは 1 契約(または StockSharp の条件でのロットに相当)です。

インジケーターとデータフロー

  • MACD (ファースト EMA 5、スロー EMA 13、シグナル 1) — トリプルトップ/トリプルボトム構造を検出するために使用されるメインインジケーター。信号線は使用されません。この戦略は MACD のメインラインのみに依存します。
  • EMA(7) および EMA(21) — ポジション管理中に使用されるショートおよびミディアム平均。
  • SMA(98) および EMA(365) — スケールアウト トリガーを形成する低速フィルター。

実装は、設定されたローソク足タイプをサブスクライブし、Bind / BindEx を通じてインジケーターをバインドします。不完全なデータに作用することを避けるために、完成したキャンドルのみが処理されます。

エントリールール

短いセットアップ

  1. MACD メインラインが 上限アクティベーション レベル (デフォルト 0.0030) を超えたときにセットアップを準備します。
  2. MACD が前回およびその直前の両方の値を超える極大値を出力し、その後 上限しきい値 (デフォルト 0.0045) を下回ると、最初のピークを登録します。
  3. MACD がしきい値を超えて戻り、極大値が高くなって、再びしきい値を下回った場合は、2 番目のピークを登録します。
  4. MACD が 3 つの連続したバーでしきい値を下回った状態で 3 回目のロールオーバーが発生し、最後の極大値が前の極大値よりも低い場合のパターンを確認します。
  5. ロングポジションが存在しない場合は、残りのロングエクスポージャーをフラットにし、設定されたボリュームでショートポジションをオープンします。

長いセットアップ

  1. MACD メインラインが Lower Activation レベル (デフォルト -0.0030) を下回ったときにセットアップを準備します。
  2. MACD が前の 2 つの値を下回る極小値を出力し、下限しきい値 (デフォルトは -0.0045) を超えたら、最初の谷を登録します。
  3. MACD がしきい値を下回り、下限値に達し、再びしきい値を超えた場合、2 番目の谷を登録します。
  4. MACD が 3 本のローソク足のしきい値を上回っており、3 回目の上昇が観察され、最新の底値が前の底値よりも高ければ、強気パターンを確認します。
  5. 残りの短い露出をフラットにし、設定されたボリュームを購入します。

このロジックは、元の MQ4 ファイル内のネストされた stopsstops1、および aop_ok* フラグをミラーリングします。これには、MACD がアクティブ化バンドを過ぎてリトレースするたびにリセットが含まれます。

貿易管理

  • スケールアウト — 未実現利益 ((Close − Entry) * Position として計算) が ProfitThreshold (デフォルトの 5 価格単位) を超えると、戦略は 2 段階の出口を適用します。
    • ステージ 1 (ロング): 前回のローソク足の終値は EMA(21) を超えている必要があります。この戦略は、最初のロングポジションの 3 分の 1 を売却します。空売りの場合、要件は EMA(21) を下回る前の終値であり、最初の空売り数量の 3 分の 1 が買い戻されます。
    • ステージ 2 (ロング): 前回のローソク足の高値が SMA(98) と EMA(365) の平均を突破する必要があります。元のロングポジションの半分がクローズされました。ショートはこれを反映しており、以前の最低値が平均フィルターを下回っています。
  • 残留位置 — スケーリング シーケンス後にこのポートで管理されないまま残り、ソース EA と一致します。
  • リスク注文 — MetaTrader バージョンは、ローリング高値と安値に基づいてストップロス注文とテイクプロフィット注文を出しました。 StockSharp は保護命令の管理方法が異なるため、このポートはストップ/ターゲットを自動接続しません。ユーザーは、必要に応じて戦略を StartProtection() または外部リスク モジュールと組み合わせることができます。

パラメーター

名前 デフォルト 説明
Volume 1 各エントリーで提出された取引サイズ。
CandleType 30分の時間枠 インジケーターの計算に使用されるローソク足シリーズ。
FastEmaLength / SlowEmaLength 5/13 MACD の速い周期と遅い EMA 周期。
UpperThreshold / LowerThreshold 0.0045 / −0.0045 パターン確定が発生する消耗帯。
UpperActivation / LowerActivation 0.0030 / −0.0030 弱気/強気のセットアップを準備するアウターバンド。
EmaOneLength / EmaTwoLength 7/21 視覚化およびスケーリング ロジック用の補助 EMA。
SmaLength 98 ステージ 2 の終了時に、低速の SMA が EMA(365) とともに使用されます。
EmaFourLength 365 ステージ 2 の終了中に使用される長期的な EMA。
ProfitThreshold 5 スケールアウトする前に必要な最小未実現損益 (価格 * ボリューム単位)。

実践メモ

  • ブローカー アダプターが部分的なポジション削減をサポートしていることを確認してください。元の EA は 1/3 と 1/2 の部分を閉じました。このポートは成行注文を使用して同じ部分を複製します。
  • 保護命令は自動的に付加されないため、ハードストップが必要な場合は、StartProtection() を有効にするか、カスタム リスク ルールを追加することを検討してください。
  • 利益のしきい値は、生の価格 * 数量単位で表されます。元の MQ4 コードの「5 通貨単位」の仮定と一致するように、商品のピップ サイズまたはティック値に従って調整します。
  • この戦略では、スムーズな MACD ダイナミクスが期待されます。過度のノイズや非流動性の機器により、3 ピーク ロジックのトリガーが妨げられる場合があります。

MQ4版との違い

  • 繰り返しの iMACD 呼び出しの代わりに、StockSharp インジケーター バインディングを使用します。
  • 未実現利益の計算は PositionPositionAvgPrice に依存しているため、ブローカーの丸めルールが MetaTrader の OrderProfit() と異なる可能性があります。
  • ストップロス注文と利食い注文は自動生成されません。必要に応じて、手動のリスク ツールを追加する必要があります。
  • MQ4 パラメータ sum_bars_bup は、元のソースでは使用されていなかったため、存在しません。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// MACD pattern strategy inspired by the MetaTrader advisor "MacdPatternTraderv03".
/// </summary>
public class MacdPatternTraderV03Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<decimal> _upperThreshold;
	private readonly StrategyParam<decimal> _upperActivation;
	private readonly StrategyParam<decimal> _lowerThreshold;
	private readonly StrategyParam<decimal> _lowerActivation;
	private readonly StrategyParam<int> _emaOneLength;
	private readonly StrategyParam<int> _emaTwoLength;
	private readonly StrategyParam<int> _smaLength;
	private readonly StrategyParam<int> _emaFourLength;
	private readonly StrategyParam<decimal> _profitThreshold;

	private decimal? _previousMacd;
	private decimal? _olderMacd;
	private decimal _entryPrice;

	private bool _isAboveUpperActivation;
	private bool _firstUpperDropConfirmed;
	private bool _secondUpperDropConfirmed;
	private bool _sellReady;
	private decimal _firstUpperPeak;
	private decimal _secondUpperPeak;

	private bool _isBelowLowerActivation;
	private bool _firstLowerRiseConfirmed;
	private bool _secondLowerRiseConfirmed;
	private bool _buyReady;
	private decimal _firstLowerTrough;
	private decimal _secondLowerTrough;

	private decimal? _emaTwoValue;
	private decimal? _smaValue;
	private decimal? _emaFourValue;

	private ICandleMessage _previousCandle;

	private int _longScaleStage;
	private int _shortScaleStage;
	private decimal _initialLongPosition;
	private decimal _initialShortPosition;

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public MacdPatternTraderV03Strategy()
	{

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for calculations", "General");

		_fastEmaLength = Param(nameof(FastEmaLength), 5)
		.SetDisplay("Fast EMA", "Fast period used inside MACD", "MACD");

		_slowEmaLength = Param(nameof(SlowEmaLength), 13)
		.SetDisplay("Slow EMA", "Slow period used inside MACD", "MACD");

		_upperThreshold = Param(nameof(UpperThreshold), 50m)
		.SetDisplay("Upper Threshold", "Level that confirms bearish exhaustion", "MACD");

		_upperActivation = Param(nameof(UpperActivation), 30m)
		.SetDisplay("Upper Activation", "Level that arms the bearish pattern", "MACD");

		_lowerThreshold = Param(nameof(LowerThreshold), -50m)
		.SetDisplay("Lower Threshold", "Level that confirms bullish exhaustion", "MACD");

		_lowerActivation = Param(nameof(LowerActivation), -30m)
		.SetDisplay("Lower Activation", "Level that arms the bullish pattern", "MACD");

		_emaOneLength = Param(nameof(EmaOneLength), 7)
		.SetDisplay("EMA #1", "Short EMA used for scaling out", "Management");

		_emaTwoLength = Param(nameof(EmaTwoLength), 21)
		.SetDisplay("EMA #2", "Second EMA used for scaling out", "Management");

		_smaLength = Param(nameof(SmaLength), 98)
		.SetDisplay("SMA", "Simple moving average used for scaling out", "Management");

		_emaFourLength = Param(nameof(EmaFourLength), 365)
		.SetDisplay("EMA #4", "Slow EMA used for scaling out", "Management");

		_profitThreshold = Param(nameof(ProfitThreshold), 5m)
		.SetDisplay("Profit Threshold", "Unrealized PnL required before scaling out", "Management");
	}


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

	/// <summary>
	/// Fast EMA length inside MACD.
	/// </summary>
	public int FastEmaLength
	{
		get => _fastEmaLength.Value;
		set => _fastEmaLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length inside MACD.
	/// </summary>
	public int SlowEmaLength
	{
		get => _slowEmaLength.Value;
		set => _slowEmaLength.Value = value;
	}

	/// <summary>
	/// Upper threshold that marks MACD exhaustion for shorts.
	/// </summary>
	public decimal UpperThreshold
	{
		get => _upperThreshold.Value;
		set => _upperThreshold.Value = value;
	}

	/// <summary>
	/// Upper activation level that arms the short pattern.
	/// </summary>
	public decimal UpperActivation
	{
		get => _upperActivation.Value;
		set => _upperActivation.Value = value;
	}

	/// <summary>
	/// Lower threshold that marks MACD exhaustion for longs.
	/// </summary>
	public decimal LowerThreshold
	{
		get => _lowerThreshold.Value;
		set => _lowerThreshold.Value = value;
	}

	/// <summary>
	/// Lower activation level that arms the long pattern.
	/// </summary>
	public decimal LowerActivation
	{
		get => _lowerActivation.Value;
		set => _lowerActivation.Value = value;
	}

	/// <summary>
	/// Short EMA used for position management.
	/// </summary>
	public int EmaOneLength
	{
		get => _emaOneLength.Value;
		set => _emaOneLength.Value = value;
	}

	/// <summary>
	/// Second EMA used for position management.
	/// </summary>
	public int EmaTwoLength
	{
		get => _emaTwoLength.Value;
		set => _emaTwoLength.Value = value;
	}

	/// <summary>
	/// SMA length used for position management.
	/// </summary>
	public int SmaLength
	{
		get => _smaLength.Value;
		set => _smaLength.Value = value;
	}

	/// <summary>
	/// Slow EMA used for position management.
	/// </summary>
	public int EmaFourLength
	{
		get => _emaFourLength.Value;
		set => _emaFourLength.Value = value;
	}

	/// <summary>
	/// Minimum unrealized PnL before scaling out (in price units * volume).
	/// </summary>
	public decimal ProfitThreshold
	{
		get => _profitThreshold.Value;
		set => _profitThreshold.Value = value;
	}

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

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

		var macd = new MovingAverageConvergenceDivergence();
		macd.ShortMa.Length = FastEmaLength;
		macd.LongMa.Length = SlowEmaLength;

		var emaOne = new ExponentialMovingAverage { Length = EmaOneLength };
		var emaTwo = new ExponentialMovingAverage { Length = EmaTwoLength };
		var sma = new SimpleMovingAverage { Length = SmaLength };
		var emaFour = new ExponentialMovingAverage { Length = EmaFourLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(macd, emaOne, emaTwo, sma, emaFour, ProcessCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, macd);
			DrawIndicator(area, emaOne);
			DrawIndicator(area, emaTwo);
			DrawIndicator(area, sma);
			DrawIndicator(area, emaFour);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal macdMain, decimal emaOne, decimal emaTwo, decimal sma, decimal emaFour)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_emaTwoValue = emaTwo;
		_smaValue = sma;
		_emaFourValue = emaFour;

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			CacheMacd(macdMain);
			_previousCandle = candle;
			return;
		}

		if (_previousMacd is null || _olderMacd is null)
		{
			CacheMacd(macdMain);
			_previousCandle = candle;
			return;
		}

		var macdPrev = _previousMacd.Value;
		var macdPrev2 = _olderMacd.Value;

		EvaluateSellPattern(macdMain, macdPrev, macdPrev2);
		EvaluateBuyPattern(macdMain, macdPrev, macdPrev2);
		ManageOpenPosition(candle);

		CacheMacd(macdMain);
		_previousCandle = candle;
	}

	private void EvaluateSellPattern(decimal macdCurrent, decimal macdPrevious, decimal macdPrevious2)
	{
		if (macdCurrent > UpperActivation)
		_isAboveUpperActivation = true;

		if (_isAboveUpperActivation && macdCurrent < macdPrevious && macdPrevious > macdPrevious2 && macdPrevious > _firstUpperPeak && !_firstUpperDropConfirmed)
		_firstUpperPeak = macdPrevious;

		if (_firstUpperPeak > 0m && macdCurrent < UpperThreshold)
		_firstUpperDropConfirmed = true;

		if (macdCurrent < UpperActivation)
		{
			ResetSellPattern();
			return;
		}

		if (_firstUpperDropConfirmed && macdCurrent > UpperThreshold && macdCurrent < macdPrevious && macdPrevious > macdPrevious2 && macdPrevious > _firstUpperPeak && macdPrevious > _secondUpperPeak && !_secondUpperDropConfirmed)
		_secondUpperPeak = macdPrevious;

		if (_secondUpperPeak > 0m && macdCurrent < UpperThreshold)
		_secondUpperDropConfirmed = true;

		if (_secondUpperDropConfirmed && macdCurrent < UpperThreshold && macdPrevious < UpperThreshold && macdPrevious2 < UpperThreshold && macdCurrent < macdPrevious && macdPrevious > macdPrevious2 && macdPrevious < _secondUpperPeak)
		_sellReady = true;

		if (!_sellReady)
		return;

		EnterShort();
	}

	private void EvaluateBuyPattern(decimal macdCurrent, decimal macdPrevious, decimal macdPrevious2)
	{
		if (macdCurrent < LowerActivation)
		_isBelowLowerActivation = true;

		if (_isBelowLowerActivation && macdCurrent > macdPrevious && macdPrevious < macdPrevious2 && macdPrevious < _firstLowerTrough && !_firstLowerRiseConfirmed)
		_firstLowerTrough = macdPrevious;

		if (_firstLowerTrough < 0m && macdCurrent > LowerThreshold)
		_firstLowerRiseConfirmed = true;

		if (macdCurrent > LowerActivation)
		{
			ResetBuyPattern();
			return;
		}

		if (_firstLowerRiseConfirmed && macdCurrent < LowerThreshold && macdCurrent > macdPrevious && macdPrevious < macdPrevious2 && macdPrevious < _firstLowerTrough && macdPrevious < _secondLowerTrough && !_secondLowerRiseConfirmed)
		_secondLowerTrough = macdPrevious;

		if (_secondLowerTrough < 0m && macdCurrent > LowerThreshold)
		_secondLowerRiseConfirmed = true;

		if (_secondLowerRiseConfirmed && macdCurrent > LowerThreshold && macdPrevious > LowerThreshold && macdPrevious2 > LowerThreshold && macdCurrent > macdPrevious && macdPrevious < macdPrevious2 && macdPrevious > _secondLowerTrough)
		_buyReady = true;

		if (!_buyReady)
		return;

		EnterLong();
	}

	private void EnterShort()
	{
		var currentPosition = Position;
		var flattenVolume = currentPosition > 0m ? currentPosition : 0m;
		if (flattenVolume > 0m)
			SellMarket(flattenVolume);

		var entryVolume = Volume + Math.Max(0m, Position);
		if (entryVolume <= 0m)
		{
			ResetSellPattern();
			_sellReady = false;
			return;
		}

		SellMarket(entryVolume);
		_entryPrice = _previousCandle?.ClosePrice ?? 0m;
		_initialShortPosition = Math.Abs(Position);
		_shortScaleStage = 0;
		_longScaleStage = 0;
		_sellReady = false;
		ResetSellPattern();
		ResetBuyPattern();
	}

	private void EnterLong()
	{
		var currentPosition = Position;
		var flattenVolume = currentPosition < 0m ? -currentPosition : 0m;
		if (flattenVolume > 0m)
			BuyMarket(flattenVolume);

		var entryVolume = Volume + Math.Max(0m, -Position);
		if (entryVolume <= 0m)
		{
			ResetBuyPattern();
			_buyReady = false;
			return;
		}

		BuyMarket(entryVolume);
		_entryPrice = _previousCandle?.ClosePrice ?? 0m;
		_initialLongPosition = Math.Max(0m, Position);
		_longScaleStage = 0;
		_shortScaleStage = 0;
		_buyReady = false;
		ResetBuyPattern();
		ResetSellPattern();
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position == 0m)
		{
			_longScaleStage = 0;
			_shortScaleStage = 0;
			_initialLongPosition = 0m;
			_initialShortPosition = 0m;
			return;
		}

		var previousCandle = _previousCandle;
		if (previousCandle is null)
		return;

		var profitThreshold = ProfitThreshold;
		if (profitThreshold <= 0m)
		return;

		var unrealized = GetUnrealizedPnL(candle);
		if (unrealized < profitThreshold)
		return;

		if (Position > 0m)
		{
			if (_emaTwoValue is decimal emaTwo && previousCandle.ClosePrice > emaTwo && _longScaleStage == 0)
			{
				var volume = Math.Min(Position, _initialLongPosition / 3m);
				if (volume > 0m)
				{
					SellMarket(volume);
					_longScaleStage = 1;
				}
			}

			if (_smaValue is decimal sma && _emaFourValue is decimal emaFour && previousCandle.HighPrice > (sma + emaFour) / 2m && _longScaleStage == 1)
			{
				var volume = Math.Min(Position, _initialLongPosition / 2m);
				if (volume > 0m)
				{
					SellMarket(volume);
					_longScaleStage = 2;
				}
			}
		}
		else if (Position < 0m)
		{
			var shortPosition = -Position;
			if (_emaTwoValue is decimal emaTwo && previousCandle.ClosePrice < emaTwo && _shortScaleStage == 0)
			{
				var volume = Math.Min(shortPosition, _initialShortPosition / 3m);
				if (volume > 0m)
				{
					BuyMarket(volume);
					_shortScaleStage = 1;
				}
			}

			if (_smaValue is decimal sma && _emaFourValue is decimal emaFour && previousCandle.LowPrice < (sma + emaFour) / 2m && _shortScaleStage == 1)
			{
				var volume = Math.Min(shortPosition, _initialShortPosition / 2m);
				if (volume > 0m)
				{
					BuyMarket(volume);
					_shortScaleStage = 2;
				}
			}
		}
	}

	private void CacheMacd(decimal macdValue)
	{
		_olderMacd = _previousMacd;
		_previousMacd = macdValue;
	}

	private decimal GetUnrealizedPnL(ICandleMessage candle)
	{
		if (Position == 0m)
			return 0m;

		if (_entryPrice == 0m)
			return 0m;

		var diff = candle.ClosePrice - _entryPrice;
		return diff * Position;
	}

	private void ResetSellPattern()
	{
		_isAboveUpperActivation = false;
		_firstUpperDropConfirmed = false;
		_secondUpperDropConfirmed = false;
		_sellReady = false;
		_firstUpperPeak = 0m;
		_secondUpperPeak = 0m;
	}

	private void ResetBuyPattern()
	{
		_isBelowLowerActivation = false;
		_firstLowerRiseConfirmed = false;
		_secondLowerRiseConfirmed = false;
		_buyReady = false;
		_firstLowerTrough = 0m;
		_secondLowerTrough = 0m;
	}

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

		_previousMacd = null;
		_olderMacd = null;
		_entryPrice = 0m;

		_isAboveUpperActivation = false;
		_firstUpperDropConfirmed = false;
		_secondUpperDropConfirmed = false;
		_sellReady = false;
		_firstUpperPeak = 0m;
		_secondUpperPeak = 0m;

		_isBelowLowerActivation = false;
		_firstLowerRiseConfirmed = false;
		_secondLowerRiseConfirmed = false;
		_buyReady = false;
		_firstLowerTrough = 0m;
		_secondLowerTrough = 0m;

		_emaTwoValue = null;
		_smaValue = null;
		_emaFourValue = null;

		_previousCandle = null;

		_longScaleStage = 0;
		_shortScaleStage = 0;
		_initialLongPosition = 0m;
		_initialShortPosition = 0m;
	}
}