事后交易分析:在策略测试器中选择尾随停止和新的止损位·综合运用
📊

事后交易分析:在策略测试器中选择尾随停止和新的止损位·综合运用

(3/3)·把 SAR 与均线尾随接进回测 EA,看哪些止损组合能把实盘亏损翻成测试盈利

新手友好 第 3/3 篇

接上篇,我们已经能把真实账户交易复现到策略测试器并改止损位扭亏为盈。这一篇把抛物线转向和移动平均线两类尾随停止接进同一套 EA,在测试器里跑不同参数组合,看尾随逻辑会怎么改写原先的盈亏结构。

「尾随止损的实例分支与激活逻辑」

上面这段 MT5 自定义类代码,展示了两种尾随止损模式的实例化分支,以及失败兜底与激活判定。 当模式为 VIDYA 尾随时,若传入的 period_cmo 小于 1,则强制用 9 作为 CMO 周期;ma_period 为 0 时退回 12。SAR 模式里,sar_step 低于 0.0001 则用 0.02,sar_max 低于 0.02 则用 0.2——这些硬编码下限能避免参数误传导致指标失效。 实例化后若 m_trailing 仍为 NULL,函数直接返回 false,说明对象没建起来;否则调用 SetActive(true) 并返回 true,尾随才算挂上。 CSymbolTradeExt::Trailing(ulong pos_ticket) 里只做了一件事:指针非空就跑 Run(pos_ticket)。你在 EA 里每 tick 调这个函数并传持仓 ticket,就能让对应持仓按选定模式自动挪止损。外汇与贵金属波动剧烈,自动尾随只降低回撤概率,不保证不扫损。

MQL5 / C++
    this.m_trailing=new CTrailingByVIDYA(this.Symbol(), this.m_timeframe, magic, (period_cmo<class="num">1 ? class="num">9 : period_cmo), (ma_period==class="num">0 ? class="num">12 : ma_period), ma_shift, ma_price, start, step, offset);
      break;
    case TRAILING_MODE_SAR     :
      this.m_trailing=new CTrailingBySAR(this.Symbol(), this.m_timeframe, magic, (sar_step<class="num">0.0001 ? class="num">0.02 : sar_step), (sar_max<class="num">0.02 ? class="num">0.2 : sar_max), start, step, offset);
      break;
    class="kw">default :
      break;
   }
class=class="str">"cmt">//--- something went wrong - class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(this.m_trailing==NULL)
     class="kw">return false;

class=class="str">"cmt">//--- all is well - make the trail active and class="kw">return &class="macro">#x27;true&class="macro">#x27;
   this.m_trailing.SetActive(true);
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Start trailing of the position specified by the ticket            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSymbolTradeExt::Trailing(class="kw">const class="type">ulong pos_ticket)
  {
   if(this.m_trailing!=NULL)
     this.m_trailing.Run(pos_ticket);
   }

◍ 尾随停止实测:哪种算法能把亏损盘拉正

把前一篇的_history EA 复制进同一目录,把 CSymbolTrade 类头换成 CSymbolTradeExt,再挂上 Trailings.mqh,就能在原有历史重放框架里加尾随逻辑。EA 输入里新增尾随类型枚举,InpMAPeriod 默认给 0,意思是各均线自带周期默认值,传 0 时尾随类自动用指标内部默认,免得手写错周期。 测试器里先跑原始交易:同账户、同杠杆、同历史成交,结果净亏 658 美元。接着把初始止损统一设 100 点,尾随算法逐个轮换,其余参数不动。 简单尾随反而亏 746.1 美元,比裸盘更糟;抛物线转向(SAR)尾随翻正,盈利 541.8 美元。换均线类尾随后分化明显:VIDYA 亏 283.3 美元,MA 赚 563.1,AMA 赚 806.5,FRAMA 赚 1291.6,TEMA 赚 1355.1,DEMA 赚 1397.1 美元。 外汇与贵金属品种波动跳空频繁,回测盈利不等于实盘可复制,DEMA 尾随的高收益只是在该历史样本上的概率性优势,实盘仍可能回撤。开 MT5 把附带 EA 拖进策略测试器,按流水账推荐的初始日期和杠杆重跑上面九种模式,重点看 DEMA 和 FRAMA 在近期行情里是否还维持正期望。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                    TradingByHistoryDeals_Ext.mq5 |
class=class="str">"cmt">//|                    Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include "SymbolTradeExt.mqh"
class="macro">#include "Trailings.mqh"
enum ENUM_TESTING_MODE
  {
   TESTING_MODE_ORIGIN,       class=class="str">"cmt">/* Original trading                         */
   TESTING_MODE_SLTP,         class=class="str">"cmt">/* Specified StopLoss and TakeProfit values */
   TESTING_MODE_TRAIL_SIMPLE, class=class="str">"cmt">/* Simple Trailing                           */
   TESTING_MODE_TRAIL_SAR,    class=class="str">"cmt">/* Trailing by Parabolic SAR indicator       */
   TESTING_MODE_TRAIL_AMA,    class=class="str">"cmt">/* Trailing by AMA indicator                 */

跟踪止损的多种指标切换入口

EA 在测试模式下用枚举把 trailing 方式拆得很细:DEMA、FRAMA、MA、TEMA、VIDYA 各占一项,方便在策略测试器里横向比哪条曲线跟单更跟手。 输入参数里 InpTestingMode 默认是 TESTING_MODE_ORIGIN,也就是不干预、沿用原始止损;要测别的轨道就把这个枚举改掉。InpStopLoss 写死 300 点,是点数不是价格,黄金和欧美这种不同点值品种直接套会差出好几倍风险,外汇贵金属本身高杠杆高波动,调参前先在 MT5 里确认合约规格。 InpTestedSymbol 留空代表取当前测试品种,InpTestedMagic 默认 -1 表示不挑魔术码全接;InpShowDataInLog 默认 false,想扒每笔跟踪数据再开 true,不然日志干净但看不见底层。

MQL5 / C++
TESTING_MODE_TRAIL_DEMA,   class=class="str">"cmt">/* Trailing by DEMA indicator              */
TESTING_MODE_TRAIL_FRAMA,  class=class="str">"cmt">/* Trailing by FRAMA indicator             */
TESTING_MODE_TRAIL_MA,     class=class="str">"cmt">/* Trailing by MA indicator                */
TESTING_MODE_TRAIL_TEMA,   class=class="str">"cmt">/* Trailing by TEMA indicator              */
TESTING_MODE_TRAIL_VIDYA,  class=class="str">"cmt">/* Trailing by VIDYA indicator             */
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- input parameters
input   group                " - Strategy parameters - "
input   class="type">class="kw">string               InpTestedSymbol   = "";             class=class="str">"cmt">/* The symbol being tested in the tester      */
input   class="type">long                 InpTestedMagic     = -class="num">1;             class=class="str">"cmt">/* The magic number being tested in the tester */
input   class="type">bool                 InpShowDataInLog   = false;          class=class="str">"cmt">/* Show collected data in the log              */
input   group                " - Stops parameters - "
input   ENUM_TESTING_MODE    InpTestingMode     = TESTING_MODE_ORIGIN; class=class="str">"cmt">/* Testing Mode                            */
input   class="type">int                  InpStopLoss        = class="num">300;            class=class="str">"cmt">/* StopLoss in points                         */

「EA 输入参数的分组与风控初值」

在 MT5 的 EA 源码里,用 input group 可以把杂乱的外部参数按逻辑归组,上面这段就把止盈、 trailing 和指标参数切成了三块,面板上调参不会一头雾水。 InpTakeProfit 默认 500 点,意味着开仓后若价格朝有利方向跑够 500 点才可能触碰止盈;外汇与贵金属杠杆高,实际点值随合约规格变动,盲设容易过早离场。 Initial StopLoss 和 TakeProfit 都由布尔开关控制(默认 true),关掉就只靠 trailing 护身,适合想让均线自己拽止损的人。 Trailing 三件套:Start=150 点才启动,Step=50 点跟进,Offset=0 表示止损贴着极值走;若你做 XAUUSD 这种毛刺多的品种,Offset 设个 20~30 点可能减少被洗。 指标组里 InpMAPeriod=0 是个暗坑——0 在 iMA 里常代表用收盘价线或报错,真要挂 MA 过滤得填 10/20 这类实在周期,否则 trailing 计算可能直接废掉。

MQL5 / C++
input   class="type">int                InpTakeProfit      =  class="num">500;                 class=class="str">"cmt">/* TakeProfit in points                    */
input   group              " - Trailing Parameters -"
input   class="type">bool               InpSetStopLoss      =  true;                 class=class="str">"cmt">/* Set Initial StopLoss                    */
input   class="type">bool               InpSetTakeProfit    =  true;                 class=class="str">"cmt">/* Set Initial TakeProfit                  */
input   class="type">int                InpTrailingStart    =  class="num">150;                  class=class="str">"cmt">/* Trailing start                          */ class=class="str">"cmt">// Profit in points to start trailing
input   class="type">int                InpTrailingStep     =  class="num">50;                   class=class="str">"cmt">/* Trailing step in points                 */
input   class="type">int                InpTrailingOffset   =  class="num">0;                    class=class="str">"cmt">/* Trailing offset in points               */
input   group              " - Indicator Parameters -"
input   ENUM_TIMEFRAMES    InpIndTimeframe     =  PERIOD_CURRENT;       class=class="str">"cmt">/* Indicator&class="macro">#x27;s timeframe                   */ class=class="str">"cmt">// Timeframe of the indicator used in trailing calculation
input   class="type">int                InpMAPeriod         =  class="num">0;                    class=class="str">"cmt">/* MA Period                               */

◍ 多指标协同的参数入口怎么配

把均线、AMA、VIDYA 与抛物线 SAR 揉进同一个 EA 时,最先要定的是 input 参数块。下面这段声明直接决定了后续信号计算的采样窗口与平滑方式,复制进 MT5 的 EA 头文件即可编译。 InpMAShift=0 表示均线不做水平偏移;InpFastEMAPeriod=2 与 InpSlowEMAPeriod=30 构成 AMA 快慢线跨度,2 周期对价格反应极敏、30 周期滤噪偏稳。InpCMOPeriod=9 是 VIDYA 用的钱德动量周期,9 根 K 线是短线惯用长度。 SAR 的步长与上限给的是 0.02 与 0.2,即每次反转加速 2 个基点、封顶 20 个基点,这是 MT5 默认抛物线参数,黄金与欧美盘都可直接测。InpAppliedPrice 锁 PRICE_CLOSE、InpMAMethod 用 MODE_SMA,说明均线以收盘价算简单平均。 InpDataIndex=1 指定从指标缓冲区取第 1 号柱(0 为最新未收线柱),回测时若发现信号慢一根,多半是这里没改对。外汇与贵金属杠杆高,参数跑通不等于实盘能扛回撤,先用策略测试器看样本外表现。

MQL5 / C++
input                class="type">int                 InpMAShift         = class="num">0;            class=class="str">"cmt">/* MA Shift                                   */
input                class="type">int                 InpFastEMAPeriod   = class="num">2;            class=class="str">"cmt">/* AMA Fast EMA Period                        */
input                class="type">int                 InpSlowEMAPeriod   = class="num">30;           class=class="str">"cmt">/* AMA Slow EMA Period                        */
input                class="type">int                 InpCMOPeriod       = class="num">9;            class=class="str">"cmt">/* VIDYA CMO Period                           */
input                class="type">class="kw">double              InpSARStep         = class="num">0.02;         class=class="str">"cmt">/* Parabolic SAR Step                         */
input                class="type">class="kw">double              InpSARMax          = class="num">0.2;          class=class="str">"cmt">/* Parabolic SAR Max                          */
input                ENUM_APPLIED_PRICE  InpAppliedPrice    = PRICE_CLOSE;   class=class="str">"cmt">/* MA Applied Price                           */
input                ENUM_MA_METHOD      InpMAMethod        = MODE_SMA;      class=class="str">"cmt">/* MA Smoothing Method                        */
input                class="type">int                 InpDataIndex       = class="num">1;            class=class="str">"cmt">/* Indicator data index                       */ class=class="str">"cmt">// Bar of data received frrom the indicator

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the pointer to the symbol trading object by name           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSymbolTrade *GetSymbolTrade(class="kw">const class="type">class="kw">string symbol, CArrayObj *list)

按成交记录聚合交易品种对象

多品种 EA 在跑批处理时,常需要把一笔笔成交按 symbol 归并到各自的交易对象里。下面这段逻辑就是干这件事:遍历成交数组,遇非买卖单直接跳过,只把真实持仓单纳入统计。 核心在 CreateListSymbolTrades:先判空数组,空则打印错误并返回 false;否则循环里用 SymbTradeTmp 做探针,list_symbols.Search 查重,命中就复用,未命中(index==WRONG_VALUE)才 new 一个 CSymbolTradeExt 并加入列表。 注意两个容易漏的点:添加对象失败必须 delete 防内存泄漏;res 用 &= 累积,只要有一笔处理失败,整体返回就是 false。外汇与贵金属杠杆高,这类对象管理 bug 可能在极端滑点行情下放大持仓风险。 代码片段里高亮行展示了从设置读取 trailing 参数与模式枚举的接法,开 MT5 把 InpTestingMode 改成不同 ENUM_TRAILING_MODE 值,能直接观察追踪止损行为差异。

MQL5 / C++
  {
  SymbTradeTmp.SetSymbol(symbol);
  list.Sort();
  class="type">int index=list.Search(&SymbTradeTmp);
  class="kw">return list.At(index);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creates an array of used symbols                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols)
  {
  class="type">bool res=true;                   class=class="str">"cmt">// result
  class="type">MqlParam param[class="num">7]={};             class=class="str">"cmt">// trailing parameters
  class="type">int total=(class="type">int)array_deals.Size();class=class="str">"cmt">// total number of deals in the array

class=class="str">"cmt">//--- if the deal array is empty, class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(total==class="num">0)
    {
      PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__);
      class="kw">return false;
    }

class=class="str">"cmt">//--- in a loop through the deal array
  CSymbolTradeExt *SymbolTrade=NULL;
  for(class="type">int i=class="num">0; i<total; i++)
    {
      class=class="str">"cmt">//--- get the next deal and, if it is neither buy nor sell, move on to the next one
      SDeal deal_str=array_deals[i];
      if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL)
        class="kw">continue;

      class=class="str">"cmt">//--- find a trading object in the list whose symbol is equal to the deal symbol
      class="type">class="kw">string symbol=deal_str.Symbol();
      SymbTradeTmp.SetSymbol(symbol);
      list_symbols.Sort();
      class="type">int index=list_symbols.Search(&SymbTradeTmp);

      class=class="str">"cmt">//--- if the index of the desired object in the list is -class="num">1, there is no such object in the list
      if(index==WRONG_VALUE)
        {
          class=class="str">"cmt">//--- we create a new trading symbol object and, if creation fails,
          class=class="str">"cmt">//--- add &class="macro">#x27;false&class="macro">#x27; to the result and move on to the next deal
          SymbolTrade=new CSymbolTradeExt(symbol, InpIndTimeframe);
          if(SymbolTrade==NULL)
            {
              res &=false;
              class="kw">continue;
            }
          class=class="str">"cmt">//--- if failed to add a symbol trading object to the list,
          class=class="str">"cmt">//--- class="kw">delete the newly created object, add &class="macro">#x27;false&class="macro">#x27; to the result
          class=class="str">"cmt">//--- and we move on to the next deal
          if(!list_symbols.Add(SymbolTrade))
            {
              class="kw">delete SymbolTrade;
              res &=false;
              class="kw">continue;
            }
          class=class="str">"cmt">//--- initialize trailing specified in the settings in the trading object
          ENUM_TRAILING_MODE mode=(ENUM_TRAILING_MODE)InpTestingMode;

「跟踪止损参数的分支装配逻辑」

多品种跟踪止损框架里,参数不是写死的,而是按模式在运行时灌进 MqlParam 数组。SetTrailingParams 这个函数干的就是这件事:先 ZeroMemory 清掉旧值,再用 switch 根据 ENUM_TRAILING_MODE 把外部输入变量塞进对应下标。 SAR 模式只占用 param[0]、param[1] 两个双精度槽位,分别放步进 InpSARStep 和上限 InpSARMax;AMA 模式则铺得更宽,从 param[0] 的周期到 param[5] 的慢速 EMA 周期,中间还跳过了 param[3] 直接写 param[4],说明该指标内部枚举留了空位。 这种写法意味着你加一种新尾随模式,只需在 switch 里补一个 case 并约定好 param 下标含义,SymbolTrade.SetTrailing 的调用签名不用动。开 MT5 把这段贴进 EA,改 InpSARStep 从 0.02 调到 0.05,回测里止损触发频率会明显上升,外汇与贵金属杠杆品种须警惕滑点放大带来的额外风险。

MQL5 / C++
class="type">void SetTrailingParams(class="kw">const ENUM_TRAILING_MODE mode, class="type">MqlParam &param[])
  {
class=class="str">"cmt">//--- reset all parameters
   ZeroMemory(param);
class=class="str">"cmt">//--- depending on the selected trailing type, we set the indicator parameters
   class="kw">switch(mode)
     {
     case TRAILING_MODE_SAR       :
       param[class="num">0].type=TYPE_DOUBLE;
       param[class="num">0].double_value=InpSARStep;
       param[class="num">1].type=TYPE_DOUBLE;
       param[class="num">1].double_value=InpSARMax;
       break;
     case TRAILING_MODE_AMA       :
       param[class="num">0].type=TYPE_INT;
       param[class="num">0].integer_value=InpMAPeriod;
       param[class="num">1].type=TYPE_INT;
       param[class="num">1].integer_value=InpMAShift;
       param[class="num">2].type=TYPE_INT;
       param[class="num">2].integer_value=InpAppliedPrice;
       param[class="num">4].type=TYPE_INT;
       param[class="num">4].integer_value=InpFastEMAPeriod;
       param[class="num">5].type=TYPE_INT;
       param[class="num">5].integer_value=InpSlowEMAPeriod;
       break;
     case TRAILING_MODE_DEMA      :

◍ 追踪止损各模式的参数注入差异

在 EA 的追踪止损模块里,不同模式通过 switch-case 把外部输入变量写进 param[] 结构体数组,供后续指标句柄调用。 FRAMA、MA、TEMA、VIDYA 四种模式都先填 param[0]~param[2]:周期 InpMAPeriod、偏移 InpMAShift、应用价格 InpAppliedPrice,类型统一为 TYPE_INT。 MA 模式比前几个多塞了一个 param[3].integer_value=InpMAMethod,用来指定平滑方法(SMA/EMA 等);VIDYA 则跳到 param[6] 写 InpCMOPeriod,说明 CMO 周期在参数表里的槽位是第 7 个。 SIMPLE 和 default 分支直接 break,不注入任何 param——意味着这两个模式走的是固定点数或外部逻辑,不依赖指标参数。开 MT5 把这段贴进 trailing 函数,改 TRAILING_MODE_XXX 宏就能看到 param 槽位变化。

MQL5 / C++
param[class="num">0].type=TYPE_INT;
param[class="num">0].integer_value=InpMAPeriod;

param[class="num">1].type=TYPE_INT;
param[class="num">1].integer_value=InpMAShift;
param[class="num">2].type=TYPE_INT;
param[class="num">2].integer_value=InpAppliedPrice;
break;

case TRAILING_MODE_FRAMA  :
param[class="num">0].type=TYPE_INT;
param[class="num">0].integer_value=InpMAPeriod;

param[class="num">1].type=TYPE_INT;
param[class="num">1].integer_value=InpMAShift;
param[class="num">2].type=TYPE_INT;
param[class="num">2].integer_value=InpAppliedPrice;
break;

case TRAILING_MODE_MA      :
param[class="num">0].type=TYPE_INT;
param[class="num">0].integer_value=InpMAPeriod;

param[class="num">1].type=TYPE_INT;
param[class="num">1].integer_value=InpMAShift;
param[class="num">2].type=TYPE_INT;
param[class="num">2].integer_value=InpAppliedPrice;
param[class="num">3].type=TYPE_INT;
param[class="num">3].integer_value=InpMAMethod;
break;

case TRAILING_MODE_TEMA    :
param[class="num">0].type=TYPE_INT;
param[class="num">0].integer_value=InpMAPeriod;

param[class="num">1].type=TYPE_INT;
param[class="num">1].integer_value=InpMAShift;
param[class="num">2].type=TYPE_INT;
param[class="num">2].integer_value=InpAppliedPrice;
break;

case TRAILING_MODE_VIDYA  :
param[class="num">0].type=TYPE_INT;
param[class="num">0].integer_value=InpMAPeriod;

param[class="num">1].type=TYPE_INT;
param[class="num">1].integer_value=InpMAShift;
param[class="num">2].type=TYPE_INT;
param[class="num">2].integer_value=InpAppliedPrice;
param[class="num">6].type=TYPE_INT;
param[class="num">6].integer_value=InpCMOPeriod;
break;

case TRAILING_MODE_SIMPLE  :
break;

class="kw">default:
break;
}

按历史成交回放下单的逻辑骨架

在 MT5 里做历史回放式交易,核心是先声明两个开关:是否给每笔初始单挂止损、挂止盈。下面这段输入变量把 InpSetStopLossInpSetTakeProfit 都默认置为 true,意味着回放时原样复刻带防护单的成交,若你只想测裸进场,改 false 即可。 TradeByHistory() 这个函数承担遍历任务:它从 ExtListSymbols 取总数,逐个取出 CSymbolTradeExt 对象,再拿对象里的当前成交 CDeal。过滤条件很硬——必须按 magic 和 symbol 匹配,且只认 DEAL_TYPE_BUY / DEAL_TYPE_SELL 两类市价成交,其余全 continue 跳过。 时间维度上,函数用 deal.TicketTester()>0 排除已在测试器处理过的单,再用 obj.CheckTime(deal.Time()) 卡进场时刻。只有 entry==DEAL_ENTRY_IN 的市价入场成交,才会进入后续止损止盈尺寸设定;注释里写明原版交易初期不挂停止单,也就是回放引擎把防护单的挂载时机留给了后面分支。外汇与贵金属品种波动剧烈,回放时若漏掉止损开关,实盘映射可能放大滑点风险。

MQL5 / C++
input   class="type">bool            InpSetStopLoss    =  true;             class=class="str">"cmt">/* Set Initial StopLoss                          */
input   class="type">bool            InpSetTakeProfit  =  true;             class=class="str">"cmt">/* Set Initial TakeProfit                        */
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading by history                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TradeByHistory(class="kw">const class="type">class="kw">string symbol="", class="kw">const class="type">long magic=-class="num">1)
  {
   class="type">class="kw">datetime time=class="num">0;
   class="type">int total=ExtListSymbols.Total();   class=class="str">"cmt">// number of trading objects in the list

class=class="str">"cmt">//--- in a loop by all symbol trading objects
   for(class="type">int i=class="num">0; i<total; i++)
     {
      class=class="str">"cmt">//--- get another trading object
      CSymbolTradeExt *obj=ExtListSymbols.At(i);
      if(obj==NULL)
         class="kw">continue;

      class=class="str">"cmt">//--- get the current deal pointed to by the deal list index
      CDeal *deal=obj.GetDealCurrent();
      if(deal==NULL)
         class="kw">continue;

      class=class="str">"cmt">//--- sort the deal by magic number and symbol
      if((magic>-class="num">1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol))
         class="kw">continue;

      class=class="str">"cmt">//--- sort the deal by type(only buy/sell deals)
      ENUM_DEAL_TYPE type=deal.TypeDeal();
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL)
         class="kw">continue;

      class=class="str">"cmt">//--- if this is a deal already handled in the tester, move on to the next one
      if(deal.TicketTester()>class="num">0)
         class="kw">continue;

      class=class="str">"cmt">//--- if the deal time has not yet arrived, move to the next trading object of the next symbol
      if(!obj.CheckTime(deal.Time()))
         class="kw">continue;
      class=class="str">"cmt">//--- in case of a market entry deal
      ENUM_DEAL_ENTRY entry=deal.Entry();
      if(entry==DEAL_ENTRY_IN)
        {
         class=class="str">"cmt">//--- set the sizes of stop orders depending on the stop setting method
         class=class="str">"cmt">//--- stop orders are not used initially(for original trading)

「跟单开仓时止损止盈的两种赋值路径」

在复制账户成交记录做回测时,止损止盈的写入逻辑分两条岔路:一种是强制按预设 SL/TP 值下单,另一种是走移动止损模式、按开关逐项判定。 先看第一段:当测试模式被设为 TESTING_MODE_SLTP,直接调用 CorrectStopLoss / CorrectTakeProfit 按品种规范化 ExtStopLoss、ExtTakeProfit,不给其他分支留口子。 第二段 else 分支里,只要不是 TESTING_MODE_ORIGIN,就根据 InpSetStopLoss、InpSetTakeProfit 两个布尔开关分别决定是否挂 SL 和 TP;若两个都关,sl、tp 保持初始 0,相当于市价裸单进场。外汇与贵金属杠杆高,裸单在跳空时可能瞬间扩大浮亏。 最后用 obj.Buy / obj.Sell 把成交推出去,返回 ticket 大于 0 才算真正开仓成功,随后把处理计数 +1 并回写测试器票据号。建议直接把下面代码粘进 MT5 脚本,把 InpTestingMode 切到两种模式各跑一次,对比成交单的 SL/TP 字段差异。

MQL5 / C++
ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
class="type">class="kw">double sl=class="num">0;
class="type">class="kw">double tp=class="num">0;
class=class="str">"cmt">//--- in case of the mode for setting the specified stop order values 
if(InpTestingMode==TESTING_MODE_SLTP)
  {
   class=class="str">"cmt">//--- get correct values for StopLoss and TakeProfit
   sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
   tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
  }
class=class="str">"cmt">//--- otherwise, if testing with trailing stops
else
  {
   if(InpTestingMode!=TESTING_MODE_ORIGIN)
    {
     class=class="str">"cmt">//--- if allowed in the settings, we set correct stop orders for the positions being opened
     if(InpSetStopLoss)
       sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss);
     if(InpSetTakeProfit)
       tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit);
    }
  }

class=class="str">"cmt">//--- open a position by deal type
class="type">ulong ticket=(type==DEAL_TYPE_BUY  ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) :
               type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : class="num">0);

class=class="str">"cmt">//--- if a position is opened(we received its ticket)
if(ticket>class="num">0)
  {
   class=class="str">"cmt">//--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
   obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
   deal.SetTicketTester(ticket);
   class=class="str">"cmt">//--- get the position ID in the tester and write it to the properties of the deal object

◍ 平仓单在回测器里的两种消化路径

把真实账户历史里的平仓成交(DEAL_ENTRY_OUT / INOUT / OUT_BY)映射到 MT5 策略测试器时,核心是先拿到这笔平仓对应的开仓成交对象。代码里用 GetDealInByPosID 按持仓 ID 反查,若返回 NULL 就直接 continue 跳过,说明该持仓的建仓记录已丢失或尚未载入。 若反查到的开仓成交里存了测试器 ticket(ticket_tester),但值为 0,大概率该持仓在测试器中已经关闭。此时 PrintFormat 打出 'Could not get position ticket, apparently position #%I64d (#%I64d) is already closed',并调用 SetNextDealIndex 后 continue,避免重复平仓报错。 在原始历史复现模式(TESTING_MODE_ORIGIN)下,会真正用 ClosePos(ticket_tester) 在测试器里平掉该仓位;成功则返回真,把已处理成交数 NumProcessedDeals 加 1,并把测试器 ticket 写回 deal 对象。其他模式(如挂单算法模式)则跳过实际平仓动作,仅计数并登记 ticket,因为平仓单不参与策略信号构建。 最后只要 deal.TicketTester() 大于 0,就认为这笔成交已成功消化,调用 SetNextDealIndex 推进列表指针。外汇与贵金属品种波动剧烈,回测映射逻辑务必在实盘历史样本上跑通再用于风控推演。

MQL5 / C++
class="type">long pos_id_tester=class="num">0;
if(HistoryDealSelect(ticket))
  {
   pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
   deal.SetPosIDTester(pos_id_tester);
  }
 }

class=class="str">"cmt">//--- in case of a market exit deal
if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY)
  {
   class=class="str">"cmt">//--- get a deal a newly opened position is based on
   CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID());
   if(deal_in==NULL)
     class="kw">continue;
   class=class="str">"cmt">//--- get the position ticket in the tester from the properties of the opening deal
   class=class="str">"cmt">//--- if the ticket is zero, then most likely the position in the tester is already closed
   class="type">ulong ticket_tester=deal_in.TicketTester();
   if(ticket_tester==class="num">0)
     {
      PrintFormat("Could not get position ticket, apparently position #%I64d(#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester());
      obj.SetNextDealIndex();
      class="kw">continue;
     }
   class=class="str">"cmt">//--- if we reproduce the original trading history in the tester,
   if(InpTestingMode==TESTING_MODE_ORIGIN)
     {
      class=class="str">"cmt">//--- if the position is closed by ticket
      if(obj.ClosePos(ticket_tester))
        {
         class=class="str">"cmt">//--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object 
         obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
         deal.SetTicketTester(ticket_tester);
        }
     }
   class=class="str">"cmt">//--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped;
   class=class="str">"cmt">//--- accordingly, for the closing deal, we simply increase the number of deals handled by the tester and
   class=class="str">"cmt">//--- write the deal ticket in the tester to the properties of the deal object
   else
     {
      obj.SetNumProcessedDeals(obj.NumProcessedDeals()+class="num">1);
      deal.SetTicketTester(ticket_tester);
     }
   }
class=class="str">"cmt">//--- if a ticket is now set in the deal object, then the deal has been successfully handled -
class=class="str">"cmt">//--- set the deal index in the list to the next deal
if(deal.TicketTester()>class="num">0)
  {
   obj.SetNextDealIndex();
  }

用 OnTick 跑历史成交的追踪止损

这段逻辑把「追踪持仓」和「回放历史成交」拆成了两个独立调用,只在策略测试器里生效。OnTick 首行用 MQLInfoInteger(MQL_TESTER) 拦截实盘,避免你不小心挂到真账户上乱尾随。 Trailing() 函数遍历 PositionsTotal() 得到的全部持仓,按票据取 magic 与 symbol,再用外部输入 InpTestedMagic / InpTestedSymbol 做过滤,只处理匹配的那部分。匹配到的品种会拿到对应的 CSymbolTradeExt 对象,调用其 Trailing(ticket) 方法移动止损。 回测区间 2024.09.13 至今、初始本金 3000 且杠杆 1:500 的设定下,AUDUSD 成交对象累计 222 笔、EURJPY 累计 120 笔。外汇与贵金属杠杆交易风险高,回测表现不代表实盘概率,请先在 MT5 策略测试器用同样参数验证再考虑迁移。 让小布替你跑这套 把上面代码直接贴进 EA 的 OnTick,设 InpTestedSymbol="AUDUSD" 跑一遍,看 222 笔成交里 trailing 触发了几回,比读文档直观。

MQL5 / C++
class="type">void Trailing(class="type">void)
  {
class=class="str">"cmt">//--- variables for getting position properties
   class="type">long   magic=-class="num">1;
   class="type">class="kw">string symbol="";

class=class="str">"cmt">//--- in a loop through all 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 ticket=PositionGetTicket(i);
      if(ticket==class="num">0)
         class="kw">continue;
     class=class="str">"cmt">//--- get the magic number and position symbol
      ResetLastError();
      if(!PositionGetInteger(POSITION_MAGIC, magic))
        {
         Print("PositionGetInteger() failed. Error ", GetLastError());
         class="kw">continue;
        }
      if(!PositionGetString(POSITION_SYMBOL, symbol))
        {
         Print("PositionGetString() failed. Error ", GetLastError());
         class="kw">continue;
        }
      
      class=class="str">"cmt">//--- if the position does not meet the specified conditions of the magic number and symbol, we move to the next one
      if((InpTestedMagic>-class="num">1 && magic!=InpTestedMagic) || (InpTestedSymbol!="" && symbol!=InpTestedSymbol))
         class="kw">continue;
      
      class=class="str">"cmt">//--- get a trading object by a symbol name and call its method for trailing a position by ticket
      CSymbolTradeExt *obj=GetSymbolTrade(symbol, &ExtListSymbols);
      if(obj!=NULL)
         obj.Trailing(ticket);
     }
  }

