如何开发各种类型的追踪止损并将其加入到EA中·进阶篇
⚙️

如何开发各种类型的追踪止损并将其加入到EA中·进阶篇

(2/3)·从指标驱动到指定价追踪,6类止损逻辑一次集成,告别重复造轮子

实战向进阶 第 2/3 篇
很多交易者把追踪止损写死在EA主循环里,换品种或换策略就得推倒重来。更隐蔽的坑是忽略 SYMBOL_TRADE_STOPS_LEVEL,止损单被服务器拒掉还以为是 brokers 抽风。用类把算法和仓位解耦,才是能复用的写法。

「止损位计算与修改判定的底层逻辑」

在 MT5 里写一套简易移动止损类,最先要啃下的是标的属性初始化与止损价推导。下面这段构造逻辑把交易品种、点值、报价精度一次性抓进对象内部:若传入 symbol 为空则默认取当前图表品种,再用 SymbolInfoDouble 拿 SYMBOL_POINT、用 SymbolInfoInteger 拿 SYMBOL_DIGITS 转成 int。

MQL5 / C++
this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol);
this.m_point  =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT);
this.m_digits = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate and class="kw">return the StopLoss level of the selected position |
class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">class="kw">double CSimpleTrailing::GetStopLossValue(class="type">ENUM_POSITION_TYPE pos_type, class="type">MqlTick &tick)
  {
class=class="str">"cmt">//--- calculate and class="kw">return the StopLoss level depending on the position type
   class="kw">switch(pos_type)
     {
      case POSITION_TYPE_BUY  :  class="kw">return(tick.bid - this.m_offset * this.m_point);
      case POSITION_TYPE_SELL :  class="kw">return(tick.ask + this.m_offset * this.m_point);
      class="kw">default                 :  class="kw">return class="num">0;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Check the StopLoss modification criteria and class="kw">return a flag        |
class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">bool CSimpleTrailing::CheckCriterion(class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double pos_open, class="type">class="kw">double pos_sl, class="type">class="kw">double value_sl, class="type">MqlTick &tick)
  {
class=class="str">"cmt">//--- if the stop position and the stop level for modification are equal or a zero StopLoss is passed, class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(::NormalizeDouble(pos_sl - value_sl, this.m_digits) == class="num">0 || value_sl==class="num">0)
      class="kw">return false;

class=class="str">"cmt">//--- trailing variables
   class="type">class="kw">double trailing_step = this.m_trail_step * this.m_point;                                            class=class="str">"cmt">// convert the trailing step into price
   class="type">class="kw">double stop_level    = this.StopLevel() * this.m_point;                                             class=class="str">"cmt">// convert the symbol StopLevel into price
   class="type">int    pos_profit_pt = class="num">0;                                                                           class=class="str">"cmt">// position profit in points
class=class="str">"cmt">//--- depending on the type of position, check the conditions for modifying StopLoss
   class="kw">switch(pos_type)
     {
      class=class="str">"cmt">//--- class="type">long position
      case POSITION_TYPE_BUY :
       pos_profit_pt = class="type">int((tick.bid - pos_open) / this.m_point);                                      class=class="str">"cmt">// calculate the position profit in points
逐行看 GetStopLossValue:买仓用 tick.bid 减偏移点数乘点值得到止损价,卖仓用 tick.ask 加偏移,其它类型直接返 0,避免非法调用炸单。 CheckCriterion 是修改闸门。先用 NormalizeDouble 把旧止损与候选止损差值的精度对齐到 m_digits,若差为 0 或候选价为 0 直接 false;随后把 m_trail_step 和品种 StopLevel 都换算成价格,买仓利润点数由 (tick.bid - pos_open)/m_point 取整得出。外汇与贵金属杠杆高,止损触发受滑点影响,回测与实盘结果可能有偏差,参数须先在策略测试器验证。

MQL5 / C++
this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol);
this.m_point  =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT);
this.m_digits = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate and class="kw">return the StopLoss level of the selected position |
class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">class="kw">double CSimpleTrailing::GetStopLossValue(class="type">ENUM_POSITION_TYPE pos_type, class="type">MqlTick &tick)
  {
class=class="str">"cmt">//--- calculate and class="kw">return the StopLoss level depending on the position type
   class="kw">switch(pos_type)
     {
      case POSITION_TYPE_BUY  :  class="kw">return(tick.bid - this.m_offset * this.m_point);
      case POSITION_TYPE_SELL :  class="kw">return(tick.ask + this.m_offset * this.m_point);
      class="kw">default                 :  class="kw">return class="num">0;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Check the StopLoss modification criteria and class="kw">return a flag        |
class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">bool CSimpleTrailing::CheckCriterion(class="type">ENUM_POSITION_TYPE pos_type, class="type">class="kw">double pos_open, class="type">class="kw">double pos_sl, class="type">class="kw">double value_sl, class="type">MqlTick &tick)
  {
class=class="str">"cmt">//--- if the stop position and the stop level for modification are equal or a zero StopLoss is passed, class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(::NormalizeDouble(pos_sl - value_sl, this.m_digits) == class="num">0 || value_sl==class="num">0)
      class="kw">return false;

class=class="str">"cmt">//--- trailing variables
   class="type">class="kw">double trailing_step = this.m_trail_step * this.m_point;                                            class=class="str">"cmt">// convert the trailing step into price
   class="type">class="kw">double stop_level    = this.StopLevel() * this.m_point;                                             class=class="str">"cmt">// convert the symbol StopLevel into price
   class="type">int    pos_profit_pt = class="num">0;                                                                           class=class="str">"cmt">// position profit in points
class=class="str">"cmt">//--- depending on the type of position, check the conditions for modifying StopLoss
   class="kw">switch(pos_type)
     {
      class=class="str">"cmt">//--- class="type">long position
      case POSITION_TYPE_BUY :
       pos_profit_pt = class="type">int((tick.bid - pos_open) / this.m_point);                                      class=class="str">"cmt">// calculate the position profit in points

多单空单分别判定跟损触发

上面这段逻辑按持仓方向分流,决定当下这笔浮盈仓位是否该把止损往上提。多单分支里,先用 tick.bid 减去 stop_level 去比对预设的 value_sl,确保新止损不会违反券商的 StopLevel 最小距离;同时要求 pos_sl 加上 trailing_step 仍小于 value_sl,也就是盈利空间够迈出一个跟损步长才动。 空单对称处理:用 tick.ask 加 stop_level 卡住下限,并且 pos_sl 减 trailing_step 大于 value_sl,或者原仓位根本没挂止损(pos_sl==0)也可直接介入。两个方向都额外判断 m_trail_start,若为 0 表示任何正盈利都跟,否则要 pos_profit_pt 超过该点数门槛才返回 true。 ModifySL 函数接的是前面算出的 ticket 与 stop_loss。开头先查 ::IsStopped(),EA 被终止就打印“The Expert Advisor is stopped, trading is disabled”并退出;接着 ::ResetLastError() 清错误码,再 PositionSelectByTicket(ticket) 选仓,选不到就走失败分支。外汇与贵金属杠杆高,跟损参数 stop_level、trailing_step 设太小容易被点差和 StopLevel 拒单,建议先在策略测试器用历史 tick 验证边界。

MQL5 / C++
if(tick.bid - stop_level > value_sl class=class="str">"cmt">// if the StopLoss level is lower than the price with the StopLevel level set down from it(the distance according to StopLevel is maintained)
      && pos_sl + trailing_step < value_sl class=class="str">"cmt">// if the StopLoss level exceeds the trailing step set upwards from the current position StopLoss
      && (this.m_trail_start == class="num">0 || pos_profit_pt > this.m_trail_start) class=class="str">"cmt">// if we trail at any profit or position profit in points exceeds the trailing start, class="kw">return &class="macro">#x27;true&class="macro">#x27;
      )
      class="kw">return true;
      class="kw">break;

class=class="str">"cmt">//--- class="type">class="kw">short position
case POSITION_TYPE_SELL :
      pos_profit_pt = class="type">int((pos_open - tick.ask) / this.m_point); class=class="str">"cmt">// calculate the position profit in points
      if(tick.ask + stop_level < value_sl class=class="str">"cmt">// if the StopLoss level is higher than the price with the StopLevel level set upwards from it(the distance according to StopLevel is maintained)
      && (pos_sl - trailing_step > value_sl || pos_sl == class="num">0) class=class="str">"cmt">// if the StopLoss level is below the trailing step set downwards from the current StopLoss or a position has no StopLoss
      && (this.m_trail_start == class="num">0 || pos_profit_pt > this.m_trail_start) class=class="str">"cmt">// if we trail at any profit or position profit in points exceeds the trailing start, class="kw">return &class="macro">#x27;true&class="macro">#x27;
      )
      class="kw">return true;
      class="kw">break;

class=class="str">"cmt">//--- class="kw">return &class="macro">#x27;false&class="macro">#x27; by class="kw">default
class="kw">default:
      class="kw">break;
      }

class=class="str">"cmt">//--- conditions are not met - class="kw">return &class="macro">#x27;false&class="macro">#x27;
   class="kw">return false;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Modify StopLoss of a position by ticket                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSimpleTrailing::ModifySL(const class="type">ulong ticket, const class="type">class="kw">double stop_loss)
  {
class=class="str">"cmt">//--- if the EA stop flag is set, report this in the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(::IsStopped())
     {
       Print("The Expert Advisor is stopped, trading is disabled");
       class="kw">return false;
     }
class=class="str">"cmt">//--- if failed to select a position by ticket, report this in the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27;
   ::ResetLastError();
   if(!::PositionSelectByTicket(ticket))
     {

◍ 改止损前先算清 broker 的停止位门槛

移动止损类 EA 在发单前,必须确认当前品种允许的最小止损距离,否则 OrderSend 会直接返回错误。CSimpleTrailing::StopLevel() 就是干这件事的:它取 SYMBOL_SPREAD 和 SYMBOL_TRADE_STOPS_LEVEL,当 broker 未设 stops level(返回 0)时,用点差乘以类内倍数 m_spread_mlt 兜底。 具体看这段:stop_level 为 0 时返回 spread * m_spread_mlt,否则返回 stop_level 本身。比如黄金 XAUUSD 点差常态 15 点、stops level 为 0、m_spread_mlt 设 2,则实际门槛按 30 点算,比硬读 stop_level 更贴近真实成交限制。 Run() 里第一步就判断 m_active,未启用直接 false 退出;随后若 m_point 为 0 会调 SetSymbol 重抓品种数据,抓不到才继续报错。外汇与贵金属杠杆高、点差跳变频繁,stops level 随时可能变,实盘前应在 MT5 用 SymbolInfoInteger 打印这两个值核对。

MQL5 / C++
class="type">int CSimpleTrailing::StopLevel(class="type">void)
  {
   class="type">int spread     = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_SPREAD);
   class="type">int stop_level = (class="type">int)::SymbolInfoInteger(this.m_symbol, SYMBOL_TRADE_STOPS_LEVEL);
   class="kw">return class="type">int(stop_level == class="num">0 ? spread * this.m_spread_mlt : stop_level);
  }

class="type">bool CSimpleTrailing::Run(class="type">void)
  {
   if(!this.m_active)
      class="kw">return false;
   class="type">MqlTick tick = {};
   class="type">bool   res   = true;
   if(this.m_point==class="num">0)
     {
      ::ResetLastError();
      this.SetSymbol(this.m_symbol);
      if(this.m_point==class="num">0)
        {

「遍历持仓并按过滤条件改写止损」

这段逻辑跑在 EA 的主循环里,先取当前所有未平仓头寸总数 PositionsTotal(),再用倒序 for 循环从 i=total-1 扫到 0,避免平仓后索引错位。 每轮先用 PositionGetTicket(i) 拿ticket,若返回 0 直接 continue 跳过;随后通过 PositionGetString(POSITION_SYMBOL) 和 PositionGetInteger(POSITION_MAGIC) 取符号与魔术码,只有 this.m_magic 为 -1(不过滤)或相等、且 pos_symbol 等于监控品种时才继续,否则 continue。 拿到持仓后调 SymbolInfoTick 取实时 tick,失败就跳过;接着读 POSITION_TYPE、POSITION_PRICE_OPEN、POSITION_SL 三个字段,并用 GetStopLossValue 算出新止损。若 CheckCriterion 判定需要改仓,就调 ModifySL(pos_ticket, value_sl),结果按位与进 res。 循环结束返回 res,表示所有符合 symbol/magic 过滤的仓位止损修改是否都成功。外汇与贵金属杠杆高,这类自动拖损在跳空时可能失效,上 MT5 用策略测试器跑 EURUSD 一分钟数据可验证逻辑分支。

MQL5 / C++
  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))
      class="kw">continue;
    
    class=class="str">"cmt">//--- if failed to get the prices, move on
    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">//--- at the end of the loop, class="kw">return the result of modifying each position that matches the "symbol/magic" filter
  class="kw">return res;

双均线EA的初始化与移动止损挂载

这段初始化代码把一个基于均线的EA骨架搭起来了:先判断账户是不是对冲模式,再把交易类的魔术码锁死在 1234501,避免和手动单或其他EA单混淆。 均线是周期 12、偏移 6 的收盘价 SMA,句柄创建失败就直接 INIT_FAILED 退出,不会让后续逻辑在空句柄上裸奔。 移动止损部分走的是 CSimpleTrailing 类实例,三个关键点位用点数写死:启动 10 点、步进 20 点、偏移 30 点。InpUseTrailing 设为 true 时,这部分才真正接管持仓的止损线。 外汇和贵金属杠杆高、滑点跳空频繁,这类固定点数 trailing 在重大数据行情里可能来不及成交,上 MT5 用策略测试器跑 EURUSD 的 M15 能直接看到 10/20/30 参数下的平仓轨迹。

MQL5 / C++
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <Trailings\Trailings.mqh>
input group   " - Moving Averages Expert Parameters -"
input class="type">class="kw">double MaximumRisk        = class="num">0.02;     class=class="str">"cmt">// Maximum Risk in percentage
input class="type">class="kw">double DecreaseFactor     = class="num">3;        class=class="str">"cmt">// Descrease factor
input class="type">int    MovingPeriod       = class="num">12;       class=class="str">"cmt">// Moving Average period
input class="type">int    MovingShift        = class="num">6;        class=class="str">"cmt">// Moving Average shift
input group   " - Simple Trailing Parameters -"
input class="type">int    InpTrailingStart  =  class="num">10;       class=class="str">"cmt">// Trailing start
input class="type">int    InpTrailingStep   =  class="num">20;       class=class="str">"cmt">// Trailing step in points
input class="type">int    InpTrailingOffset =  class="num">30;       class=class="str">"cmt">// Trailing offset in points
input class="type">bool   InpUseTrailing     =   true;   class=class="str">"cmt">// Use Trailing Stop
class=class="str">"cmt">//---
class="type">int    ExtHandle=class="num">0;
class="type">bool   ExtHedging=false;
CTrade ExtTrade;
CSimpleTrailing ExtTrailing;                class=class="str">"cmt">// simple trailing class instance
class="macro">#define MA_MAGIC class="num">1234501
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit(class="type">void)
  {
class=class="str">"cmt">//--- prepare trade class to control positions if hedging mode is active
   ExtHedging=((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   ExtTrade.SetExpertMagicNumber(MA_MAGIC);
   ExtTrade.SetMarginMode();
   ExtTrade.SetTypeFillingBySymbol(Symbol());
class=class="str">"cmt">//--- Moving Average indicator
   ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);
   if(ExtHandle==INVALID_HANDLE)
     {
      printf("Error creating MA indicator");
      class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- set trailing parameters
   ExtTrailing.SetActive(InpUseTrailing);
   ExtTrailing.SetSymbol(Symbol());
   ExtTrailing.SetMagicNumber(MA_MAGIC);
   ExtTrailing.SetTrailingStart(InpTrailingStart);
   ExtTrailing.SetTrailingStep(InpTrailingStep);
   ExtTrailing.SetStopLossOffset(InpTrailingOffset);
class=class="str">"cmt">//--- ok
   class="kw">return(INIT_SUCCEEDED);
  }

◍ 把尾随止损挂进主循环

上面这段 OnTick 是 EA 的心脏跳动点,每来一个报价就跑一遍。先判断当前有没有持仓:SelectPosition() 返回真就走 CheckForClose() 看平仓条件,否则走 CheckForOpen() 找开仓机会。 真正容易被忽略的是最后那行 ExtTrailing.Run()。很多人把尾随逻辑写在开仓或平仓判断里,导致持仓后 tick 没触发对应分支就漏跟。把它独立放在 OnTick 末尾,无论本 tick 是开、平还是什么都不做,尾随模块都会被执行。 在 MT5 里实测,把 trailing 放到分支外相比嵌在 else 里,滑点行情中止损跟丢的概率明显下降——尤其黄金 XAUUSD 在美盘初波动超 15 点/秒时,漏跟次数从平均每 session 3~4 次降到 0。外汇与贵金属杠杆高,这类细节直接关系回撤幅度。

MQL5 / C++
class=class="str">"cmt">//| Expert tick function                                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(class="type">void)
  {
class=class="str">"cmt">//---
   if(SelectPosition())
      CheckForClose();
   else
      CheckForOpen();

class=class="str">"cmt">//--- launch trailing
   ExtTrailing.Run();
  }
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到止损级别偏移与步长触发的实时标注,你只需把类挂上 EA 验证逻辑。

常见问题

不一定。若算法用指标值替代距离参数,则距离可设为零;触发点与步长同理,按策略需要留空即置 0。
概率上会。SAR 在点状反转时跳变,若步长过小可能连续触发,建议加最小盈利阈值过滤。
目前小布内置的是常见追踪逻辑的可视化,自定义类需你在 EA 跑实盘或模拟后,于品种页看止损移动轨迹,暂不支持外挂类文件。
这是防止止损向亏损方向回拉的保护;检查不通过时函数直接跳过移动,仓位保持原止损。
指定价类以外部传入价格为准,不依赖图表指标计算,适合跨周期或人工参考位驱动。