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>
/// EMA crossover strategy with hedged stop orders and trailing management.
/// Converted from the MQL version of "EMA Cross Contest Hedged".
/// </summary>
public class EmaCrossContestHedgedStrategy : Strategy
{
public enum TradeBarOptions
{
Current,
Previous
}
private readonly StrategyParam<int> _pendingOrderCount;
private readonly StrategyParam<int> _macdFastLength;
private readonly StrategyParam<int> _macdSlowLength;
private readonly StrategyParam<int> _macdSignalLength;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _hedgeLevelPips;
private readonly StrategyParam<bool> _closeOppositePositions;
private readonly StrategyParam<bool> _useMacdFilter;
private readonly StrategyParam<int> _pendingExpirationSeconds;
private readonly StrategyParam<int> _shortMaPeriod;
private readonly StrategyParam<int> _longMaPeriod;
private readonly StrategyParam<TradeBarOptions> _tradeBar;
private readonly StrategyParam<DataType> _candleType;
private decimal? _emaShortLast;
private decimal? _emaShortPrevLast;
private decimal? _emaLongLast;
private decimal? _emaLongPrevLast;
private decimal? _macdLast;
private decimal _currentVolume;
private decimal _entryPrice;
private decimal? _longStop;
private decimal? _longTakeProfit;
private decimal? _shortStop;
private decimal? _shortTakeProfit;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
private readonly List<PendingOrder> _pendingOrders = new();
private sealed class PendingOrder
{
public Sides Side { get; init; }
public decimal Price { get; init; }
public decimal? StopLoss { get; init; }
public decimal? TakeProfit { get; init; }
public DateTimeOffset ExpireTime { get; init; }
}
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
public int HedgeLevelPips
{
get => _hedgeLevelPips.Value;
set => _hedgeLevelPips.Value = value;
}
public bool CloseOppositePositions
{
get => _closeOppositePositions.Value;
set => _closeOppositePositions.Value = value;
}
public bool UseMacdFilter
{
get => _useMacdFilter.Value;
set => _useMacdFilter.Value = value;
}
/// <summary>
/// Number of pending stop orders per direction.
/// </summary>
public int PendingOrderCount
{
get => _pendingOrderCount.Value;
set => _pendingOrderCount.Value = value;
}
public int PendingExpirationSeconds
{
get => _pendingExpirationSeconds.Value;
set => _pendingExpirationSeconds.Value = value;
}
/// <summary>
/// Fast moving average length for the MACD filter.
/// </summary>
public int MacdFastLength
{
get => _macdFastLength.Value;
set => _macdFastLength.Value = value;
}
/// <summary>
/// Slow moving average length for the MACD filter.
/// </summary>
public int MacdSlowLength
{
get => _macdSlowLength.Value;
set => _macdSlowLength.Value = value;
}
/// <summary>
/// Signal moving average length for the MACD filter.
/// </summary>
public int MacdSignalLength
{
get => _macdSignalLength.Value;
set => _macdSignalLength.Value = value;
}
public int ShortMaPeriod
{
get => _shortMaPeriod.Value;
set => _shortMaPeriod.Value = value;
}
public int LongMaPeriod
{
get => _longMaPeriod.Value;
set => _longMaPeriod.Value = value;
}
public TradeBarOptions TradeBar
{
get => _tradeBar.Value;
set => _tradeBar.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public EmaCrossContestHedgedStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetDisplay("Order Volume", "Order size", "General")
.SetGreaterThanZero();
_stopLossPips = Param(nameof(StopLossPips), 140)
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 120)
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 30)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 1)
.SetDisplay("Trailing Step (pips)", "Minimum profit before trailing adjusts", "Risk");
_hedgeLevelPips = Param(nameof(HedgeLevelPips), 6)
.SetDisplay("Hedge Level (pips)", "Distance between hedging stop orders", "Orders");
_closeOppositePositions = Param(nameof(CloseOppositePositions), false)
.SetDisplay("Close Opposite", "Close positions on opposite crossover", "Risk");
_useMacdFilter = Param(nameof(UseMacdFilter), false)
.SetDisplay("Use MACD", "Require MACD confirmation", "Filters");
_pendingOrderCount = Param(nameof(PendingOrderCount), 1)
.SetGreaterThanZero()
.SetDisplay("Pending Orders", "Pending stop orders per side", "Orders");
_pendingExpirationSeconds = Param(nameof(PendingExpirationSeconds), 65535)
.SetDisplay("Pending Expiration (s)", "Lifetime of hedging stop orders in seconds", "Orders");
_macdFastLength = Param(nameof(MacdFastLength), 4)
.SetGreaterThanZero()
.SetDisplay("MACD Fast Length", "Fast EMA length for MACD", "Indicators");
_macdSlowLength = Param(nameof(MacdSlowLength), 24)
.SetGreaterThanZero()
.SetDisplay("MACD Slow Length", "Slow EMA length for MACD", "Indicators");
_macdSignalLength = Param(nameof(MacdSignalLength), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Signal Length", "Signal EMA length for MACD", "Indicators");
_shortMaPeriod = Param(nameof(ShortMaPeriod), 4)
.SetGreaterThanZero()
.SetDisplay("Short EMA Period", "Fast EMA length", "Indicators");
_longMaPeriod = Param(nameof(LongMaPeriod), 24)
.SetGreaterThanZero()
.SetDisplay("Long EMA Period", "Slow EMA length", "Indicators");
_tradeBar = Param(nameof(TradeBar), TradeBarOptions.Previous)
.SetDisplay("Trade Bar", "Use current or previous bar for signals", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for calculations", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_emaShortLast = null;
_emaShortPrevLast = null;
_emaLongLast = null;
_emaLongPrevLast = null;
_macdLast = null;
_currentVolume = 0m;
_entryPrice = 0m;
_longStop = null;
_longTakeProfit = null;
_shortStop = null;
_shortTakeProfit = null;
_longTrailingStop = null;
_shortTrailingStop = null;
_pendingOrders.Clear();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (ShortMaPeriod >= LongMaPeriod)
throw new InvalidOperationException("Short EMA period must be less than long EMA period.");
Volume = OrderVolume;
var shortEma = new ExponentialMovingAverage { Length = ShortMaPeriod };
var longEma = new ExponentialMovingAverage { Length = LongMaPeriod };
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFastLength },
LongMa = { Length = MacdSlowLength }
},
SignalMa = { Length = MacdSignalLength }
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(shortEma, longEma, macd, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, shortEma);
DrawIndicator(area, longEma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue shortValue, IIndicatorValue longValue, IIndicatorValue macdValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!shortValue.IsFinal || !longValue.IsFinal)
return;
var emaShort = shortValue.ToDecimal();
var emaLong = longValue.ToDecimal();
decimal? macdCurrent = null;
if (macdValue.IsFinal)
{
var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
if (macdTyped.Macd is decimal macdLine)
macdCurrent = macdLine;
}
ProcessPendingOrders(candle);
var cross = DetectCross(emaShort, emaLong);
decimal? macdFilterValue = null;
if (UseMacdFilter)
{
macdFilterValue = TradeBar == TradeBarOptions.Current ? macdCurrent : _macdLast;
if (!macdFilterValue.HasValue)
{
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
}
if (!IsFormedAndOnlineAndAllowTrading())
{
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
if (_currentVolume > 0m)
{
if (CloseOppositePositions && cross == 2)
{
ExitLong();
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
if (CheckLongStops(candle))
{
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
}
else if (_currentVolume < 0m)
{
if (CloseOppositePositions && cross == 1)
{
ExitShort();
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
if (CheckShortStops(candle))
{
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
}
if (_currentVolume == 0m)
{
if (cross == 1 && (!UseMacdFilter || macdFilterValue >= 0m))
{
EnterLong(candle.ClosePrice, candle.CloseTime);
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
if (cross == 2 && (!UseMacdFilter || macdFilterValue <= 0m))
{
EnterShort(candle.ClosePrice, candle.CloseTime);
UpdateHistory(emaShort, emaLong, macdCurrent);
return;
}
}
UpdateHistory(emaShort, emaLong, macdCurrent);
}
private void ProcessPendingOrders(ICandleMessage candle)
{
if (_pendingOrders.Count == 0)
return;
var now = candle.CloseTime;
var orders = _pendingOrders.ToArray();
foreach (var order in orders)
{
if (order == null)
continue;
if (order.ExpireTime <= now)
{
_pendingOrders.Remove(order);
continue;
}
var triggered = order.Side == Sides.Buy
? candle.HighPrice >= order.Price
: candle.LowPrice <= order.Price;
if (!triggered)
continue;
if (!_pendingOrders.Remove(order))
continue;
if (order.Side == Sides.Buy)
{
BuyMarket(OrderVolume);
RegisterLongEntry(order.Price, OrderVolume, order.StopLoss, order.TakeProfit);
}
else
{
SellMarket(OrderVolume);
RegisterShortEntry(order.Price, OrderVolume, order.StopLoss, order.TakeProfit);
}
}
}
private void EnterLong(decimal price, DateTimeOffset time)
{
BuyMarket(OrderVolume);
RegisterLongEntry(price, OrderVolume,
StopLossPips > 0 ? price - PipToPrice(StopLossPips) : null,
TakeProfitPips > 0 ? price + PipToPrice(TakeProfitPips) : null);
_shortStop = null;
_shortTakeProfit = null;
_shortTrailingStop = null;
CreatePendingOrders(time, price, Sides.Buy);
}
private void EnterShort(decimal price, DateTimeOffset time)
{
SellMarket(OrderVolume);
RegisterShortEntry(price, OrderVolume,
StopLossPips > 0 ? price + PipToPrice(StopLossPips) : null,
TakeProfitPips > 0 ? price - PipToPrice(TakeProfitPips) : null);
_longStop = null;
_longTakeProfit = null;
_longTrailingStop = null;
CreatePendingOrders(time, price, Sides.Sell);
}
private void RegisterLongEntry(decimal price, decimal volume, decimal? stop, decimal? take)
{
var previousVolume = _currentVolume;
_currentVolume += volume;
if (previousVolume <= 0m)
_entryPrice = price;
else
_entryPrice = ((previousVolume * _entryPrice) + (volume * price)) / _currentVolume;
if (stop.HasValue)
_longStop = _longStop.HasValue ? Math.Max(_longStop.Value, stop.Value) : stop;
if (take.HasValue)
_longTakeProfit = _longTakeProfit.HasValue ? Math.Max(_longTakeProfit.Value, take.Value) : take;
_longTrailingStop = null;
}
private void RegisterShortEntry(decimal price, decimal volume, decimal? stop, decimal? take)
{
var previousVolume = _currentVolume;
_currentVolume -= volume;
if (previousVolume >= 0m)
_entryPrice = price;
else
_entryPrice = ((Math.Abs(previousVolume) * _entryPrice) + (volume * price)) / Math.Abs(_currentVolume);
if (stop.HasValue)
_shortStop = _shortStop.HasValue ? Math.Min(_shortStop.Value, stop.Value) : stop;
if (take.HasValue)
_shortTakeProfit = _shortTakeProfit.HasValue ? Math.Min(_shortTakeProfit.Value, take.Value) : take;
_shortTrailingStop = null;
}
private bool CheckLongStops(ICandleMessage candle)
{
var trailingDistance = PipToPrice(TrailingStopPips);
var trailingStep = PipToPrice(TrailingStepPips);
if (TrailingStopPips > 0 && _currentVolume > 0m)
{
var profit = candle.ClosePrice - _entryPrice;
if (profit > trailingDistance + trailingStep)
{
var minAdvance = candle.ClosePrice - (trailingDistance + trailingStep);
var newStop = candle.ClosePrice - trailingDistance;
if (!_longTrailingStop.HasValue || _longTrailingStop.Value < minAdvance)
_longTrailingStop = newStop;
}
}
var effectiveStop = _longStop;
if (_longTrailingStop.HasValue)
effectiveStop = effectiveStop.HasValue ? Math.Max(effectiveStop.Value, _longTrailingStop.Value) : _longTrailingStop;
if (effectiveStop.HasValue && candle.LowPrice <= effectiveStop.Value)
{
ExitLong();
return true;
}
if (_longTakeProfit.HasValue && candle.HighPrice >= _longTakeProfit.Value)
{
ExitLong();
return true;
}
return false;
}
private bool CheckShortStops(ICandleMessage candle)
{
var trailingDistance = PipToPrice(TrailingStopPips);
var trailingStep = PipToPrice(TrailingStepPips);
if (TrailingStopPips > 0 && _currentVolume < 0m)
{
var profit = _entryPrice - candle.ClosePrice;
if (profit > trailingDistance + trailingStep)
{
var maxAdvance = candle.ClosePrice + trailingDistance + trailingStep;
var newStop = candle.ClosePrice + trailingDistance;
if (!_shortTrailingStop.HasValue || _shortTrailingStop.Value > maxAdvance)
_shortTrailingStop = newStop;
}
}
var effectiveStop = _shortStop;
if (_shortTrailingStop.HasValue)
effectiveStop = effectiveStop.HasValue ? Math.Min(effectiveStop.Value, _shortTrailingStop.Value) : _shortTrailingStop;
if (effectiveStop.HasValue && candle.HighPrice >= effectiveStop.Value)
{
ExitShort();
return true;
}
if (_shortTakeProfit.HasValue && candle.LowPrice <= _shortTakeProfit.Value)
{
ExitShort();
return true;
}
return false;
}
private void ExitLong()
{
if (_currentVolume <= 0m)
return;
SellMarket(_currentVolume);
_currentVolume = 0m;
_entryPrice = 0m;
_longStop = null;
_longTakeProfit = null;
_longTrailingStop = null;
}
private void ExitShort()
{
if (_currentVolume >= 0m)
return;
BuyMarket(Math.Abs(_currentVolume));
_currentVolume = 0m;
_entryPrice = 0m;
_shortStop = null;
_shortTakeProfit = null;
_shortTrailingStop = null;
}
private int DetectCross(decimal emaShort, decimal emaLong)
{
decimal prevShort;
decimal prevLong;
decimal currentShort;
decimal currentLong;
if (TradeBar == TradeBarOptions.Current)
{
if (!_emaShortLast.HasValue || !_emaLongLast.HasValue)
return 0;
prevShort = _emaShortLast.Value;
prevLong = _emaLongLast.Value;
currentShort = emaShort;
currentLong = emaLong;
}
else
{
if (!_emaShortLast.HasValue || !_emaLongLast.HasValue || !_emaShortPrevLast.HasValue || !_emaLongPrevLast.HasValue)
return 0;
prevShort = _emaShortPrevLast.Value;
prevLong = _emaLongPrevLast.Value;
currentShort = _emaShortLast.Value;
currentLong = _emaLongLast.Value;
}
if (prevShort < prevLong && currentShort > currentLong)
return 1;
if (prevShort > prevLong && currentShort < currentLong)
return 2;
return 0;
}
private void UpdateHistory(decimal emaShort, decimal emaLong, decimal? macdCurrent)
{
_emaShortPrevLast = _emaShortLast;
_emaLongPrevLast = _emaLongLast;
_emaShortLast = emaShort;
_emaLongLast = emaLong;
if (macdCurrent.HasValue)
_macdLast = macdCurrent;
}
private decimal PipToPrice(int pips)
{
if (pips <= 0)
return 0m;
var step = Security?.PriceStep ?? 1m;
var decimals = Security?.Decimals ?? 0;
var multiplier = (decimals == 3 || decimals == 5) ? 10m : 1m;
return pips * step * multiplier;
}
private void CreatePendingOrders(DateTimeOffset time, decimal price, Sides side)
{
_pendingOrders.Clear();
if (HedgeLevelPips <= 0)
return;
var distance = PipToPrice(HedgeLevelPips);
if (distance <= 0m)
return;
var expiration = PendingExpirationSeconds > 0
? time + TimeSpan.FromSeconds(PendingExpirationSeconds)
: DateTimeOffset.MaxValue;
var stopOffset = StopLossPips > 0 ? PipToPrice(StopLossPips) : 0m;
var takeOffset = TakeProfitPips > 0 ? PipToPrice(TakeProfitPips) : 0m;
for (var i = 1; i <= PendingOrderCount; i++)
{
var levelPrice = side == Sides.Buy
? price + distance * i
: price - distance * i;
decimal? stop = null;
decimal? take = null;
if (StopLossPips > 0)
stop = side == Sides.Buy
? levelPrice - stopOffset
: levelPrice + stopOffset;
if (TakeProfitPips > 0)
take = side == Sides.Buy
? levelPrice + takeOffset
: levelPrice - takeOffset;
_pendingOrders.Add(new PendingOrder
{
Side = side,
Price = levelPrice,
StopLoss = stop,
TakeProfit = take,
ExpireTime = expiration
});
}
}
}
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.Indicators import ExponentialMovingAverage, MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class ema_cross_contest_hedged_strategy(Strategy):
"""
EMA crossover strategy with hedged stop orders and trailing management.
Converted from the MQL EMA Cross Contest Hedged.
"""
def __init__(self):
super(ema_cross_contest_hedged_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe for calculations", "General")
self._stop_loss_pips = self.Param("StopLossPips", 140) \
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 120) \
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 30) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 1) \
.SetDisplay("Trailing Step (pips)", "Minimum profit before trailing adjusts", "Risk")
self._hedge_level_pips = self.Param("HedgeLevelPips", 6) \
.SetDisplay("Hedge Level (pips)", "Distance between hedging stop orders", "Orders")
self._close_opposite = self.Param("CloseOppositePositions", False) \
.SetDisplay("Close Opposite", "Close positions on opposite crossover", "Risk")
self._use_macd_filter = self.Param("UseMacdFilter", False) \
.SetDisplay("Use MACD", "Require MACD confirmation", "Filters")
self._pending_order_count = self.Param("PendingOrderCount", 1) \
.SetDisplay("Pending Orders", "Pending stop orders per side", "Orders")
self._pending_expiration_sec = self.Param("PendingExpirationSeconds", 65535) \
.SetDisplay("Pending Expiration (s)", "Lifetime of hedging stop orders", "Orders")
self._short_ma_period = self.Param("ShortMaPeriod", 4) \
.SetDisplay("Short EMA Period", "Fast EMA length", "Indicators")
self._long_ma_period = self.Param("LongMaPeriod", 24) \
.SetDisplay("Long EMA Period", "Slow EMA length", "Indicators")
self._use_previous_bar = self.Param("UsePreviousBar", True) \
.SetDisplay("Use Previous Bar", "Use previous bar for signals", "General")
self._ema_short_last = None
self._ema_short_prev_last = None
self._ema_long_last = None
self._ema_long_prev_last = None
self._macd_last = None
self._current_volume = 0.0
self._entry_price = 0.0
self._long_stop = None
self._long_tp = None
self._short_stop = None
self._short_tp = None
self._long_trailing = None
self._short_trailing = None
self._pending_orders = []
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(ema_cross_contest_hedged_strategy, self).OnReseted()
self._ema_short_last = None
self._ema_short_prev_last = None
self._ema_long_last = None
self._ema_long_prev_last = None
self._macd_last = None
self._current_volume = 0.0
self._entry_price = 0.0
self._long_stop = None
self._long_tp = None
self._short_stop = None
self._short_tp = None
self._long_trailing = None
self._short_trailing = None
self._pending_orders = []
def OnStarted2(self, time):
super(ema_cross_contest_hedged_strategy, self).OnStarted2(time)
short_ema = ExponentialMovingAverage()
short_ema.Length = self._short_ma_period.Value
long_ema = ExponentialMovingAverage()
long_ema.Length = self._long_ma_period.Value
macd = MovingAverageConvergenceDivergenceSignal()
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(short_ema, long_ema, macd, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, short_ema)
self.DrawIndicator(area, long_ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, short_value, long_value, macd_value):
if candle.State != CandleStates.Finished:
return
if short_value.IsEmpty or long_value.IsEmpty:
return
ema_short = float(short_value.Value)
ema_long = float(long_value.Value)
macd_current = None
if not macd_value.IsEmpty:
macd_line = macd_value.Macd
if macd_line is not None:
macd_current = float(macd_line)
self._process_pending_orders(candle)
cross = self._detect_cross(ema_short, ema_long)
macd_filter_val = None
if self._use_macd_filter.Value:
if self._use_previous_bar.Value:
macd_filter_val = self._macd_last
else:
macd_filter_val = macd_current
if macd_filter_val is None:
self._update_history(ema_short, ema_long, macd_current)
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._update_history(ema_short, ema_long, macd_current)
return
if self._current_volume > 0:
if self._close_opposite.Value and cross == 2:
self._exit_long()
self._update_history(ema_short, ema_long, macd_current)
return
if self._check_long_stops(candle):
self._update_history(ema_short, ema_long, macd_current)
return
elif self._current_volume < 0:
if self._close_opposite.Value and cross == 1:
self._exit_short()
self._update_history(ema_short, ema_long, macd_current)
return
if self._check_short_stops(candle):
self._update_history(ema_short, ema_long, macd_current)
return
if self._current_volume == 0:
if cross == 1 and (not self._use_macd_filter.Value or macd_filter_val >= 0):
self._enter_long(float(candle.ClosePrice), candle.CloseTime)
self._update_history(ema_short, ema_long, macd_current)
return
if cross == 2 and (not self._use_macd_filter.Value or macd_filter_val <= 0):
self._enter_short(float(candle.ClosePrice), candle.CloseTime)
self._update_history(ema_short, ema_long, macd_current)
return
self._update_history(ema_short, ema_long, macd_current)
def _process_pending_orders(self, candle):
if not self._pending_orders:
return
now = candle.CloseTime
to_remove = []
for order in self._pending_orders:
if order["expire_time"] <= now:
to_remove.append(order)
continue
triggered = False
if order["side"] == "buy":
triggered = float(candle.HighPrice) >= order["price"]
else:
triggered = float(candle.LowPrice) <= order["price"]
if triggered:
to_remove.append(order)
if order["side"] == "buy":
self.BuyMarket()
self._register_long_entry(order["price"], 1.0, order.get("stop"), order.get("take"))
else:
self.SellMarket()
self._register_short_entry(order["price"], 1.0, order.get("stop"), order.get("take"))
for o in to_remove:
if o in self._pending_orders:
self._pending_orders.remove(o)
def _enter_long(self, price, time):
self.BuyMarket()
sl = price - self._pip_to_price(self._stop_loss_pips.Value) if self._stop_loss_pips.Value > 0 else None
tp = price + self._pip_to_price(self._take_profit_pips.Value) if self._take_profit_pips.Value > 0 else None
self._register_long_entry(price, 1.0, sl, tp)
self._short_stop = None
self._short_tp = None
self._short_trailing = None
self._create_pending_orders(time, price, "buy")
def _enter_short(self, price, time):
self.SellMarket()
sl = price + self._pip_to_price(self._stop_loss_pips.Value) if self._stop_loss_pips.Value > 0 else None
tp = price - self._pip_to_price(self._take_profit_pips.Value) if self._take_profit_pips.Value > 0 else None
self._register_short_entry(price, 1.0, sl, tp)
self._long_stop = None
self._long_tp = None
self._long_trailing = None
self._create_pending_orders(time, price, "sell")
def _register_long_entry(self, price, volume, stop, take):
prev_vol = self._current_volume
self._current_volume += volume
if prev_vol <= 0:
self._entry_price = price
else:
self._entry_price = (prev_vol * self._entry_price + volume * price) / self._current_volume
if stop is not None:
self._long_stop = max(self._long_stop, stop) if self._long_stop is not None else stop
if take is not None:
self._long_tp = max(self._long_tp, take) if self._long_tp is not None else take
self._long_trailing = None
def _register_short_entry(self, price, volume, stop, take):
prev_vol = self._current_volume
self._current_volume -= volume
if prev_vol >= 0:
self._entry_price = price
else:
self._entry_price = (abs(prev_vol) * self._entry_price + volume * price) / abs(self._current_volume)
if stop is not None:
self._short_stop = min(self._short_stop, stop) if self._short_stop is not None else stop
if take is not None:
self._short_tp = min(self._short_tp, take) if self._short_tp is not None else take
self._short_trailing = None
def _check_long_stops(self, candle):
trail_dist = self._pip_to_price(self._trailing_stop_pips.Value)
trail_step = self._pip_to_price(self._trailing_step_pips.Value)
if self._trailing_stop_pips.Value > 0 and self._current_volume > 0:
profit = float(candle.ClosePrice) - self._entry_price
if profit > trail_dist + trail_step:
new_stop = float(candle.ClosePrice) - trail_dist
min_adv = float(candle.ClosePrice) - (trail_dist + trail_step)
if self._long_trailing is None or self._long_trailing < min_adv:
self._long_trailing = new_stop
eff_stop = self._long_stop
if self._long_trailing is not None:
eff_stop = max(eff_stop, self._long_trailing) if eff_stop is not None else self._long_trailing
if eff_stop is not None and float(candle.LowPrice) <= eff_stop:
self._exit_long()
return True
if self._long_tp is not None and float(candle.HighPrice) >= self._long_tp:
self._exit_long()
return True
return False
def _check_short_stops(self, candle):
trail_dist = self._pip_to_price(self._trailing_stop_pips.Value)
trail_step = self._pip_to_price(self._trailing_step_pips.Value)
if self._trailing_stop_pips.Value > 0 and self._current_volume < 0:
profit = self._entry_price - float(candle.ClosePrice)
if profit > trail_dist + trail_step:
new_stop = float(candle.ClosePrice) + trail_dist
max_adv = float(candle.ClosePrice) + trail_dist + trail_step
if self._short_trailing is None or self._short_trailing > max_adv:
self._short_trailing = new_stop
eff_stop = self._short_stop
if self._short_trailing is not None:
eff_stop = min(eff_stop, self._short_trailing) if eff_stop is not None else self._short_trailing
if eff_stop is not None and float(candle.HighPrice) >= eff_stop:
self._exit_short()
return True
if self._short_tp is not None and float(candle.LowPrice) <= self._short_tp:
self._exit_short()
return True
return False
def _exit_long(self):
if self._current_volume <= 0:
return
self.SellMarket()
self._current_volume = 0.0
self._entry_price = 0.0
self._long_stop = None
self._long_tp = None
self._long_trailing = None
def _exit_short(self):
if self._current_volume >= 0:
return
self.BuyMarket()
self._current_volume = 0.0
self._entry_price = 0.0
self._short_stop = None
self._short_tp = None
self._short_trailing = None
def _detect_cross(self, ema_short, ema_long):
if self._use_previous_bar.Value:
if (self._ema_short_last is None or self._ema_long_last is None or
self._ema_short_prev_last is None or self._ema_long_prev_last is None):
return 0
prev_s = self._ema_short_prev_last
prev_l = self._ema_long_prev_last
cur_s = self._ema_short_last
cur_l = self._ema_long_last
else:
if self._ema_short_last is None or self._ema_long_last is None:
return 0
prev_s = self._ema_short_last
prev_l = self._ema_long_last
cur_s = ema_short
cur_l = ema_long
if prev_s < prev_l and cur_s > cur_l:
return 1
if prev_s > prev_l and cur_s < cur_l:
return 2
return 0
def _update_history(self, ema_short, ema_long, macd_current):
self._ema_short_prev_last = self._ema_short_last
self._ema_long_prev_last = self._ema_long_last
self._ema_short_last = ema_short
self._ema_long_last = ema_long
if macd_current is not None:
self._macd_last = macd_current
def _pip_to_price(self, pips):
if pips <= 0:
return 0.0
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 1.0
decimals = 0
if self.Security is not None and self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
mult = 10.0 if decimals == 3 or decimals == 5 else 1.0
return pips * step * mult
def _create_pending_orders(self, time, price, side):
self._pending_orders = []
hedge_pips = self._hedge_level_pips.Value
if hedge_pips <= 0:
return
distance = self._pip_to_price(hedge_pips)
if distance <= 0:
return
exp_sec = self._pending_expiration_sec.Value
expiration = time + TimeSpan.FromSeconds(exp_sec) if exp_sec > 0 else None
stop_offset = self._pip_to_price(self._stop_loss_pips.Value) if self._stop_loss_pips.Value > 0 else 0
take_offset = self._pip_to_price(self._take_profit_pips.Value) if self._take_profit_pips.Value > 0 else 0
count = self._pending_order_count.Value
for i in range(1, count + 1):
if side == "buy":
level_price = price + distance * i
else:
level_price = price - distance * i
stop = None
take = None
if self._stop_loss_pips.Value > 0:
stop = level_price - stop_offset if side == "buy" else level_price + stop_offset
if self._take_profit_pips.Value > 0:
take = level_price + take_offset if side == "buy" else level_price - take_offset
self._pending_orders.append({
"side": side,
"price": level_price,
"stop": stop,
"take": take,
"expire_time": expiration if expiration is not None else time + TimeSpan.FromDays(365)
})
def CreateClone(self):
return ema_cross_contest_hedged_strategy()