在 GitHub 上查看
TrailingStopFrCn 策略
概述
TrailingStopFrCnStrategy 是 MetaTrader 智能交易系统 TrailingStopFrCn.mq4 的 StockSharp 版本。原始脚本负责管理已有持仓的止损,支持固定距离、比尔·威廉姆斯分形以及最近 K 线高低点三种跟踪方式。本策略在保持原有灵活性的同时,使用 StockSharp 的高级 API 订阅蜡烛图与 Level 1 行情,监控当前净持仓,并自动更新保护性止损订单。
策略只关注风险控制,不会创建新的开仓订单。它跟踪 Strategy.Security 的净持仓,当持仓方向反转时取消旧的止损单,并为全部净持仓发送一个新的止损单,从而复现原始 EA 的管理逻辑。
跟踪逻辑
- 固定距离跟踪:当
TrailingStopPips 大于零时,策略与原版 EA 的 TrailingStop 参数一致。多头止损放在 bestBid - distance,空头止损放在 bestAsk + distance,其中 distance = TrailingStopPips × 点值。
- 分形跟踪:当
TrailingStopPips = 0 且 TrailingMode = Fractals 时,策略会识别五根 K 线的比尔·威廉姆斯分形。每根完成的蜡烛都会存入缓存,当历史足够时,向前数两根的蜡烛会被评估为潜在分形。最近一次且与当前价格至少相差 MinStopDistancePips 的分形价位将成为新的止损候选。
- K 线高低点跟踪:当
TrailingStopPips = 0 且 TrailingMode = Candles 时,策略会在最近 99 根已完成蜡烛中查找第一个与当前价格间隔不少于 MinStopDistancePips 的低点(多头)或高点(空头)。
候选止损算出后,策略继续执行与原始 MQL 代码一致的保护规则:
- OnlyProfit:只有当新的止损价位能够锁定利润时才会移动止损(多头止损高于入场价,空头止损低于入场价)。
- OnlyWithoutLoss:一旦当前止损已经保证不亏损,便停止继续跟踪。
- 止损只会沿着有利方向移动:多头向上、空头向下。
由于 StockSharp 以净仓方式管理头寸,止损订单的数量等于 Math.Abs(Position),不会分拆到单独的交易票据。
参数
| 参数 |
说明 |
OnlyProfit |
仅在新的止损价能够锁定利润时移动止损,对应原 EA 的 OnlyProfit。 |
OnlyWithoutLoss |
当当前止损已处于入场价或更有利位置时停止继续跟踪,对应原 EA 的 OnlyWithoutLoss。 |
TrailingStopPips |
固定跟踪距离(以点数表示)。设为 0 时启用分形或 K 线高低点模式。 |
MinStopDistancePips |
当前价格与止损之间的最小距离(点数)。用于模拟经纪商的 MODE_STOPLEVEL 限制。 |
TrailingMode |
当 TrailingStopPips = 0 时选择跟踪来源:Fractals(五根 K 线分形)或 Candles(最近高低点)。 |
CandleType |
用于计算分形或摆动点的蜡烛类型,默认为 1 小时时间框架。 |
行为说明
- 策略订阅 Level 1 行情以获得最优买价与最优卖价。固定距离跟踪会在价格更新时立即反应;分形/蜡烛模式在新蜡烛完成时更新候选止损。
- 当持仓方向发生变化时,现有的止损订单会先被取消,再根据新方向重新下单。
- 如果暂时找不到合适的止损候选(例如历史数据不足),策略会保持当前止损不变。
- 当经纪商没有最小止损距离要求时,可将
MinStopDistancePips 设置为 0。
- StockSharp 只跟踪净持仓,因此不会区分 MetaTrader 中的单个票据;一个止损订单覆盖全部仓位。
- 原脚本中的
Magic 过滤不再需要,策略天然只作用于自身的证券对象。
- 跟踪更新由完成的蜡烛和 Level 1 行情驱动,而不是每秒轮询一次。
- 不再绘制 MetaTrader 中的图形对象;若需要可使用 StockSharp 自带的图表工具。
使用建议
- 将本策略与任何负责开仓的策略同时运行,只要目标证券上出现净持仓,TrailingStopFrCn 就会自动附加止损。
- 根据需要调整
CandleType。较高周期带来更平滑的止损轨迹,较低周期响应更快。
- 按经纪商的最小止损限制校准
MinStopDistancePips。过低的数值可能导致订单被拒绝。
- 在历史回测时请确保蜡烛数据与 Level 1 行情可用,否则止损逻辑可能无法触发。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// TrailingStopFrCn: EMA crossover with ATR trailing stops.
/// </summary>
public class TrailingStopFrCnStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastEmaLength;
private readonly StrategyParam<int> _slowEmaLength;
private readonly StrategyParam<int> _atrLength;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
public TrailingStopFrCnStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_fastEmaLength = Param(nameof(FastEmaLength), 12)
.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators");
_slowEmaLength = Param(nameof(SlowEmaLength), 26)
.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
protected override void OnReseted()
{
base.OnReseted();
_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(fastEma, slowEma, atr, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fastEma); DrawIndicator(area, slowEma); DrawOwnTrades(area); }
}
private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished) return;
if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
var close = candle.ClosePrice;
if (Position > 0)
{
if ((fastVal < slowVal && _prevFast >= _prevSlow) || close <= _entryPrice - atrVal * 2m) { SellMarket(); _entryPrice = 0; }
}
else if (Position < 0)
{
if ((fastVal > slowVal && _prevFast <= _prevSlow) || close >= _entryPrice + atrVal * 2m) { BuyMarket(); _entryPrice = 0; }
}
if (Position == 0)
{
if (fastVal > slowVal && _prevFast <= _prevSlow) { _entryPrice = close; BuyMarket(); }
else if (fastVal < slowVal && _prevFast >= _prevSlow) { _entryPrice = close; SellMarket(); }
}
_prevFast = fastVal; _prevSlow = slowVal;
}
}
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
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
class trailing_stop_fr_cn_strategy(Strategy):
def __init__(self):
super(trailing_stop_fr_cn_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._fast_ema_length = self.Param("FastEmaLength", 12) \
.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators")
self._slow_ema_length = self.Param("SlowEmaLength", 26) \
.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def FastEmaLength(self):
return self._fast_ema_length.Value
@property
def SlowEmaLength(self):
return self._slow_ema_length.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(trailing_stop_fr_cn_strategy, self).OnStarted2(time)
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._fast_ema = ExponentialMovingAverage()
self._fast_ema.Length = self.FastEmaLength
self._slow_ema = ExponentialMovingAverage()
self._slow_ema.Length = self.SlowEmaLength
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._fast_ema, self._slow_ema, self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, fast_val, slow_val, atr_val):
if candle.State != CandleStates.Finished:
return
fv = float(fast_val)
sv = float(slow_val)
av = float(atr_val)
if self._prev_fast == 0 or self._prev_slow == 0 or av <= 0:
self._prev_fast = fv
self._prev_slow = sv
return
close = float(candle.ClosePrice)
if self.Position > 0:
if (fv < sv and self._prev_fast >= self._prev_slow) or close <= self._entry_price - av * 2.0:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if (fv > sv and self._prev_fast <= self._prev_slow) or close >= self._entry_price + av * 2.0:
self.BuyMarket()
self._entry_price = 0.0
if self.Position == 0:
if fv > sv and self._prev_fast <= self._prev_slow:
self._entry_price = close
self.BuyMarket()
elif fv < sv and self._prev_fast >= self._prev_slow:
self._entry_price = close
self.SellMarket()
self._prev_fast = fv
self._prev_slow = sv
def OnReseted(self):
super(trailing_stop_fr_cn_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
def CreateClone(self):
return trailing_stop_fr_cn_strategy()