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
- Suscríbase al tipo de vela configurado y calcule dos SMA con períodos Rápido SMA y Lento SMA.
- Calcule el diferencial actual (
fast - slow) y compárelo con el diferencial anterior para obtener el valor de divergencia. - Ingrese largo si la divergencia es positiva, mayor o igual a Umbral de compra y menor o igual a Umbral de permanencia fuera.
- Ingrese short si la divergencia es negativa, menor o igual a
-Buy Thresholdy mayor o igual a-Stay Out Threshold. - Invierta una posición existente cada vez que aparezca una señal opuesta.
- 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 Bufferuna 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 Lossen 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/StepPricedel 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;
}
}
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()