趋势背离交易者(经典转换)
该策略将 MetaTrader 4 的 Divergence Trader 专家顾问迁移到 StockSharp 的高级 API。系统在所选蜡烛价格(默认使用开盘价)上计算两条简单移动平均线,并观察快速均线与慢速均线之间的距离如何随每根柱线变化:
- 当价差向上扩大且背离值处于 Buy Threshold 与 Stay Out Threshold 之间时,开多仓或平掉现有空单。
- 当价差向下扩大且背离值处于上述区间的相反范围内时,开空仓或平掉现有多单。
策略仅处理已完成的蜡烛,与原始 EA 的逐棒逻辑保持一致。所有交易管理均通过高级方法 BuyMarket / SellMarket 完成。
交易规则
- 订阅配置的蜡烛类型,并按 Fast SMA 与 Slow SMA 周期计算两条 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) 在仓位盈利达到指定点数后,将止损移动到
入场价 ± Break-Even Buffer。 - Trailing Stop (pips) 在交易盈利后跟踪价格;设为 9999 即与原 EA 一样关闭该功能。
- 当浮动盈亏达到 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)或 0。
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()