在 GitHub 上查看

Dealers Trade MACD MQL4 策略

Dealers Trade MACD MQL4 策略是 MetaTrader 4 上 "Dealers Trade v7.74" 智能交易系统的完整移植版本。该实现保留了原策略的金字塔加仓、MACD 斜率判别以及账户保护机制,并针对 StockSharp 的净持仓模式重新整理了仓位管理,适合在 H4 和 D1 等中长周期图表上进行波段交易。

策略工作流程

  • 信号判定:订阅指定周期的 K 线并计算经典 MACD(快 EMA、慢 EMA、信号 EMA)。当当前柱的 MACD 主线高于上一柱时视为看多动能;低于上一柱时视为看空动能。ReverseCondition 参数可用于反转信号方向。
  • 加仓间距:任一时刻仅维护一个方向的订单篮子。若 MACD 给出做多信号,会先发送基础 Buy 市价单;之后只有在价格相对上一单下移至少 SpacingPips * PriceStep 时才会继续买入,从而复制原脚本的“摊低成本”行为。做空逻辑对称处理。
  • 手数控制:基础手数取 FixedVolume,或者在启用 UseRiskSizing 时根据账户权益和 RiskPercent 计算。IsStandardAccount 用于区分标准账户与迷你账户(迷你账户手数缩小 10 倍)。每增加一单都会乘以 LotMultiplier,并受 MaxVolume 限制。
  • 风险管理:按照 StopLossPipsTakeProfitPips 为每笔订单设置固定止损止盈。盈利达到 TrailingStopPips + SpacingPips 时,止损会沿趋势方向上移/下移 TrailingStopPips,模拟原策略的移动止损。
  • 账户保护:当打开的订单数达到 MaxTrades - OrdersToProtect,且总体浮动盈利超过 SecureProfit,策略会平掉最新的仓位以锁定利润。这一逻辑与 MQL4 版本中的 AccountProtection 模块一致。

参数

参数 默认值 说明
CandleType H4 用于计算 MACD 的 K 线周期。
FixedVolume 0.1 未启用风险管理时使用的基础手数。
UseRiskSizing true 是否根据账户权益计算手数。
RiskPercent 2 启用风险管理时的风险占比。
IsStandardAccount true 标准账户为 true;迷你账户设为 false(手数 ÷10)。
MaxVolume 5 单笔订单允许的最大手数。
LotMultiplier 1.5 同一篮子中每增加一单的手数乘数。
MaxTrades 5 同时持有的最大订单数量。
SpacingPips 4 相邻订单之间的最小点差。
OrdersToProtect 3 启动账户保护前保留的订单数量。
AccountProtection true 是否启用账户保护逻辑。
SecureProfit 50 触发保护所需的浮动盈利(账户货币)。
TakeProfitPips 30 每笔订单的止盈点数。
StopLossPips 90 每笔订单的止损点数。
TrailingStopPips 15 移动止损的固定距离。
ReverseCondition false 反转 MACD 斜率的方向判定。
MacdFast 14 MACD 快速 EMA 周期。
MacdSlow 26 MACD 慢速 EMA 周期。
MacdSignal 1 MACD 信号 EMA 周期。

注意事项

  • StockSharp 采用净持仓模式,因此无法同时持有同一品种的多头和空头篮子;切换方向时会先关闭原方向仓位。
  • 账户保护计算浮盈时使用 PriceStepStepPrice。若证券未提供这类元数据,将回退到 0.0001 的点值与 1 的价格步长,必要时请调整阈值。
  • 启用风险管理时必须提供正值的 StopLossPips;若止损为 0,将无法计算手数并跳过下单。
  • 策略只在收盘后评估信号。与 MQL4 中的逐笔运算相比,部分信号可能延后一根 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>
