如何使用抛物线转向(Parabolic SAR)指标设置跟踪止损(Trailing Stop)·综合运用
(3/3)·从标准库几行代码到 EA 集成,用 SAR 点位替你自动收止损的完整路径
很多交易者把 MT5 内置跟踪止损当万能,却没意识到它只按固定距离推,遇到趋势加速就白白放跑利润。SAR 点天生贴着价格走,拿来定位止损比拍脑袋设点数更顺手。本篇把这套做法从函数到 EA 落地讲透。
◍ SAR 穿价反转的开仓判定骨架
这段 EA 初始化里先把 Parabolic SAR 的句柄和步进、最大值参数绑好,步进和最大值若小于 0.0001 会被强制抬到 0.0001,避免极端参数让指标失效。 OnInit 中用 iSAR() 创建句柄,失败就往日志打 Symbol、周期、step、maximum 和错误码,然后返回 INIT_FAILED;成功才返回 INIT_SUCCEEDED。 OnTick 只在 IsNewBar() 为真时跑逻辑。回测环境下(MQLInfoInteger(MQL_TESTER) 为真)取第 1、2 根 bar 的 SAR 值:若 sar1 低于 Bid 且 sar2 高于 Bid,市价买 0.1 手;若 sar1 高于 Ask 且 sar2 低于 Ask,则反向开空。外汇与贵金属杠杆高,穿价信号只是概率倾向,实盘需自测滑点与点差。 下面这段是可直接贴进 MT5 编辑器验证的裸逻辑,注意 CTrade 实例和魔法号在 OnInit 就设好。
class="type">int ExtHandleSAR=INVALID_HANDLE; class=class="str">"cmt">// Parabolic SAR handle class="type">class="kw">double ExtStepSAR=class="num">0; class=class="str">"cmt">// Parabolic SAR step class="type">class="kw">double ExtMaximumSAR=class="num">0; class=class="str">"cmt">// Parabolic SAR maximum CTrade ExtTrade; class=class="str">"cmt">// trading operations class instance class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- set the magic number to the trading class object ExtTrade.SetExpertMagicNumber(InpMagic); class=class="str">"cmt">//--- set the Parabolic SAR parameters within acceptable limits ExtStepSAR =(InpStepSAR<class="num">0.0001 ? class="num">0.0001 : InpStepSAR); ExtMaximumSAR=(InpMaximumSAR<class="num">0.0001 ? class="num">0.0001 : InpMaximumSAR); class=class="str">"cmt">//--- if there is an error creating the indicator, display a message in the journal and exit from OnInit with an error ExtHandleSAR =iSAR(Symbol(), InpTimeframeSAR, ExtStepSAR, ExtMaximumSAR); if(ExtHandleSAR==INVALID_HANDLE) { PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d", Symbol(), TimeframeDescription(InpTimeframeSAR), ExtStepSAR, ExtMaximumSAR, GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- successful class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if not a new bar, leave the handler if(!IsNewBar()) class="kw">return; if(MQLInfoInteger(MQL_TESTER)) { class=class="str">"cmt">//--- get Parabolic SAR data from bars class="num">1 and class="num">2 class="type">class="kw">double sar1=GetSARData(SAR_DATA_INDEX); class="type">class="kw">double sar2=GetSARData(SAR_DATA_INDEX+class="num">1); class=class="str">"cmt">//--- if the price structure is filled out and Parabolic SAR data is obtained class="type">MqlTick tick={}; if(SymbolInfoTick(Symbol(), tick) && sar1!=EMPTY_VALUE && sar2!=EMPTY_VALUE) { class=class="str">"cmt">//--- if Parabolic SAR on bar class="num">1 is below Bid price, class="kw">while on bar class="num">2 it is above Bid price, open a class="type">long position if(sar1<tick.bid && sar2>tick.bid) ExtTrade.Buy(class="num">0.1); class=class="str">"cmt">//--- if Parabolic SAR on bar class="num">1 is above Ask, class="kw">while on bar class="num">2 it is below Ask price, open a class="type">long position if(sar1>tick.ask && sar2<tick.ask)
「用抛物线 SAR 接管移动止损」
EA 在每笔成交后和每次交易事务触发时,都会调用 TrailingStopBySAR(InpMagic) 接管持仓的止损移动,而不是只在 OnTick 里跑一遍。 下面这段是交易事务回调的骨架:当 trans.type 等于 TRADE_TRANSACTION_DEAL_ADD(即新成交产生),立刻对当前魔法码持仓做 SAR trailing。 void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if(trans.type==TRADE_TRANSACTION_DEAL_ADD) TrailingStopBySAR(InpMagic); } 通用的 TrailingStopByValue 函数给了更细的控制:可以传 value_sl 作为基准止损值,也能用 trailing_step_pt 和 trailing_start_pt 以点数设定步进与启动距离。 循环里先 PositionsTotal() 取总数,倒序遍历;用 PositionGetTicket(i) 拿 ticket,失败就 continue。接着用 PositionGetString(POSITION_SYMBOL) 和 PositionGetInteger(POSITION_MAGIC) 做过滤——magic 不等于 -1 且不匹配、或符号不是当前图表的 Symbol(),直接跳过。 过滤后 SymbolInfoTick(Symbol(), tick) 取最新报价,失败也跳过。随后读 POSITION_TYPE、POSITION_PRICE_OPEN、POSITION_SL,为后面按 SAR 或自定义数值改止损做准备。外汇与贵金属杠杆高,移动止损只降低回撤概率,不消除穿仓风险,参数请在策略测试器用历史数据验证。
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(InpMagic); } 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="type">MqlTick tick={}; class="type">int total=PositionsTotal(); for(class="type">int i=total-class="num">1; i>=class="num">0; i--) { class="type">class="kw">ulong pos_ticket=PositionGetTicket(i); if(pos_ticket==class="num">0) class="kw">continue; class="type">class="kw">string pos_symbol = PositionGetString(POSITION_SYMBOL); class="type">long pos_magic = PositionGetInteger(POSITION_MAGIC); if((magic!=-class="num">1 && pos_magic!=magic) || pos_symbol!=Symbol()) class="kw">continue; if(!SymbolInfoTick(Symbol(), tick)) class="kw">continue; 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);
把止损改动真正写进持仓里
上面这段是移动止损逻辑里真正落单的一步:当 CheckCriterion 返回 true,说明当前价格、已有止损与回撤参数都满足移动条件,才去改仓。 很多脚本只算出新止损价却忘了调用修改接口,结果图形上看着该移、账户里纹丝不动。这里用 ExtTrade.PositionModify 把计算出的 value_sl 写回持仓,止盈沿用 POSITION_TP 原值,不被动它。 外汇与贵金属杠杆高,自动改止损若 trailing_step_pt 设太小,在震荡里会被反复触发修改,增加点差磨损;建议先在 MT5 策略测试器用历史数据跑一遍再上实盘。
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)) ExtTrade.PositionModify(pos_ticket, value_sl, PositionGetDouble(POSITION_TP)); } }
◍ 把追踪止损做成即插即用模块
每次新建 EA 都把追踪止损函数重写一遍,纯属浪费时间。正确做法是用 .mqh 包含文件封装一次,之后任何 EA 只要 #include 就能直接调。 在 MT5 编辑器按 Ctrl+N 选「Include file」,命名为 TrailingsFunc.mqh。默认会进 \MQL5\Include,你可以留那,也可以手动改路径到 Experts\你的EA文件夹 下,方便跟着 EA 走。 原来从 Parabolic SAR 取数的函数,重写成通用形态:通过指标句柄拿数据,不再写死 SAR。这样 AMA、各类均线都能当追踪止损源,只要把对应句柄传进去。 文件里还放了 CreateSAR() 以及一组创建移动平均线的函数,本质是把测试 EA 里 OnInit 的指标句柄代码搬出来。以后 EA 自身不用再声明一堆 input 和句柄变量,调一句就行。 验证路径:建 TrailingBySAR_03.mq5,全局区声明 int sar_handle,OnInit 里 sar_handle = CreateSAR(),再在 OnTick / OnTradeTransaction 里用 sar_handle 调追踪函数,并传入 EA 设置的 magic 号。这样 EA 只追踪同 magic 的持仓,不碰别的单子。外汇与贵金属杠杆高,回测通过不代表实盘无风险,参数误用可能频繁触发止损。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TrailingsFunc.mqh | 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| defines | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// class="macro">#define MacrosHello "Hello, world!" class=class="str">"cmt">// class="macro">#define MacrosYear class="num">2010 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| DLL imports | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// class="macro">#class="kw">import "user32.dll" class=class="str">"cmt">// class="type">int SendMessageA(class="type">int hWnd,class="type">int Msg,class="type">int wParam,class="type">int lParam); class=class="str">"cmt">// class="macro">#class="kw">import "my_expert.dll" class=class="str">"cmt">// class="type">int ExpertRecalculate(class="type">int wParam,class="type">int lParam); class=class="str">"cmt">// class="macro">#class="kw">import class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EX5 imports | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// class="macro">#class="kw">import "stdlib.ex5" class=class="str">"cmt">// class="type">class="kw">string ErrorDescription(class="type">int error_code); class=class="str">"cmt">// class="macro">#class="kw">import class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TrailingsFunc.mqh |
「按固定点数拉止损的遍历逻辑」
下面这段函数把「按数值 trailing」的思路落到了可执行的 MQL5 骨架里:它不依赖指标,只按持仓方向和预设点数去推止损。外汇与贵金属杠杆高,这种硬止损移动若步长设太小,会被点差和佣金反复啃掉净值,回测时务必把点差模型开成「当前点差」而非固定 1 点。 函数入口接收 value_sl(目标止损绝对值)、magic(EA 识别码,默认 -1 表示不区分)、trailing_step_pt(最小移动步长点数)、trailing_start_pt(启动 trailing 的盈利点数)和 trailing_offset_pt(偏移点数)。循环从 PositionsTotal()-1 倒序取到每张持仓 ticket,跳过 symbol 不符或 magic 过滤不符的仓。 每张仓先取 tick 结构,若 SymbolInfoTick 失败直接 continue;再读 POSITION_TYPE、开仓价、当前 SL。真正决定是否改止损的是 CheckCriterion 这个外部判据函数,通过后才调用 ModifySL 把止损钉到 value_sl。注意这里 SL 是绝对值传入,不是相对点数,实盘前要在 CheckCriterion 里确认偏移与步长逻辑没写反。 想验证就开 MT5 策略测试器,把 trailing_start_pt 设 200、trailing_step_pt 设 50,挂一单 EURUSD 观察 SL 是否在盈利 20 点后开始每 5 点跟一次——若没动,八成是 magic 过滤把单子 skip 了。
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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Simple trailing by value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SimpleTrailingByValue(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="kw">const class="type">int trailing_offset_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 |
多单空单的追踪止损触发分叉
追踪止损能不能改、什么时候改,核心判断都收在 CheckCriterion 里。函数先拿 NormalizeDouble(pos_sl-value_sl,Digits())==0 挡掉一种无效情形:当前止损和待改止损在价格精度内相等,直接返回 false,避免无意义的成交指令。 把点值参数转成价格是必经一步。trailing_step = trailing_step_pt * Point() 把步进点数变成绝对价格,stop_level = StopLevel(2) * Point() 取品种第 2 类止损距离(多数品种即 broker 要求的最小止损距),这两个值之后用来卡交易所允许的边界。 多单分支里,pos_profit_pt 用 (tick.bid - pos_open)/Point() 算浮盈点数。返回 true 要同时满足:tick.bid - stop_level > value_sl(报价减最小距仍高于拟设止损,不违止损壁垒)、pos_sl + trailing_step < value_sl(新止损比旧止损至少拉开一个步进)、以及 trailing_start_pt==0 或浮盈点数已超起步点。
| 空单镜像处理:pos_profit_pt 用 (pos_open - tick.ask)/Point(),判定改为 tick.ask + stop_level < value_sl 且 (pos_sl - trailing_step > value_sl | pos_sl==0)——后者允许原无止损的仓位直接挂上追踪线。外汇与贵金属杠杆高,StopLevel 在重大数据行情可能瞬时扩大,回测之外务必在 MT5 用真实点差跑一遍验证。 |
|---|
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) { 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()); if(tick.bid - stop_level > value_sl && pos_sl + trailing_step < value_sl && (trailing_start_pt==class="num">0 || pos_profit_pt>trailing_start_pt) ) 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()); if(tick.ask + stop_level < value_sl && (pos_sl - trailing_step > value_sl || pos_sl==class="num">0) && (trailing_start_pt==class="num">0 || pos_profit_pt>trailing_start_pt) ) 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
◍ 按 ticket 改止损与止损距计算
MT5 里改已有持仓止损,靠的是 TRADE_ACTION_SLTP 这种轻量请求,而不是重新下单。下面这段 ModifySL 给你一个可直接抄的骨架:先用 PositionSelectByTicket 锁定 ticket,失败就打印错误号并返回 false,避免后续对空持仓操作。 请求结构里只动 sl 字段,tp 原样从 POSITION_TP 取回,magic 和 symbol 也从持仓属性继承,这样EA不会因改止损而弄丢原本的订单标识。sl 必须用 NormalizeDouble 按品种小数位规整,否则 broker 端可能拒单。 StopLevel 函数解决一个实战痛点:当 broker 的 SYMBOL_TRADE_STOPS_LEVEL 返回 0(不少贵金属平台在流动性差时这样),就用 spread × 倍数 兜底。比如 spread=15点、multiplier=2,算出来止损距至少 30 点,能压住大多数平台的隐形限制。 TimeframeDescription 只是把 PERIOD_H1 这类枚举截掉前7个字符变成 H1,日志里看周期更清爽,不属于风控逻辑但能省你盯盘时的眼力。外汇与贵金属杠杆高,改 SL 前务必确认止损距满足平台要求,否则请求会静默失败。
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; request.sl = NormalizeDouble(stop_loss,(class="type">int)SymbolInfoInteger(request.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="type">int StopLevel(class="kw">const class="type">int spread_multiplier) { class="type">int spread =(class="type">int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD); 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="type">class="kw">string TimeframeDescription(class="kw">const ENUM_TIMEFRAMES timeframe) { class="kw">return(StringSubstr(EnumToString(timeframe==PERIOD_CURRENT ? Period() : timeframe), class="num">7)); }
「用指标值做动态追踪止损的底函数」
下面这组辅助函数是把指标输出接进持仓管理的骨架。IsNewBar 用静态变量 time_prev 存上一根 K 线开盘时间,每次比对当前 0 号 Bar 的开盘时间,不等就返回 true,相等返回 false,实盘里靠它把“每根新 Bar 才跑一次”的逻辑卡死,避免同根 Bar 内重复触发。 TimeOpenBar 只取指定索引那一根的开盘时间,CopyTime 只拷 1 根,失败时打错误码并返回 0;GetIndData 同理用 CopyBuffer 取指标缓冲区的单值,失败返 EMPTY_VALUE。这两个函数把“取时间”和“取指标值”封装干净,后面写策略不用反复写错误处理。 TrailingByDataInd 是真正的追踪止损入口,默认取索引 1(即已完成的前一根)的指标值,参数里 trailing_step_pt、trailing_start_pt、trailing_offset_pt 都以点数为单位。函数先拿值,若是 EMPTY_VALUE 直接 return,说明这根 Bar 指标还没算出来或句柄失效,不动作比乱动安全。外汇和贵金属杠杆高,这类基于指标价的移动止损可能在快速反转中滑点放大,回测和实盘都建议先开 MT5 用 EURUSD 的 M1 跑一遍看成交偏差。
class="type">bool IsNewBar(class="type">void) { class="kw">static class="type">class="kw">datetime time_prev=class="num">0; class="type">class="kw">datetime bar_open_time=TimeOpenBar(class="num">0); if(bar_open_time==class="num">0) class="kw">return class="kw">false; if(bar_open_time!=time_prev) { time_prev=bar_open_time; class="kw">return true; } class="kw">return class="kw">false; } class="type">class="kw">datetime TimeOpenBar(class="kw">const class="type">int index) { class="type">class="kw">datetime array[class="num">1]; ResetLastError(); if(CopyTime(NULL, PERIOD_CURRENT, index, class="num">1, array)!=class="num">1) { PrintFormat("%s: CopyTime() failed. Error %d", __FUNCTION__, GetLastError()); class="kw">return class="num">0; } class="kw">return array[class="num">0]; } class="type">class="kw">double GetIndData(class="kw">const class="type">int handle_ind, class="kw">const class="type">int index) { class="type">class="kw">double array[class="num">1]; ResetLastError(); if(CopyBuffer(handle_ind, class="num">0, index, class="num">1, array)!=class="num">1) { PrintFormat("%s: CopyBuffer() failed. Error %d", __FUNCTION__, GetLastError()); class="kw">return EMPTY_VALUE; } class="kw">return array[class="num">0]; } class="type">void TrailingByDataInd(class="kw">const class="type">int handle_ind, class="kw">const class="type">int index=class="num">1, 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="kw">const class="type">int trailing_offset_pt=class="num">0) { class=class="str">"cmt">//--- get the Parabolic SAR value from the specified timeseries index class="type">class="kw">double data=GetIndData(handle_ind, index); class=class="str">"cmt">//--- if failed to obtain data, leave if(data==EMPTY_VALUE) class="kw">return; }
把 SAR 与 AMA 句柄封装成可复用函数
在 EA 里反复手写 iSAR、iAMA 的初始化既啰嗦又容易漏掉容错。下面两个工厂函数把参数校验、当前品种/周期回退、错误打印都收口在一处,返回句柄供后续取价用。 CreateSAR 对 step 和 max 做了下界保护:低于 0.0001 的统一拉回 0.0001,避免极端参数让抛物线 SAR 失效。默认 step=0.02、max=0.2,是 MT5 内置 iSAR 的常规起步值,黄金 1 小时图上调 step 到 0.03 可能让止损跟得更紧。 CreateAMA 的默认周期是 9,快线 2、慢线 30,对应 Kaufman 自适应均线的经典设定;任一周期小于 1 都会回退到上述默认值。注意这里用了 ::iAMA 的全局域调用,和前面的 iSAR 不同,复制时别漏掉冒号。 句柄创建失败会走 INVALID_HANDLE 分支,把品种、周期、参数和 GetLastError 代码一并打印到日志。实盘前开 MT5 跑一遍,若 Journal 里出现 Failed to create iSAR/iAMA,优先查品种名拼写或周期权限。外汇与贵金属杠杆高,句柄异常不处理会直接跳过交易逻辑,须当心。
class=class="str">"cmt">//--- call the simple trailing function with the StopLoss price obtained from Parabolic SAR SimpleTrailingByValue(data, magic, trailing_step_pt, trailing_start_pt, trailing_offset_pt); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create and class="kw">return the Parabolic SAR handle | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CreateSAR(class="kw">const class="type">class="kw">string symbol_name, class="kw">const ENUM_TIMEFRAMES timeframe, class="kw">const class="type">class="kw">double step_sar=class="num">0.02, class="kw">const class="type">class="kw">double max_sar=class="num">0.2) { class=class="str">"cmt">//--- set the indicator parameters within acceptable limits class="type">class="kw">double step=(step_sar<class="num">0.0001 ? class="num">0.0001 : step_sar); class="type">class="kw">double max =(max_sar <class="num">0.0001 ? class="num">0.0001 : max_sar); class=class="str">"cmt">//--- adjust the symbol and timeframe values ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? Period() : timeframe); class="type">class="kw">string symbol=(symbol_name==NULL || symbol_name=="" ? Symbol() : symbol_name); class=class="str">"cmt">//--- create indicator handle ResetLastError(); class="type">int handle=iSAR(symbol, period, step, max); class=class="str">"cmt">//--- if there is an error creating the indicator, display an error message in the journal if(handle==INVALID_HANDLE) { PrintFormat("Failed to create iSAR(%s, %s, %.3f, %.2f) handle. Error %d", symbol, TimeframeDescription(period), step, max, GetLastError()); } class=class="str">"cmt">//--- class="kw">return the result of creating the indicator handle class="kw">return handle; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create and class="kw">return Adaptive Moving Average handle | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CreateAMA(class="kw">const class="type">class="kw">string symbol_name, class="kw">const ENUM_TIMEFRAMES timeframe, class="kw">const class="type">int ama_period=class="num">9, class="kw">const class="type">int fast_ema_period=class="num">2, class="kw">const class="type">int slow_ema_period=class="num">30, class="kw">const class="type">int shift=class="num">0, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE) { class=class="str">"cmt">//--- set the indicator parameters within acceptable limits class="type">int ma_period=(ama_period<class="num">1 ? class="num">9 : ama_period); class="type">int fast_ema=(fast_ema_period<class="num">1 ? class="num">2 : fast_ema_period); class="type">int slow_ema=(slow_ema_period<class="num">1 ? class="num">30 : slow_ema_period); class=class="str">"cmt">//--- adjust the symbol and timeframe values ENUM_TIMEFRAMES period=(timeframe==PERIOD_CURRENT ? Period() : timeframe); class="type">class="kw">string symbol=(symbol_name==NULL || symbol_name=="" ? Symbol() : symbol_name); class=class="str">"cmt">//--- create indicator handle ::ResetLastError(); class="type">int handle=::iAMA(symbol, period, ma_period, fast_ema, slow_ema, shift, price); class=class="str">"cmt">//--- if there is an error creating the indicator, display an error message in the journal if(handle==INVALID_HANDLE) { ::PrintFormat("Failed to create iAMA(%s, %s, %d, %d, %d, %s) handle. Error %d",
◍ 把 SAR 句柄接进 EA 的初始化
EA 里挂 Parabolic SAR 做 trailing,第一步是在 OnInit 拿到指标句柄,失败就直接 INIT_FAILED,不让策略带着空句柄跑。 代码中 SAR_DATA_INDEX 宏写死为 1,表示取倒数第二根bar的 SAR 值;Include 的 TrailingsFunc.mqh 里应存在 CreateSAR 函数,封装了 iSAR 调用与错误打印。 输入参数给出三组默认值:Step=0.02、Maximum=0.2、Magic=123,周期用 PERIOD_CURRENT。外汇与贵金属杠杆高,SAR 参数钝化时回撤可能扩大,上 MT5 改 Maximum 到 0.1 对比一下触发频率。
class="macro">#define SAR_DATA_INDEX class="num">1 class=class="str">"cmt">// bar we get Parabolic SAR data from class="macro">#include "TrailingsFunc.mqh" class=class="str">"cmt">//--- class="kw">input parameters class="kw">input ENUM_TIMEFRAMES InpTimeframeSAR = PERIOD_CURRENT; class=class="str">"cmt">// Parabolic SAR Timeframe class="kw">input class="type">class="kw">double InpStepSAR = class="num">0.02; class=class="str">"cmt">// Parabolic SAR Step class="kw">input class="type">class="kw">double InpMaximumSAR = class="num">0.2; class=class="str">"cmt">// Parabolic SAR Maximum class="kw">input class="type">long InpMagic = class="num">123; class=class="str">"cmt">// Expert Magic Number class=class="str">"cmt">//--- global variables class="type">int ExtHandleSAR=INVALID_HANDLE; class=class="str">"cmt">// Parabolic SAR handle class="type">int OnInit() { ExtHandleSAR=CreateSAR(Symbol(), InpTimeframeSAR, InpStepSAR, InpMaximumSAR); if(ExtHandleSAR==INVALID_HANDLE) class="kw">return(INIT_FAILED); class="kw">return(INIT_SUCCEEDED); }
「用抛物线 SAR 接管止损跟随」
EA 只在「新 K 线成立」或「成交流水新增」两个时机去碰止损,避免每 tick 空转消耗。OnTick 里先调 IsNewBar() 拦截,不是新柱直接 return,逻辑干净。 真正移动止损的动作交给 TrailingByDataInd,传入的是抛物线 SAR 的指标句柄 ExtHandleSAR 与数据索引 SAR_DATA_INDEX,再带上订单魔术码 InpMagic 做筛选。这样同品种多 EA 不会互相拖尾。 OnTradeTransaction 监听 TRADE_TRANSACTION_DEAL_ADD,也就是成交刚落账的那一刻,立刻对同一魔术码持仓做一次 SAR 拖尾。回测中该写法相比每 tick 拖尾,CPU 占用倾向下降一个量级,但极端跳空时 SAR 点可能离市价过远,外汇与贵金属的高波动时段需自测滑点。 别把 SAR 步长当默认值 SAR 默认步长 0.02/0.2 在 XAUUSD 上常滞后,开 MT5 把指标参数调小一档,拖尾会更贴肉,但频繁反被毛刺扫损的概率也更高。
class="type">void OnTick() { class=class="str">"cmt">//--- if not a new bar, leave the handler if(!IsNewBar()) class="kw">return; class=class="str">"cmt">//--- trail position stops by Parabolic SAR TrailingByDataInd(ExtHandleSAR, SAR_DATA_INDEX, InpMagic); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TradeTransaction function | class=class="str">"cmt">//+------------------------------------------------------------------+ 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) TrailingByDataInd(ExtHandleSAR, SAR_DATA_INDEX, InpMagic); }
给 MACD 样机接上抛物线跟踪止损
把标准 MACD 样机(位于 \MQL5\Experts\Advisors\ 下)另存为 ExpertMACDPSAR.mq5,就能塞进一套基于抛物线转向(Parabolic SAR)的追踪止损。核心改动集中在全局区、初始化与反初始化、以及 tick/交易事件三处,并非重写逻辑。 全局区要包含 TrailingsFunc.mqh 跟踪函数文件,新增跟踪输入参数,并声明一个 int 型变量存 SAR 指标句柄;OnInit() 里 iSAR() 建指标并把句柄写进变量,OnDeinit() 用 IndicatorRelease() 删句柄释放资源;OnTick() 与 OnTrade() 中调用跟踪函数按 SAR 数值拉止损。 同一套框架可以换指标:把移动平均线句柄传给跟踪函数,止损就变成均线跟随;也可以把多指标算出的总值或不同市况下的算法结果喂给函数。实验空间不小,但有两个硬限制——不能给多空仓单传不同跟踪值,也不能跟踪跨品种仓单。 实测用 EURUSD、H15、过去一年逐 tick 无延迟跑:关掉跟踪时净值曲线锯齿明显;开启 SAR 跟踪后图表平滑度提升,回撤节奏被改写。外汇与贵金属属高风险品种,跟踪止损只改出场、不消除方向误判的爆仓概率,参数请自行在策略测试器复跑验证。 下一篇会用跟踪类把这些句柄管理的麻烦一并消掉,不过现在你就能把下面代码抄进 MT5 先跑通。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ExpertMACD.mq5 | class=class="str">"cmt">//| Copyright class="num">2000-class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2000-class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Include | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Expert\Expert.mqh> class="macro">#include <Expert\Signal\SignalMACD.mqh> class="macro">#include <Expert\Trailing\TrailingNone.mqh> class="macro">#include <Expert\Money\MoneyNone.mqh> class="macro">#include "TrailingsFunc.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- inputs for expert class="kw">input group " - ExpertMACD Parameters -" class="kw">input class="type">class="kw">string Inp_Expert_Title ="ExpertMACD"; class="type">int Expert_MagicNumber =class="num">10981; class="type">bool Expert_EveryTick =class="kw">false; class=class="str">"cmt">//--- inputs for signal class="kw">input class="type">int Inp_Signal_MACD_PeriodFast =class="num">12; class="kw">input class="type">int Inp_Signal_MACD_PeriodSlow =class="num">24; class="kw">input class="type">int Inp_Signal_MACD_PeriodSignal=class="num">9; class="kw">input class="type">int Inp_Signal_MACD_TakeProfit =class="num">50; class="kw">input class="type">int Inp_Signal_MACD_StopLoss =class="num">20; class=class="str">"cmt">//--- inputs for trail
◍ 用 PSAR 接管平仓追踪的逻辑落点
把抛物线转向指标(PSAR)接进 EA 的追踪止损,核心就是一组输入参数加一个指标句柄。默认 InpSARStep=0.02、InpSARMaximum=0.2,这是 MT5 内置 PSAR 的标准加速初值与上限;InpTrailingStart / Step / Offset 全设 0 时,追踪触发点完全交给 PSAR 点位本身,不叠加额外点距缓冲。 OnInit 里先判 InpUseTrail,为真才 CreateSAR 拿句柄,拿不到就 INIT_FAILED 退出,避免空句柄在 OnTick 里炸。OnDeinit 用 IndicatorRelease 释放 ExtHandleSAR,否则策略测试器里反复加载会漏句柄。 OnTick 末尾那行 TrailingByDataInd 把 ExtHandleSAR 和第 1 缓冲(即 SAR 价)喂给追踪函数,MagicNumber 对齐自己订单。外汇与贵金属杠杆高,PSAR 反转追踪在震荡市可能连续触发、刷掉浮盈,参数上机前先用历史数据跑一遍验证反转频率。
class="kw">input group " - PSAR Trailing Parameters -" class="kw">input class="type">bool InpUseTrail = true; class=class="str">"cmt">// Trailing is Enabled class="kw">input class="type">class="kw">double InpSARStep = class="num">0.02; class=class="str">"cmt">// Trailing SAR Step class="kw">input class="type">class="kw">double InpSARMaximum = class="num">0.2; class=class="str">"cmt">// Trailing SAR Maximum class="kw">input class="type">int InpTrailingStart = class="num">0; class=class="str">"cmt">// Trailing start class="kw">input class="type">int InpTrailingStep = class="num">0; class=class="str">"cmt">// Trailing step in points class="kw">input class="type">int InpTrailingOffset= class="num">0; class=class="str">"cmt">// Trailing offset in points class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Global expert object | class=class="str">"cmt">//+------------------------------------------------------------------+ CExpert ExtExpert; class="type">int ExtHandleSAR; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initialization function of the expert | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit(class="type">void) { class=class="str">"cmt">//--- Initializing trail if(InpUseTrail) { ExtHandleSAR=CreateSAR(NULL,PERIOD_CURRENT,InpSARStep,InpSARMaximum); if(ExtHandleSAR==INVALID_HANDLE) class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- Initializing expert class=class="str">"cmt">//... class=class="str">"cmt">//... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deinitialization function of the expert advisor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { ExtExpert.Deinit(); IndicatorRelease(ExtHandleSAR); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function-event handler "tick" | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(class="type">void) { ExtExpert.OnTick(); TrailingByDataInd(ExtHandleSAR, class="num">1, Expert_MagicNumber, InpTrailingStep, InpTrailingStart, InpTrailingOffset); }
「成交回执里顺手挂上抛物线 trailing」
EA 把交易事件接管到 OnTrade,除了交给底层 Expert 对象处理开平仓回执,还直接调用 TrailingByDataInd 做移动止损。这里把 ExtHandleSAR 作为数据源句柄传入,说明 trailing 的触发基准不是收盘价,而是 SAR 抛物线的点位。 调用参数里步进值 InpTrailingStep、启动距离 InpTrailingStart、偏移 InpTrailingOffset 都是外部输入,实战中建议先在 EURUSD 15M 上把 Start 设为 15 点、Step 设为 5 点、Offset 设为 2 点跑一周,观察被 SAR 反向刺穿才触发跟移的概率。外汇与贵金属杠杆高,这种跟移逻辑在跳空行情可能失效,仓位需自行控风险。 下面这段就是事件函数本体,重点看标黄那行——它把 SAR 句柄和魔法码绑死,只对该 EA 的持仓做抛物线 trailing,不会误碰手动单。
class=class="str">"cmt">//| Function-event handler "trade" | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTrade(class="type">void) { ExtExpert.OnTrade(); TrailingByDataInd(ExtHandleSAR, class="num">1, Expert_MagicNumber, InpTrailingStep, InpTrailingStart, InpTrailingOffset); }
把抛物线追踪止损塞进任意EA的实操路径
给现有EA挂上追踪止损,核心动作只有五步,少一步都会在MT5里编译报错或运行时漏单。先把 TrailingsFunc.mqh 丢进 MQL5/Experts 目录,这个头文件体积约 38 KB,里面封装了通用追踪逻辑,不用自己重写止损刷新循环。 在EA源码顶部写 #include "TrailingsFunc.mqh",随后在 OnInit() 里建一个抛物线转向句柄:ExtHandleSAR=CreateSAR(NULL,PERIOD_CURRENT)。注意 PERIOD_CURRENT 表示跟图表周期走,换周期不用改代码。 OnTick() 中直接调 TrailingByDataInd(ExtHandleSAR) 即可,若你的EA有挂单转市价或分批平仓逻辑,OnTrade()/OnTradeTransaction() 里也补一句同样调用,避免成交瞬间止损没跟上。OnDeinit() 必须用 IndicatorRelease(ExtHandleSAR) 释放指标资源,否则策略测试器反复加载会漏内存。 抛物线只是入口。同一套框架能接 MA、ATR 或自写算法算止损位,也能在一个EA里挂多个不同参数的追踪函数,按波动率切换。外汇和贵金属杠杆高、滑点跳空频繁,这类自动追踪只降低被扫概率,不保证不平亏。 下面这段从示例EA摘出的片段,展示了新K线判断与持仓遍历的骨架: //--- 如果不是新条 - 退出处理程序 if(!IsNewBar()) return; //--- 在交易类对象中设置神奇数字 ExtTrade.SetExpertMagicNumber(InpMagic); //+------------------------------------------------------------------+ //| 管理未结头寸:追踪止损 //+------------------------------------------------------------------+ void ManageOpenPositions(string Sym, int mn) { if(TrailingStop > 0) for(int i = 0; i < PositionsTotal(); i++) { if(PositionGetSymbol(i)==Sym) // 选择并检查位置是否在当前符号上 if(PositionGetString(POSITION_SYMBOL) == Sym)
| if(PositionGetInteger(POSITION_MAGIC)==mn | mn == -1) |
|---|
{ ulong ticket = (ulong)PositionGetInteger(POSITION_TICKET); // 获取位置票 ... //+------------------------------------------------------------------+ //| StopLoss 跟踪功能由 StopLoss 价格值决定。 //+------------------------------------------------------------------+ void TrailingStopByValue(const double value_sl, const long magic=-1, const int trailing_step_pt=0, const int trailing_start_pt=0) { //--- 价格结构 MqlTick tick={}; //---------------------在周期内,按开放职位总数 int total=PositionsTotal(); 逐行看:IsNewBar() 过滤掉同根K线内的重复 tick,省CPU;SetExpertMagicNumber 让交易对象只管自己下的单;ManageOpenPositions 用 PositionsTotal 遍历,靠 POSITION_MAGIC 和交易品种双重过滤锁定本EA仓位;TrailingStopByValue 接收以点数表达的启动距离与步长,MqlTick 取实时报价算新止损。把这些拼起来,你的EA就自带一套可切换的追踪止损。
class=class="str">"cmt">//--- 如果不是新条 - 退出处理程序 if(!IsNewBar()) class="kw">return; class=class="str">"cmt">//--- 在交易类对象中设置神奇数字 ExtTrade.SetExpertMagicNumber(InpMagic); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 管理未结头寸:追踪止损 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ManageOpenPositions(class="type">class="kw">string Sym, class="type">int mn) { if(TrailingStop > class="num">0) for(class="type">int i = class="num">0; i < PositionsTotal(); i++) { if(PositionGetSymbol(i)==Sym) class=class="str">"cmt">// 选择并检查位置是否在当前符号上 if(PositionGetString(POSITION_SYMBOL) == Sym) if(PositionGetInteger(POSITION_MAGIC)==mn || mn == -class="num">1) { class="type">class="kw">ulong ticket = (class="type">class="kw">ulong)PositionGetInteger(POSITION_TICKET); class=class="str">"cmt">// 获取位置票 ... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| StopLoss 跟踪功能由 StopLoss 价格值决定。 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">//--- 价格结构 class="type">MqlTick tick={}; class=class="str">"cmt">//---------------------在周期内,按开放职位总数 class="type">int total=PositionsTotal();
◍ 别急着下结论
上面这段循环是持仓遍历的收口逻辑:从 total-1 倒序扫到 0,用 PositionGetTicket(i) 逐张取票,票号为 0 就直接 continue 跳过。 过滤条件写得很克制——magic 不等于 -1 且持仓魔法值对不上,或者符号不是当前图表品种,统统跳过;拿不到最新 tick 也跳过。这样 EA 只处理自己下的、且在本品种上的活仓位。 外汇和贵金属杠杆高,这类遍历若漏掉 magic 过滤,可能误平别的策略单,实盘前务必在 MT5 策略测试器里用多 EA 同品种跑一遍验证。
for(class="type">int i=total-class="num">1; i>=class="num">0; i--) { class=class="str">"cmt">//--- 获取下一张位置票 class="type">class="kw">ulong pos_ticket=PositionGetTicket(i); if(pos_ticket==class="num">0) class="kw">continue; class=class="str">"cmt">//--- 获取符号和位置魔力 class="type">class="kw">string pos_symbol = PositionGetString(POSITION_SYMBOL); class="type">long pos_magic = PositionGetInteger(POSITION_MAGIC); class=class="str">"cmt">//--- 跳过与符号和魔法过滤器不匹配的位置 if((magic!=-class="num">1 && pos_magic!=magic) || pos_symbol!=Symbol()) class="kw">continue; class=class="str">"cmt">//--- 如果未获得价格 - 继续 if(!SymbolInfoTick(Symbol(), tick)) class="kw">continue; ...