如何使用抛物线转向(Parabolic SAR)指标设置跟踪止损(Trailing Stop)·进阶篇
📉

如何使用抛物线转向(Parabolic SAR)指标设置跟踪止损(Trailing Stop)·进阶篇

(2/3)· 接上篇基础铺垫,本文拆解如何用 Parabolic SAR 重写 Trailing Stop 逻辑,绕开平台内置的僵硬距离限制

实战向进阶 第 2/3 篇
很多交易者直接挂 MT5 自带的追踪止损,却没意识到它只按固定点数硬跟,遇到剧烈反转常被冻在 FreezeLevel 外平不掉。把 SAR 点当止损位的人更少,结果该收的利润在震荡里吐回去。

按止损金额做尾随的底层循环

下面这段 MT5 代码实现了一个按固定止损金额(而非点数)来尾随持仓的核心函数。它先读取当前品种的最小止损级别 stop_level,若该值为 0,则退而用 spread*spread_multiplier 作为下限,否则直接用经纪商规定的 stop_level。 TrailingStopByValue 遍历所有持仓,用 PositionsTotal 拿总数,倒序从 i=total-1 跑到 0。每张单先取 ticket,再比对 magic 与 symbol,不匹配就 continue 跳过;若 SymbolInfoTick 取不到实时 tick 也直接跳过,避免用陈旧价格算止损。 拿到持仓类型、开仓价、当前 SL 后,交给 CheckCriterion 判断是否满足移动条件,满足就调 ModifySL 改止损。注意这里 value_sl 是价格绝对距离,trailing_step_pt 和 trailing_start_pt 才是点数触发门槛,两者混用容易在跨品种时算错。 外汇与贵金属杠杆高,stop_level 在重大数据行情可能被经纪商临时放大,回测里 0 的值在实盘未必为 0,开 MT5 把 SYMBOL_TRADE_STOPS_LEVEL 打印出来核对再挂 EA 更稳妥。

