在 GitHub 上查看

MACD零轴过滤止盈策略

概述

本策略复刻 MetaTrader 5 专家顾问“Robot_MACD”的核心思想:利用 MACD 与信号线的交叉,并结合零轴过滤条件进行交易。策略仅针对单一标的,在确认 MACD 位置位于零轴的特定一侧后入场,并为每笔交易附加固定点数的止盈目标,与原始 EA 的点差止盈设定保持一致。

数据与指标

  • 核心数据:单一 K 线订阅(默认 5 分钟周期),可通过参数 CandleType 自由调整以适应不同市场。
  • 指标设置
    • MovingAverageConvergenceDivergenceSignal(MACD + 信号线 + 柱状图)。默认参数为 12/26 EMA 与 9 周期信号线,与 MQL 输入保持一致。

交易逻辑

  1. 等待 MACD 指标给出当前值与上一周期数值。
  2. 根据 MACD 与信号线的相对位置识别交叉:
    • 看多交叉:上一周期 MACD ≤ 上一周期信号线,且当前周期 MACD > 当前周期信号线。
    • 看空交叉:上一周期 MACD ≥ 上一周期信号线,且当前周期 MACD < 当前周期信号线。
  3. 持仓管理
    • 持有多单时出现看空交叉立即平仓。
    • 持有空单时出现看多交叉立即平仓。
  4. 入场条件(仅在无持仓且资金充足时):
    • 看多交叉且当前 MACD、信号线均位于零轴下方时买入做多。
    • 看空交叉且当前 MACD、信号线均位于零轴上方时卖出做空。
  5. 调用 StartProtection 以绝对价格单位设置止盈距离,距离 = TakeProfitPoints × 品种最小价格步长,完全对应 EA 中的点值止盈。

风险控制

  • 每笔订单都会附带固定止盈(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 的 K 线类型/周期 5 分钟 K 线

实施细节

  • 使用 BuyMarket / SellMarket 下达市价单,与 MQL 代码中的 CTrade 行为一致。
  • 零轴过滤保证只有在 MACD 柱状图位于同一侧时才会开仓,避免逆势信号。
  • 资金校验依赖 Portfolio.CurrentValue;若运行环境未提供该值,校验会默认通过,从而保持策略在模拟或历史回测中的可用性。
  • 若宿主平台支持图表,策略会绘制 K 线、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;
	}
}