GitHub で見る

MACDゼロラインフィルター付き利益確定戦略

概要

この戦略は、MACDシグナルラインのクロスオーバーにゼロラインフィルターを加えたMetaTrader 5エキスパート「Robot_MACD」を再現します。単一の銘柄で動作し、MACDラインのゼロに対する位置によって確認されるモメンタムの反転を探します。元の実装のポイントベースの利益目標を再現するため、すべての注文に固定距離の利益確定が付加されます。

データとインジケーター

  • 主要データ: 単一ローソク足サブスクリプション(デフォルト5分足)。CandleTypeパラメーターで取引市場に合わせた時間軸に変更可能です。
  • インジケーター:
    • MovingAverageConvergenceDivergenceSignal(MACD + シグナル + ヒストグラム)。デフォルトは12/26 EMAと9期間シグナルラインで、MQLパラメーターと一致しています。

トレードロジック

  1. MACDの計算がMACDラインとシグナルラインの現在値と前回値を提供するまで待機します。
  2. 強気および弱気のクロスオーバーを識別します:
    • 強気クロスオーバー: 前回MACD ≤ 前回シグナル かつ 現在MACD > 現在シグナル。
    • 弱気クロスオーバー: 前回MACD ≥ 前回シグナル かつ 現在MACD < 現在シグナル。
  3. ポジション管理:
    • 弱気クロスオーバーが現れたときにロングポジションをクローズ。
    • 強気クロスオーバーが現れたときにショートポジションをクローズ。
  4. エントリー条件(ポジションがなく十分な資金がある場合のみ):
    • MACDとシグナルの両方がゼロ以下に留まっている強気クロスオーバーでロングエントリー。
    • MACDとシグナルの両方がゼロ以上に留まっている弱気クロスオーバーでショートエントリー。
  5. 注文登録時にStartProtectionを呼び出し、価格ポイントで測定した絶対距離で固定の利益確定を設定します。距離は設定されたポイント値に銘柄の価格ステップを掛けたものです。

リスク管理

  • すべての注文にはTakeProfitPointsで定義された利益確定が付加されます。ベースロジックにはストップロスがなく、ソースEAとの同等性を保っています。
  • 新しい注文を出す前に、ポートフォリオの価値がMinimumCapitalPerVolume * VolumePerTrade以上であることを確認します。これはMQLバージョンの空きマージンガード(FreeMargin() < 1000 * Lots)をエミュレートしています。

パラメーター

パラメーター 説明 デフォルト値
MacdFast MACDの高速EMA期間。 12
MacdSlow MACDの低速EMA期間。 26
MacdSignal シグナルラインのスムージング期間。 9
TakeProfitPoints 価格ポイントで表した利益確定距離。 300
VolumePerTrade 各エントリーに使用する取引量(ロット)。 1
MinimumCapitalPerVolume 取引ロットごとに必要な最小ポートフォリオ価値。 1000
CandleType MACDインジケーターに使用するローソク足タイプ(時間軸)。 5分足

実装上の注意

  • 注文はBuyMarket/SellMarketで実行され、CTrade経由のマーケット注文を使用したEAと同一です。
  • ゼロラインフィルターにより、MQLスクリプトと同様に、MACDヒストグラムの反対側での取引エントリーを防ぎます。
  • ポートフォリオ価値チェックはPortfolio.CurrentValueに依存します。取引環境がこの値を提供しない場合、チェックは自動的に通過し、シミュレーションアカウントでも使用可能です。
  • ホストアプリケーションにチャートエリアが利用可能な場合、チャート描画セクションはローソク足、MACDインジケーター、および取引マーカーをプロットします。
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 cross strategy with zero line filter and fixed take profit.
/// </summary>
public class MacdZeroFilterTakeProfitStrategy : Strategy
{
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<decimal> _volumePerTrade;
	private readonly StrategyParam<decimal> _minimumCapitalPerVolume;
	private readonly StrategyParam<DataType> _candleType;

	private MovingAverageConvergenceDivergenceSignal _macd = null!;
	private decimal? _previousMacd;
	private decimal? _previousSignal;

