Stopreversal Tm-Strategie
Übersicht
Die Stopreversal Tm-Strategie ist eine direkte Übersetzung des ursprünglichen MetaTrader 5 Expert Advisors Exp_Stopreversal_Tm.mq5. Die Handelsidee folgt dem benutzerdefinierten Indikator Stopreversal, der einen dynamischen Trailing Stop um den Preis herum aufrechthält und Umkehralarme erzeugt, wenn der Preis diese Trailing-Grenze kreuzt. Die Strategie operiert auf einem einzelnen Instrument und einem einzelnen Kerzen-Feed und ist für den Trendumkehr-Handel mit einem benutzerdefinierten Sitzungsfilter ausgelegt.
Signalgenerierung
Der Stopreversal-Indikator berechnet einen Referenzpreis aus dem gewählten angewendeten Preismodus und passt dann ein Trailing-Stop-Level um Sensitivity (den nPips-Parameter) an. Wenn der neue angewendete Preis über den Trailing Stop steigt, während sich der vorherige Balken darunter befand, wird ein bullisches Signal erzeugt. Umgekehrt erscheint ein bärisches Signal, wenn der neue Preis unter den Trailing Stop fällt, nachdem er darüber war. Jedes bullische Signal fordert gleichzeitig das Schließen bestehender Short-Positionen und das Öffnen eines neuen Longs, während jedes bärische Signal Longs schließt und Shorts öffnet.
Um das Verhalten der ursprünglichen MetaTrader-Implementierung zu reproduzieren, kann die Strategie die Ausführung von Signalen um mehrere abgeschlossene Balken verzögern (Signal Bar Delay). Dies repliziert den SignalBar-Eingang des Expert Advisors und verhindert den Handel auf der noch entstehenden Kerze.
Sitzungsfilter und Positionshandling
Der Expert Advisor erlaubte den Handel nur innerhalb eines bestimmten Zeitfensters. Die konvertierte Strategie behält die gleiche Logik: Wenn das Use Time Filter-Flag aktiviert ist, sind Aufträge nur innerhalb der durch Start Hour/Minute und End Hour/Minute konfigurierten Sitzung erlaubt. Wenn die aktuelle Zeit das erlaubte Fenster verlässt, wird jede offene Position sofort geschlossen. Signalgesteuerte Ausstiege bleiben aktiv, auch wenn die Sitzung deaktiviert ist.
Die Strategie arbeitet mit Netto-Positionen. Eine Schließaktion wird immer vor einem entgegengesetzten Einstieg ausgeführt, wodurch sichergestellt wird, dass die Richtung ohne überlappende Expositionen wechselt.
Parameter
- Allow Buy Entries / Allow Sell Entries – Öffnung neuer Long- oder Short-Positionen aktivieren oder deaktivieren, wenn das entsprechende Signal empfangen wird.
- Allow Long Exits / Allow Short Exits – steuert, ob entgegengesetzte Signale bestehende Positionen schließen dürfen.
- Use Time Filter – schaltet das Handelssitzungs-Fenster ein.
- Start Hour / Start Minute / End Hour / End Minute – definiert den inklusiven Start und das exklusive Ende des Handelsfensters. Der Zeitfilter unterstützt Übernacht-Sitzungen, bei denen die Endzeit früher als die Startzeit liegt.
- Sensitivity (
nPips) – relativer Abstand (ausgedrückt als Multiplikator, z.B. 0.004 = 0.4%), der verwendet wird, um den Trailing Stop näher oder weiter vom Preis zu bewegen.
- Signal Bar Delay (
SignalBar) – Anzahl abgeschlossener Kerzen, die gewartet werden soll, bevor auf ein Signal reagiert wird. 0 führt sofort bei der Schlusskerze aus, 1 reproduziert das Standardverhalten, auf dem vorherigen Balken zu handeln.
- Candle Type – Zeitrahmen der Kerzen-Subskription für Indikatorberechnungen.
- Applied Price – Wahl der Preisserie (Schluss, Eröffnung, Medianpreis, Trendfolgemodi, Demark-Preis usw.), die die Trailing-Stop-Berechnung speist.
Implementierungshinweise
- Der Indikator ist direkt in der Strategie implementiert, ohne sich auf externe Buffer zu verlassen, sodass die
nPips-Trailing-Stop-Logik dem ursprünglichen MQL5-Code entspricht.
- Sitzungsverwaltung und Signalsequenzierung folgen dem ursprünglichen Experten, einschließlich der Priorität, bestehende Exposition zu schließen, bevor neue Trades eröffnet werden.
- Die Konvertierung konzentriert sich auf die High-Level-StockSharp-API: Kerzen-Subskriptionen, verzögerte Signal-Warteschlange und Marktaufträge (
BuyMarket / SellMarket). Money-Management-Funktionen, die an MetaTrader-Kontometriken gebunden waren, wurden weggelassen, da StockSharp-Strategien bereits mit expliziten Positionsgrößen arbeiten.
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>
/// Stopreversal trailing stop strategy with a configurable trading session filter.
/// </summary>
public class StopreversalTmStrategy : Strategy
{
/// <summary>
/// Available price sources for the Stopreversal trailing stop.
/// </summary>
public enum StopreversalAppliedPrices
{
Close = 1,
Open,
High,
Low,
Median,
Typical,
Weighted,
Simple,
Quarter,
TrendFollow0,
TrendFollow1,
Demark
}
private readonly StrategyParam<bool> _allowBuyEntry;
private readonly StrategyParam<bool> _allowSellEntry;
private readonly StrategyParam<bool> _allowBuyExit;
private readonly StrategyParam<bool> _allowSellExit;
private readonly StrategyParam<bool> _useTimeFilter;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _startMinute;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _endMinute;
private readonly StrategyParam<decimal> _nPips;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<StopreversalAppliedPrices> _appliedPrice;
private readonly List<SignalInfo> _signalQueue = new();
private decimal? _previousAppliedPrice;
private decimal? _previousStopLevel;
public StopreversalTmStrategy()
{
_allowBuyEntry = Param(nameof(AllowBuyEntry), true)
.SetDisplay("Allow Buy Entries", "Enable opening long positions on bullish signals", "Signals")
;
_allowSellEntry = Param(nameof(AllowSellEntry), true)
.SetDisplay("Allow Sell Entries", "Enable opening short positions on bearish signals", "Signals")
;
_allowBuyExit = Param(nameof(AllowBuyExit), true)
.SetDisplay("Allow Long Exits", "Close existing long positions when a sell signal arrives", "Signals")
;
_allowSellExit = Param(nameof(AllowSellExit), true)
.SetDisplay("Allow Short Exits", "Close existing short positions when a buy signal arrives", "Signals")
;
_useTimeFilter = Param(nameof(UseTimeFilter), false)
.SetDisplay("Use Time Filter", "Restrict trading to the configured session", "Session");
_startHour = Param(nameof(StartHour), 0)
.SetRange(0, 23)
.SetDisplay("Start Hour", "Session start hour (0-23)", "Session")
;
_startMinute = Param(nameof(StartMinute), 0)
.SetRange(0, 59)
.SetDisplay("Start Minute", "Session start minute (0-59)", "Session")
;
_endHour = Param(nameof(EndHour), 23)
.SetRange(0, 23)
.SetDisplay("End Hour", "Session end hour (0-23)", "Session")
;
_endMinute = Param(nameof(EndMinute), 59)
.SetRange(0, 59)
.SetDisplay("End Minute", "Session end minute (0-59)", "Session")
;
_nPips = Param(nameof(Npips), 0.004m)
.SetGreaterThanZero()
.SetDisplay("Sensitivity", "Relative offset used to build the trailing stop", "Indicator")
;
_signalBar = Param(nameof(SignalBar), 1)
.SetNotNegative()
.SetDisplay("Signal Bar Delay", "Number of completed bars to wait before acting", "Indicator")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
_appliedPrice = Param(nameof(AppliedPrice), StopreversalAppliedPrices.Close)
.SetDisplay("Applied Price", "Price source for the trailing stop", "Indicator")
;
}
public bool AllowBuyEntry { get => _allowBuyEntry.Value; set => _allowBuyEntry.Value = value; }
public bool AllowSellEntry { get => _allowSellEntry.Value; set => _allowSellEntry.Value = value; }
public bool AllowBuyExit { get => _allowBuyExit.Value; set => _allowBuyExit.Value = value; }
public bool AllowSellExit { get => _allowSellExit.Value; set => _allowSellExit.Value = value; }
public bool UseTimeFilter { get => _useTimeFilter.Value; set => _useTimeFilter.Value = value; }
public int StartHour { get => _startHour.Value; set => _startHour.Value = value; }
public int StartMinute { get => _startMinute.Value; set => _startMinute.Value = value; }
public int EndHour { get => _endHour.Value; set => _endHour.Value = value; }
public int EndMinute { get => _endMinute.Value; set => _endMinute.Value = value; }
public decimal Npips { get => _nPips.Value; set => _nPips.Value = value; }
public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public StopreversalAppliedPrices AppliedPrice { get => _appliedPrice.Value; set => _appliedPrice.Value = value; }
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousAppliedPrice = null;
_previousStopLevel = null;
_signalQueue.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_previousAppliedPrice = null;
_previousStopLevel = null;
_signalQueue.Clear();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
// no protection needed
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var price = GetAppliedPrice(candle);
if (_previousAppliedPrice is null || _previousStopLevel is null)
{
_previousAppliedPrice = price;
_previousStopLevel = price;
EnqueueSignal(new SignalInfo(false, false, false, false), candle.CloseTime);
return;
}
var prevPrice = _previousAppliedPrice.Value;
var prevStop = _previousStopLevel.Value;
var trailingStop = CalculateTrailingStop(price, prevPrice, prevStop);
var buySignal = price > trailingStop && prevPrice < prevStop;
var sellSignal = price < trailingStop && prevPrice > prevStop;
_previousStopLevel = trailingStop;
_previousAppliedPrice = price;
var action = new SignalInfo(
buySignal && AllowBuyEntry,
sellSignal && AllowSellEntry,
sellSignal && AllowBuyExit,
buySignal && AllowSellExit
);
EnqueueSignal(action, candle.CloseTime);
}
private void EnqueueSignal(SignalInfo signal, DateTime currentTime)
{
_signalQueue.Add(signal);
while (_signalQueue.Count > SignalBar)
{
var action = _signalQueue[0];
try { _signalQueue.RemoveAt(0); } catch { }
HandleSignal(action, currentTime);
}
}
private void HandleSignal(SignalInfo signal, DateTime currentTime)
{
var inWindow = !UseTimeFilter || IsWithinTradingWindow(currentTime);
if (UseTimeFilter && !inWindow && Position != 0)
{
if (Position > 0)
SellMarket();
else
BuyMarket();
}
if (signal.CloseLong && Position > 0)
SellMarket();
if (signal.CloseShort && Position < 0)
BuyMarket();
if (!UseTimeFilter || inWindow)
{
if (signal.OpenLong && Position <= 0)
BuyMarket();
if (signal.OpenShort && Position >= 0)
SellMarket();
}
}
private decimal CalculateTrailingStop(decimal price, decimal prevPrice, decimal prevStop)
{
var shift = Npips;
if (price == prevStop)
return prevStop;
if (prevPrice < prevStop && price < prevStop)
return Math.Min(prevStop, price * (1 + shift));
if (prevPrice > prevStop && price > prevStop)
return Math.Max(prevStop, price * (1 - shift));
return price > prevStop
? price * (1 - shift)
: price * (1 + shift);
}
private decimal GetAppliedPrice(ICandleMessage candle)
{
return AppliedPrice switch
{
StopreversalAppliedPrices.Close => candle.ClosePrice,
StopreversalAppliedPrices.Open => candle.OpenPrice,
StopreversalAppliedPrices.High => candle.HighPrice,
StopreversalAppliedPrices.Low => candle.LowPrice,
StopreversalAppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
StopreversalAppliedPrices.Typical => (candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 3m,
StopreversalAppliedPrices.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
StopreversalAppliedPrices.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
StopreversalAppliedPrices.Quarter => (candle.OpenPrice + candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
StopreversalAppliedPrices.TrendFollow0 => candle.ClosePrice > candle.OpenPrice
? candle.HighPrice
: candle.ClosePrice < candle.OpenPrice
? candle.LowPrice
: candle.ClosePrice,
StopreversalAppliedPrices.TrendFollow1 => candle.ClosePrice > candle.OpenPrice
? (candle.HighPrice + candle.ClosePrice) / 2m
: candle.ClosePrice < candle.OpenPrice
? (candle.LowPrice + candle.ClosePrice) / 2m
: candle.ClosePrice,
StopreversalAppliedPrices.Demark => CalculateDemarkPrice(candle),
_ => candle.ClosePrice
};
}
private static decimal CalculateDemarkPrice(ICandleMessage candle)
{
var result = candle.HighPrice + candle.LowPrice + candle.ClosePrice;
if (candle.ClosePrice < candle.OpenPrice)
result = (result + candle.LowPrice) / 2m;
else if (candle.ClosePrice > candle.OpenPrice)
result = (result + candle.HighPrice) / 2m;
else
result = (result + candle.ClosePrice) / 2m;
return ((result - candle.LowPrice) + (result - candle.HighPrice)) / 2m;
}
private bool IsWithinTradingWindow(DateTime time)
{
var start = new TimeSpan(StartHour, StartMinute, 0);
var end = new TimeSpan(EndHour, EndMinute, 0);
var current = time.TimeOfDay;
if (start == end)
return false;
if (start < end)
return current >= start && current < end;
return current >= start || current < end;
}
private readonly struct SignalInfo
{
public SignalInfo(bool openLong, bool openShort, bool closeLong, bool closeShort)
{
OpenLong = openLong;
OpenShort = openShort;
CloseLong = closeLong;
CloseShort = closeShort;
}
public bool OpenLong { get; }
public bool OpenShort { get; }
public bool CloseLong { get; }
public bool CloseShort { get; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class stopreversal_tm_strategy(Strategy):
"""Stopreversal TM: trailing stop reversal strategy with configurable trading session filter."""
def __init__(self):
super(stopreversal_tm_strategy, self).__init__()
self._allow_buy_entry = self.Param("AllowBuyEntry", True) \
.SetDisplay("Allow Buy Entries", "Enable opening long positions on bullish signals", "Signals")
self._allow_sell_entry = self.Param("AllowSellEntry", True) \
.SetDisplay("Allow Sell Entries", "Enable opening short positions on bearish signals", "Signals")
self._allow_buy_exit = self.Param("AllowBuyExit", True) \
.SetDisplay("Allow Long Exits", "Close existing long positions when a sell signal arrives", "Signals")
self._allow_sell_exit = self.Param("AllowSellExit", True) \
.SetDisplay("Allow Short Exits", "Close existing short positions when a buy signal arrives", "Signals")
self._use_time_filter = self.Param("UseTimeFilter", False) \
.SetDisplay("Use Time Filter", "Restrict trading to the configured session", "Session")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "Session start hour (0-23)", "Session")
self._start_minute = self.Param("StartMinute", 0) \
.SetDisplay("Start Minute", "Session start minute (0-59)", "Session")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Session end hour (0-23)", "Session")
self._end_minute = self.Param("EndMinute", 59) \
.SetDisplay("End Minute", "Session end minute (0-59)", "Session")
self._n_pips = self.Param("Npips", 0.004) \
.SetGreaterThanZero() \
.SetDisplay("Sensitivity", "Relative offset used to build the trailing stop", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar Delay", "Number of completed bars to wait before acting", "Indicator")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles used for calculations", "General")
# Price types: 1=Close, 2=Open, 3=High, 4=Low, 5=Median, 6=Typical, 7=Weighted,
# 8=Simple, 9=Quarter, 10=TrendFollow0, 11=TrendFollow1, 12=Demark
self._applied_price = self.Param("AppliedPrice", 1) \
.SetDisplay("Applied Price", "Price source for the trailing stop", "Indicator")
self._signal_queue = []
self._previous_applied_price = None
self._previous_stop_level = None
@property
def AllowBuyEntry(self):
return self._allow_buy_entry.Value
@property
def AllowSellEntry(self):
return self._allow_sell_entry.Value
@property
def AllowBuyExit(self):
return self._allow_buy_exit.Value
@property
def AllowSellExit(self):
return self._allow_sell_exit.Value
@property
def UseTimeFilter(self):
return self._use_time_filter.Value
@property
def StartHour(self):
return int(self._start_hour.Value)
@property
def StartMinute(self):
return int(self._start_minute.Value)
@property
def EndHour(self):
return int(self._end_hour.Value)
@property
def EndMinute(self):
return int(self._end_minute.Value)
@property
def Npips(self):
return float(self._n_pips.Value)
@property
def SignalBar(self):
return int(self._signal_bar.Value)
@property
def CandleType(self):
return self._candle_type.Value
@property
def AppliedPrice(self):
return int(self._applied_price.Value)
def _get_applied_price(self, candle):
c = float(candle.ClosePrice)
o = float(candle.OpenPrice)
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
ap = self.AppliedPrice
if ap == 1:
return c
elif ap == 2:
return o
elif ap == 3:
return h
elif ap == 4:
return lo
elif ap == 5:
return (h + lo) / 2.0
elif ap == 6:
return (c + h + lo) / 3.0
elif ap == 7:
return (2.0 * c + h + lo) / 4.0
elif ap == 8:
return (o + c) / 2.0
elif ap == 9:
return (o + c + h + lo) / 4.0
elif ap == 10:
if c > o:
return h
elif c < o:
return lo
return c
elif ap == 11:
if c > o:
return (h + c) / 2.0
elif c < o:
return (lo + c) / 2.0
return c
elif ap == 12:
return self._calc_demark(candle)
return c
def _calc_demark(self, candle):
c = float(candle.ClosePrice)
o = float(candle.OpenPrice)
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
res = h + lo + c
if c < o:
res = (res + lo) / 2.0
elif c > o:
res = (res + h) / 2.0
else:
res = (res + c) / 2.0
return ((res - lo) + (res - h)) / 2.0
def _calc_trailing_stop(self, price, prev_price, prev_stop):
shift = self.Npips
if price == prev_stop:
return prev_stop
if prev_price < prev_stop and price < prev_stop:
return min(prev_stop, price * (1.0 + shift))
if prev_price > prev_stop and price > prev_stop:
return max(prev_stop, price * (1.0 - shift))
if price > prev_stop:
return price * (1.0 - shift)
return price * (1.0 + shift)
def _is_within_trading_window(self, time):
sh = self.StartHour
sm = self.StartMinute
eh = self.EndHour
em = self.EndMinute
start_minutes = sh * 60 + sm
end_minutes = eh * 60 + em
current_minutes = time.Hour * 60 + time.Minute
if start_minutes == end_minutes:
return False
if start_minutes < end_minutes:
return current_minutes >= start_minutes and current_minutes < end_minutes
return current_minutes >= start_minutes or current_minutes < end_minutes
def OnStarted2(self, time):
super(stopreversal_tm_strategy, self).OnStarted2(time)
self._previous_applied_price = None
self._previous_stop_level = None
self._signal_queue = []
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = self._get_applied_price(candle)
if self._previous_applied_price is None or self._previous_stop_level is None:
self._previous_applied_price = price
self._previous_stop_level = price
self._enqueue_signal((False, False, False, False), candle.CloseTime)
return
prev_price = self._previous_applied_price
prev_stop = self._previous_stop_level
trailing_stop = self._calc_trailing_stop(price, prev_price, prev_stop)
buy_signal = price > trailing_stop and prev_price < prev_stop
sell_signal = price < trailing_stop and prev_price > prev_stop
self._previous_stop_level = trailing_stop
self._previous_applied_price = price
action = (
buy_signal and self.AllowBuyEntry,
sell_signal and self.AllowSellEntry,
sell_signal and self.AllowBuyExit,
buy_signal and self.AllowSellExit
)
self._enqueue_signal(action, candle.CloseTime)
def _enqueue_signal(self, signal, current_time):
self._signal_queue.append(signal)
while len(self._signal_queue) > self.SignalBar:
action = self._signal_queue.pop(0)
self._handle_signal(action, current_time)
def _handle_signal(self, signal, current_time):
open_long, open_short, close_long, close_short = signal
in_window = not self.UseTimeFilter or self._is_within_trading_window(current_time)
if self.UseTimeFilter and not in_window and self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
if close_long and self.Position > 0:
self.SellMarket()
if close_short and self.Position < 0:
self.BuyMarket()
if not self.UseTimeFilter or in_window:
if open_long and self.Position <= 0:
self.BuyMarket()
if open_short and self.Position >= 0:
self.SellMarket()
def OnReseted(self):
super(stopreversal_tm_strategy, self).OnReseted()
self._previous_applied_price = None
self._previous_stop_level = None
self._signal_queue = []
def CreateClone(self):
return stopreversal_tm_strategy()