Дивергенция Трейдер (классическая конверсия)
Эта стратегия переносит советника MetaTrader 4 Divergence Trader на высокоуровневый API StockSharp. Рассчитываются две простые скользящие средние по выбранной цене свечи (по умолчанию — открытие). Система отслеживает, как расстояние между быстрой и медленной средними меняется от бара к бару:
- Когда спред расширяется вверх и значение дивергенции находится между параметрами Buy Threshold и Stay Out Threshold, открывается длинная позиция либо закрывается существующая короткая.
- Когда спред расширяется вниз в зеркальном диапазоне, открывается короткая позиция либо закрывается существующая длинная.
Обрабатываются только завершённые свечи, что полностью повторяет логику оригинального советника. Управление позициями выполняется через высокоуровневые вызовы BuyMarket и SellMarket.
Торговые правила
- Подписаться на выбранный тип свечей и рассчитать две SMA с периодами Fast SMA и Slow SMA.
- Вычислить текущий спред (
fast - slow) и сравнить его с предыдущим значением для получения дивергенции. - Входить в покупку, если дивергенция положительная, ≥ Buy Threshold и ≤ Stay Out Threshold.
- Входить в продажу, если дивергенция отрицательная, ≤
-Buy Thresholdи ≥-Stay Out Threshold. - Переворачивать позицию при появлении противоположного сигнала.
- Ограничивать новые входы локальным временным окном между 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import SimpleMovingAverage
class divergence_trader_classic_strategy(Strategy):
def __init__(self):
super(divergence_trader_classic_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 0.1) \
.SetDisplay("Order Volume", "Volume used when opening a new position", "Trading")
self._fast_period = self.Param("FastPeriod", 7) \
.SetDisplay("Fast SMA", "Period for the fast simple moving average", "Indicators")
self._slow_period = self.Param("SlowPeriod", 88) \
.SetDisplay("Slow SMA", "Period for the slow simple moving average", "Indicators")
self._buy_threshold = self.Param("BuyThreshold", 10.0) \
.SetDisplay("Buy Threshold", "Minimal divergence needed to allow long entries", "Signals")
self._stay_out_threshold = self.Param("StayOutThreshold", 1000.0) \
.SetDisplay("Stay Out Threshold", "Upper divergence bound disabling new entries", "Signals")
self._take_profit_pips = self.Param("TakeProfitPips", 0.0) \
.SetDisplay("Take Profit (pips)", "Distance in pips used to exit winners", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 0.0) \
.SetDisplay("Stop Loss (pips)", "Maximum adverse excursion tolerated", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 9999.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing distance; 9999 disables trailing", "Risk")
self._break_even_pips = self.Param("BreakEvenPips", 9999.0) \
.SetDisplay("Break-Even Trigger (pips)", "Profit in pips before moving stop to break-even", "Risk")
self._break_even_buffer_pips = self.Param("BreakEvenBufferPips", 2.0) \
.SetDisplay("Break-Even Buffer (pips)", "Buffer in pips added to the break-even stop", "Risk")
self._basket_profit_currency = self.Param("BasketProfitCurrency", 75.0) \
.SetDisplay("Basket Profit", "Floating profit that forces closing all positions", "Basket")
self._basket_loss_currency = self.Param("BasketLossCurrency", 9999.0) \
.SetDisplay("Basket Loss", "Floating loss that forces closing all positions", "Basket")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "Hour when trading becomes active (0-23)", "Schedule")
self._stop_hour = self.Param("StopHour", 24) \
.SetDisplay("Stop Hour", "Hour when trading stops accepting new entries (1-24)", "Schedule")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used to calculate signals", "General")
self._previous_spread = None
self._pip_size = 0.0
self._max_basket_pnl = 0.0
self._min_basket_pnl = 0.0
self._break_even_price = None
self._trailing_stop_price = None
self._highest_price = 0.0
self._lowest_price = 0.0
self._entry_price = 0.0
@property
def OrderVolume(self):
return self._order_volume.Value
@property
def FastPeriod(self):
return self._fast_period.Value
@property
def SlowPeriod(self):
return self._slow_period.Value
@property
def BuyThreshold(self):
return self._buy_threshold.Value
@property
def StayOutThreshold(self):
return self._stay_out_threshold.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def BreakEvenPips(self):
return self._break_even_pips.Value
@property
def BreakEvenBufferPips(self):
return self._break_even_buffer_pips.Value
@property
def BasketProfitCurrency(self):
return self._basket_profit_currency.Value
@property
def BasketLossCurrency(self):
return self._basket_loss_currency.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def StopHour(self):
return self._stop_hour.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(divergence_trader_classic_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._fast_sma = SimpleMovingAverage()
self._fast_sma.Length = self.FastPeriod
self._slow_sma = SimpleMovingAverage()
self._slow_sma.Length = self.SlowPeriod
self._previous_spread = None
self._break_even_price = None
self._trailing_stop_price = None
self._highest_price = 0.0
self._lowest_price = 0.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._fast_sma, self._slow_sma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_value = float(fast_value)
slow_value = float(slow_value)
self._manage_open_position(candle)
if self._evaluate_basket_pnl(float(candle.ClosePrice)):
self._previous_spread = fast_value - slow_value
return
if not self._fast_sma.IsFormed or not self._slow_sma.IsFormed:
self._previous_spread = fast_value - slow_value
return
current_spread = fast_value - slow_value
divergence = current_spread - self._previous_spread if self._previous_spread is not None else 0.0
self._previous_spread = current_spread
if not self._is_within_trading_hours(candle.CloseTime):
return
ov = float(self.OrderVolume)
if ov <= 0:
return
buy_thr = float(self.BuyThreshold)
stay_out = float(self.StayOutThreshold)
if divergence >= buy_thr and divergence <= stay_out:
if self.Position < 0:
self.BuyMarket(abs(self.Position))
if self.Position <= 0:
self._reset_position_tracking()
self.BuyMarket(ov)
elif divergence <= -buy_thr and divergence >= -stay_out:
if self.Position > 0:
self.SellMarket(self.Position)
if self.Position >= 0:
self._reset_position_tracking()
self.SellMarket(ov)
def _manage_open_position(self, candle):
if self.Position == 0:
self._reset_position_tracking()
return
entry_price = self._entry_price
if entry_price == 0:
return
pip_size = self._ensure_pip_size()
tp_pips = float(self.TakeProfitPips)
sl_pips = float(self.StopLossPips)
be_pips = float(self.BreakEvenPips)
be_buffer = float(self.BreakEvenBufferPips)
trail_pips = float(self.TrailingStopPips)
take_profit_distance = tp_pips * pip_size if tp_pips > 0 else 0.0
stop_loss_distance = sl_pips * pip_size if sl_pips > 0 else 0.0
break_even_distance = be_pips * pip_size if be_pips > 0 and be_pips < 9000 else 0.0
break_even_buffer = be_buffer * pip_size if be_buffer > 0 else 0.0
trailing_distance = trail_pips * pip_size if trail_pips > 0 and trail_pips < 9000 else 0.0
abs_position = abs(self.Position)
high_price = float(candle.HighPrice)
low_price = float(candle.LowPrice)
close_price = float(candle.ClosePrice)
if self.Position > 0:
if self._highest_price == 0:
self._highest_price = entry_price
self._highest_price = max(self._highest_price, high_price)
profit_distance = close_price - entry_price
if break_even_distance > 0 and profit_distance >= break_even_distance and self._break_even_price is None:
self._break_even_price = entry_price + break_even_buffer
if self._break_even_price is not None and low_price <= self._break_even_price:
self.SellMarket(abs_position)
self._reset_position_tracking()
return
if trailing_distance > 0 and profit_distance >= trailing_distance:
candidate = self._highest_price - trailing_distance
if self._trailing_stop_price is None or candidate > self._trailing_stop_price:
self._trailing_stop_price = candidate
if self._trailing_stop_price is not None and low_price <= self._trailing_stop_price:
self.SellMarket(abs_position)
self._reset_position_tracking()
return
if take_profit_distance > 0 and profit_distance >= take_profit_distance:
self.SellMarket(abs_position)
self._reset_position_tracking()
return
if stop_loss_distance > 0 and low_price <= entry_price - stop_loss_distance:
self.SellMarket(abs_position)
self._reset_position_tracking()
elif self.Position < 0:
if self._lowest_price == 0:
self._lowest_price = entry_price
self._lowest_price = min(self._lowest_price, low_price)
profit_distance = entry_price - close_price
if break_even_distance > 0 and profit_distance >= break_even_distance and self._break_even_price is None:
self._break_even_price = entry_price - break_even_buffer
if self._break_even_price is not None and high_price >= self._break_even_price:
self.BuyMarket(abs_position)
self._reset_position_tracking()
return
if trailing_distance > 0 and profit_distance >= trailing_distance:
candidate = self._lowest_price + trailing_distance
if self._trailing_stop_price is None or candidate < self._trailing_stop_price:
self._trailing_stop_price = candidate
if self._trailing_stop_price is not None and high_price >= self._trailing_stop_price:
self.BuyMarket(abs_position)
self._reset_position_tracking()
return
if take_profit_distance > 0 and profit_distance >= take_profit_distance:
self.BuyMarket(abs_position)
self._reset_position_tracking()
return
if stop_loss_distance > 0 and high_price >= entry_price + stop_loss_distance:
self.BuyMarket(abs_position)
self._reset_position_tracking()
def _evaluate_basket_pnl(self, last_price):
bp = float(self.BasketProfitCurrency)
bl = float(self.BasketLossCurrency)
if bp <= 0 and bl <= 0:
return False
if self.Position == 0:
return False
entry_price = self._entry_price
if entry_price == 0:
return False
step = self._ensure_pip_size()
price_move = last_price - entry_price if self.Position > 0 else entry_price - last_price
pip_move = price_move / step if step > 0 else price_move
currency_pnl = pip_move * step * abs(self.Position)
self._max_basket_pnl = max(self._max_basket_pnl, currency_pnl)
self._min_basket_pnl = min(self._min_basket_pnl, currency_pnl)
should_close_profit = bp > 0 and currency_pnl >= bp
should_close_loss = bl > 0 and currency_pnl <= -bl
if should_close_profit or should_close_loss:
self._close_all_positions()
return True
return False
def _close_all_positions(self):
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
self._reset_position_tracking()
def _reset_position_tracking(self):
self._break_even_price = None
self._trailing_stop_price = None
self._highest_price = 0.0
self._lowest_price = 0.0
def _is_within_trading_hours(self, time):
hour = time.Hour
start = self.StartHour
stop = self.StopHour
if start == stop:
return True
if start < stop:
return hour >= start and hour < stop
return hour >= start or hour < stop
def _calculate_pip_size(self):
ps = self.Security.PriceStep if self.Security is not None else None
step = float(ps) if ps is not None else 0.0
return step if step > 0 else 0.0001
def _ensure_pip_size(self):
if self._pip_size <= 0:
self._pip_size = self._calculate_pip_size()
return self._pip_size
def OnOwnTradeReceived(self, trade):
super(divergence_trader_classic_strategy, self).OnOwnTradeReceived(trade)
if self.Position != 0 and self._entry_price == 0:
self._entry_price = float(trade.Trade.Price)
if self.Position == 0:
self._entry_price = 0.0
def OnReseted(self):
super(divergence_trader_classic_strategy, self).OnReseted()
self._previous_spread = None
self._pip_size = 0.0
self._max_basket_pnl = 0.0
self._min_basket_pnl = 0.0
self._break_even_price = None
self._trailing_stop_price = None
self._highest_price = 0.0
self._lowest_price = 0.0
self._entry_price = 0.0
def CreateClone(self):
return divergence_trader_classic_strategy()