	/// <summary>
	/// Initializes a new instance of the <see cref="MacdZeroFilterTakeProfitStrategy"/> class.
	/// </summary>
	public MacdZeroFilterTakeProfitStrategy()
	{
		_macdFast = Param(nameof(MacdFast), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast Period", "Fast EMA period used by MACD", "Indicators")
			
			.SetOptimize(8, 20, 2);

		_macdSlow = Param(nameof(MacdSlow), 26)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow Period", "Slow EMA period used by MACD", "Indicators")
			
			.SetOptimize(20, 40, 2);

		_macdSignal = Param(nameof(MacdSignal), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal Period", "Signal smoothing period for MACD", "Indicators")
			
			.SetOptimize(6, 15, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
			
			.SetOptimize(100, 600, 50);

		_volumePerTrade = Param(nameof(VolumePerTrade), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Number of lots to trade on each entry", "General")
			
			.SetOptimize(0.5m, 5m, 0.5m);

		_minimumCapitalPerVolume = Param(nameof(MinimumCapitalPerVolume), 1000m)
			.SetGreaterThanZero()
			.SetDisplay("Capital per Volume", "Minimum portfolio value required per traded lot", "Risk Management")
			
			.SetOptimize(500m, 5000m, 500m);

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

	/// <summary>
	/// Fast EMA period used by MACD.
	/// </summary>
	public int MacdFast
	{
		get => _macdFast.Value;
		set => _macdFast.Value = value;
	}

	/// <summary>
	/// Slow EMA period used by MACD.
	/// </summary>
	public int MacdSlow
	{
		get => _macdSlow.Value;
		set => _macdSlow.Value = value;
	}

	/// <summary>
	/// Signal smoothing period for MACD.
	/// </summary>
	public int MacdSignal
	{
		get => _macdSignal.Value;
		set => _macdSignal.Value = value;
	}

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

	/// <summary>
	/// Number of lots to trade on each entry.
	/// </summary>
	public decimal VolumePerTrade
	{
		get => _volumePerTrade.Value;
		set => _volumePerTrade.Value = value;
	}

	/// <summary>
	/// Minimum portfolio value required per traded lot.
	/// </summary>
	public decimal MinimumCapitalPerVolume
	{
		get => _minimumCapitalPerVolume.Value;
		set => _minimumCapitalPerVolume.Value = value;
	}

	/// <summary>
	/// Timeframe for MACD calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousMacd = null;
		_previousSignal = null;
	}

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

		Volume = VolumePerTrade;

		_macd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFast },
				LongMa = { Length = MacdSlow },
			},
			SignalMa = { Length = MacdSignal }
		};

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

		var step = Security?.PriceStep ?? 1m;
		StartProtection(
			takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
			stopLoss: new Unit(0m),
			useMarketOrders: true);

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

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Use only completed candles to avoid double counting signals.
		if (candle.State != CandleStates.Finished)
			return;

		// Ensure the strategy is ready to trade and has a working connector.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (macdValue is not MovingAverageConvergenceDivergenceSignalValue macdTyped)
			return;

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

		if (_previousMacd is null || _previousSignal is null)
		{
			_previousMacd = macd;
			_previousSignal = signal;
			return;
		}

		var previousMacd = _previousMacd.Value;
		var previousSignal = _previousSignal.Value;

		var crossedUp = previousMacd <= previousSignal && macd > signal;
		var crossedDown = previousMacd >= previousSignal && macd < signal;

		if (Position > 0 && crossedDown)
		{
			// Close long position on bearish crossover.
			SellMarket();
		}
		else if (Position < 0 && crossedUp)
		{
			// Close short position on bullish crossover.
			BuyMarket();
		}

		if (Position == 0)
		{
			var requiredCapital = MinimumCapitalPerVolume * VolumePerTrade;
			if (HasEnoughCapital(requiredCapital))
			{
				if (crossedUp && macd < 0m && signal < 0m)
				{
					// Enter long when MACD crosses above signal under the zero line.
					BuyMarket();
				}
				else if (crossedDown && macd > 0m && signal > 0m)
				{
					// Enter short when MACD crosses below signal above the zero line.
					SellMarket();
				}
			}
		}

		_previousMacd = macd;
		_previousSignal = signal;
	}

	private bool HasEnoughCapital(decimal requiredCapital)
	{
		var currentValue = Portfolio?.CurrentValue;

		if (currentValue is null)
			return true;

		if (currentValue.Value >= requiredCapital)
			return true;

		this.LogInfo($"Insufficient capital: available {currentValue.Value:F2}, required {requiredCapital:F2}.");
		return false;
	}
}