Estrategia de Ruptura de Cruce Doble de MA
Descripción general
Esta estrategia reproduce el asesor experto MetaTrader "DoubleMA Crossover" dentro del framework de StockSharp. La lógica monitorea una media móvil rápida y una lenta, espera un cruce direccional, y luego requiere una confirmación de ruptura antes de entrar al mercado. El algoritmo gestiona solo una posición a la vez e incluye comportamiento opcional de trailing stop que imita los tres modos de trailing originales.
Cómo funciona
- Detección de señal – Se calculan dos medias móviles simples (predeterminados: 2 y 5) en la serie de velas seleccionada. Un cruce alcista ocurre cuando el promedio rápido cruza por encima del lento y viceversa para un cruce bajista.
- Confirmación de ruptura – Después de un cruce, la estrategia almacena un nivel de ruptura definido en pasos de precio (
BreakoutPips). Se abre una posición solo cuando el precio alcanza ese nivel en una vela posterior, replicando el comportamiento de la orden stop de la versión MQL. - Gestión de posición – Solo se permite una única posición. Mientras una operación está activa, la estrategia monitorea el stop-loss, el take-profit y el tipo de trailing stop configurado. Los rastreadores internos emulan la ejecución del lado del bróker para mantener el comportamiento determinista en backtests.
- Filtro de sesión – El trading puede restringirse a una ventana de tiempo específica (
StartHour..StopHour). La estrategia aún gestiona operaciones abiertas fuera de la ventana pero no crea nuevos niveles de ruptura cuando el filtro bloquea el trading. - Trailing stops – Se soportan tres modos de trailing: trailing inmediato con la distancia de stop inicial, trailing después de una distancia personalizada, y la lógica de tres niveles con cambios de breakeven igual que el EA original.
Parámetros
| Parámetro | Descripción |
|---|---|
FastMaPeriod, SlowMaPeriod |
Períodos de las medias móviles simples rápida y lenta. |
BreakoutPips |
Distancia en pasos de precio añadida al cierre de la vela de señal para definir el disparador de ruptura. |
StopLossPips, TakeProfitPips |
Stop protector y take profit opcional en pasos de precio. Establecer take profit en cero para deshabilitarlo. |
UseTrailingStop |
Habilita la gestión del trailing stop. |
TrailingMode |
Tipo de trailing: Type1 usa la distancia de stop original, Type2 espera una distancia personalizada (TrailingStopPips), Type3 usa los tres niveles MQL. |
TrailingStopPips |
Distancia para el trailing de Type2. |
Level1TriggerPips, Level1OffsetPips |
Primer nivel de disparo y offset para el trailing de Type3 (mueve el stop a breakeven por defecto). |
Level2TriggerPips, Level2OffsetPips |
Segundo nivel de disparo y offset para el trailing de Type3. |
Level3TriggerPips, Level3OffsetPips |
Tercer nivel de disparo y offset para el trailing de Type3 (convierte a un trailing stop clásico). |
UseTimeLimit, StartHour, StopHour |
Habilita el filtro de sesión de trading y define el rango de horas inclusivo. |
CandleType |
Serie de velas usada para los cálculos de señal. |
TradeVolume |
Volumen de orden expresado en lotes. |
Modos de Trailing Stop
- Type1 – Mueve el stop usando la distancia de stop-loss original una vez que el precio avanza esa cantidad.
- Type2 – Espera hasta que el precio se mueva
TrailingStopPipsantes de trailing, luego bloquea la ganancia a esa distancia. - Type3 – Usa tres niveles: los dos primeros desplazan el stop por los offsets definidos, y el tercero convierte a un trailing stop continuo usando el cierre actual y
Level3OffsetPips.
Consejos de uso
- Alinear
BreakoutPipscon el tamaño de tick del instrumento para mantener el mismo comportamiento que el asesor experto MetaTrader. - Revisar el filtro de sesión para que coincida con los horarios de trading; el predeterminado permite entradas entre las 11:00 y las 16:00 hora local.
- Deshabilitar el filtro de tiempo (
UseTimeLimit = false) para instrumentos de 24/7. - Al probar el trailing de tipo 3, asegurarse de que los valores de offset no sean mayores que sus niveles de disparo correspondientes; de lo contrario, el stop puede permanecer detrás del precio de entrada.
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()