class="type">void OnTick()
  {
class=class="str">"cmt">//--- work only in the strategy tester
   if(!MQLInfoInteger(MQL_TESTER))
      class="kw">return;
     
class=class="str">"cmt">//--- Trail open positions
   Trailing();

class=class="str">"cmt">//---  Handle the list of deals from the file
   TradeByHistory(InpTestedSymbol, InpTestedMagic);
  }

「各货币对回测成交笔数分布」

把多品种 EA 跑完历史回测后,交易对象的总成交笔数差异极大,这直接反映各品种在样本期内的信号触发频率。 EURUSD 累计 526 笔、GBPUSD 352 笔、NZDUSD 182 笔,主流欧系与商品系货币对贡献了绝大多数成交。USDCHF 250 笔、USDJPY 150 笔、XAUUSD(现货黄金)118 笔,也具备统计意义。 USDCAD 仅 22 笔,样本过薄,据此做参数优化可能失真,建议单独拉长测试周期或剔除。外汇与贵金属杠杆交易高风险,回测笔数不等于实盘胜率。

MQL5 / C++
class="num">3. EURUSD trade object. Total deals: class="num">526
  class="num">4. GBPUSD trade object. Total deals: class="num">352
  class="num">5. NZDUSD trade object. Total deals: class="num">182
  class="num">6. USDCAD trade object. Total deals: class="num">22
  class="num">7. USDCHF trade object. Total deals: class="num">250
  class="num">8. USDJPY trade object. Total deals: class="num">150
  class="num">9. XAUUSD trade object. Total deals: class="num">118

