Ver en GitHub

Trader de divergencia (conversión clásica)

Esta estrategia reproduce el comportamiento del MetaTrader asesor experto 4 Divergence Trader dentro del StockSharp nivel alto API. Se calculan dos medias móviles simples sobre el precio de la vela seleccionada (abierta de forma predeterminada). El sistema monitorea cómo cambia la distancia entre los promedios rápido y lento de una barra a la siguiente:

  • Cuando el diferencial se amplía al alza y el valor de divergencia se mantiene entre el umbral de compra y el umbral de estancia fuera, se abre una posición larga o se cubre una posición corta existente.
  • Cuando el diferencial se amplía a la baja dentro de los umbrales reflejados, se ingresa una posición corta o se cierra una operación larga existente.

Sólo se utilizan velas completas, que coinciden con el procesamiento barra por barra del asesor experto original. Todas las reglas de administración se implementan con llamadas de alto nivel impulsadas por eventos (BuyMarket/SellMarket).

Reglas comerciales

  1. Suscríbase al tipo de vela configurado y calcule dos SMA con períodos Rápido SMA y Lento SMA.
  2. Calcule el diferencial actual (fast - slow) y compárelo con el diferencial anterior para obtener el valor de divergencia.
  3. Ingrese largo si la divergencia es positiva, mayor o igual a Umbral de compra y menor o igual a Umbral de permanencia fuera.
  4. Ingrese short si la divergencia es negativa, menor o igual a -Buy Threshold y mayor o igual a -Stay Out Threshold.
  5. Invierta una posición existente cada vez que aparezca una señal opuesta.
  6. Restrinja las nuevas entradas a la ventana de hora local entre Hora de inicio y Hora de finalización (se admite el ajuste después de la medianoche).

Gestión de riesgos

  • Los niveles fijos opcionales de Take Profit (pips) y Stop Loss (pips) se monitorean en los máximos y mínimos de las velas.
  • El Trigger de punto de equilibrio (pips) mueve el stop a entry ± Break-Even Buffer una vez que la posición gana el número especificado de pips.
  • El Trailing Stop (pips) sigue el precio más favorable una vez que la operación genera ganancias. La configuración 9999 desactiva el trailing stop, reflejando el valor predeterminado original EA.
  • La gestión de la cesta cierra todas las exposiciones abiertas cuando las pérdidas y ganancias no realizadas alcanzan el beneficio de la cesta o caen por debajo de -Basket Loss en la moneda de la cuenta.

Parámetros

Parámetro Descripción
Order Volume Volumen utilizado cuando se abre una nueva posición.
Fast SMA / Slow SMA Períodos para las dos medias móviles simples.
Applied Price El componente de vela se trasladó a ambos promedios móviles.
Buy Threshold Límite de divergencia inferior que permite operaciones largas.
Stay Out Threshold Límite de divergencia superior por encima del cual no se realizan nuevas operaciones.
Take Profit (pips) / Stop Loss (pips) Salidas duras opcionales medidas en pips.
Trailing Stop (pips) Distancia de seguimiento que se aplica después de que la operación se vuelve rentable.
Break-Even Trigger (pips) Beneficio en pips requerido antes de mover el stop al punto de equilibrio.
Break-Even Buffer (pips) Se agregó un amortiguador adicional al punto de equilibrio.
Basket Profit / Basket Loss Límites de capital global en la moneda de la cuenta.
Start Hour / Stop Hour Ventana de sesión de negociación local.
Candle Type Plazo utilizado para la suscripción y los cálculos de velas.

Notas de uso

  • Adjunte la estrategia a un valor y establezca el tipo de vela que coincida con el período de tiempo del gráfico original.
  • Asegúrese de que las propiedades PriceStep/StepPrice del instrumento estén configuradas para que los controles basados en pips funcionen correctamente.
  • Para deshabilitar funciones como el trailing stop o el cambio de equilibrio, mantenga sus parámetros en el valor centinela heredado (9999) o cero.
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>
/// Classic divergence trading strategy converted from the MetaTrader 4 "Divergence Trader" expert.
/// The strategy compares a fast and a slow simple moving average and monitors how the spread between
/// them changes from bar to bar. A widening spread to the upside triggers long trades while a widening
/// spread to the downside triggers short trades. Risk management mimics the original MQL behaviour with
/// optional profit targets, stop-loss, trailing stop, break-even shift and basket level exits.
/// </summary>
public class DivergenceTraderClassicStrategy : Strategy
{
	public enum CandlePrices
	{
		Open,
		Close,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<CandlePrices> _appliedPrice;
	private readonly StrategyParam<decimal> _buyThreshold;
	private readonly StrategyParam<decimal> _stayOutThreshold;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _breakEvenPips;
	private readonly StrategyParam<decimal> _breakEvenBufferPips;
	private readonly StrategyParam<decimal> _basketProfitCurrency;
	private readonly StrategyParam<decimal> _basketLossCurrency;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _stopHour;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _fastSma;
	private SimpleMovingAverage _slowSma;
	private decimal? _previousSpread;
	private decimal _pipSize;
	private decimal _maxBasketPnL;
	private decimal _minBasketPnL;
	private decimal? _breakEvenPrice;
	private decimal? _trailingStopPrice;
	private decimal _highestPrice;
	private decimal _lowestPrice;
	private decimal _entryPrice;

