Открыть на GitHub

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

Перенесённая стратегия Dealers Trade v7.74 MACD – это пирамидальная система, которая открывает серии сделок по направлению наклона основной линии MACD. Она разрабатывалась для таймфреймов H4 и D1, где импульсные развороты менее шумные и дают времени для наращивания позиций.

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

  • Определение сигнала. На каждом закрытом баре выбранного таймфрейма рассчитывается MACD. Рост основной линии воспринимается как бычий сигнал, падение – как медвежий. При необходимости направление можно инвертировать параметром ReverseCondition.
  • Управление размером позиции. Первая заявка берёт фиксированный объём FixedVolume. Если он равен нулю, объём рассчитывается от капитала счёта по доле RiskPercent и расстоянию до стоп-лосса. Каждый следующий вход умножается на VolumeMultiplier в степени текущего количества сделок (1.6, 1.6², 1.6³ …). Новый ордер отправляется только при выполнении двух условий: цена отошла минимум на IntervalPoints * PriceStep от последней покупки и не превышены лимиты MaxPositions и MaxVolume.
  • Сопровождение сделок. Для каждой позиции запоминаются собственные уровни стоп-лосса и тейк-профита, рассчитанные в шагах цены (StopLossPoints, TakeProfitPoints). Если задан TrailingStopPoints, стоп подтягивается вслед за ценой, когда плавающая прибыль превышает сумму TrailingStopPoints + TrailingStepPoints, что повторяет логику MQL-версии.
  • Защита счёта. Когда число открытых сделок больше PositionsForProtection, а суммарная нереализованная прибыль достигает SecureProfit, стратегия фиксирует наиболее прибыльную позицию, прежде чем продолжить пирамиду.

Параметры

Параметр Значение по умолчанию Описание
CandleType H4 Таймфрейм, по которому строятся свечи и рассчитывается MACD.
FixedVolume 0.1 Базовый объём первой сделки. Ноль включает риск-менеджмент по проценту капитала.
RiskPercent 5 Доля капитала, которую можно потерять при одном входе, если FixedVolume = 0.
StopLossPoints 90 Расстояние до стоп-лосса в шагах цены. Ноль отключает жёсткий стоп.
TakeProfitPoints 30 Расстояние до тейк-профита в шагах цены. Ноль отключает цель.
TrailingStopPoints 15 Базовое расстояние трейлинг-стопа в шагах цены.
TrailingStepPoints 5 Минимальное дополнительное движение цены до следующего подтягивания стопа.
MaxPositions 5 Максимальное число одновременно открытых сделок.
IntervalPoints 15 Минимальное расстояние между соседними входами в шагах цены.
SecureProfit 50 Порог прибыли (в валюте котировки) для срабатывания защиты счёта.
AccountProtection true Включает защиту счёта.
PositionsForProtection 3 Минимальное число сделок для работы защиты.
ReverseCondition false Инвертировать направление сигнала MACD.
MacdFastPeriod 14 Быстрая EMA в MACD.
MacdSlowPeriod 26 Медленная EMA в MACD.
MacdSignalPeriod 1 Длина сигнальной EMA MACD (в оригинале равна 1).
MaxVolume 5 Максимальный совокупный объём позиции.
VolumeMultiplier 1.6 Множитель объёма для каждой новой сделки.

