Открыть на GitHub

Дивергенция Трейдер (классическая конверсия)

Эта стратегия переносит советника MetaTrader 4 Divergence Trader на высокоуровневый API StockSharp. Рассчитываются две простые скользящие средние по выбранной цене свечи (по умолчанию — открытие). Система отслеживает, как расстояние между быстрой и медленной средними меняется от бара к бару:

  • Когда спред расширяется вверх и значение дивергенции находится между параметрами Buy Threshold и Stay Out Threshold, открывается длинная позиция либо закрывается существующая короткая.
  • Когда спред расширяется вниз в зеркальном диапазоне, открывается короткая позиция либо закрывается существующая длинная.

Обрабатываются только завершённые свечи, что полностью повторяет логику оригинального советника. Управление позициями выполняется через высокоуровневые вызовы BuyMarket и SellMarket.

Торговые правила

  1. Подписаться на выбранный тип свечей и рассчитать две SMA с периодами Fast SMA и Slow SMA.
  2. Вычислить текущий спред (fast - slow) и сравнить его с предыдущим значением для получения дивергенции.
  3. Входить в покупку, если дивергенция положительная, ≥ Buy Threshold и ≤ Stay Out Threshold.
  4. Входить в продажу, если дивергенция отрицательная, ≤ -Buy Threshold и ≥ -Stay Out Threshold.
  5. Переворачивать позицию при появлении противоположного сигнала.
  6. Ограничивать новые входы локальным временным окном между Start Hour и Stop Hour (поддерживается переход через полночь).

Управление рисками

  • Опциональные Take Profit (pips) и Stop Loss (pips) контролируются по максимуму/минимуму свечи.
  • Параметр Break-Even Trigger (pips) переносит стоп в область безубытка entry ± Break-Even Buffer, когда позиция достигает заданного количества пунктов.
  • Trailing Stop (pips) сопровождает цену при движении в прибыль. Значение 9999 отключает трейлинг, как и в оригинальном советнике.
  • Управление корзиной закрывает все позиции при достижении Basket Profit или падении ниже -Basket Loss по нереализованной прибыли/убытку в валюте счёта.

Параметры

Параметр Описание
Order Volume Объём, используемый при открытии новой позиции.
Fast SMA / Slow SMA Периоды двух простых скользящих средних.
Applied Price Компонента свечи, поступающая в расчёт средних.
Buy Threshold Нижняя граница дивергенции, позволяющая длинные сделки.
Stay Out Threshold Верхняя граница дивергенции, выше которой входы блокируются.
Take Profit (pips) / Stop Loss (pips) Жёсткие выходы, измеряемые в пунктах.
Trailing Stop (pips) Расстояние трейлинг-стопа после выхода в прибыль.
Break-Even Trigger (pips) Прибыль в пунктах для переноса стопа в безубыток.
Break-Even Buffer (pips) Дополнительный буфер для стопа безубытка.
Basket Profit / Basket Loss Глобальные ограничения по плавающей прибыли/убытку.
Start Hour / Stop Hour Локальное торговое время.
Candle Type Таймфрейм свечей для расчёта сигналов.

Рекомендации по использованию

  • Привяжите стратегию к инструменту и выберите таймфрейм, соответствующий исходному графику.
  • Убедитесь, что свойства инструмента PriceStep/StepPrice корректны — это необходимо для пунктовых вычислений.
  • Чтобы отключить функции вроде трейлинг-стопа или переноса в безубыток, оставьте их параметры равными 9999 или нулю.
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;
	}
}