抛物线转向追踪止损:用SAR点位重构MT5自我保护逻辑(基础篇)
(1/3)· 多数交易者只会用平台内置尾随止损,却不知SAR点能算出更贴趋势的退出轨迹
很多人在MT5里点开追踪止损就以为万事大吉,实则内置机制只用固定点数跟随,遇到剧烈回调常被扫在噪音里。SAR指标的点位本可以替止损画出更顺趋势的轨道,只是多数EA脚本没把它接进来。先把底层禁止项与指标特性读透,后面两篇才能直接上手改代码。
◍ 用抛物线转向做动态跟踪止损
抛物线转向(Parabolic SAR)在 MT5 里本身只是个趋势反转提示指标,但把它接进 EA 的仓位管理逻辑,就能变成一套随价格加速而收紧的跟踪止损。核心思路是:多单把止损不断上移到最近一个 SAR 点,空单则下移到 SAR 点,这样价格回吐到转向位之前就被踢出。 在 MQL5 里取当前 SAR 值用 iSAR(),它返回的是对应柱的止损价位。注意 SAR 在每根柱开盘就固定,所以用上一根已闭合柱的 SAR 来挂止损,比用当前未闭合柱更稳,能躲掉实时重算造成的反复插针。 下面这段是最小可用片段:先拿到 SAR 句柄,再 CopyBuffer 取倒数第二根柱的数值,最后用 PositionModify 改止损。外汇和贵金属杠杆高,SAR 在震荡市会频繁翻转,实盘前务必在策略测试器里用至少 3 个月 tick 数据回测。
class="type">int sarHandle = iSAR(_Symbol, _Period, class="num">0.02, class="num">0.2); class="type">class="kw">double sarBuf[class="num">1]; CopyBuffer(sarHandle, class="num">0, class="num">1, class="num">1, sarBuf); class=class="str">"cmt">// 取倒数第二根柱的 SAR class="type">class="kw">double newSl = sarBuf[class="num">0]; if(PositionSelect(_Symbol)) PositionModify(_Symbol, newSl, PositionGetDouble(POSITION_TP));
「把抛物线转向塞进EA当追踪止损」
Parabolic SAR 原本是 Welles Wilder 用来标价格反转点的指标,但在 MQL5 里它能被直接借来当动态追踪止损线用——比固定点数 trailing stop 更贴合趋势坡度。MT5 自带的 CTrade 标准库已经封装了大部分下单与改仓逻辑,你不需要从零写 OrderSend。 实际做起来就是几行事:在 OnTick 里读 iSAR 当前值,若是多单就把止损价刷到 SAR 下方一点,空单反之,再调 CTrade::OrderModify 或 PositionModify。比起手写 trailing stop 的循环判断,这套做法把“止损该跟多快”交给了 SAR 的加速因子。 外汇与贵金属杠杆高,SAR 在震荡市会频繁翻转,盲目跟止损可能被来回扫损。建议先在 EURUSD 的 M15 上跑无风险回测,观察 SAR 步长(默认 0.02 起、上限 0.2)对扫损率的影响再上实盘。
用抛物线转向做动态止损锚点
MT5 自带追踪止损只能按固定点数跟价,逻辑单一。若想要更专业的滑动止损,可用 MQL5 自行编写函数,在移动止损前先做合规检查:比如 StopLevel(止损最小允许距离)与 FreezeLevel(冻结区,价格进入后禁止修改持仓)。价格若已逼近原止损且间距小于 FreezeLevel,会直接触发平仓且无法再调。 不同追踪止损算法的核心差异,在于如何计算新的止损价。抛物线转向(Parabolic SAR)适合当这个“锚”:它在图表上以价格上/下方的点呈现,点在下方偏多、上方偏空,随趋势自动拉近止损,既保护浮盈,又能在点穿价格时给出平仓信号。 SAR 的步长与最大值两个参数可直接调,适配不同波动与周期,全时间框架可用。但注意外汇、贵金属属高风险品种,在震荡或低波动段,SAR 点频繁翻面可能让仓位过早被扫,实盘前应在 MT5 用历史数据验证参数敏感性。
◍ 用抛物线转向做仓位追踪止损的模块拆解
一套追踪止损 EA 本质上可拆成两个自给自足的模块:止损水平计算模块、止损修改模块。本文用抛物线转向(Parabolic SAR)充当前者,后者只负责按交易品种与 magic 编号筛选持仓并搬动止损。 若 magic 设为 -1,图表上该交易品种的所有已开仓位都会被跟踪;指定 magic 时仅跟踪对应编号的仓位。追踪触发点只有两个:新 K 线形成、或新仓单成交(依赖 OnTradeTransaction 事件)。 过滤链很实:先过交易品种 / magic,再过服务器 StopLevel、跟踪步长、前一止损位置、以及盈利点数启动阈值。四项全过才改止损。其中 StopLevel 若为 0 表示浮动值,通常等于 2~3 倍点差,需自行用点差乘系数补算,否则会报“无效止损”。 下面这段是 EA 头部定义与输入参数,SAR_DATA_INDEX=1 表示取第一根 K 线的 SAR 值作为初始跟踪位,时间框架默认用当前图表周期:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TrailingBySAR_01.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">#define SAR_DATA_INDEX class="num">1 class=class="str">"cmt">// bar we get Parabolic SAR data from class=class="str">"cmt">//--- class="kw">input parameters class="kw">input ENUM_TIMEFRAMES InpTimeframeSAR = PERIOD_CURRENT; class=class="str">"cmt">// Parabolic SAR Timeframe
「给 SAR 指标挂上句柄前的参数兜底」
在 MT5 的 EA 初始化阶段,先通过 input 把抛物线 SAR 的步长与最大值暴露给使用者:默认步长 0.02、最大值 0.2,这两个值直接决定点阵加速的快慢。 代码里用 ExtStepSAR 和 ExtMaximumSAR 做了一层下限保护——任何小于 0.0001 的输入都会被强行拉回 0.0001,避免极端参数让 iSAR 创建失败或计算失真。 随后调用 iSAR(Symbol(), InpTimeframeSAR, ExtStepSAR, ExtMaximumSAR) 拿指标句柄。若返回 INVALID_HANDLE,说明当前品种或周期不支持该调用,此时用 PrintFormat 把品种、周期、参数和错误码打印到日志,方便你直接在 MT5 终端排查,而不是盲目跑空策略。外汇与贵金属波动剧烈,SAR 参数错配可能放大误判,实盘前务必在策略测试器核对句柄有效性。
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=class="str">"cmt">//--- global variables 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 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 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">//--- } 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) { } 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 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());
把 K 线状态与 SAR 取值封装成可复用函数
指标初始化收尾若返回 INIT_FAILED,说明句柄或参数有问题,EA 不会跑;正常路径落 INIT_SUCCEEDED 即可。下面几个辅助函数把常用动作收口,避免在 OnCalculate 里反复写底层调用。 TimeframeDescription 只做一件事:把 PERIOD_CURRENT 这类枚举转成字符串并截掉前 7 个字符,例如 PERIOD_M15 变成 M15,日志和面板显示更干净。TimeOpenBar 用 CopyTime 取指定索引那根 K 线的开盘时间,失败返回 0,调用前 ResetLastError 是必须的,否则拿到的错误码可能过期。 新 K 线判断靠静态变量 time_prev 存上一根开盘时间,和当前 0 号 Bar 的开盘时间比对,不等就置新值并返回 true。这套写法在 1 分钟图上每秒调用也不会误触发,只在 Bar 边界翻转一次。 GetSARData 从 ExtHandleSAR 的 0 号缓冲区按索引取 1 个 SAR 值,拿不到就返回 EMPTY_VALUE,外层据此跳过信号计算。StopLevel 读 SYMBOL_SPREAD 并乘系数,给下单止损距离留缓冲——外汇和贵金属点差跳动大,这种缓冲在高波动时段可能直接决定订单是否被拒。
class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- successful class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return timeframe description | class=class="str">"cmt">//+------------------------------------------------------------------+ 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)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the bar opening time by timeseries index | class=class="str">"cmt">//+------------------------------------------------------------------+ 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return new bar opening flag | class=class="str">"cmt">//+------------------------------------------------------------------+ 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return Parabolic SAR data from the specified timeseries index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double GetSARData(class="kw">const class="type">int index) { class="type">class="kw">double array[class="num">1]; ResetLastError(); if(CopyBuffer(ExtHandleSAR, 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return StopLevel value of the current symbol in points | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int StopLevel(class="kw">const class="type">int spread_multiplier) { class="type">int spread =(class="type">int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD);