如何使用抛物线转向(Parabolic SAR)指标设置跟踪止损(Trailing Stop)·进阶篇
(2/3)· 接上篇基础铺垫,本文拆解如何用 Parabolic SAR 重写 Trailing Stop 逻辑,绕开平台内置的僵硬距离限制
按止损金额做尾随的底层循环
下面这段 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 更稳妥。
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 两个参数,就能在策略测试器里观察不同启动阈值下止损被改动的频次。外汇与贵金属杠杆高,回测通过不代表实盘胜率,参数需结合点差和滑点重验。
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 表示不区分魔术码。
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(); }
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 日志里的脚本占用能明显降一截。外汇与贵金属杠杆高,事件触发不代表风险可控,参数仍要按账户承受力调。
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 验证前先开模拟盘。
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 模板,能直接验证上面两段代码的中止与改单行为。
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); }