在 GitHub 上查看

MACD 自动化策略示例

概述

该策略使用 StockSharp 高级 API 复刻 MetaTrader 4 的“Example of MACD Automated”专家顾问。系统同时监控两个周期上的 MACD 主线,当趋势过滤器一致时才开仓。止损和止盈以价格最小变动单位表示,仓位大小遵循原始 EA 的 AdvancedMM 资金管理,会累加最近亏损交易的手数。

交易逻辑

  1. 高周期过滤 – 在高周期(默认:日线)计算的 MACD(12, 26, 9) 主线为正值时允许做多,为负值时允许做空。
  2. 入场周期确认 – 入场周期(默认:15 分钟)上使用相同参数的 MACD 需要与高周期方向一致。
  3. 单笔持仓 – 任意时刻只有一笔仓位,只有在止损或止盈平仓之后才会寻找新的入场机会。
  4. 保护性订单 – 止损与止盈以价格步长的倍数表示,对应 MT4 中的 StopLossTakeProfit 输入,填 0 表示关闭。
  5. 高级资金管理 – 资金管理模块在连续亏损时将亏损单的手数累加用于下一笔交易,在盈利之后恢复到基础手数,完全模拟 AdvancedMM() 函数。

参数

名称 说明 默认值
BaseVolume AdvancedMM 逻辑使用的基础下单手数。 0.01
StopLossPoints 以价格步长表示的止损距离,0 表示不设置。 50
TakeProfitPoints 以价格步长表示的止盈距离,0 表示不设置。 30
MacdFastLength 两个周期 MACD 的快线 EMA 周期。 12
MacdSlowLength MACD 的慢线 EMA 周期。 26
MacdSignalLength MACD 信号线周期。 9
EntryCandleType 入场所用的 K 线周期。 15 分钟
FilterCandleType 趋势过滤所用的高周期。 1 天

仓位管理

  • 每次建仓都会根据标的的 PriceStep 重新计算止损和止盈价位。
  • 当单根 K 线触及任一保护价位时,策略假设订单在该价格成交,并记录实际盈亏。
  • 每次平仓后 AdvancedMM 会调整下一笔交易的手数:
    • 历史交易少于两笔 → 使用基础手数;
    • 最近一笔为亏损 → 复用该笔交易的手数;
    • 在最近一次盈利之前存在连续亏损 → 将这些亏损单的手数累加;
    • 其他情况 → 回到基础手数。

