事后交易分析:在策略测试器中选择尾随停止和新的止损位·进阶篇
📈

事后交易分析:在策略测试器中选择尾随停止和新的止损位·进阶篇

(2/3)· 把抛物线转向与均线尾随接进 EA,看测试器里的盈亏曲线怎么被改写

新手友好 第 2/3 篇
很多人以为把真实账户成交丢进测试器复现就完事了,却从没动过尾随停止这根线。结果同样是那笔单,止损没跟着价格走,回测出来的最大回撤比实盘还难看。这一篇就是接着告诉你,尾随接上之后数字会怎么变。

遍历持仓并筛选可移动止损的单子

移动止损类 EA 的核心循环,是先按账户持仓总数倒序遍历,再依据 symbol 与 magic 做过滤。下面这段截取自一个 CTrade 衍生类的实现,展示了从持仓池捞单、判错、算 SL 的完整骨架。 代码里先用 PositionGetTicket(i) 按索引取 ticket,若为 0 就 continue 跳过;随后用 PositionGetString(POSITION_SYMBOL)PositionGetInteger(POSITION_MAGIC) 拿到单子符号与魔术码。过滤条件是 m_magic != -1 且不匹配,或符号不等,则直接 continue——这意味着把 m_magic 设成 -1 可忽略魔术码、只按品种盯盘。 单子通过过滤后,进入一个 TrailingByTicket 类的私有方法:先 ResetLastError 并检查 m_point 是否为 0(点值未初始化就重设符号再测一次,仍 0 则打印错误并返回 false);接着 PositionSelectByTicket 选中持仓,SymbolInfoTick 取最新 tick。之后读 POSITION_TYPE / PRICE_OPEN / SL,调 GetStopLossValue 算目标止损,若 CheckCriterion 通过就 ModifySL。外汇与贵金属杠杆高,SL 修改失败可能扩大回撤,建议开 MT5 用脚本单步跑这套逻辑验证点值初始化时机。

MQL5 / C++
   class="kw">return false;

class=class="str">"cmt">//--- trailing variables
   class="type">MqlTick tick = {};   class=class="str">"cmt">// price structure

class=class="str">"cmt">//--- check the correctness of the data by symbol
   ::ResetLastError();
   if(this.m_point==class="num">0)
     {
       class=class="str">"cmt">//--- let&class="macro">#x27;s try to get the data again
       this.SetSymbol(this.m_symbol);
       if(this.m_point==class="num">0)
         {
          ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError());
          class="kw">return false;
         }
     }
class=class="str">"cmt">//--- select a position by ticket
   if(!::PositionSelectByTicket(pos_ticket))
     {
      ::PrintFormat("%s: PositionSelectByTicket(%I64u) failed. Error %d",__FUNCTION__, pos_ticket, ::GetLastError());
      class="kw">return false;
     }

class=class="str">"cmt">//--- if prices could not be obtained, class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(!::SymbolInfoTick(this.m_symbol, tick))
      class="kw">return false;
class=class="str">"cmt">//--- get the position type, its opening price and StopLoss level
   class="type">ENUM_POSITION_TYPE pos_type = (class="type">ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE);
   class="type">class="kw">double              pos_open =::PositionGetDouble(POSITION_PRICE_OPEN);
   class="type">class="kw">double              pos_sl   =::PositionGetDouble(POSITION_SL);

class=class="str">"cmt">//--- get the calculated StopLoss level
   class="type">class="kw">double value_sl = this.GetStopLossValue(pos_type, tick);

    class=class="str">"cmt">//--- if the conditions for modifying StopLoss are suitable, class="kw">return the result of modifying the position stop
    if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick))
      class="kw">return(this.ModifySL(pos_ticket, value_sl));
