Stopreversal Tm 策略
概述
Stopreversal Tm 策略完整移植自 MetaTrader 5 专家顾问 Exp_Stopreversal_Tm.mq5。策略基于自定义指标 Stopreversal,它利用选定的价格类型计算跟踪止损,并在价格越过该止损时发出趋势反转信号。策略仅针对一个标的和一组 K 线数据运行,并保留了原策略可配置的交易时段过滤功能。
信号生成
Stopreversal 指标从所选价格模式(收盘价、开盘价、趋势跟随价、Demark 价等)取样,然后按照 Sensitivity(即 nPips 参数)调整动态止损。当新的价格高于止损且上一根 K 线仍低于止损时,产生做多信号;当新的价格跌破止损且上一根 K 线仍高于止损时,产生做空信号。做多信号会要求平掉现有空头并开多,做空信号则会平多并开空。
为了复现原 MQL5 程序的行为,可以通过 Signal Bar Delay(对应原输入 SignalBar)延迟若干根已完成的 K 线再执行信号,从而避免在尚未收盘的蜡烛上开仓或平仓。
交易时段与仓位处理
原专家顾问允许用户限制交易时段。本移植版本同样提供 Use Time Filter 开关,并通过 Start Hour/Minute 与 End Hour/Minute 设定交易窗口。若当前时间超出允许范围,策略会立即平掉净持仓。即便关闭时段过滤,信号驱动的平仓仍会生效。
策略按照净仓方式运作。每当方向发生改变时,先执行平仓再执行反向开仓,确保不会出现同时持有多空的情况。
参数说明
- Allow Buy Entries / Allow Sell Entries – 控制在收到多头或空头信号时是否允许开仓。
- Allow Long Exits / Allow Short Exits – 控制是否允许由相反信号触发的平仓动作。
- Use Time Filter – 打开或关闭交易时段限制。
- Start Hour / Start Minute / End Hour / End Minute – 定义交易时段的起止时间(开始时间包含,结束时间不包含),支持跨夜时段。
- Sensitivity (
nPips) – 调整跟踪止损距离的比例系数,例如0.004表示 0.4%。 - Signal Bar Delay (
SignalBar) – 指定在信号触发后需要等待的已完成 K 线数量,0表示立即执行,1为默认的上一根 K 线。 - Candle Type – 计算指标时使用的 K 线周期。
- Applied Price – 选择用于计算的价格类型(收盘价、均价、趋势跟随价、Demark 价等)。
实现细节
- 跟踪止损逻辑直接在策略内部实现,与原始 MQL5 指标保持一致,无需额外的缓冲区。
- 时段管理与信号执行顺序遵循原策略:先平仓后反向开仓。
- 转换版本使用 StockSharp 的高级 API(K 线订阅、信号延迟队列、
BuyMarket/SellMarket市价单)。MetaTrader 账号层面的资金管理在 StockSharp 中没有直接对应,因此未实现该部分。
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()