说明

  • 转换版本保持了原策略的风格,仅依靠止损和止盈退出,不会因为 MACD 反向而强制平仓。
  • 请确保标的证券提供有效的 PriceStep 信息,以便正确换算点差距离。
  • 策略只处理已完成的 K 线,需在提供完整 K 线数据的环境中使用。
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>
/// Conversion of the "Example of MACD Automated" MQL4 expert advisor.
/// The strategy waits for MACD agreement on two timeframes and uses AdvancedMM sizing.
/// </summary>
public class ExampleOfMacdAutomatedStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _macdSignalLength;
	private readonly StrategyParam<DataType> _entryCandleType;
	private readonly StrategyParam<DataType> _filterCandleType;

	private MovingAverageConvergenceDivergenceSignal _entryMacd = null!;
	private MovingAverageConvergenceDivergenceSignal _filterMacd = null!;

	private decimal? _lastEntryMacd;
	private decimal? _lastFilterMacd;

	private readonly List<TradeInfo> _tradeHistory = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryVolume;
	private int _entryDirection;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExampleOfMacdAutomatedStrategy"/> class.
	/// </summary>
	public ExampleOfMacdAutomatedStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Starting order volume for AdvancedMM", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (steps)", "Stop-loss distance in price steps", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Take Profit (steps)", "Take-profit distance in price steps", "Risk")
			;

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

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

		_macdSignalLength = Param(nameof(MacdSignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
			;

		_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Entry Timeframe", "Working timeframe for entries", "General");

		_filterCandleType = Param(nameof(FilterCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Filter Timeframe", "Higher timeframe used as trend filter", "General");
	}

	/// <summary>
	/// Base volume parameter.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

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

	/// <summary>
	/// MACD fast EMA length.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// MACD slow EMA length.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// MACD signal EMA length.
	/// </summary>
	public int MacdSignalLength
	{
		get => _macdSignalLength.Value;
		set => _macdSignalLength.Value = value;
	}

	/// <summary>
	/// Timeframe used for entries.
	/// </summary>
	public DataType EntryCandleType
	{
		get => _entryCandleType.Value;
		set => _entryCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe used as a trend filter.
	/// </summary>
	public DataType FilterCandleType
	{
		get => _filterCandleType.Value;
		set => _filterCandleType.Value = value;
	}

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

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

		_lastEntryMacd = null;
		_lastFilterMacd = null;
		_tradeHistory.Clear();
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;

		_entryMacd?.Reset();
		_filterMacd?.Reset();
	}

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

		// Create MACD indicators for entry and filter timeframes.
		_entryMacd = CreateMacd();
		_filterMacd = CreateMacd();

		var entrySubscription = SubscribeCandles(EntryCandleType);
		entrySubscription
			.BindEx(_entryMacd, ProcessEntryCandle)
			.Start();

		var filterSubscription = SubscribeCandles(FilterCandleType);
		filterSubscription
			.BindEx(_filterMacd, ProcessFilterCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, entrySubscription);
			DrawIndicator(area, _entryMacd);
			DrawIndicator(area, _filterMacd);
			DrawOwnTrades(area);
		}
	}

	private MovingAverageConvergenceDivergenceSignal CreateMacd()
	{
		// Instantiate MACD with shared parameters for both timeframes.
		return new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = MacdFastLength },
				LongMa = { Length = MacdSlowLength },
			},
			SignalMa = { Length = MacdSignalLength }
		};
	}

	private void ProcessFilterCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Process only completed candles on the filter timeframe.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		_lastFilterMacd = macd.Macd;
	}

	private void ProcessEntryCandle(ICandleMessage candle, IIndicatorValue macdValue)
	{
		// Ensure that we operate on final candle values only.
		if (candle.State != CandleStates.Finished)
		return;

		var macd = (IMovingAverageConvergenceDivergenceSignalValue)macdValue;
		var currentEntryMacd = macd.Macd;

		// Manage protective exits before searching for new entries.
		if (HandleProtection(candle))
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Skip further processing if there is still an open position.
		if (Position != 0)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (!_entryMacd.IsFormed || !_filterMacd.IsFormed)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		if (_lastEntryMacd is not decimal previousEntryMacd || _lastFilterMacd is not decimal filterMacdValue)
		{
			_lastEntryMacd = currentEntryMacd;
			return;
		}

		// Enter only on a zero-line crossover aligned with the higher timeframe filter.
		if (previousEntryMacd <= 0m && currentEntryMacd > 0m && filterMacdValue > 0m)
		{
			EnterPosition(candle.ClosePrice, true);
		}
		else if (previousEntryMacd >= 0m && currentEntryMacd < 0m && filterMacdValue < 0m)
		{
			EnterPosition(candle.ClosePrice, false);
		}

		_lastEntryMacd = currentEntryMacd;
	}

	private void EnterPosition(decimal price, bool isLong)
	{
		var volume = CalculateTradeVolume();
		if (volume <= 0m)
		return;

		if (isLong)
		{
			BuyMarket(volume);
			RegisterEntry(price, volume, 1);
		}
		else
		{
			SellMarket(volume);
			RegisterEntry(price, volume, -1);
		}
	}

	private void RegisterEntry(decimal price, decimal volume, int direction)
	{
		// Store entry information for later profit calculation.
		_entryPrice = price;
		_entryVolume = volume;
		_entryDirection = direction;

		UpdateProtectionLevels(price, direction > 0);
	}

	private void UpdateProtectionLevels(decimal price, bool isLong)
	{
		var point = GetPointValue();

		if (point <= 0m)
		{
			_stopPrice = null;
			_takeProfitPrice = null;
			return;
		}

		if (isLong)
		{
			_stopPrice = StopLossPoints > 0m ? price - StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price + TakeProfitPoints * point : null;
		}
		else
		{
			_stopPrice = StopLossPoints > 0m ? price + StopLossPoints * point : null;
			_takeProfitPrice = TakeProfitPoints > 0m ? price - TakeProfitPoints * point : null;
		}
	}

	private bool HandleProtection(ICandleMessage candle)
	{
		if (Position == 0 || _entryDirection == 0)
		return false;

		if (_entryDirection > 0)
		{
			if (TryGetLongExitPrice(candle, out var exitPrice))
			{
				SellMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}
		else
		{
			if (TryGetShortExitPrice(candle, out var exitPrice))
			{
				BuyMarket(Math.Abs(Position));
				RegisterClosedTrade(exitPrice);
				return true;
			}
		}

		return false;
	}

	private bool TryGetLongExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private bool TryGetShortExitPrice(ICandleMessage candle, out decimal exitPrice)
	{
		exitPrice = 0m;

		if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
		{
			exitPrice = _stopPrice.Value;
			return true;
		}

		if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
		{
			exitPrice = _takeProfitPrice.Value;
			return true;
		}

		return false;
	}

	private void RegisterClosedTrade(decimal exitPrice)
	{
		if (!_entryPrice.HasValue || _entryVolume <= 0m || _entryDirection == 0)
		return;

		var entryPrice = _entryPrice.Value;
		var volume = _entryVolume;
		var direction = _entryDirection;

		var profit = (exitPrice - entryPrice) * direction * volume;

		_tradeHistory.Add(new TradeInfo(volume, profit));
		if (_tradeHistory.Count > 200)
		_tradeHistory.RemoveAt(0);

		_entryPrice = null;
		_entryVolume = 0m;
		_entryDirection = 0;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	private decimal CalculateTradeVolume()
	{
		var baseVolume = BaseVolume;
		if (baseVolume <= 0m)
		return 0m;

		if (_tradeHistory.Count < 2)
		return baseVolume;

		var advancedLots = 0m;
		var profit1 = false;
		var profit2 = false;
		var firstIteration = true;

		for (var i = _tradeHistory.Count - 1; i >= 0; i--)
		{
			var trade = _tradeHistory[i];
			var isProfit = trade.Profit >= 0m;

			if (isProfit && profit1)
			return baseVolume;

			if (firstIteration)
			{
				if (isProfit)
				{
					profit1 = true;
				}
				else
				{
					return trade.Volume;
				}

				firstIteration = false;
			}

			if (isProfit && profit2)
			return advancedLots > 0m ? advancedLots : baseVolume;

			if (isProfit)
			{
				profit2 = true;
			}
			else
			{
				profit1 = false;
				profit2 = false;
				advancedLots += trade.Volume;
			}
		}

		return advancedLots > 0m ? advancedLots : baseVolume;
	}

	private decimal GetPointValue()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}

	private readonly struct TradeInfo
	{
		public TradeInfo(decimal volume, decimal profit)
		{
			Volume = volume;
			Profit = profit;
		}

		public decimal Volume { get; }
		public decimal Profit { get; }
	}
}