/// Dealers Trade MACD strategy converted from the original MQL4 version (Dealers Trade v7.74).
/// </summary>
public class DealersTradeMacdMql4Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<bool> _useRiskSizing;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<bool> _isStandardAccount;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _lotMultiplier;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<int> _spacingPips;
	private readonly StrategyParam<int> _ordersToProtect;
	private readonly StrategyParam<bool> _accountProtection;
	private readonly StrategyParam<decimal> _secureProfit;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<int> _macdFast;
	private readonly StrategyParam<int> _macdSlow;
	private readonly StrategyParam<int> _macdSignal;

	private MovingAverageConvergenceDivergence _macd;
	private List<PositionState> _positions;
	private decimal? _previousMacd;
	private decimal _pipSize;
	private decimal _stepValue;
	private int _cooldown;

	/// <summary>
	/// Initializes a new instance of <see cref="DealersTradeMacdMql4Strategy"/>.
	/// </summary>
	public DealersTradeMacdMql4Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for signals", "General");

		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
			.SetDisplay("Fixed Volume", "Lot size when risk sizing is disabled", "Risk");

		_useRiskSizing = Param(nameof(UseRiskSizing), true)
			.SetDisplay("Use Risk Sizing", "Enable balance based money management", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 2m)
			.SetDisplay("Risk Percent", "Percentage of equity used when sizing dynamically", "Risk");

		_isStandardAccount = Param(nameof(IsStandardAccount), true)
			.SetDisplay("Standard Account", "True for standard (1.0 lot) accounts, false for mini", "Risk");

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetDisplay("Max Volume", "Upper cap for any single order", "Risk")
			.SetGreaterThanZero();

		_lotMultiplier = Param(nameof(LotMultiplier), 1.5m)
			.SetDisplay("Lot Multiplier", "Multiplier applied to subsequent entries", "Money Management")
			.SetGreaterThanZero();

		_maxTrades = Param(nameof(MaxTrades), 1)
			.SetDisplay("Max Trades", "Maximum simultaneous positions", "Money Management")
			.SetGreaterThanZero();

		_spacingPips = Param(nameof(SpacingPips), 200)
			.SetDisplay("Spacing (pips)", "Minimum price movement before adding", "Money Management")
			.SetNotNegative();

		_ordersToProtect = Param(nameof(OrdersToProtect), 3)
			.SetDisplay("Orders To Protect", "Number of trades kept when protection triggers", "Money Management")
			.SetNotNegative();

		_accountProtection = Param(nameof(AccountProtection), true)
			.SetDisplay("Account Protection", "Close last trade once secure profit is reached", "Money Management");

		_secureProfit = Param(nameof(SecureProfit), 50m)
			.SetDisplay("Secure Profit", "Currency profit required to lock gains", "Money Management")
			.SetNotNegative();

		_takeProfitPips = Param(nameof(TakeProfitPips), 200)
			.SetDisplay("Take Profit (pips)", "Take profit distance from entry", "Risk")
			.SetNotNegative();

		_stopLossPips = Param(nameof(StopLossPips), 500)
			.SetDisplay("Stop Loss (pips)", "Initial stop loss distance", "Risk")
			.SetNotNegative();

		_trailingStopPips = Param(nameof(TrailingStopPips), 100)
			.SetDisplay("Trailing Stop (pips)", "Trailing distance applied after activation", "Risk")
			.SetNotNegative();

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Condition", "Invert MACD slope interpretation", "General");

		_macdFast = Param(nameof(MacdFast), 14)
			.SetDisplay("MACD Fast", "Fast EMA length", "Indicators")
			.SetGreaterThanZero();

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

		_macdSignal = Param(nameof(MacdSignal), 1)
			.SetDisplay("MACD Signal", "Signal EMA length", "Indicators")
			.SetGreaterThanZero();
	}

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

	/// <summary>
	/// Fixed order volume in lots.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Enables balance based position sizing.
	/// </summary>
	public bool UseRiskSizing
	{
		get => _useRiskSizing.Value;
		set => _useRiskSizing.Value = value;
	}

	/// <summary>
	/// Risk percentage applied when <see cref="UseRiskSizing"/> is true.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Indicates whether the account uses standard lot sizes.
	/// </summary>
	public bool IsStandardAccount
	{
		get => _isStandardAccount.Value;
		set => _isStandardAccount.Value = value;
	}

	/// <summary>
	/// Maximum volume allowed for a single order.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the base size for subsequent entries.
	/// </summary>
	public decimal LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneously open trades.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Minimum spacing between entries expressed in pips.
	/// </summary>
	public int SpacingPips
	{
		get => _spacingPips.Value;
		set => _spacingPips.Value = value;
	}

	/// <summary>
	/// Number of orders that should remain protected before adding new exposure.
	/// </summary>
	public int OrdersToProtect
	{
		get => _ordersToProtect.Value;
		set => _ordersToProtect.Value = value;
	}

	/// <summary>
	/// Enables the secure profit exit block.
	/// </summary>
	public bool AccountProtection
	{
		get => _accountProtection.Value;
		set => _accountProtection.Value = value;
	}

	/// <summary>
	/// Profit target used by the protection block.
	/// </summary>
	public decimal SecureProfit
	{
		get => _secureProfit.Value;
		set => _secureProfit.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Inverts the MACD slope interpretation.
	/// </summary>
	public bool ReverseCondition
	{
		get => _reverseCondition.Value;
		set => _reverseCondition.Value = value;
	}

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

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

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_macd = null;
		_positions = null;
		_previousMacd = null;
		_pipSize = 0;
		_stepValue = 0;
		_cooldown = 0;
	}

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

		_positions = new List<PositionState>();

		_macd = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = MacdSlow },
			new ExponentialMovingAverage { Length = MacdFast }
		);

		_pipSize = GetPriceStep();
		_stepValue = Security?.PriceStep ?? 0m;
		if (_stepValue <= 0m)
			_stepValue = 1m;

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

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdResult)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!macdResult.IsFinal || !_macd.IsFormed)
			return;

		var macdValue = macdResult.GetValue<decimal>();

		UpdateTrailingAndStops(candle);

		if (_cooldown > 0)
		{
			_cooldown--;
			_previousMacd = macdValue;
			return;
		}

		var openTrades = _positions.Count;
		var allowNewTrade = openTrades < MaxTrades;

		if (_previousMacd is null)
		{
			_previousMacd = macdValue;
			return;
		}

		var direction = Math.Sign(macdValue - _previousMacd.Value);
		if (ReverseCondition)
			direction = -direction;

		if (AccountProtection && openTrades >= Math.Max(1, MaxTrades - OrdersToProtect))
		{
			var totalProfit = CalculateTotalProfit(candle.ClosePrice);
			if (totalProfit >= SecureProfit)
			{
				CloseLastPosition();
				_cooldown = 3;
				_previousMacd = macdValue;
				return;
			}
		}

		if (allowNewTrade && direction > 0)
			TryOpen(Sides.Buy, candle);
		else if (allowNewTrade && direction < 0)
			TryOpen(Sides.Sell, candle);

		_previousMacd = macdValue;
	}

	private void TryOpen(Sides side, ICandleMessage candle)
	{
		var price = candle.ClosePrice;
		var spacing = SpacingPips * _pipSize;

		if (side == Sides.Buy)
		{
			var reference = GetReferencePrice(Sides.Buy);
			if (reference != 0m && reference - price < spacing)
				return;
		}
		else
		{
			var reference = GetReferencePrice(Sides.Sell);
			if (reference != 0m && price - reference < spacing)
				return;
		}

		var volume = CalculateVolume();
		if (volume <= 0m)
			return;

		var sameSideCount = CountPositions(side);
		if (sameSideCount > 0)
		{
			volume *= Pow(LotMultiplier, sameSideCount);
		}

		volume = NormalizeVolume(Math.Min(volume, MaxVolume));
		if (volume <= 0m)
			return;

		var stopDistance = StopLossPips * _pipSize;
		var takeDistance = TakeProfitPips * _pipSize;

		if (side == Sides.Buy)
			BuyMarket();
		else
			SellMarket();

		var state = new PositionState
		{
			Side = side,
			Volume = volume,
			EntryPrice = price,
			StopPrice = stopDistance > 0m ? (side == Sides.Buy ? price - stopDistance : price + stopDistance) : (decimal?)null,
			TakeProfitPrice = takeDistance > 0m ? (side == Sides.Buy ? price + takeDistance : price - takeDistance) : (decimal?)null
		};

		_positions.Add(state);
		_cooldown = 3;
	}

	private void UpdateTrailingAndStops(ICandleMessage candle)
	{
		var trailingDistance = TrailingStopPips * _pipSize;
		var activationDistance = (TrailingStopPips + SpacingPips) * _pipSize;

		for (var i = _positions.Count - 1; i >= 0; i--)
		{
			var state = _positions[i];

			if (state.Side == Sides.Buy)
			{
				if (state.TakeProfitPrice is decimal tp && candle.HighPrice >= tp)
				{
					SellMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (state.StopPrice is decimal sl && candle.LowPrice <= sl)
				{
					SellMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (TrailingStopPips > 0 && candle.ClosePrice - state.EntryPrice >= activationDistance)
				{
					var candidate = candle.ClosePrice - trailingDistance;
					if (state.StopPrice is null || state.StopPrice < candidate)
						state.StopPrice = candidate;
				}
			}
			else
			{
				if (state.TakeProfitPrice is decimal tp && candle.LowPrice <= tp)
				{
					BuyMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (state.StopPrice is decimal sl && candle.HighPrice >= sl)
				{
					BuyMarket();
					_positions.RemoveAt(i);
					_cooldown = 3;
					continue;
				}

				if (TrailingStopPips > 0 && state.EntryPrice - candle.ClosePrice >= activationDistance)
				{
					var candidate = candle.ClosePrice + trailingDistance;
					if (state.StopPrice is null || state.StopPrice > candidate)
						state.StopPrice = candidate;
				}
			}
		}
	}

	private decimal CalculateVolume()
	{
		decimal baseVolume;

		if (UseRiskSizing)
		{
			if (Portfolio is null)
				return 0m;

			var balance = Portfolio.CurrentValue ?? 0m;
			if (balance <= 0m)
				return 0m;

			var rawLots = Math.Ceiling(balance * (RiskPercent / 100m) / 10000m);
			if (!IsStandardAccount)
				rawLots /= 10m;

			baseVolume = rawLots;
		}
		else
		{
			baseVolume = FixedVolume;
		}

		return baseVolume;
	}

	private decimal CalculateTotalProfit(decimal currentPrice)
	{
		decimal profit = 0m;

		foreach (var state in _positions)
		{
			var priceDifference = state.Side == Sides.Buy
				? currentPrice - state.EntryPrice
				: state.EntryPrice - currentPrice;

			var steps = _pipSize > 0m ? priceDifference / _pipSize : priceDifference;
			profit += steps * _stepValue * state.Volume;
		}

		return profit;
	}

	private void CloseLastPosition()
	{
		if (_positions.Count == 0)
			return;

		var index = _positions.Count - 1;
		var state = _positions[index];

		if (state.Side == Sides.Buy)
			SellMarket();
		else
			BuyMarket();

		_positions.RemoveAt(index);
	}

	private decimal GetReferencePrice(Sides side)
	{
		for (var i = _positions.Count - 1; i >= 0; i--)
		{
			var state = _positions[i];
			if (state.Side == side)
				return state.EntryPrice;
		}

		return 0m;
	}

	private int CountPositions(Sides side)
	{
		var count = 0;
		for (var i = 0; i < _positions.Count; i++)
		{
			if (_positions[i].Side == side)
				count++;
		}

		return count;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0)
				steps = 1;
			volume = steps * step;
		}
		else
		{
			volume = Math.Round(volume, 1, MidpointRounding.AwayFromZero);
			if (volume <= 0m)
				volume = 0.1m;
		}

		return volume;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0m)
			return step;

		var decimals = Security?.Decimals ?? 0;
		if (decimals > 0)
			return (decimal)Math.Pow(10, -decimals);

		return 0.0001m;
	}

	private static decimal Pow(decimal value, int power)
	{
		if (power <= 0)
			return 1m;

		return (decimal)Math.Pow((double)value, power);
	}

	private sealed class PositionState
	{
		public Sides Side { get; set; }
		public decimal Volume { get; set; }
		public decimal EntryPrice { get; set; }
		public decimal? StopPrice { get; set; }
		public decimal? TakeProfitPrice { get; set; }
	}
}