在 GitHub 上查看

MACD 零轴过滤交叉策略

概览

MACD 零轴过滤交叉策略是 MetaTrader4 智能交易系统 Robot_MACD_12.26.9 的 C# 版本移植。原始 EA 监控 MACD 主线与信号线的交叉,并要求做多信号只在两条线都位于零轴下方时触发、做空信号只在两条线都位于零轴上方时触发。StockSharp 版本完整保留该核心思想,并结合框架提供的统一风控功能:可配置的账户余额过滤、点差单位的止盈管理以及支持优化的参数系统。

策略基于可配置周期的收盘 K 线工作。通过高层 API 的 BindEx 机制直接接收 MovingAverageConvergenceDivergenceSignal 指标输出,避免手动调用 GetValue,完全符合仓库的代码规范。

策略逻辑

指标计算

  • MACD 主线 – 默认使用 12 与 26 周期指数均线之差。
  • 信号线 – 对 MACD 主线再做一次 9 周期指数平滑。
  • 零轴过滤 – 判断两条线相对零轴的位置,用于筛选哪些交叉可以产生交易。

入场规则

  • 做多
    • MACD[t-1] < Signal[t-1]MACD[t] > Signal[t],表示 MACD 向上穿越信号线。
    • 穿越后 MACD 与信号线都在零轴下方。
    • 当前净仓位必须为零或为空头;若存在空头,会先平仓再等待下一次机会。
    • 可选的余额过滤器要求组合价值高于 MinimumBalancePerVolume * LotVolume
  • 做空
    • MACD[t-1] > Signal[t-1]MACD[t] < Signal[t],表示 MACD 向下穿越信号线。
    • 穿越后两条线都在零轴上方。
    • 当前净仓位必须为零或多头;若持有多头会先平仓。
    • 余额过滤器同样适用于空头信号。

出场规则

  • 反向交叉离场 – 一旦 MACD 线向持仓反方向穿越信号线,立即以市价平仓,完全对应原 EA 的离场逻辑。
  • 固定止盈 – 通过 StartProtection 设置以“点”为单位的止盈值,对应 MT4 参数 TakeProfit

风险管理

  • 交易量LotVolume 参数等价于 MT4 的手数设置,每次下单使用相同的数量。
  • 余额过滤MinimumBalancePerVolume 与交易量相乘得到所需的最低账户价值。如果组合价值不足,策略会写日志并跳过该次交易。
  • 数据完整性 – 仅处理已完成的 K 线,并确保 IsFormedAndOnlineAndAllowTrading() 返回真值后才执行逻辑。

参数说明

参数 描述
FastPeriod MACD 快速 EMA 的周期。
SlowPeriod MACD 慢速 EMA 的周期。
SignalPeriod MACD 信号线的平滑周期。
TakeProfitPoints 以点为单位的止盈距离,设置为 0 表示关闭止盈。
LotVolume 下单手数,与 MT4 版本保持一致。
MinimumBalancePerVolume 每单位交易量所需的最低账户价值,设置为 0 可关闭过滤器。
CandleType 用于计算指标的 K 线周期。

其他说明

  • 策略全部注释均为英文,符合仓库要求。
  • 仅提供 C# 实现,本目录没有 Python 版本。
  • 若要复现 MT4 上的表现,请选择与原图表一致的时间周期,并保持相同的手数设置。
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>
/// Port of the MetaTrader 4 expert advisor Robot_MACD_12.26.9.
/// Trades MACD signal-line crossovers, but only enters longs while both lines stay below zero and shorts while they stay above zero.
/// Includes an optional balance filter and a fixed take-profit expressed in instrument points.
/// </summary>
public class MacdZeroFilteredCrossStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _signalPeriod;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _lotVolume;
	private readonly StrategyParam<decimal> _minimumBalancePerVolume;
	private readonly StrategyParam<DataType> _candleType;

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

	/// <summary>
	/// Fast EMA length used by MACD.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA length used by MACD.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Signal line smoothing length for MACD.
	/// </summary>
	public int SignalPeriod
	{
		get => _signalPeriod.Value;
		set => _signalPeriod.Value = value;
	}

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

	/// <summary>
	/// Base trading volume that mirrors the "Lots" setting in the original robot.
	/// </summary>
	public decimal LotVolume
	{
		get => _lotVolume.Value;
		set => _lotVolume.Value = value;
	}

	/// <summary>
	/// Minimum account value required per traded volume unit before opening new positions.
	/// </summary>
	public decimal MinimumBalancePerVolume
	{
		get => _minimumBalancePerVolume.Value;
		set => _minimumBalancePerVolume.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public MacdZeroFilteredCrossStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Short EMA period for MACD", "MACD")
			
			.SetOptimize(6, 18, 1);

		_slowPeriod = Param(nameof(SlowPeriod), 26)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Long EMA period for MACD", "MACD")
			
			.SetOptimize(20, 40, 2);

		_signalPeriod = Param(nameof(SignalPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("Signal Period", "Signal line length for MACD", "MACD")
			
			.SetOptimize(6, 12, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 300m)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Fixed take-profit distance in price points", "Risk Management");

		_lotVolume = Param(nameof(LotVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Volume", "Trading volume per order", "Trading")
			.SetOptimize(1m, 5m, 1m);

		_minimumBalancePerVolume = Param(nameof(MinimumBalancePerVolume), 1000m)
			.SetNotNegative()
			.SetDisplay("Balance per Volume", "Required balance per volume unit before opening trades", "Risk Management");

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

	/// <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)
	{

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

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

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

		if (TakeProfitPoints > 0m)
		{
			StartProtection(new Unit(TakeProfitPoints, UnitTypes.Absolute), null);
		}

		base.OnStarted2(time);
	}

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

		// Skip processing when the strategy is not ready or trading is disabled.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var typed = (MovingAverageConvergenceDivergenceSignalValue)macdValue;

		// Ensure both MACD and signal components are available before calculating.
		if (typed.Macd is not decimal macdLine || typed.Signal is not decimal signalLine)
			return;

		if (_previousMacd is decimal prevMacd && _previousSignal is decimal prevSignal)
		{
			var crossUp = prevMacd < prevSignal && macdLine > signalLine;
			var crossDown = prevMacd > prevSignal && macdLine < signalLine;

			// Close existing long position when MACD crosses below the signal line.
			if (crossDown && Position > 0m)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_previousMacd = macdLine;
				_previousSignal = signalLine;
				return;
			}

			// Close existing short position when MACD crosses above the signal line.
			if (crossUp && Position < 0m)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_previousMacd = macdLine;
				_previousSignal = signalLine;
				return;
			}

			// Enter long only when the crossover happens below zero (momentum still negative).
			if (crossUp && macdLine < 0m && signalLine < 0m && Position <= 0m && HasRequiredBalance())
			{
				var volume = LotVolume;
				BuyMarket(volume);
			}

			// Enter short only when the crossover happens above zero (momentum still positive).
			else if (crossDown && macdLine > 0m && signalLine > 0m && Position >= 0m && HasRequiredBalance())
			{
				var volume = LotVolume;
				SellMarket(volume);
			}
		}

		_previousMacd = macdLine;
		_previousSignal = signalLine;
	}

	private bool HasRequiredBalance()
	{
		// If portfolio information is not available, assume requirements are met.
		var balance = Portfolio?.CurrentValue;
		if (balance is null)
			return true;

		var required = MinimumBalancePerVolume * LotVolume;
		if (required <= 0m)
			return true;

		if (balance.Value >= required)
			return true;

		return false;
	}
}