	/// <summary>
	/// Initializes a new instance of <see cref="DivergenceTraderClassicStrategy"/>.
	/// </summary>
	public DivergenceTraderClassicStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used when opening a new position.", "Trading")
		;

		_fastPeriod = Param(nameof(FastPeriod), 7)
		.SetGreaterThanZero()
		.SetDisplay("Fast SMA", "Period for the fast simple moving average.", "Indicators")
		;

		_slowPeriod = Param(nameof(SlowPeriod), 88)
		.SetGreaterThanZero()
		.SetDisplay("Slow SMA", "Period for the slow simple moving average.", "Indicators")
		;

		_appliedPrice = Param(nameof(AppliedPrice), CandlePrices.Open)
		.SetDisplay("Applied Price", "Price component forwarded into the moving averages.", "Indicators");

		_buyThreshold = Param(nameof(BuyThreshold), 10m)
		.SetDisplay("Buy Threshold", "Minimal divergence needed to allow long entries.", "Signals")
		;

		_stayOutThreshold = Param(nameof(StayOutThreshold), 1000m)
		.SetDisplay("Stay Out Threshold", "Upper divergence bound disabling new entries.", "Signals")
		;

		_takeProfitPips = Param(nameof(TakeProfitPips), 0m)
		.SetDisplay("Take Profit (pips)", "Distance in pips used to exit winners.", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetDisplay("Stop Loss (pips)", "Maximum adverse excursion tolerated.", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 9999m)
		.SetDisplay("Trailing Stop (pips)", "Trailing distance; 9999 disables trailing just like the EA.", "Risk");

		_breakEvenPips = Param(nameof(BreakEvenPips), 9999m)
		.SetDisplay("Break-Even Trigger (pips)", "Profit in pips required before moving the stop to break-even.", "Risk");

		_breakEvenBufferPips = Param(nameof(BreakEvenBufferPips), 2m)
		.SetDisplay("Break-Even Buffer (pips)", "Buffer in pips added to the break-even stop.", "Risk");

		_basketProfitCurrency = Param(nameof(BasketProfitCurrency), 75m)
		.SetDisplay("Basket Profit", "Floating profit that forces closing all positions.", "Basket");

		_basketLossCurrency = Param(nameof(BasketLossCurrency), 9999m)
		.SetDisplay("Basket Loss", "Floating loss that forces closing all positions.", "Basket");

		_startHour = Param(nameof(StartHour), 0)
		.SetDisplay("Start Hour", "Hour when trading becomes active (0-23).", "Schedule");

		_stopHour = Param(nameof(StopHour), 24)
		.SetDisplay("Stop Hour", "Hour when trading stops accepting new entries (1-24).", "Schedule");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used to calculate signals.", "General");
	}

	/// <summary>
	/// Base volume for new positions.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Period for the fast moving average.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Period for the slow moving average.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Price component forwarded into both moving averages.
	/// </summary>
	public CandlePrices AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	/// <summary>
	/// Divergence value required before long trades can be opened.
	/// </summary>
	public decimal BuyThreshold
	{
		get => _buyThreshold.Value;
		set => _buyThreshold.Value = value;
	}

