ダイバージェンストレーダー(クラシックコンバージョン)
この戦略は、StockSharp の高レベル API 内の MetaTrader 4 エキスパート アドバイザー Divergence Trader の動作を再現します。選択したローソク足価格に基づいて 2 つの単純な移動平均が計算されます (デフォルトではオープン)。システムは、高速平均と低速平均の間の距離がバーごとにどのように変化するかを監視します。
- スプレッドが上向きに拡大し、乖離値が 買い閾値 と *ステイアウト閾値 * の間に留まる場合、ロングポジションがオープンされるか、既存のショートポジションがカバーされます。
- スプレッドがミラーリングされたしきい値内で下側に拡大すると、ショートポジションが入力されるか、既存のロング取引がクローズされます。
元のエキスパートアドバイザーのバーごとの処理と一致する、完成したローソク足のみが使用されます。すべての管理ルールは、イベント駆動型の高レベル呼び出し (BuyMarket / SellMarket) で実装されます。
取引ルール
- 設定されたローソク足タイプをサブスクライブし、Fast SMA と Slow SMA の期間で 2 つの SMA を計算します。
- 現在のスプレッド (
fast - slow) を計算し、前のスプレッドと比較して発散値を取得します。 - 乖離がプラスで、買い閾値以上、ステイアウト閾値以下の場合はロングと入力します。
- 発散が負の場合、
-Buy Threshold以下、かつ-Stay Out Threshold以上の場合は short を入力します。 - 反対のシグナルが現れるたびに、既存のポジションを反転させます。
- 新しいエントリを 開始時間 と 終了時間 の間のローカル タイム ウィンドウに制限します (午前 0 時を超えるラップはサポートされています)。
リスク管理
- オプションの固定 テイクプロフィット (pips) および ストップロス (pips) レベルは、ローソク足の高値/安値で監視されます。
- 損益分岐点トリガー (ピップ) は、ポジションが指定されたピップ数を獲得すると、ストップを
entry ± Break-Even Bufferに移動します。 - トレーリングストップ (pips) は、トレードで利益が得られると最も有利な価格に従います。 9999 を設定すると、トレーリング ストップが無効になり、元の EA のデフォルトが反映されます。
- バスケット管理は、未実現損益が バスケット利益 に達するか、口座通貨で
-Basket Lossを下回ると、未決済のエクスポージャーをすべてクローズします。
パラメーター
| パラメータ | 説明 |
|---|---|
Order Volume |
新しいポジションをオープンするときに使用されるボリューム。 |
Fast SMA / Slow SMA |
2 つの単純移動平均の期間。 |
Applied Price |
ローソク足コンポーネントは両方の移動平均に転送されます。 |
Buy Threshold |
より低いダイバージェンス境界によりロングトレードが可能になります。 |
Stay Out Threshold |
それを超えると新たな取引が行われない上限の発散境界。 |
Take Profit (pips) / Stop Loss (pips) |
ピップ単位で測定されるオプションのハードエグジット。 |
Trailing Stop (pips) |
トレードで利益が出た後に適用されるトレーリング距離。 |
Break-Even Trigger (pips) |
損益分岐点にストップを移動する前に必要な利益 (pips)。 |
Break-Even Buffer (pips) |
損益分岐点ストップに追加のバッファーが追加されました。 |
Basket Profit / Basket Loss |
アカウント通貨でのグローバル資本制限。 |
Start Hour / Stop Hour |
ローカル取引セッションウィンドウ。 |
Candle Type |
キャンドルのサブスクリプションと計算に使用される時間枠。 |
使用上の注意
- ストラテジーを証券にアタッチし、元のチャートの時間枠と一致するローソク足のタイプを設定します。
- pip ベースのコントロールが正しく機能するように、楽器の
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()