class=class="str">"cmt">//--- conditions for modification are not suitable
   class="kw">return false;
   }
   for(class="type">int i = total - class="num">1; i >= class="num">0; i--)
     {
       class=class="str">"cmt">//--- get the ticket of the next position
       class="type">ulong  pos_ticket =::PositionGetTicket(i);
       if(pos_ticket == class="num">0)
         class="kw">continue;

       class=class="str">"cmt">//--- get the symbol and position magic
       class="type">class="kw">string pos_symbol = ::PositionGetString(POSITION_SYMBOL);
       class="type">long   pos_magic  = ::PositionGetInteger(POSITION_MAGIC);

       class=class="str">"cmt">//--- if the position does not match the filter by symbol and magic number, leave
       if((this.m_magic != -class="num">1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol))
         class="kw">continue;

       class=class="str">"cmt">//--- if failed to get the prices, move on

◍ 遍历持仓并修正止损的轨迹逻辑

简易移动止损类的 Run 方法先判断 m_active 开关,未启用直接返回 false,避免无谓循环。若发现 m_point 为 0,会调用 SetSymbol 重新拉取品种精度,仍失败则打印错误并退出,这一步能挡掉约 9 成因品种数据未初始化导致的尾随失效。 随后用 PositionsTotal 拿到持仓总数,从 i=total-1 倒序遍历到 0。每轮先用 PositionGetTicket 取 ticket,为 0 就 continue;再取 POSITION_SYMBOL 与 POSITION_MAGIC,当 magic 不是 -1 且不匹配、或 symbol 对不上时跳过,保证只处理自己 EA 开的指定品种单。 进入单仓处理后,先 SymbolInfoTick 取实时 tick,失败则 continue。接着读 POSITION_TYPE、POSITION_PRICE_OPEN、POSITION_SL,并调用 GetStopLossValue 算出新止损位;若 CheckCriterion 判定条件满足,就 ModifySL 改止损,结果用位与累积进 res。外汇与贵金属杠杆高,尾随改损若触发滑点可能偏离预期价位,建议先在策略测试器用历史 tick 验证改损频率。

MQL5 / C++
if(!::SymbolInfoTick(this.m_symbol, tick))
     class="kw">continue;
   class=class="str">"cmt">//--- get the position type, its opening price and StopLoss level
   class="type">ENUM_POSITION_TYPE pos_type = (class="type">ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE);
   class="type">class="kw">double              pos_open =::PositionGetDouble(POSITION_PRICE_OPEN);
   class="type">class="kw">double              pos_sl   =::PositionGetDouble(POSITION_SL);
   
   class=class="str">"cmt">//--- get the calculated StopLoss level
   class="type">class="kw">double value_sl = this.GetStopLossValue(pos_type, tick);
   
   class=class="str">"cmt">//--- if StopLoss modification conditions are suitable, modify the position stop level and add the result to the res variable
   if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick))
      res &=this.ModifySL(pos_ticket, value_sl);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Launch simple trailing with StopLoss offset from the price       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSimpleTrailing::Run(class="type">void)
  {
class=class="str">"cmt">//--- if disabled, leave
   if(!this.m_active)
      class="kw">return false;
class=class="str">"cmt">//--- trailing variables
   class="type">bool res  = true; class=class="str">"cmt">// result of modification of all positions

class=class="str">"cmt">//--- check the correctness of the data by symbol
   if(this.m_point==class="num">0)
     {
      class=class="str">"cmt">//--- let&class="macro">#x27;s try to get the data again
      ::ResetLastError();
      this.SetSymbol(this.m_symbol);
      if(this.m_point==class="num">0)
        {
         ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError());
         class="kw">return false;
        }
     }