	/// <summary>
	/// Maximum divergence that still allows trades. Above this value trading is skipped.
	/// </summary>
	public decimal StayOutThreshold
	{
		get => _stayOutThreshold.Value;
		set => _stayOutThreshold.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips. Zero keeps the trade open until an opposite signal.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

	/// <summary>
	/// Trailing stop distance in pips. Use a very large value to disable the trail.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Profit trigger for moving the stop to break-even.
	/// </summary>
	public decimal BreakEvenPips
	{
		get => _breakEvenPips.Value;
		set => _breakEvenPips.Value = value;
	}

	/// <summary>
	/// Additional buffer applied when shifting the stop to break-even.
	/// </summary>
	public decimal BreakEvenBufferPips
	{
		get => _breakEvenBufferPips.Value;
		set => _breakEvenBufferPips.Value = value;
	}

	/// <summary>
	/// Basket profit threshold in account currency.
	/// </summary>
	public decimal BasketProfitCurrency
	{
		get => _basketProfitCurrency.Value;
		set => _basketProfitCurrency.Value = value;
	}

	/// <summary>
	/// Basket loss threshold in account currency.
	/// </summary>
	public decimal BasketLossCurrency
	{
		get => _basketLossCurrency.Value;
		set => _basketLossCurrency.Value = value;
	}

	/// <summary>
	/// Hour of the day when new trades are allowed.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour of the day when new trades are blocked.
	/// </summary>
	public int StopHour
	{
		get => _stopHour.Value;
		set => _stopHour.Value = value;
	}

	/// <summary>
	/// Candle type (timeframe) used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_fastSma = null;
		_slowSma = null;
		_previousSpread = null;
		_pipSize = 0m;
		_maxBasketPnL = 0m;
		_minBasketPnL = 0m;
		_breakEvenPrice = null;
		_trailingStopPrice = null;
		_highestPrice = 0m;
		_lowestPrice = 0m;
		_entryPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

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

		_pipSize = CalculatePipSize();

		_fastSma = new SMA { Length = FastPeriod };
		_slowSma = new SMA { Length = SlowPeriod };

		_previousSpread = null;
		_breakEvenPrice = null;
		_trailingStopPrice = null;
		_highestPrice = 0m;
		_lowestPrice = 0m;

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_fastSma, _slowSma, ProcessCandle)
		.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _fastSma);
			DrawIndicator(area, _slowSma);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		// Work only with fully formed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Update trailing logic for existing positions before acting on new signals.
		ManageOpenPosition(candle);

		// Respect basket limits from the legacy EA.
		if (EvaluateBasketPnL(candle.ClosePrice))
		{
			_previousSpread = fastValue - slowValue;
			return;
		}

		if (_fastSma == null || _slowSma == null)
			return;

		if (!_fastSma.IsFormed || !_slowSma.IsFormed)
		{
			_previousSpread = fastValue - slowValue;
			return;
		}

		var currentSpread = fastValue - slowValue;
		var divergence = _previousSpread.HasValue ? currentSpread - _previousSpread.Value : 0m;
		_previousSpread = currentSpread;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (!IsWithinTradingHours(candle.CloseTime))
			return;

		if (OrderVolume <= 0m)
			return;

