Double MA Crossover Breakout Strategy
Overview
This strategy reproduces the "DoubleMA Crossover" MetaTrader expert adviser inside the StockSharp framework. The logic monitors a fast and a slow simple moving average, waits for a directional crossover, and then requires a breakout confirmation before entering the market. The algorithm manages only one position at a time and includes optional trailing stop behaviour that mimics the original three trailing modes.
How It Works
- Signal detection – Two simple moving averages (defaults: 2 and 5) are calculated on the selected candle series. A bullish crossover occurs when the fast average crosses above the slow one and vice versa for a bearish crossover.
- Breakout confirmation – After a crossover the strategy stores a breakout level defined in price steps (
BreakoutPips). A position is opened only when price reaches that level on a subsequent candle, replicating the stop order behaviour from the MQL version. - Position management – Only a single position is allowed. While a trade is active the strategy monitors stop-loss, take-profit, and the configured trailing stop type. The internal trackers emulate broker-side execution to keep behaviour deterministic in backtests.
- Session filter – Trading can be restricted to a specific time window (
StartHour..StopHour). The strategy still manages open trades outside the window but does not create new breakout levels when the filter blocks trading. - Trailing stops – Three trailing modes are supported: immediate trailing with the initial stop distance, trailing after a custom distance, and the three-level logic with breakeven shifts just like the original EA.
Parameters
| Parameter | Description |
|---|---|
FastMaPeriod, SlowMaPeriod |
Periods of the fast and slow simple moving averages. |
BreakoutPips |
Distance in price steps added to the signal candle close to define the breakout trigger. |
StopLossPips, TakeProfitPips |
Protective stop and optional take profit in price steps. Set take profit to zero to disable it. |
UseTrailingStop |
Enables trailing stop management. |
TrailingMode |
Trailing type: Type1 uses the original stop distance, Type2 waits for a custom distance (TrailingStopPips), Type3 uses the three MQL levels. |
TrailingStopPips |
Distance for Type2 trailing. |
Level1TriggerPips, Level1OffsetPips |
First trigger level and offset for Type3 trailing (moves stop to breakeven by default). |
Level2TriggerPips, Level2OffsetPips |
Second trigger level and offset for Type3 trailing. |
Level3TriggerPips, Level3OffsetPips |
Third trigger level and offset for Type3 trailing (converts to a classical trailing stop). |
UseTimeLimit, StartHour, StopHour |
Enables the trading session filter and defines the inclusive hour range. |
CandleType |
Candle series used for signal calculations. |
TradeVolume |
Order volume expressed in lots. |
Trailing Stop Modes
- Type1 – Moves the stop using the original stop-loss distance once price advances by that amount.
- Type2 – Waits until price moves by
TrailingStopPipsbefore trailing, then locks profit at that distance. - Type3 – Uses three levels: the first two shift the stop by the defined offsets, and the third converts to a continuous trailing stop using the current close and
Level3OffsetPips.
Usage Tips
- Align
BreakoutPipswith the instrument tick size to maintain the same behaviour as the MetaTrader expert adviser. - Review the session filter to match your trading hours; the default allows entries between 11:00 and 16:00 local time.
- Disable the time filter (
UseTimeLimit = false) for 24/7 instruments. - When testing trailing type 3, ensure the offset values are not larger than their corresponding trigger levels; otherwise the stop may remain behind the entry price.
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>
/// Double moving average crossover with breakout confirmation and trailing protection.
/// </summary>
public class DoubleMaCrossoverStrategy : Strategy
{
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _breakoutPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<bool> _useTrailingStop;
private readonly StrategyParam<TrailingTypes> _trailingMode;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _level1TriggerPips;
private readonly StrategyParam<int> _level1OffsetPips;
private readonly StrategyParam<int> _level2TriggerPips;
private readonly StrategyParam<int> _level2OffsetPips;
private readonly StrategyParam<int> _level3TriggerPips;
private readonly StrategyParam<int> _level3OffsetPips;
private readonly StrategyParam<bool> _useTimeLimit;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _stopHour;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _tradeVolume;
private decimal _prevFast;
private decimal _prevSlow;
private bool _hasPrev;
private decimal? _pendingBuyPrice;
private decimal? _pendingSellPrice;
private decimal? _entryPrice;
private decimal? _currentStop;
private decimal? _currentTakeProfit;
private decimal _maxPriceSinceEntry;
private decimal _minPriceSinceEntry;
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
public int BreakoutPips
{
get => _breakoutPips.Value;
set => _breakoutPips.Value = value;
}
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public bool UseTrailingStop
{
get => _useTrailingStop.Value;
set => _useTrailingStop.Value = value;
}
public TrailingTypes TrailingMode
{
get => _trailingMode.Value;
set => _trailingMode.Value = value;
}
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
public int Level1TriggerPips
{
get => _level1TriggerPips.Value;
set => _level1TriggerPips.Value = value;
}
public int Level1OffsetPips
{
get => _level1OffsetPips.Value;
set => _level1OffsetPips.Value = value;
}
public int Level2TriggerPips
{
get => _level2TriggerPips.Value;
set => _level2TriggerPips.Value = value;
}
public int Level2OffsetPips
{
get => _level2OffsetPips.Value;
set => _level2OffsetPips.Value = value;
}
public int Level3TriggerPips
{
get => _level3TriggerPips.Value;
set => _level3TriggerPips.Value = value;
}
public int Level3OffsetPips
{
get => _level3OffsetPips.Value;
set => _level3OffsetPips.Value = value;
}
public bool UseTimeLimit
{
get => _useTimeLimit.Value;
set => _useTimeLimit.Value = value;
}
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
public int StopHour
{
get => _stopHour.Value;
set => _stopHour.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
public DoubleMaCrossoverStrategy()
{
_fastMaPeriod = Param(nameof(FastMaPeriod), 5)
.SetDisplay("Fast MA Period", "Period for the fast moving average.", "General")
;
_slowMaPeriod = Param(nameof(SlowMaPeriod), 15)
.SetDisplay("Slow MA Period", "Period for the slow moving average.", "General")
;
_breakoutPips = Param(nameof(BreakoutPips), 15)
.SetDisplay("Breakout Pips", "Distance in price steps added before submitting an entry.", "General")
;
_stopLossPips = Param(nameof(StopLossPips), 25)
.SetDisplay("Stop Loss Pips", "Protective stop expressed in price steps.", "Risk")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 0)
.SetDisplay("Take Profit Pips", "Take profit distance expressed in price steps.", "Risk")
;
_useTrailingStop = Param(nameof(UseTrailingStop), false)
.SetDisplay("Use Trailing", "Enable trailing stop management.", "Risk");
_trailingMode = Param(nameof(TrailingMode), TrailingTypes.Type3)
.SetDisplay("Trailing Type", "Trailing stop behaviour.", "Risk")
;
_trailingStopPips = Param(nameof(TrailingStopPips), 40)
.SetDisplay("Trailing Stop Pips", "Trailing distance used by type 2 trailing.", "Risk")
;
_level1TriggerPips = Param(nameof(Level1TriggerPips), 20)
.SetDisplay("Level 1 Trigger", "Profit in price steps required before the first trailing adjustment.", "Risk")
;
_level1OffsetPips = Param(nameof(Level1OffsetPips), 20)
.SetDisplay("Level 1 Offset", "Offset in price steps applied after the first trigger.", "Risk")
;
_level2TriggerPips = Param(nameof(Level2TriggerPips), 30)
.SetDisplay("Level 2 Trigger", "Profit in price steps required before the second trailing adjustment.", "Risk")
;
_level2OffsetPips = Param(nameof(Level2OffsetPips), 20)
.SetDisplay("Level 2 Offset", "Offset in price steps applied after the second trigger.", "Risk")
;
_level3TriggerPips = Param(nameof(Level3TriggerPips), 50)
.SetDisplay("Level 3 Trigger", "Profit in price steps required before the third trailing adjustment.", "Risk")
;
_level3OffsetPips = Param(nameof(Level3OffsetPips), 20)
.SetDisplay("Level 3 Offset", "Offset in price steps applied after the third trigger.", "Risk")
;
_useTimeLimit = Param(nameof(UseTimeLimit), false)
.SetDisplay("Use Time Limit", "Restrict the creation of new orders to a trading window.", "Schedule");
_startHour = Param(nameof(StartHour), 11)
.SetDisplay("Start Hour", "Hour when new setups become valid.", "Schedule")
;
_stopHour = Param(nameof(StopHour), 16)
.SetDisplay("Stop Hour", "Hour after which no new setups are created.", "Schedule")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for analysis.", "General");
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetDisplay("Volume", "Order volume in lots.", "Trading")
.SetGreaterThanZero()
;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Reset internal buffers before processing market data.
ResetState();
Volume = TradeVolume;
var fastMa = new SMA { Length = FastMaPeriod };
var slowMa = new SMA { Length = SlowMaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(fastMa, slowMa, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastMa, decimal slowMa)
{
if (candle.State != CandleStates.Finished)
{
return;
}
if (!_hasPrev)
{
_prevFast = fastMa;
_prevSlow = slowMa;
_hasPrev = true;
return;
}
// Identify crossovers based on the two moving averages.
var crossUp = fastMa > slowMa && _prevFast <= _prevSlow;
var crossDown = fastMa < slowMa && _prevFast >= _prevSlow;
// Manage the current position before checking for fresh breakouts.
ManageOpenPosition(candle, crossUp, crossDown);
TriggerPendingEntries(candle);
if (UseTimeLimit && !IsTradingTime(candle.OpenTime))
{
if (crossUp)
{
_pendingSellPrice = null;
}
if (crossDown)
{
_pendingBuyPrice = null;
}
_prevFast = fastMa;
_prevSlow = slowMa;
return;
}
if (crossDown)
{
_pendingBuyPrice = null;
}
if (crossUp)
{
_pendingSellPrice = null;
}
if (Position == 0)
{
var breakout = GetBreakoutDistance();
if (crossUp)
{
_pendingBuyPrice = candle.ClosePrice + breakout;
}
else if (crossDown)
{
_pendingSellPrice = candle.ClosePrice - breakout;
}
}
TriggerPendingEntries(candle);
_prevFast = fastMa;
_prevSlow = slowMa;
}
private void ManageOpenPosition(ICandleMessage candle, bool crossUp, bool crossDown)
{
if (Position == 0)
{
if (_entryPrice.HasValue)
{
// Clear trailing information once the position is flat.
ResetPositionState();
}
return;
}
if (_entryPrice is null)
{
_entryPrice = candle.ClosePrice;
_maxPriceSinceEntry = candle.ClosePrice;
_minPriceSinceEntry = candle.ClosePrice;
}
UpdateExtremes(candle);
UpdateTrailingStop(candle);
if (CheckStopsAndTargets(candle))
{
return;
}
if (Position > 0 && crossDown)
{
ExitLong();
return;
}
if (Position < 0 && crossUp)
{
ExitShort();
}
}
private void UpdateExtremes(ICandleMessage candle)
{
var high = candle.HighPrice;
var low = candle.LowPrice;
_maxPriceSinceEntry = Math.Max(_maxPriceSinceEntry, high);
_minPriceSinceEntry = Math.Min(_minPriceSinceEntry, low);
}
private void UpdateTrailingStop(ICandleMessage candle)
{
if (!UseTrailingStop || _entryPrice is null)
{
return;
}
var entryPrice = _entryPrice.Value;
var closePrice = candle.ClosePrice;
var step = GetPriceStep();
switch (TrailingMode)
{
case TrailingTypes.Type1:
{
UpdateType1Trailing(entryPrice, closePrice, step);
break;
}
case TrailingTypes.Type2:
{
UpdateType2Trailing(entryPrice, closePrice, step);
break;
}
case TrailingTypes.Type3:
{
UpdateType3Trailing(entryPrice, closePrice, step);
break;
}
}
}
private void UpdateType1Trailing(decimal entryPrice, decimal closePrice, decimal step)
{
var distance = step * Math.Abs(StopLossPips);
if (distance == 0)
{
return;
}
if (Position > 0)
{
if (_maxPriceSinceEntry - entryPrice >= distance)
{
var candidate = closePrice - distance;
UpdateStopForLong(candidate);
}
}
else if (Position < 0)
{
if (entryPrice - _minPriceSinceEntry >= distance)
{
var candidate = closePrice + distance;
UpdateStopForShort(candidate);
}
}
}
private void UpdateType2Trailing(decimal entryPrice, decimal closePrice, decimal step)
{
var distance = step * Math.Abs(TrailingStopPips);
if (distance == 0)
{
return;
}
if (Position > 0)
{
if (_maxPriceSinceEntry - entryPrice >= distance)
{
var candidate = closePrice - distance;
UpdateStopForLong(candidate);
}
}
else if (Position < 0)
{
if (entryPrice - _minPriceSinceEntry >= distance)
{
var candidate = closePrice + distance;
UpdateStopForShort(candidate);
}
}
}
private void UpdateType3Trailing(decimal entryPrice, decimal closePrice, decimal step)
{
var trigger1 = step * Math.Abs(Level1TriggerPips);
if (trigger1 > 0)
{
if (Position > 0 && _maxPriceSinceEntry - entryPrice >= trigger1)
{
var candidate = entryPrice + trigger1 - step * Math.Abs(Level1OffsetPips);
UpdateStopForLong(candidate);
}
else if (Position < 0 && entryPrice - _minPriceSinceEntry >= trigger1)
{
var candidate = entryPrice - trigger1 + step * Math.Abs(Level1OffsetPips);
UpdateStopForShort(candidate);
}
}
var trigger2 = step * Math.Abs(Level2TriggerPips);
if (trigger2 > 0)
{
if (Position > 0 && _maxPriceSinceEntry - entryPrice >= trigger2)
{
var candidate = entryPrice + trigger2 - step * Math.Abs(Level2OffsetPips);
UpdateStopForLong(candidate);
}
else if (Position < 0 && entryPrice - _minPriceSinceEntry >= trigger2)
{
var candidate = entryPrice - trigger2 + step * Math.Abs(Level2OffsetPips);
UpdateStopForShort(candidate);
}
}
var trigger3 = step * Math.Abs(Level3TriggerPips);
if (trigger3 > 0)
{
if (Position > 0 && _maxPriceSinceEntry - entryPrice >= trigger3)
{
var candidate = closePrice - step * Math.Abs(Level3OffsetPips);
UpdateStopForLong(candidate);
}
else if (Position < 0 && entryPrice - _minPriceSinceEntry >= trigger3)
{
var candidate = closePrice + step * Math.Abs(Level3OffsetPips);
UpdateStopForShort(candidate);
}
}
}
private void UpdateStopForLong(decimal candidate)
{
if (!_currentStop.HasValue || candidate > _currentStop.Value)
{
_currentStop = candidate;
}
}
private void UpdateStopForShort(decimal candidate)
{
if (!_currentStop.HasValue || candidate < _currentStop.Value)
{
_currentStop = candidate;
}
}
private bool CheckStopsAndTargets(ICandleMessage candle)
{
// Simulate broker-side stop loss and take profit execution.
if (Position > 0)
{
if (_currentTakeProfit.HasValue && candle.HighPrice >= _currentTakeProfit.Value)
{
ExitLong();
return true;
}
if (_currentStop.HasValue && candle.LowPrice <= _currentStop.Value)
{
ExitLong();
return true;
}
}
else if (Position < 0)
{
if (_currentTakeProfit.HasValue && candle.LowPrice <= _currentTakeProfit.Value)
{
ExitShort();
return true;
}
if (_currentStop.HasValue && candle.HighPrice >= _currentStop.Value)
{
ExitShort();
return true;
}
}
return false;
}
private void TriggerPendingEntries(ICandleMessage candle)
{
// Skip processing when a position already exists.
if (Position != 0)
{
if (Position > 0)
{
_pendingBuyPrice = null;
}
else
{
_pendingSellPrice = null;
}
return;
}
if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice)
{
// Breakout confirmed on the long side.
EnterLong(buyPrice);
_pendingBuyPrice = null;
_pendingSellPrice = null;
}
else if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice)
{
// Breakout confirmed on the short side.
EnterShort(sellPrice);
_pendingBuyPrice = null;
_pendingSellPrice = null;
}
}
private void EnterLong(decimal price)
{
if (TradeVolume <= 0)
{
return;
}
Volume = TradeVolume;
BuyMarket();
// Initialize tracking variables for the new long trade.
_entryPrice = price;
_currentStop = StopLossPips > 0 ? price - GetPriceStep() * Math.Abs(StopLossPips) : null;
_currentTakeProfit = TakeProfitPips > 0 ? price + GetPriceStep() * Math.Abs(TakeProfitPips) : null;
_maxPriceSinceEntry = price;
_minPriceSinceEntry = price;
}
private void EnterShort(decimal price)
{
if (TradeVolume <= 0)
{
return;
}
Volume = TradeVolume;
SellMarket();
// Initialize tracking variables for the new short trade.
_entryPrice = price;
_currentStop = StopLossPips > 0 ? price + GetPriceStep() * Math.Abs(StopLossPips) : null;
_currentTakeProfit = TakeProfitPips > 0 ? price - GetPriceStep() * Math.Abs(TakeProfitPips) : null;
_maxPriceSinceEntry = price;
_minPriceSinceEntry = price;
}
private void ExitLong()
{
Volume = TradeVolume;
SellMarket();
ResetPositionState();
}
private void ExitShort()
{
Volume = TradeVolume;
BuyMarket();
ResetPositionState();
}
private bool IsTradingTime(DateTimeOffset time)
{
if (!UseTimeLimit)
{
return true;
}
var hour = time.Hour;
if (StartHour <= StopHour)
{
return hour >= StartHour && hour <= StopHour;
}
return hour >= StartHour || hour <= StopHour;
}
private decimal GetBreakoutDistance()
{
return GetPriceStep() * Math.Abs(BreakoutPips);
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
return step > 0 ? step : 1m;
}
private void ResetPositionState()
{
_entryPrice = null;
_currentStop = null;
_currentTakeProfit = null;
_maxPriceSinceEntry = 0m;
_minPriceSinceEntry = 0m;
_pendingBuyPrice = null;
_pendingSellPrice = null;
}
private void ResetState()
{
_prevFast = 0m;
_prevSlow = 0m;
_hasPrev = false;
ResetPositionState();
}
public enum TrailingTypes
{
Type1,
Type2,
Type3
}
}
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 SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class double_ma_crossover_strategy(Strategy):
"""
Double moving average crossover with breakout confirmation and trailing protection.
Uses SMA crossover to generate pending entries that fire on breakout.
Manages stop-loss, take-profit, and multi-level trailing stop.
"""
def __init__(self):
super(double_ma_crossover_strategy, self).__init__()
self._fast_ma_period = self.Param("FastMaPeriod", 5) \
.SetDisplay("Fast MA Period", "Period for the fast moving average", "General")
self._slow_ma_period = self.Param("SlowMaPeriod", 15) \
.SetDisplay("Slow MA Period", "Period for the slow moving average", "General")
self._breakout_pips = self.Param("BreakoutPips", 15) \
.SetDisplay("Breakout Pips", "Distance in price steps added before entry", "General")
self._stop_loss_pips = self.Param("StopLossPips", 25) \
.SetDisplay("Stop Loss Pips", "Protective stop in price steps", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 0) \
.SetDisplay("Take Profit Pips", "Take profit distance in price steps", "Risk")
self._use_trailing = self.Param("UseTrailingStop", False) \
.SetDisplay("Use Trailing", "Enable trailing stop management", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 40) \
.SetDisplay("Trailing Stop Pips", "Trailing distance in price steps", "Risk")
self._level1_trigger = self.Param("Level1TriggerPips", 20) \
.SetDisplay("Level 1 Trigger", "Profit in steps for first trailing adjustment", "Risk")
self._level1_offset = self.Param("Level1OffsetPips", 20) \
.SetDisplay("Level 1 Offset", "Offset in steps after first trigger", "Risk")
self._level2_trigger = self.Param("Level2TriggerPips", 30) \
.SetDisplay("Level 2 Trigger", "Profit in steps for second trailing adjustment", "Risk")
self._level2_offset = self.Param("Level2OffsetPips", 20) \
.SetDisplay("Level 2 Offset", "Offset in steps after second trigger", "Risk")
self._level3_trigger = self.Param("Level3TriggerPips", 50) \
.SetDisplay("Level 3 Trigger", "Profit in steps for third trailing adjustment", "Risk")
self._level3_offset = self.Param("Level3OffsetPips", 20) \
.SetDisplay("Level 3 Offset", "Offset in steps after third trigger", "Risk")
self._use_time_limit = self.Param("UseTimeLimit", False) \
.SetDisplay("Use Time Limit", "Restrict entries to trading window", "Schedule")
self._start_hour = self.Param("StartHour", 11) \
.SetDisplay("Start Hour", "Hour when new setups become valid", "Schedule")
self._stop_hour = self.Param("StopHour", 16) \
.SetDisplay("Stop Hour", "Hour after which no new setups created", "Schedule")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles for analysis", "General")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
self._pending_buy_price = None
self._pending_sell_price = None
self._entry_price = None
self._current_stop = None
self._current_tp = None
self._max_price = 0.0
self._min_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(double_ma_crossover_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(double_ma_crossover_strategy, self).OnStarted2(time)
self._reset_state()
fast_ma = SimpleMovingAverage()
fast_ma.Length = self._fast_ma_period.Value
slow_ma = SimpleMovingAverage()
slow_ma.Length = self._slow_ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(fast_ma, slow_ma, self._process_candle).Start()
def _process_candle(self, candle, fast_ma, slow_ma):
if candle.State != CandleStates.Finished:
return
fast_ma = float(fast_ma)
slow_ma = float(slow_ma)
if not self._has_prev:
self._prev_fast = fast_ma
self._prev_slow = slow_ma
self._has_prev = True
return
cross_up = fast_ma > slow_ma and self._prev_fast <= self._prev_slow
cross_down = fast_ma < slow_ma and self._prev_fast >= self._prev_slow
self._manage_open_position(candle, cross_up, cross_down)
self._trigger_pending_entries(candle)
if self._use_time_limit.Value and not self._is_trading_time(candle.OpenTime):
if cross_up:
self._pending_sell_price = None
if cross_down:
self._pending_buy_price = None
self._prev_fast = fast_ma
self._prev_slow = slow_ma
return
if cross_down:
self._pending_buy_price = None
if cross_up:
self._pending_sell_price = None
if self.Position == 0:
breakout = self._get_breakout_distance()
if cross_up:
self._pending_buy_price = float(candle.ClosePrice) + breakout
elif cross_down:
self._pending_sell_price = float(candle.ClosePrice) - breakout
self._trigger_pending_entries(candle)
self._prev_fast = fast_ma
self._prev_slow = slow_ma
def _manage_open_position(self, candle, cross_up, cross_down):
if self.Position == 0:
if self._entry_price is not None:
self._reset_position_state()
return
if self._entry_price is None:
self._entry_price = float(candle.ClosePrice)
self._max_price = float(candle.ClosePrice)
self._min_price = float(candle.ClosePrice)
self._update_extremes(candle)
self._update_trailing_stop(candle)
if self._check_stops_and_targets(candle):
return
if self.Position > 0 and cross_down:
self.SellMarket()
self._reset_position_state()
return
if self.Position < 0 and cross_up:
self.BuyMarket()
self._reset_position_state()
def _update_extremes(self, candle):
self._max_price = max(self._max_price, float(candle.HighPrice))
self._min_price = min(self._min_price, float(candle.LowPrice))
def _update_trailing_stop(self, candle):
if not self._use_trailing.Value or self._entry_price is None:
return
entry = self._entry_price
close = float(candle.ClosePrice)
step = self._get_price_step()
# Level 3 trailing (type 3 logic from C#)
trigger1 = step * abs(self._level1_trigger.Value)
if trigger1 > 0:
if self.Position > 0 and self._max_price - entry >= trigger1:
candidate = entry + trigger1 - step * abs(self._level1_offset.Value)
self._update_stop_long(candidate)
elif self.Position < 0 and entry - self._min_price >= trigger1:
candidate = entry - trigger1 + step * abs(self._level1_offset.Value)
self._update_stop_short(candidate)
trigger2 = step * abs(self._level2_trigger.Value)
if trigger2 > 0:
if self.Position > 0 and self._max_price - entry >= trigger2:
candidate = entry + trigger2 - step * abs(self._level2_offset.Value)
self._update_stop_long(candidate)
elif self.Position < 0 and entry - self._min_price >= trigger2:
candidate = entry - trigger2 + step * abs(self._level2_offset.Value)
self._update_stop_short(candidate)
trigger3 = step * abs(self._level3_trigger.Value)
if trigger3 > 0:
if self.Position > 0 and self._max_price - entry >= trigger3:
candidate = close - step * abs(self._level3_offset.Value)
self._update_stop_long(candidate)
elif self.Position < 0 and entry - self._min_price >= trigger3:
candidate = close + step * abs(self._level3_offset.Value)
self._update_stop_short(candidate)
def _update_stop_long(self, candidate):
if self._current_stop is None or candidate > self._current_stop:
self._current_stop = candidate
def _update_stop_short(self, candidate):
if self._current_stop is None or candidate < self._current_stop:
self._current_stop = candidate
def _check_stops_and_targets(self, candle):
if self.Position > 0:
if self._current_tp is not None and float(candle.HighPrice) >= self._current_tp:
self.SellMarket()
self._reset_position_state()
return True
if self._current_stop is not None and float(candle.LowPrice) <= self._current_stop:
self.SellMarket()
self._reset_position_state()
return True
elif self.Position < 0:
if self._current_tp is not None and float(candle.LowPrice) <= self._current_tp:
self.BuyMarket()
self._reset_position_state()
return True
if self._current_stop is not None and float(candle.HighPrice) >= self._current_stop:
self.BuyMarket()
self._reset_position_state()
return True
return False
def _trigger_pending_entries(self, candle):
if self.Position != 0:
if self.Position > 0:
self._pending_buy_price = None
else:
self._pending_sell_price = None
return
step = self._get_price_step()
if self._pending_buy_price is not None and float(candle.HighPrice) >= self._pending_buy_price:
price = self._pending_buy_price
self.BuyMarket()
self._entry_price = price
sl = self._stop_loss_pips.Value
tp = self._take_profit_pips.Value
self._current_stop = price - step * abs(sl) if sl > 0 else None
self._current_tp = price + step * abs(tp) if tp > 0 else None
self._max_price = price
self._min_price = price
self._pending_buy_price = None
self._pending_sell_price = None
elif self._pending_sell_price is not None and float(candle.LowPrice) <= self._pending_sell_price:
price = self._pending_sell_price
self.SellMarket()
self._entry_price = price
sl = self._stop_loss_pips.Value
tp = self._take_profit_pips.Value
self._current_stop = price + step * abs(sl) if sl > 0 else None
self._current_tp = price - step * abs(tp) if tp > 0 else None
self._max_price = price
self._min_price = price
self._pending_buy_price = None
self._pending_sell_price = None
def _is_trading_time(self, time):
if not self._use_time_limit.Value:
return True
hour = time.Hour
start = self._start_hour.Value
stop = self._stop_hour.Value
if start <= stop:
return hour >= start and hour <= stop
return hour >= start or hour <= stop
def _get_breakout_distance(self):
return self._get_price_step() * abs(self._breakout_pips.Value)
def _get_price_step(self):
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
return step if step > 0 else 1.0
def _reset_position_state(self):
self._entry_price = None
self._current_stop = None
self._current_tp = None
self._max_price = 0.0
self._min_price = 0.0
self._pending_buy_price = None
self._pending_sell_price = None
def _reset_state(self):
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
self._reset_position_state()
def CreateClone(self):
return double_ma_crossover_strategy()