Особенности и ограничения

  • В MQL-версии робот мог одновременно держать длинные и короткие сделки (хедж). В StockSharp позиции неттингуются, поэтому перед сменой направления текущая противоположная позиция закрывается.
  • MACD анализируется только по закрытым свечам. Внутри бара сигнал может появиться чуть позже, зато стратегия устойчивее при тестировании на истории.
  • Все значения в "пунктах" умножаются на шаг цены инструмента PriceStep. Если провайдер не передаёт эту величину, используется запасной шаг 0.0001 – в этом случае скорректируйте параметры под ваш рынок.
  • При FixedVolume = 0 для расчёта объёма необходим ненулевой стоп-лосс. Иначе объём становится нулевым, и заявка не отправляется.
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 MQL5 implementation.
/// </summary>
public class DealersTradeMacdStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _intervalPoints;
	private readonly StrategyParam<decimal> _secureProfit;
	private readonly StrategyParam<bool> _accountProtection;
	private readonly StrategyParam<int> _positionsForProtection;
	private readonly StrategyParam<bool> _reverseCondition;
	private readonly StrategyParam<int> _macdFastPeriod;
	private readonly StrategyParam<int> _macdSlowPeriod;
	private readonly StrategyParam<int> _macdSignalPeriod;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _volumeMultiplier;

	private MovingAverageConvergenceDivergence _macd = null!;
	private decimal? _previousMacd;
	private decimal _lastEntryPrice;
	private int _cooldown;
	private readonly List<PositionState> _longPositions = new();
	private readonly List<PositionState> _shortPositions = new();

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

		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
			.SetDisplay("Fixed Volume", "Lot size used when above zero", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetDisplay("Risk %", "Risk percent when fixed volume is zero", "Risk");

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

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

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
			.SetDisplay("Trailing Stop pts", "Trailing stop distance in price steps", "Risk");

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetDisplay("Trailing Step pts", "Additional distance before trailing updates", "Risk");

		_maxPositions = Param(nameof(MaxPositions), 2)
			.SetDisplay("Max Positions", "Maximum concurrent entries", "Money Management");

		_intervalPoints = Param(nameof(IntervalPoints), 50m)
			.SetDisplay("Interval pts", "Minimum distance between new entries", "Money Management");

		_secureProfit = Param(nameof(SecureProfit), 50m)
			.SetDisplay("Secure Profit", "Profit threshold that triggers protection", "Money Management");

		_accountProtection = Param(nameof(AccountProtection), true)
			.SetDisplay("Account Protection", "Close best trade after reaching secure profit", "Money Management");

		_positionsForProtection = Param(nameof(PositionsForProtection), 3)
			.SetDisplay("Protect From", "Minimum positions before triggering protection", "Money Management");

		_reverseCondition = Param(nameof(ReverseCondition), false)
			.SetDisplay("Reverse Signal", "Invert MACD slope direction", "Trading");

		_macdFastPeriod = Param(nameof(MacdFastPeriod), 14)
			.SetDisplay("MACD Fast", "Fast EMA period", "Indicators");

		_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 26)
			.SetDisplay("MACD Slow", "Slow EMA period", "Indicators");

		_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 1)
			.SetDisplay("MACD Signal", "Signal EMA period", "Indicators");

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetDisplay("Max Volume", "Absolute cap for trade volume", "Risk")
			.SetGreaterThanZero();

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.6m)
			.SetDisplay("Volume Multiplier", "Multiplier for additional positions", "Money Management")
			.SetGreaterThanZero();
	}

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

	/// <summary>
	/// Fixed lot size. When zero risk based sizing is used.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Percent of equity risked when sizing dynamically.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.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>
	/// Trailing stop distance in price steps.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Extra distance required before the trailing stop moves.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Maximum number of open entries.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Minimum price distance between sequential entries.
	/// </summary>
	public decimal IntervalPoints
	{
		get => _intervalPoints.Value;
		set => _intervalPoints.Value = value;
	}

	/// <summary>
	/// Profit target for account protection logic.
	/// </summary>
	public decimal SecureProfit
	{
		get => _secureProfit.Value;
		set => _secureProfit.Value = value;
	}

	/// <summary>
	/// Enables profit locking when enough trades are open.
	/// </summary>
	public bool AccountProtection
	{
		get => _accountProtection.Value;
		set => _accountProtection.Value = value;
	}

	/// <summary>
	/// Minimum number of positions before account protection activates.
	/// </summary>
	public int PositionsForProtection
	{
		get => _positionsForProtection.Value;
		set => _positionsForProtection.Value = value;
	}

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

	/// <summary>
	/// MACD fast EMA period.
	/// </summary>
	public int MacdFastPeriod
	{
		get => _macdFastPeriod.Value;
		set => _macdFastPeriod.Value = value;
	}

	/// <summary>
	/// MACD slow EMA period.
	/// </summary>
	public int MacdSlowPeriod
	{
		get => _macdSlowPeriod.Value;
		set => _macdSlowPeriod.Value = value;
	}

	/// <summary>
	/// MACD signal EMA period.
	/// </summary>
	public int MacdSignalPeriod
	{
		get => _macdSignalPeriod.Value;
		set => _macdSignalPeriod.Value = value;
	}

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

	/// <summary>
	/// Multiplier applied to the base volume for each additional entry.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_macd?.Reset();
		_previousMacd = null;
		_lastEntryPrice = 0m;
		_cooldown = 0;
		_longPositions.Clear();
		_shortPositions.Clear();
	}

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

		_macd = new MovingAverageConvergenceDivergence(
			new ExponentialMovingAverage { Length = MacdSlowPeriod },
			new ExponentialMovingAverage { Length = MacdFastPeriod }
		);

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

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

		HandleTrailingAndExits(candle);

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousMacd = macdValue;
			return;
		}

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

		var openPositions = _longPositions.Count + _shortPositions.Count;
		var continueOpening = openPositions < MaxPositions;

		var direction = 0;

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

		if (macdValue > _previousMacd)
			direction = 1;
		else if (macdValue < _previousMacd)
			direction = -1;

		if (ReverseCondition)
			direction = -direction;

		if (AccountProtection && openPositions > PositionsForProtection)
		{
			var totalProfit = CalculateTotalProfit(candle.ClosePrice);
			if (totalProfit >= SecureProfit)
			{
				CloseMostProfitablePosition(candle.ClosePrice);
				_previousMacd = macdValue;
				return;
			}
		}

		if (continueOpening && direction > 0 && _shortPositions.Count == 0)
			TryOpenLong(candle);
		else if (continueOpening && direction < 0 && _longPositions.Count == 0)
			TryOpenShort(candle);

		_previousMacd = macdValue;
	}

	private void HandleTrailingAndExits(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var trailingDistance = TrailingStopPoints * step;
		var trailingActivation = (TrailingStopPoints + TrailingStepPoints) * step;

		// Collect exits first, then execute to avoid collection modification during enumeration
		var longExits = new List<PositionState>();
		var longSnapshot = _longPositions.ToList();
		foreach (var state in longSnapshot)
		{
			if (state.TakeProfitPrice > 0 && candle.HighPrice >= state.TakeProfitPrice)
			{
				longExits.Add(state);
				continue;
			}

			if (state.StopPrice > 0 && candle.LowPrice <= state.StopPrice)
			{
				longExits.Add(state);
				continue;
			}

			if (TrailingStopPoints > 0 && candle.ClosePrice - state.EntryPrice > trailingActivation)
			{
				var candidateStop = candle.ClosePrice - trailingDistance;
				if (state.StopPrice == 0m || state.StopPrice < candle.ClosePrice - trailingActivation)
					state.StopPrice = candidateStop;
			}
		}
		foreach (var state in longExits)
		{
			Volume = state.Volume;
			SellMarket();
			_longPositions.Remove(state);
			_lastEntryPrice = 0m;
		}

		var shortExits = new List<PositionState>();
		var shortSnapshot = _shortPositions.ToList();
		foreach (var state in shortSnapshot)
		{
			if (state.TakeProfitPrice > 0 && candle.LowPrice <= state.TakeProfitPrice)
			{
				shortExits.Add(state);
				continue;
			}

			if (state.StopPrice > 0 && candle.HighPrice >= state.StopPrice)
			{
				shortExits.Add(state);
				continue;
			}

			if (TrailingStopPoints > 0 && state.EntryPrice - candle.ClosePrice > trailingActivation)
			{
				var candidateStop = candle.ClosePrice + trailingDistance;
				if (state.StopPrice == 0m || state.StopPrice > candle.ClosePrice + trailingActivation)
					state.StopPrice = candidateStop;
			}
		}
		foreach (var state in shortExits)
		{
			Volume = state.Volume;
			BuyMarket();
			_shortPositions.Remove(state);
			_lastEntryPrice = 0m;
		}
	}

	private void TryOpenLong(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var interval = IntervalPoints * step;

		if (_lastEntryPrice != 0m && Math.Abs(_lastEntryPrice - candle.ClosePrice) < interval)
			return;

		var baseVolume = FixedVolume > 0 ? FixedVolume : CalculateRiskVolume(step);
		if (baseVolume <= 0)
			return;

		var openPositions = _longPositions.Count + _shortPositions.Count;
		var lotCoefficient = openPositions == 0 ? 1m : Pow(VolumeMultiplier, openPositions + 1);
		var volume = NormalizeVolume(baseVolume * lotCoefficient);
		if (volume <= 0 || volume > MaxVolume)
			return;

		var stopDistance = StopLossPoints * step;
		var takeDistance = TakeProfitPoints * step;

		Volume = volume;
		BuyMarket();

		_longPositions.Add(new PositionState
		{
			EntryPrice = candle.ClosePrice,
			Volume = volume,
			StopPrice = stopDistance > 0 ? candle.ClosePrice - stopDistance : 0m,
			TakeProfitPrice = takeDistance > 0 ? candle.ClosePrice + takeDistance : 0m
		});

		_lastEntryPrice = candle.ClosePrice;
		_cooldown = 3;
	}

	private void TryOpenShort(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var interval = IntervalPoints * step;

		if (_lastEntryPrice != 0m && Math.Abs(_lastEntryPrice - candle.ClosePrice) < interval)
			return;

		var baseVolume = FixedVolume > 0 ? FixedVolume : CalculateRiskVolume(step);
		if (baseVolume <= 0)
			return;

		var openPositions = _longPositions.Count + _shortPositions.Count;
		var lotCoefficient = openPositions == 0 ? 1m : Pow(VolumeMultiplier, openPositions + 1);
		var volume = NormalizeVolume(baseVolume * lotCoefficient);
		if (volume <= 0 || volume > MaxVolume)
			return;

		var stopDistance = StopLossPoints * step;
		var takeDistance = TakeProfitPoints * step;

		Volume = volume;
		SellMarket();

		_shortPositions.Add(new PositionState
		{
			EntryPrice = candle.ClosePrice,
			Volume = volume,
			StopPrice = stopDistance > 0 ? candle.ClosePrice + stopDistance : 0m,
			TakeProfitPrice = takeDistance > 0 ? candle.ClosePrice - takeDistance : 0m
		});

		_lastEntryPrice = candle.ClosePrice;
		_cooldown = 3;
	}

	private decimal CalculateRiskVolume(decimal priceStep)
	{
		if (StopLossPoints <= 0)
			return 0m;

		var stopDistance = StopLossPoints * priceStep;
		if (stopDistance <= 0)
			return 0m;

		if (Portfolio is null)
			return 0m;

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

		var riskAmount = equity * (RiskPercent / 100m);
		return riskAmount / stopDistance;
	}

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

		foreach (var pos in _longPositions)
			profit += (currentPrice - pos.EntryPrice) * pos.Volume;

		foreach (var pos in _shortPositions)
			profit += (pos.EntryPrice - currentPrice) * pos.Volume;

		return profit;
	}

	private void CloseMostProfitablePosition(decimal currentPrice)
	{
		PositionState best = null;
		var bestIsLong = false;
		decimal bestProfit = 0m;

		foreach (var pos in _longPositions)
		{
			var profit = (currentPrice - pos.EntryPrice) * pos.Volume;
			if (profit > bestProfit)
			{
				bestProfit = profit;
				best = pos;
				bestIsLong = true;
			}
		}

		foreach (var pos in _shortPositions)
		{
			var profit = (pos.EntryPrice - currentPrice) * pos.Volume;
			if (profit > bestProfit)
			{
				bestProfit = profit;
				best = pos;
				bestIsLong = false;
			}
		}

		if (best is null || bestProfit <= 0m)
			return;

		if (bestIsLong)
		{
			SellMarket();
			_longPositions.Remove(best);
		}
		else
		{
			BuyMarket();
			_shortPositions.Remove(best);
		}

		_lastEntryPrice = 0m;
	}

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

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0)
		{
			var steps = Math.Floor(volume / step);
			volume = steps * step;
		}

		return volume;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0)
			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 decimal EntryPrice { get; set; }
		public decimal Volume { get; set; }
		public decimal StopPrice { get; set; }
		public decimal TakeProfitPrice { get; set; }
	}
}