		// Avoid over-hedging: only reverse when the signal changes direction.
		if (divergence >= BuyThreshold && divergence <= StayOutThreshold)
		{
			if (Position < 0m)
			{
				BuyMarket(Math.Abs(Position));
			}

			if (Position <= 0m)
			{
				ResetPositionTracking();
				BuyMarket(OrderVolume);
			}
		}
		else if (divergence <= -BuyThreshold && divergence >= -StayOutThreshold)
		{
			if (Position > 0m)
			{
				SellMarket(Position);
			}

			if (Position >= 0m)
			{
				ResetPositionTracking();
				SellMarket(OrderVolume);
			}
		}
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position == 0m)
		{
			ResetPositionTracking();
			return;
		}

		var entryPrice = _entryPrice;
		if (entryPrice == 0m)
			return;

		var pipSize = EnsurePipSize();
		var takeProfitDistance = TakeProfitPips > 0m ? TakeProfitPips * pipSize : 0m;
		var stopLossDistance = StopLossPips > 0m ? StopLossPips * pipSize : 0m;
		var breakEvenDistance = BreakEvenPips > 0m && BreakEvenPips < 9000m ? BreakEvenPips * pipSize : 0m;
		var breakEvenBuffer = BreakEvenBufferPips > 0m ? BreakEvenBufferPips * pipSize : 0m;
		var trailingDistance = TrailingStopPips > 0m && TrailingStopPips < 9000m ? TrailingStopPips * pipSize : 0m;
		var absPosition = Math.Abs(Position);

		if (Position > 0m)
		{
			_highestPrice = Math.Max(_highestPrice == 0m ? entryPrice : _highestPrice, candle.HighPrice);

			var profitDistance = candle.ClosePrice - entryPrice;

			if (breakEvenDistance > 0m && profitDistance >= breakEvenDistance && _breakEvenPrice == null)
				_breakEvenPrice = entryPrice + breakEvenBuffer;

			if (_breakEvenPrice is decimal bePrice && candle.LowPrice <= bePrice)
			{
				SellMarket(absPosition);
				ResetPositionTracking();
				return;
			}

			if (trailingDistance > 0m && profitDistance >= trailingDistance)
			{
				var candidate = _highestPrice - trailingDistance;
				if (_trailingStopPrice == null || candidate > _trailingStopPrice)
					_trailingStopPrice = candidate;

				if (_trailingStopPrice is decimal trailing && candle.LowPrice <= trailing)
				{
					SellMarket(absPosition);
					ResetPositionTracking();
					return;
				}
			}

			if (takeProfitDistance > 0m && profitDistance >= takeProfitDistance)
			{
				SellMarket(absPosition);
				ResetPositionTracking();
				return;
			}

			if (stopLossDistance > 0m && candle.LowPrice <= entryPrice - stopLossDistance)
			{
				SellMarket(absPosition);
				ResetPositionTracking();
			}
		}
		else if (Position < 0m)
		{
			_lowestPrice = Math.Min(_lowestPrice == 0m ? entryPrice : _lowestPrice, candle.LowPrice);

			var profitDistance = entryPrice - candle.ClosePrice;

			if (breakEvenDistance > 0m && profitDistance >= breakEvenDistance && _breakEvenPrice == null)
				_breakEvenPrice = entryPrice - breakEvenBuffer;

			if (_breakEvenPrice is decimal bePrice && candle.HighPrice >= bePrice)
			{
				BuyMarket(absPosition);
				ResetPositionTracking();
				return;
			}

			if (trailingDistance > 0m && profitDistance >= trailingDistance)
			{
				var candidate = _lowestPrice + trailingDistance;
				if (_trailingStopPrice == null || candidate < _trailingStopPrice)
					_trailingStopPrice = candidate;

				if (_trailingStopPrice is decimal trailing && candle.HighPrice >= trailing)
				{
					BuyMarket(absPosition);
					ResetPositionTracking();
					return;
				}
			}

			if (takeProfitDistance > 0m && profitDistance >= takeProfitDistance)
			{
				BuyMarket(absPosition);
				ResetPositionTracking();
				return;
			}

			if (stopLossDistance > 0m && candle.HighPrice >= entryPrice + stopLossDistance)
			{
				BuyMarket(absPosition);
				ResetPositionTracking();
			}
		}
	}

	private bool EvaluateBasketPnL(decimal lastPrice)
	{
		if (BasketProfitCurrency <= 0m && BasketLossCurrency <= 0m)
			return false;

		if (Position == 0m)
			return false;

		var entryPrice = _entryPrice;
		if (entryPrice == 0m)
			return false;

		var step = EnsurePipSize();
		var stepValue = step;

		var priceMove = Position > 0m ? lastPrice - entryPrice : entryPrice - lastPrice;
		var pipMove = step > 0m ? priceMove / step : priceMove;
		var currencyPnL = pipMove * stepValue * Math.Abs(Position);

		_maxBasketPnL = Math.Max(_maxBasketPnL, currencyPnL);
		_minBasketPnL = Math.Min(_minBasketPnL, currencyPnL);

		var shouldCloseForProfit = BasketProfitCurrency > 0m && currencyPnL >= BasketProfitCurrency;
		var shouldCloseForLoss = BasketLossCurrency > 0m && currencyPnL <= -BasketLossCurrency;

		if (shouldCloseForProfit || shouldCloseForLoss)
		{
			CloseAllPositions();
			return true;
		}

		return false;
	}

	private void CloseAllPositions()
	{
		if (Position > 0m)
		{
			SellMarket(Position);
		}
		else if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
		}

		ResetPositionTracking();
	}

	private void ResetPositionTracking()
	{
		_breakEvenPrice = null;
		_trailingStopPrice = null;
		_highestPrice = 0m;
		_lowestPrice = 0m;
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.Hour;

		if (StartHour == StopHour)
			return true;

		if (StartHour < StopHour)
			return hour >= StartHour && hour < StopHour;

		// Overnight window that crosses midnight.
		return hour >= StartHour || hour < StopHour;
	}

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

	private decimal EnsurePipSize()
	{
		if (_pipSize <= 0m)
			_pipSize = CalculatePipSize();

		return _pipSize;
	}
}