Ver no GitHub

Trader de Divergência (Conversão Clássica)

Esta estratégia reproduz o comportamento do MetaTrader 4 consultor especialista Divergence Trader dentro do StockSharp alto nível API. Duas médias móveis simples são calculadas sobre o preço da vela selecionada (aberta por padrão). O sistema monitora como a distância entre as médias rápida e lenta muda de uma barra para outra:

  • Quando o spread aumenta para cima e o valor da divergência permanece entre o Limiar de Compra e o Limiar de Ficar Fora, uma posição longa é aberta ou uma posição curta existente é coberta.
  • Quando o spread aumenta para baixo dentro dos limites espelhados, uma posição curta é inserida ou uma negociação longa existente é fechada.

Apenas velas completas são usadas, correspondendo ao processamento barra por barra do consultor especialista original. Todas as regras de gerenciamento são implementadas com chamadas de alto nível orientadas a eventos (BuyMarket / SellMarket).

Regras de negociação

  1. Assine o tipo de vela configurado e calcule dois SMAs com períodos Rápido SMA e Lento SMA.
  2. Calcule o spread atual (fast - slow) e compare-o com o spread anterior para obter o valor da divergência.
  3. Insira comprado se a divergência for positiva, maior ou igual a Limiar de Compra e menor ou igual a Limiar de Fique Fora.
  4. Insira short se a divergência for negativa, menor ou igual a -Buy Threshold e maior ou igual a -Stay Out Threshold.
  5. Inverta uma posição existente sempre que aparecer um sinal oposto.
  6. Restrinja novas entradas à janela de horário local entre Start Hour e Stop Hour (é possível passar da meia-noite).

Gestão de risco

  • Os níveis fixos opcionais de Take Profit (pips) e Stop Loss (pips) são monitorados nas máximas/mínimas das velas.
  • O Break-Even Trigger (pips) move o stop para entry ± Break-Even Buffer assim que a posição ganha o número especificado de pips.
  • O Trailing Stop (pips) segue o preço mais favorável quando a negociação gera lucro. A configuração 9999 desativa o trailing stop, espelhando o padrão EA original.
  • O gerenciamento da cesta fecha todas as exposições abertas quando o P&L não realizado atinge Basket Profit ou cai abaixo de -Basket Loss na moeda da conta.

Parâmetros

Parâmetro Descrição
Order Volume Volume usado quando uma nova posição é aberta.
Fast SMA / Slow SMA Períodos para as duas médias móveis simples.
Applied Price Componente de vela encaminhado para ambas as médias móveis.
Buy Threshold Limite de divergência inferior que permite negociações longas.
Stay Out Threshold Limite superior de divergência acima do qual nenhuma nova negociação é realizada.
Take Profit (pips) / Stop Loss (pips) Saídas rígidas opcionais medidas em pips.
Trailing Stop (pips) Distância final aplicada depois que a negociação se torna lucrativa.
Break-Even Trigger (pips) Lucro em pips necessário antes de mover o stop para o ponto de equilíbrio.
Break-Even Buffer (pips) Buffer adicional adicionado ao ponto de equilíbrio.
Basket Profit / Basket Loss Limites de patrimônio global na moeda da conta.
Start Hour / Stop Hour Janela da sessão de negociação local.
Candle Type Prazo usado para assinatura e cálculos de velas.

Notas de uso

  • Anexe a estratégia a um título e defina o tipo de vela que corresponda ao período original do gráfico.
  • Certifique-se de que as propriedades PriceStep/StepPrice do instrumento estejam configuradas para que os controles baseados em pip funcionem corretamente.
  • Para desabilitar recursos como trailing stop ou mudança de ponto de equilíbrio, mantenha seus parâmetros no valor sentinela legado (9999) ou zero.
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;
	}
}