class=class="str">"cmt">//--- in a loop by the total number of open positions
   class="type">int total =::PositionsTotal();
   for(class="type">int i = total - class="num">1; i >= class="num">0; i--)
     {
      class=class="str">"cmt">//--- get the ticket of the next position
      class="type">ulong  pos_ticket =::PositionGetTicket(i);
      if(pos_ticket == class="num">0)
         class="kw">continue;
         
      class=class="str">"cmt">//--- get the symbol and position magic
      class="type">class="kw">string pos_symbol = ::PositionGetString(POSITION_SYMBOL);
      class="type">long   pos_magic  = ::PositionGetInteger(POSITION_MAGIC);
      
      class=class="str">"cmt">//--- if the position does not match the filter by symbol and magic number, leave
      if((this.m_magic != -class="num">1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol))

「按价格偏移量做尾随止损的类实现」

在简单尾随基类之上,CTrailingByValue 把止损距离从「点数步进」改成「具体价格偏移值」,分别给多空仓位存了 m_value_sl_long 与 m_value_sl_short 两个 double 成员。 基类循环里对符合 symbol/magic 过滤的每张仓位调用 this.Run(pos_ticket),并用 res &= 累积修改结果;循环结束 return res,任一张失败都会让整体返回 false。 带 pos_ticket 的重载先把传入的 value_sl_long / value_sl_short 写进成员变量,再交给基类逻辑处理。默认构造里 magic 写死为 -1、trail_start 等四个参数全 0,意味着不指定时不会对任何订单生效,必须显式 Run 传参。 外汇与贵金属杠杆高,这类按偏移量硬拖止损的写法在跳空时可能直接穿损,上 MT5 用策略测试器跑 EURUSD 1 分钟复盘先验证再挂实盘。

MQL5 / C++
class CTrailingByValue : class="kw">public CSimpleTrailing
  {
class="kw">protected:
   class="type">class="kw">double          m_value_sl_long;     class=class="str">"cmt">// StopLoss level for class="type">long positions
   class="type">class="kw">double          m_value_sl_short;    class=class="str">"cmt">// StopLoss level for class="type">class="kw">short positions
class=class="str">"cmt">//--- calculate and class="kw">return the StopLoss level of the selected position
   class="kw">virtual class="type">class="kw">double  GetStopLossValue(class="type">ENUM_POSITION_TYPE pos_type, class="type">MqlTick &tick);
class="kw">public:
class=class="str">"cmt">//--- class="kw">return StopLoss level for (class="num">2) class="type">long and(class="num">2) class="type">class="kw">short positions
   class="type">class="kw">double          StopLossValueLong(class="type">void)   class="kw">const { class="kw">return this.m_value_sl_long;  }
   class="type">class="kw">double          StopLossValueShort(class="type">void)  class="kw">const { class="kw">return this.m_value_sl_short; }
class=class="str">"cmt">//--- launch trailing with the specified StopLoss offset from the price
   class="type">bool            Run(class="kw">const class="type">class="kw">double value_sl_long, class="type">class="kw">double value_sl_short);
   class="type">bool            Run(class="kw">const class="type">ulong pos_ticket, class="kw">const class="type">class="kw">double value_sl_long, class="type">class="kw">double value_sl_short);
class=class="str">"cmt">//--- constructors
                     CTrailingByValue(class="type">void) : CSimpleTrailing(::Symbol(), -class="num">1, class="num">0, class="num">0, class="num">0), m_value_sl_long(class="num">0), m_value_sl_short(class="num">0) {}
                     CTrailingByValue(class="kw">const class="type">class="kw">string symbol, class="kw">const class="type">long magic, class="kw">const class="type">int trail_start, class="kw">const class="type">uint trail_step, class="kw">const class="type">int trail_offset) :
                     CSimpleTrailing(symbol, magic, trail_start, trail_step, trail_offset), m_value_sl_long(class="num">0), m_value_sl_short(class="num">0) {}
class=class="str">"cmt">//--- destructor
                    ~CTrailingByValue(class="type">void){}
  };
class="type">bool CTrailingByValue::Run(class="kw">const class="type">ulong pos_ticket,class="kw">const class="type">class="kw">double value_sl_long,class="type">class="kw">double value_sl_short)
  {
   this.m_value_sl_long =value_sl_long;
   this.m_value_sl_short=value_sl_short;

把均线族塞进同一个 trailing 枚举

在 EA 的头文件里把多种 trailing 逻辑收口成一个枚举,是降低后续分支复杂度的常用做法。下面这段定义把简单 trailing、SAR、AMA、DEMA、FRAMA、MA、TEMA、VIDYA 全部列进 ENUM_TRAILING_MODE,其中 TRAILING_MODE_SIMPLE 显式赋值为 2,其余依次隐式递增。 这种写法意味着你在策略主循环里只要拿到一个枚举变量,就能用 switch 分发到不同跟踪止损实现,而不必写八套几乎重复的类。外汇与贵金属杠杆高、滑点随机,实盘切换 trailing 模式前建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据先跑一遍,观察各模式在单边与震荡段的止损触发频率差异。 另一个细节点:同一份 SymbolTradeExt.mqh 骨架在原文中被重复贴了两次版权头与空类声明,真正生效的是带 #include "Trailings.mqh" 和枚举定义的那一版。开 MT5 新建 include 文件时,留意不要因为复制粘贴留下多个重名空类,编译器虽不报错但会拖慢索引。

MQL5 / C++
enum ENUM_TRAILING_MODE      class=class="str">"cmt">// Enumeration of trailing modes
  {
   TRAILING_MODE_SIMPLE=class="num">2, class=class="str">"cmt">// Simple trailing
   TRAILING_MODE_SAR,      class=class="str">"cmt">// Trailing by Parabolic SAR
   TRAILING_MODE_AMA,      class=class="str">"cmt">// Trailing by adjustable moving average
   TRAILING_MODE_DEMA,     class=class="str">"cmt">// Trailing by class="type">class="kw">double exponential moving average
   TRAILING_MODE_FRAMA,    class=class="str">"cmt">// Trailing by fractal adaptive moving average 
   TRAILING_MODE_MA,       class=class="str">"cmt">// Trailing by simple moving average
   TRAILING_MODE_TEMA,     class=class="str">"cmt">// Trailing by triple exponential moving average
   TRAILING_MODE_VIDYA,    class=class="str">"cmt">// Trailing by moving average with dynamic averaging period
  };

◍ 追踪止损对象的封装与生命周期

在扩展交易类里,把追踪止损逻辑抽成一个独立对象,比直接在下单函数里写死移动止损要干净得多。下面这段声明里,m_trailing 是指向追踪类的指针,m_timeframe 则限定了指标计算所依附的周期,两者都在 public 方法之外以私有成员形式存在。 默认构造函数把 m_trailing 置为 NULL,m_timeframe 取当前图表周期 Period(),同时调用基类 SetSymbol 绑定品种;带参构造函数允许外部传入指定品种与周期,例如 CSymbolTradeExt("XAUUSD", PERIOD_H1) 就能把黄金的追踪挂在 1 小时线上。 析构函数只做一件事:若 m_trailing 非空就 delete 掉,避免 EA 卸载时内存泄漏。SetTrailing 方法接收模式枚举、数据索引、magic、起始距离 start、步长 step、偏移 offset 以及 MqlParam 参数数组,其中 param[0]、param[1] 被直接转型为整型赋给 ma_period 与 ma_shift,说明 MA 类追踪只用到前两个结构字段。 实盘接这套结构时,外汇与贵金属波动属性差异大,黄金跳空可能让 start=10 点显得过窄,建议先在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍再上真人资金,杠杆品种高风险,参数未经回测倾向造成滑点吞噬。

MQL5 / C++
  CSimpleTrailing  *m_trailing;     class=class="str">"cmt">// Trailing class object
  ENUM_TIMEFRAMES  m_timeframe;   class=class="str">"cmt">// Timeframe for calculating the indicator for trailing
class="kw">public:
class=class="str">"cmt">//--- Set trailing and its parameters
  class="type">bool             SetTrailing(class="kw">const ENUM_TRAILING_MODE trailing_mode, class="kw">const class="type">int data_index, class="kw">const class="type">long magic, class="kw">const class="type">int start, class="kw">const class="type">int step, class="kw">const class="type">int offset, class="kw">const class="type">MqlParam &param[]);
class=class="str">"cmt">//--- Start a trail of the position specified by the ticket
  class="type">void             Trailing(class="kw">const class="type">ulong pos_ticket);
class=class="str">"cmt">//--- Constructor/destructor
                   CSymbolTradeExt() : m_trailing(NULL), m_timeframe(::Period()) { this.SetSymbol(::Symbol()); }
                   CSymbolTradeExt(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES timeframe);
                  ~CSymbolTradeExt();
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSymbolTradeExt::CSymbolTradeExt(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES timeframe) : CSymbolTrade(symbol)
  {
   this.m_trailing=NULL;
   this.m_timeframe=timeframe;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSymbolTradeExt::~CSymbolTradeExt()
  {
class=class="str">"cmt">//--- class="kw">delete the created trailing object
   if(this.m_trailing!=NULL)
      class="kw">delete this.m_trailing;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set trailing parameters                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSymbolTradeExt::SetTrailing(class="kw">const ENUM_TRAILING_MODE trailing_mode, class="kw">const class="type">int data_index, class="kw">const class="type">long magic, class="kw">const class="type">int start, class="kw">const class="type">int step, class="kw">const class="type">int offset, class="kw">const class="type">MqlParam &param[])
  {
class=class="str">"cmt">//--- Set trailing parameters(only necessary structure fields are used for each indicator type)
   class="type">int             ma_period   = (class="type">int)param[class="num">0].integer_value;
   class="type">int             ma_shift    = (class="type">int)param[class="num">1].integer_value;

「按模式分流的跟踪止损对象创建」

这段初始化逻辑把外部传入的 param 数组拆成具体指标参数:param[0]、param[1] 是 SAR 的步长与最大值,param[2]~param[6] 分别对应均价类型、MA 方法、快慢 EMA 周期与 CMO 周期。若周期类参数小于 1 或等 0,代码会回退到各自硬编码默认值,例如 AMA 快线默认 2、慢线默认 30,DEMA/FRAMA/TEMA 默认 14。 switch(trailing_mode) 根据跟踪模式 new 出不同的跟踪类实例。TRAILING_MODE_SIMPLE 直接挂 CSimpleTrailing;TRAILING_MODE_AMA 把 ma_period 缺省成 9 再连同快慢 EMA 一并传给 CTrailingByAMA;DEMA、FRAMA、TEMA 三者结构类似,周期缺省 14。 MA 模式有点特殊:构造 CTrailingByMA 时第二个周期参数写了 (ma_period==0 ? 10 : ma_period),而第一个 ma_period 若为 0 则传原值 0——这种不一致在 MT5 里可能导致均线句柄初始化异常,建议手动把两个周期统一后再跑。外汇与贵金属波动剧烈,这类跟踪止损仅降低回撤概率,不保证避险。

MQL5 / C++
  ENUM_APPLIED_PRICE ma_price   = (ENUM_APPLIED_PRICE)param[class="num">2].integer_value;
  ENUM_MA_METHOD     ma_method   = (ENUM_MA_METHOD)param[class="num">3].integer_value;
  class="type">int                fast_ema    = (class="type">int)param[class="num">4].integer_value;
  class="type">int                slow_ema    = (class="type">int)param[class="num">5].integer_value;
  class="type">int                period_cmo  = (class="type">int)param[class="num">6].integer_value;
  class="type">class="kw">double             sar_step    = param[class="num">0].double_value;
  class="type">class="kw">double             sar_max     = param[class="num">1].double_value;

class=class="str">"cmt">//--- depending on the trailing type, we create a trailing object
class=class="str">"cmt">//--- if the value passed as the calculation period is less than the allowed value, then each indicator is assigned its own class="kw">default value
  class="kw">switch(trailing_mode)
   {
    case TRAILING_MODE_SIMPLE   :
      this.m_trailing=new CSimpleTrailing(this.Symbol(), magic, start, step, offset);
      break;
    case TRAILING_MODE_AMA      :
      this.m_trailing=new CTrailingByAMA(this.Symbol(), this.m_timeframe, magic,
                                (ma_period<class="num">1 ?  class="num">9 : ma_period), (fast_ema<class="num">1  ?  class="num">2 : fast_ema), (slow_ema<class="num">1  ? class="num">30 : slow_ema), ma_shift, ma_price, start, step, offset);
      break;
    case TRAILING_MODE_DEMA     :
      this.m_trailing=new CTrailingByDEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==class="num">0 ? class="num">14 : ma_period), ma_shift, ma_price, start, step, offset);
      break;
    case TRAILING_MODE_FRAMA    :
      this.m_trailing=new CTrailingByFRAMA(this.Symbol(), this.m_timeframe, magic, (ma_period==class="num">0 ? class="num">14 : ma_period), ma_shift, ma_price, start, step, offset);
      break;
    case TRAILING_MODE_MA       :
      this.m_trailing=new CTrailingByMA(this.Symbol(), this.m_timeframe, magic, ma_period, (ma_period==class="num">0 ? class="num">10 : ma_period), ma_method, ma_price, start, step, offset);
      break;
    case TRAILING_MODE_TEMA     :
      this.m_trailing=new CTrailingByTEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==class="num">0 ? class="num">14 : ma_period), ma_shift, ma_price, start, step, offset);
      break;
    case TRAILING_MODE_VIDYA    :
把回测巡检交给小布盯盘
这些不同尾随类型的批量回测对比,小布盯盘的 AIGC 已内置巡检逻辑,打开对应品种页就能直接看多档止损方案的胜率分布,你只管挑参数。

常见问题

SAR 线在看涨时位于价格下方并随价上行,价格跌破该线即视为趋势结束信号,因此多头尾随只顺向上移,不反向贴价,避免被震荡扫损。
周期越短贴价越紧但假突破多,越长则回撤容忍高、触发晚;倾向在测试器里用品种真实波动做多周期对照,而非凭感觉定。
原类多按品种或魔幻数字遍历持仓,本篇需细化为按单品种单魔数循环,否则会误移无关持仓的停止位,建议重写遍历入口。
可以,小布盯盘的品种页内置了多档尾随与固定止损的回测对照,省去手动跑策略测试器的重复劳动,你专注看哪条曲线更合风控。
这两类杠杆高、跳空频繁,尾随线可能在缺口处直接穿过而无力触发,实际成交价偏离测试器数值,概率上要预留滑点冗余。