Doppel-MA-Kreuzung-Ausbruch-Strategie
Übersicht
Diese Strategie reproduziert den MetaTrader-Expertenberater "DoubleMA Crossover" im StockSharp-Framework. Die Logik überwacht einen schnellen und einen langsamen einfachen gleitenden Durchschnitt, wartet auf eine direktionale Kreuzung und erfordert dann eine Ausbruchsbestätigung, bevor der Markt betreten wird. Der Algorithmus verwaltet jeweils nur eine Position und beinhaltet optionales Trailing-Stop-Verhalten, das die drei ursprünglichen Trailing-Modi imitiert.
Funktionsweise
- Signalerkennung – Zwei einfache gleitende Durchschnitte (Standard: 2 und 5) werden auf der ausgewählten Kerzenserie berechnet. Eine bullische Kreuzung tritt auf, wenn der schnelle Durchschnitt über den langsamen kreuzt, und umgekehrt für eine bärische Kreuzung.
- Ausbruchsbestätigung – Nach einer Kreuzung speichert die Strategie ein Ausbruchsniveau, das in Preisschritten (
BreakoutPips) definiert ist. Eine Position wird nur dann geöffnet, wenn der Preis dieses Niveau auf einer nachfolgenden Kerze erreicht, was das Stop-Order-Verhalten aus der MQL-Version repliziert. - Positionsverwaltung – Es ist nur eine einzige Position erlaubt. Während ein Trade aktiv ist, überwacht die Strategie Stop-Loss, Take-Profit und den konfigurierten Trailing-Stop-Typ. Die internen Tracker emulieren die brokerseitige Ausführung, um das Verhalten in Backtests deterministisch zu halten.
- Sitzungsfilter – Der Handel kann auf ein bestimmtes Zeitfenster (
StartHour..StopHour) beschränkt werden. Die Strategie verwaltet offene Trades außerhalb des Fensters noch, erstellt aber keine neuen Ausbruchsniveaus, wenn der Filter den Handel blockiert. - Trailing Stops – Drei Trailing-Modi werden unterstützt: sofortiges Trailing mit der anfänglichen Stop-Distanz, Trailing nach einer benutzerdefinierten Distanz, und die Drei-Level-Logik mit Break-Even-Verschiebungen genau wie der ursprüngliche EA.
Parameter
| Parameter | Beschreibung |
|---|---|
FastMaPeriod, SlowMaPeriod |
Perioden des schnellen und langsamen einfachen gleitenden Durchschnitts. |
BreakoutPips |
Abstand in Preisschritten, der zum Signalkerzenschluss addiert wird, um den Ausbruchstrigger zu definieren. |
StopLossPips, TakeProfitPips |
Schützender Stop und optionaler Take Profit in Preisschritten. Take Profit auf null setzen, um ihn zu deaktivieren. |
UseTrailingStop |
Aktiviert das Trailing-Stop-Management. |
TrailingMode |
Trailing-Typ: Type1 verwendet die ursprüngliche Stop-Distanz, Type2 wartet auf eine benutzerdefinierte Distanz (TrailingStopPips), Type3 verwendet die drei MQL-Level. |
TrailingStopPips |
Distanz für Type2-Trailing. |
Level1TriggerPips, Level1OffsetPips |
Erster Triggerlevel und Offset für Type3-Trailing (verschiebt Stop standardmäßig auf Break-Even). |
Level2TriggerPips, Level2OffsetPips |
Zweiter Triggerlevel und Offset für Type3-Trailing. |
Level3TriggerPips, Level3OffsetPips |
Dritter Triggerlevel und Offset für Type3-Trailing (konvertiert zu einem klassischen Trailing Stop). |
UseTimeLimit, StartHour, StopHour |
Aktiviert den Handelssitzungsfilter und definiert den inklusiven Stundenbereich. |
CandleType |
Kerzenserie für Signalberechnungen. |
TradeVolume |
Ordervolumen in Lots. |
Trailing Stop-Modi
- Type1 – Verschiebt den Stop mit der ursprünglichen Stop-Loss-Distanz, sobald der Preis diesen Betrag vorgerückt ist.
- Type2 – Wartet, bis der Preis sich um
TrailingStopPipsbewegt hat, bevor er trailt, dann sperrt den Gewinn auf dieser Distanz. - Type3 – Verwendet drei Level: Die ersten zwei verschieben den Stop um die definierten Offsets, und der dritte konvertiert zu einem kontinuierlichen Trailing Stop unter Verwendung des aktuellen Schlusses und
Level3OffsetPips.
Verwendungstipps
BreakoutPipsmit der Instrumenten-Tick-Größe abstimmen, um das gleiche Verhalten wie der MetaTrader-Expertenberater zu erhalten.- Den Sitzungsfilter überprüfen, um die Handelszeiten abzustimmen; der Standard erlaubt Einstiege zwischen 11:00 und 16:00 Lokalzeit.
- Den Zeitfilter deaktivieren (
UseTimeLimit = false) für 24/7-Instrumente. - Beim Testen von Trailing-Typ 3 sicherstellen, dass die Offset-Werte nicht größer sind als ihre entsprechenden Triggerlevel; andernfalls kann der Stop hinter dem Einstandspreis verbleiben.
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()