MQL5 / C++
class="type">int stop_level=(class="type">int)SymbolInfoInteger(Symbol(), SYMBOL_TRADE_STOPS_LEVEL);
class="kw">return(stop_level==class="num">0 ? spread * spread_multiplier : stop_level);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trailing stop function by StopLoss price value                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TrailingStopByValue(class="kw">const class="type">class="kw">double value_sl, class="kw">const class="type">long magic=-class="num">1, class="kw">const class="type">int trailing_step_pt=class="num">0, class="kw">const class="type">int trailing_start_pt=class="num">0)
  {
class=class="str">"cmt">//--- price structure
   class="type">MqlTick tick={};
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">class="kw">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">//--- skip positions that do not match the filter by symbol and magic number
      if((magic!=-class="num">1 && pos_magic!=magic) || pos_symbol!=Symbol())
         class="kw">continue;

     class=class="str">"cmt">//--- if failed to get the prices, move on
      if(!SymbolInfoTick(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">//--- if StopLoss modification conditions are suitable, modify the position stop level
      if(CheckCriterion(pos_type, pos_open, pos_sl, value_sl, trailing_step_pt, trailing_start_pt, tick))
         ModifySL(pos_ticket, value_sl);
     }
  }
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 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">int trailing_step_pt, class="type">int trailing_start_pt, class="type">MqlTick &tick)
  {

◍ 多空仓位下的移动止损触发判定

这段逻辑决定某一持仓是否该把止损往有利方向推。开头先做了层防护:若持仓当前止损价与待修改价在 NormalizeDouble 到 Digits() 精度后相等,直接返回 false,避免无意义的下单请求。 随后把 trailing_step_pt 和 StopLevel(2) 都乘上 Point() 换算成价格单位,并初始化 pos_profit_pt 记录以点数为单位的浮动盈利。注意 StopLevel(2) 取的是品种报价外的止损最小距离限制,实盘里黄金 XAUUSD 常见 stop_level 约为 10~30 点,绕开它会报 'Trade disabled'。 对多头,先用 (tick.bid - pos_open)/Point() 算盈利点数;只有当 bid 减去 stop_level 仍高于原止损、且新止损比旧止损多出至少一个 trailing_step、并且盈利已超过 trailing_start_pt(或设为0表示任意盈利即跟)时,才返回 true。 空头镜像处理:用 (pos_open - tick.ask)/Point() 算盈利,ask 加 stop_level 要低于原止损,且新止损比旧止损低出 trailing_step 或原无止损,再叠加盈利门槛,才允许修改。未命中任何分支默认返回 false。 把下面代码直接贴进 MT5 的 EA 模块,改 trailing_start_pt 与 trailing_step_pt 两个参数,就能在策略测试器里观察不同启动阈值下止损被改动的频次。外汇与贵金属杠杆高,回测通过不代表实盘胜率,参数需结合点差和滑点重验。

MQL5 / C++
class=class="str">"cmt">//--- if the stop position and the stop level for modification are equal, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
  if(NormalizeDouble(pos_sl-value_sl, Digits())==class="num">0)
     class="kw">return class="kw">false;
  class="type">class="kw">double trailing_step = trailing_step_pt * Point(); class=class="str">"cmt">// convert the trailing step into price
  class="type">class="kw">double stop_level    = StopLevel(class="num">2) * Point();     class=class="str">"cmt">// convert the StopLevel of the symbol 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) / Point());                 class=class="str">"cmt">// calculate the position profit in points
        if(tick.bid - stop_level > value_sl                                          class=class="str">"cmt">// if the price and the StopLevel level pending from it are higher than the StopLoss level(the distance to StopLevel is observed) 
           && pos_sl + trailing_step < value_sl                                       class=class="str">"cmt">// if the StopLoss level exceeds the trailing step based on the current StopLoss
           && (trailing_start_pt==class="num">0 || pos_profit_pt>trailing_start_pt) 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;
        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) / Point());                 class=class="str">"cmt">// position profit in points
        if(tick.ask + stop_level < value_sl                                          class=class="str">"cmt">// if the price and the StopLevel level pending from it are lower than the StopLoss level(the distance to StopLevel is observed)
           && (pos_sl - trailing_step > value_sl || pos_sl==class="num">0)                       class=class="str">"cmt">// if the StopLoss level is below the trailing step based on the current StopLoss or a position has no StopLoss
           && (trailing_start_pt==class="num">0 || pos_profit_pt>trailing_start_pt) 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;
        break;

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

「用 SAR 值去拖止损的改单函数」

下面这段 MQL5 把「按票号改 SL」和「用抛物线 SAR 拖损」拆成了两个独立函数,EA 只在每根新 K 线触发一次,避免 tick 级频繁发单。外汇与贵金属波动跳空频繁,这类挂损逻辑在高杠杆下仍可能滑点放大亏损,实盘前务必在 MT5 策略测试器跑最小点差环境。 ModifySL() 先 PositionSelectByTicket 校验持仓,失败就打印错误号并返回 false;随后填 MqlTradeRequest,action 用 TRADE_ACTION_SLTP 只动止损,sl 经 NormalizeDouble 按品种小数位对齐。OrderSend 失败同样回 false,成功回 true,调用方据此判断是否继续。 TrailingStopBySAR() 取首 bar 的 SAR 值,若为 EMPTY_VALUE 直接退出;否则把 SAR 价传给 TrailingStopByValue 做实际拖损。OnTick 里用 IsNewBar() 拦掉非新 bar,每根 K 线只调一次 TrailingStopBySAR(),默认 magic=-1 表示不区分魔术码。

MQL5 / C++
class="type">bool ModifySL(class="kw">const class="type">class="kw">ulong ticket, class="kw">const class="type">class="kw">double stop_loss)
  {
  ResetLastError();
  if(!PositionSelectByTicket(ticket))
    {
    PrintFormat("%s: Failed to select position by ticket number %I64u. Error %d", __FUNCTION__, ticket, GetLastError());
    class="kw">return class="kw">false;
    }
  class="type">MqlTradeRequest  request={};
  class="type">MqlTradeResult   result ={};
  request.action     = TRADE_ACTION_SLTP;
  request.symbol     = PositionGetString(POSITION_SYMBOL);
  request.magic      = PositionGetInteger(POSITION_MAGIC);
  request.tp         = PositionGetDouble(POSITION_TP);
  request.position   = ticket;
  request.sl         = NormalizeDouble(stop_loss,(class="type">int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS));
  if(!OrderSend(request, result))
    {
    PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
    class="kw">return class="kw">false;
    }
  class="kw">return true;
  }
class="type">void TrailingStopBySAR(class="kw">const class="type">long magic=-class="num">1, class="kw">const class="type">int trailing_step_pt=class="num">0, class="kw">const class="type">int trailing_start_pt=class="num">0)
  {
  class="type">class="kw">double sar=GetSARData(SAR_DATA_INDEX);
  if(sar==EMPTY_VALUE)
    class="kw">return;
  TrailingStopByValue(sar, magic, trailing_step_pt, trailing_start_pt);
  }
class="type">void OnTick()
  {
  if(!IsNewBar())
    class="kw">return;
  TrailingStopBySAR();
  }
逐行看,ModifySL 的 ResetLastError 不能省——MT5 的错误码是全局态,不清就可能误报上一次发单的码。SAR_DATA_INDEX 若取 0 就是当前未闭合 bar 的 SAR,回测里可能用到已未来值,建议确认取的是已收盘 bar 序号。

MQL5 / C++
class="type">bool ModifySL(class="kw">const class="type">class="kw">ulong ticket, class="kw">const class="type">class="kw">double stop_loss)
  {
  ResetLastError();
  if(!PositionSelectByTicket(ticket))
    {
    PrintFormat("%s: Failed to select position by ticket number %I64u. Error %d", __FUNCTION__, ticket, GetLastError());
    class="kw">return class="kw">false;
    }
  class="type">MqlTradeRequest  request={};
  class="type">MqlTradeResult   result ={};
  request.action     = TRADE_ACTION_SLTP;
  request.symbol     = PositionGetString(POSITION_SYMBOL);
  request.magic      = PositionGetInteger(POSITION_MAGIC);
  request.tp         = PositionGetDouble(POSITION_TP);
  request.position   = ticket;
  request.sl         = NormalizeDouble(stop_loss,(class="type">int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS));
  if(!OrderSend(request, result))
    {
    PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
    class="kw">return class="kw">false;
    }
  class="kw">return true;
  }
class="type">void TrailingStopBySAR(class="kw">const class="type">long magic=-class="num">1, class="kw">const class="type">int trailing_step_pt=class="num">0, class="kw">const class="type">int trailing_start_pt=class="num">0)
  {
  class="type">class="kw">double sar=GetSARData(SAR_DATA_INDEX);
  if(sar==EMPTY_VALUE)
    class="kw">return;
  TrailingStopByValue(sar, magic, trailing_step_pt, trailing_start_pt);
  }
class="type">void OnTick()
  {
  if(!IsNewBar())
    class="kw">return;
  TrailingStopBySAR();
  }

用成交事件触发 SAR 移动止损

在 MT5 的 EA 逻辑里,没必要每根 K 线都去跑一遍指标计算,更干净的做法是绑定交易成交事件。 上面这段代码演示了在 OnTradeTransaction 里只拦截成交添加类事件(TRADE_TRANSACTION_DEAL_ADD),一旦有新成交立刻调用 TrailingStopBySAR(),由抛物线 SAR 来决定止损跟进位置。 实测这类事件驱动写法,比在 OnTick 里无差别轮询更省 CPU,尤其在黄金 XAUUSD 这种每秒多笔报价的高波动品种上,MT5 日志里的脚本占用能明显降一截。外汇与贵金属杠杆高,事件触发不代表风险可控,参数仍要按账户承受力调。

MQL5 / C++
class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction& trans,
                      class="kw">const class="type">MqlTradeRequest& request,
                      class="kw">const class="type">MqlTradeResult& result)
  {
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
      TrailingStopBySAR();
  }

◍ 用 CTrade 的 PositionModify 换掉自写改单函数

MQL5 标准库里的 CTrade 类把下单、改单这些底层调用包了一层,普通用户不必每次手搓 MqlTradeRequest 结构。原来 EA 里自己写的 ModifySL() 只改止损,现在可以直接用 CTrade::PositionModify() 顶上,逻辑几乎一致,差异在实现细节。 自写函数里用了 IsStopped() 标准函数判断 EA 是否从图表移除,而 CTrade 类里同名方法带形参 __FUNCTION__,一旦返回 true 会先往日志打一句“EA 已移除”再退出。类方法在开头清空的是头文件里声明的结构体成员,自写函数则是局部声明并零初始化;报错时自写函数会立刻 PrintFormat 出错误码,类方法只透传 OrderSend() 的返回值。 换成类方法后最直观的变化是参数:PositionModify() 要传 ticket、sl、tp 三个量。因为持仓已被选中且止盈不动,就把当前 POSITION_TP 原值塞进去即可,不必另写只改 sl 的专用函数。ModifySL() 随后可从代码里删掉。 实际把 EA 存为 TrailingBySAR_02.mq5,在 OnInit() 把输入 magic 赋给 trade.magic,OnTick() 里按 Parabolic SAR 值开仓并传 magic 进追踪函数,策略测试器用 Every tick 跑任意品种任意周期。实测首个 bar 的 SAR 值就能正确驱动追踪止损,说明 magic 过滤和类方法调用都通了。外汇与贵金属杠杆高,回测通过不代表实盘概率同向,上 MT5 验证前先开模拟盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Modify specified opened position                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTrade::PositionModify(class="kw">const class="type">class="kw">ulong ticket,class="kw">const class="type">class="kw">double sl,class="kw">const class="type">class="kw">double tp)
  {
class=class="str">"cmt">//--- check stopped
   if(IsStopped(__FUNCTION__))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- check position existence
   if(!PositionSelectByTicket(ticket))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- clean
   ClearStructures();
class=class="str">"cmt">//--- setting request
   m_request.action   =TRADE_ACTION_SLTP;
   m_request.position =ticket;
   m_request.symbol   =PositionGetString(POSITION_SYMBOL);
   m_request.magic    =m_magic;
   m_request.sl       =sl;
   m_request.tp       =tp;
class=class="str">"cmt">//--- action and class="kw">return the result
   class="kw">return(OrderSend(m_request,m_result));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Modify StopLoss of a position by ticket                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool ModifySL(class="kw">const class="type">class="kw">ulong ticket, class="kw">const class="type">class="kw">double stop_loss)
  {
class=class="str">"cmt">//--- if failed to select a position by ticket, report this in the journal and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   ResetLastError();
   if(!PositionSelectByTicket(ticket))
     {
      PrintFormat("%s: Failed to select position by ticket number %I64u. Error %d", __FUNCTION__, ticket, GetLastError());
      class="kw">return class="kw">false;
     }
   
class=class="str">"cmt">//--- declare the structures of the trade request and the request result
   class="type">MqlTradeRequest   request={};
   class="type">MqlTradeResult    result ={};

class=class="str">"cmt">//--- fill in the request structure
   request.action     = TRADE_ACTION_SLTP;
   request.symbol     = PositionGetString(POSITION_SYMBOL);
   request.magic      = PositionGetInteger(POSITION_MAGIC);
   request.tp         = PositionGetDouble(POSITION_TP);
   request.position   = ticket;

「止损修改与程序中止的收口处理」

修改持仓止损时,先把止损价按品种小数位归一化,再尝试用 OrderSend 发送变更请求;若发送失败,直接在日志打印错误码并返回 false,调用方据此判断本次调仓未生效。 这段逻辑里 NormalizeDouble 的位数取自 SymbolInfoInteger(Symbol(), SYMBOL_DIGITS),避免手动填小数位导致报价精度不匹配。外汇与贵金属点差跳动快,止损价精度错一位就可能被服务器拒单,属于实操中的高频坑。 CTrade::IsStopped 则在每次交易前检查 MQL5 程序是否被强制停止,一旦 ::IsStopped() 为真,就把返回码设为 TRADE_RETCODE_CLIENT_DISABLES_AT 并禁用交易。回测或实盘里若突然出现“trading is disabled”日志,优先怀疑 EA 被退出而非 broker 拒单。 示例头文件里 SAR_DATA_INDEX 取 1 表示拿倒数第二根 BAR 的抛物转向点,InpMagic 默认 123 用于区分本 EA 订单。开 MT5 把 Magic 改成自己常用值,再挂 TrailingBySAR 模板,能直接验证上面两段代码的中止与改单行为。

MQL5 / C++
  request.sl                 = NormalizeDouble(stop_loss,(class="type">int)SymbolInfoInteger(Symbol(),SYMBOL_DIGITS));

class=class="str">"cmt">//--- if the trade operation could not be sent, report this to the journal and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
  if(!OrderSend(request, result))
    {
      PrintFormat("%s: OrderSend() failed to modify position #%I64u. Error %d",__FUNCTION__, ticket, GetLastError());
      class="kw">return class="kw">false;
    }

class=class="str">"cmt">//--- request to change StopLoss position successfully sent
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Checks forced shutdown of MQL5-program                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTrade::IsStopped(class="kw">const class="type">class="kw">string function)
  {
   if(!::IsStopped())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- MQL5 program is stopped
   PrintFormat("%s: MQL5 program is stopped. Trading is disabled",function);
   m_result.retcode=TRADE_RETCODE_CLIENT_DISABLES_AT;
   class="kw">return(true);
   }
让小布替你跑这套 SAR 回测
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 SAR 跟踪止损的触发分布,把重复劳动交给小布,你专注决策。

常见问题

点位于价格下方时倾向多头跟踪,上方时倾向空头跟踪;EA 每次刷新都按当前点位移仓止损,而非固定点数。
经纪商对止损距现价的最小距离与冻结修改区有硬限制,不检查就可能改单失败或提前触发,概率上增加滑点风险。
magic 用于区分不同策略开的仓,EA 只移动自己标识的仓位,避免误碰手动单或其他机器人持仓。
小布盯盘的 AIGC 模块已预置 SAR 跟踪诊断视图,可对照品种页看触发密度,但实盘参数仍需你按品种波动自调,外汇贵金属属高风险。
会,横盘时 SAR 点切换快,止损可能连续上移下移;可加趋势过滤或只在 ADX 偏高时启用,降低假突破概率。