◍ 把工具请下神坛

这套尾随测试 EA 的设计目标很直接:在策略测试器里复现历史成交,把止损、止盈和尾随停止拎出来挨个试。原文给出的类库与 EA 总体积约 276 KB(Trailings.mqh 101.41 KB、SymbolTrade.mqh 53.86 KB、SymbolTradeExt.mqh 12.95 KB、TradingByHistoryDeals_Ext.mq5 81.36 KB、MQL5.zip 26.49 KB),解压进终端 MQL5 目录就能跑,不挑版本。 尾随参数在测试中是随机取的,指标全用标准值,所以别把它当圣杯。更合理的做法是对每个品种单独测,再按波动特征挑尾随档位;当前 EA 没开放逐品种自定义接口,但改造并不难,懂一点 MQL5 就能加上。 外汇和贵金属杠杆高、滑点跳空频繁,随机尾随回测盈利不代表实盘能复现,任何设置都先在小资金或模拟盘验证。 真要把它用起来,就下那几个文件丢进终端,选自己常做的货币对跑一遍历史,看哪组尾随让回撤更顺眼——工具的价值在手里而不在神坛上。

让小布替你跑这套
这些尾随停止的回测对比,小布盯盘的 AIGC 已内置品种页诊断,打开对应货币对就能看到历史止损组合的概率分布,你只管挑参数做决策。

常见问题

多头持仓时价格跌破 SAR 线就平多,空头持仓时价格升破 SAR 线就平空,SAR 始终在趋势方向的反侧作停止线。
均线本身滞后于价格,多头里止损跟在均线下方,价格回碰均线即触发,往往比价格尖顶更早离场。
原类按品种和魔幻数遍历持仓,需细化成单品种单魔幻数循环,避免多品种互相覆盖停止位。
可以,小布盯盘品种页内置了尾随与止损组合的历史回测概览,省去自己搭 EA 跑测试器的时间。
只能说概率上倾向改善,外汇贵金属属高风险,实盘还需考虑滑点和点差扰动。
那是指标内置的折返算法,转向即趋势结束或修正信号,新起点重置加速基准。