Открыть на GitHub

Стратегия Dealers Trade MACD MQL4

Стратегия Dealers Trade MACD MQL4 представляет собой прямую конвертацию советника «Dealers Trade v7.74» для MetaTrader 4. В StockSharp сохранены пирамидальный мани-менеджмент и логика наклона линии MACD, но обработка позиций адаптирована к неттинговой модели. Стратегия рассчитана на свинг-торговлю по графикам H4 и D1 и наращивает позицию в направлении тренда, пока импульс соответствует показаниям MACD.

Как работает стратегия

  • Определение сигнала. Подписка на свечи выбранного таймфрейма и расчёт классического MACD (быстрая EMA, медленная EMA, сигнальная EMA). Рост основного значения MACD относительно предыдущей свечи трактуется как восходящий импульс, падение — как нисходящий. Параметр ReverseCondition позволяет инвертировать направление для контртрендовой торговли.
  • Шаг пирамидинга. В каждый момент активна только одна корзина сделок. При сигнале на покупку открывается базовый ордер Buy, дальнейшие покупки отправляются лишь тогда, когда цена ушла вниз минимум на SpacingPips * PriceStep от цены последней покупки. Для коротких позиций действует зеркальная логика.
  • Размер лота. Используется либо фиксированный объём FixedVolume, либо (если UseRiskSizing включён) значение, рассчитанное от баланса портфеля и RiskPercent. Параметр IsStandardAccount имитирует настройку «Account is normal» и делит лоты на 10 для мини-счётов. Каждый дополнительный ордер умножается на LotMultiplier, но ограничивается MaxVolume.
  • Управление рисками. На каждую сделку устанавливаются стоп-лосс и тейк-профит согласно StopLossPips и TakeProfitPips. Когда прибыль превышает TrailingStopPips + SpacingPips, стоп подтягивается на расстояние TrailingStopPips, что воспроизводит трейлинг из исходного советника.
  • Защита счёта. При достижении MaxTrades - OrdersToProtect открытых ордеров и превышении суммарной нереализованной прибыли SecureProfit закрывается последняя сделка. Это повторяет блок «AccountProtection» из оригинальной реализации.

Параметры

Имя Значение по умолчанию Описание
CandleType H4 Таймфрейм для расчёта MACD и сигналов.
FixedVolume 0.1 Базовый лот при отключённом управлении риском.
UseRiskSizing true Включает расчёт объёма от баланса.
RiskPercent 2 Процент капитала, используемый при расчёте объёма.
IsStandardAccount 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 Период быстрой EMA в MACD.
MacdSlow 26 Период медленной EMA в MACD.
MacdSignal 1 Период сигнальной EMA в MACD.

Примечания и ограничения

  • В StockSharp используется неттинговый учёт позиций, поэтому одновременно держать лонг и шорт по одному инструменту нельзя. При смене направления противоположная корзина закрывается.
  • Логика защиты счёта использует PriceStep и StepPrice. Если инструмент не содержит этих данных, применяется шаг 0.0001 и стоимость шага 1, поэтому параметры прибыли стоит адаптировать.
  • Управление риском требует положительного значения StopLossPips. При нулевом стопе объём не рассчитывается и новые сделки игнорируются.
  • Стратегия анализирует только закрытые свечи. Внутридневные сигналы MACD из MetaTrader могут появляться на одну свечу позже, что делает тестирование более стабильным.
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; }
	}
}