Ver en GitHub

Estrategia Dealers Trade MACD

La estrategia Dealers Trade MACD es un sistema de piramidación que fue portado del asesor experto MQL5 original "Dealers Trade v7.74". Sigue la pendiente de la línea principal MACD para decidir cuándo acumular posiciones en la dirección de la tendencia. La lógica está diseñada para swing trading en gráficos H4 y D1 donde los cambios de momentum son menos ruidosos.

Cómo funciona la estrategia

  • Generación de señales – la estrategia se suscribe a velas del marco temporal seleccionado y evalúa el valor de la línea principal MACD en cada barra cerrada. Un MACD ascendente implica sesgo largo y un MACD descendente implica sesgo corto. La señal puede invertirse con el parámetro ReverseCondition para coincidir con cuentas que históricamente operaron entradas contrarias.
  • Dimensionamiento de posición – la primera orden usa el tamaño fijo FixedVolume o, si está en 0, el sistema asigna riesgo dinámicamente desde el capital del portafolio usando el parámetro RiskPercent y la distancia de stop loss configurada. Las entradas adicionales se multiplican por VolumeMultiplier elevado al recuento de posiciones actuales (p.ej. 1.6, 1.6², 1.6³, …) y solo se envían cuando el precio se ha movido al menos IntervalPoints * PriceStep desde el último relleno. Las órdenes se omiten una vez que la exposición neta excedería MaxVolume o el número de entradas alcanza MaxPositions.
  • Gestión de órdenes – cada posición mantiene sus propios objetivos de stop loss y take profit calculados desde el precio de entrada y los offsets basados en puntos (StopLossPoints, TakeProfitPoints). Si TrailingStopPoints es mayor que cero, el stop se arrastra hacia arriba (o hacia abajo para cortos) una vez que el beneficio supera TrailingStopPoints + TrailingStepPoints, emulando el comportamiento de trailing original.
  • Protección de cuenta – cuando el número de operaciones abiertas es mayor que PositionsForProtection y el beneficio no realizado agregado cruza SecureProfit, la estrategia cierra la posición más rentable para fijar ganancias antes de añadir nueva exposición. Esto refleja el bloque de "Protección de cuenta" de la versión MQL.

Parámetros

Nombre Por defecto Descripción
CandleType H4 Marco temporal usado para cálculos MACD y decisiones de operación.
FixedVolume 0.1 Tamaño de lote para la primera entrada. Establecer en 0 para habilitar el dimensionamiento basado en riesgo.
RiskPercent 5 Porcentaje del capital actual arriesgado cuando FixedVolume es cero.
StopLossPoints 90 Distancia del stop loss expresada en pasos de precio. Usar 0 para deshabilitar stops duros.
TakeProfitPoints 30 Distancia del take profit en pasos de precio. Usar 0 para deshabilitar.
TrailingStopPoints 15 Distancia del trailing stop en pasos de precio. Establecer en 0 para desactivar el trailing.
TrailingStepPoints 5 Distancia adicional que debe ganarse antes de que el trailing stop se mueva de nuevo.
MaxPositions 5 Número máximo de entradas abiertas simultáneamente.
IntervalPoints 15 Distancia mínima en pasos de precio requerida entre entradas consecutivas.
SecureProfit 50 Umbral de beneficio (en moneda de cotización) que activa la protección de cuenta.
AccountProtection true Habilita cerrar la operación con mejor rendimiento cuando se alcanza el objetivo de beneficio seguro.
PositionsForProtection 3 Número mínimo de operaciones que deben estar abiertas antes de que la protección pueda activarse.
ReverseCondition false Invierte la interpretación de la pendiente MACD.
MacdFastPeriod 14 Longitud EMA rápida para el indicador MACD.
MacdSlowPeriod 26 Longitud EMA lenta para el indicador MACD.
MacdSignalPeriod 1 Longitud EMA de señal para el indicador MACD (establecida en 1 en el asesor experto original).
MaxVolume 5 Límite superior para el tamaño de posición acumulado.
VolumeMultiplier 1.6 Multiplicador aplicado al tamaño base para cada nueva entrada.

Notas y limitaciones

  • El experto MQL original podía mantener posiciones largas y cortas cubiertas simultáneamente. StockSharp usa posiciones neteadas por defecto, por lo que este port cierra la exposición opuesta antes de añadir nuevas operaciones en la otra dirección.
  • Los valores MACD se evalúan solo en velas cerradas. Las señales intrabarra pueden aparecer más tarde que en la implementación MQL basada en ticks, pero el comportamiento es mucho más estable para pruebas históricas.
  • Todas las distancias basadas en puntos se multiplican por el PriceStep del instrumento. Si el instrumento no proporciona esos metadatos, la estrategia recurre a un paso de 0.0001, así que ajuste los parámetros al operar instrumentos con diferentes tamaños de tick.
  • Cuando FixedVolume es cero, la estrategia requiere una distancia de stop loss no cero para calcular el dimensionamiento basado en riesgo. Si el stop está deshabilitado, el volumen por defecto es cero y no se envía ninguna operación.
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; }
	}
}