事后交易分析基础篇:在策略测试器中接入尾随停止与重设止损位(基础篇)
(1/3)· 很多交易者改了止损就以为稳了,却从没在测试器里验证尾随停止到底动了什么奶酪
本文是「事后交易分析」系列的第 1 部分,聚焦如何把真实账户的交易记录搬进策略测试器,并接上抛物线转向与移动平均线两类尾随停止。我们先把基础框架和类细化讲清楚,后面两篇再跑实测与对比。不少人在实盘里凭感觉挪止损,却从没在离线环境里看过尾随逻辑究竟怎么改写盈亏曲线。
在策略测试器里挑尾随停止
MT5 的策略测试器里可以直接事后验证不同尾随停止(Trailing Stop)逻辑对持仓的影响,不用先上实盘。打开测试器的「设置—尾随停止」下拉,能看到平台内置的固定点数、百分比、以及按 ATR 等多档选项,选完跑一遍历史数据就能看到净值曲线和回撤变化。 这套方法的价值在于把「止损位怎么跟」从拍脑袋变成可复现的实验。比如同一段 EURUSD 的 H1 历史,固定 30 点尾随和 ATR(14)×1.5 尾随,最大回撤可能差出 20% 以上,而胜率倾向此消彼长——外汇和贵金属杠杆高,这种差异直接决定账户生死,务必先在测试器跑透再谈参数。 文章作者 Artyom Trishkin 在 2025 年 11 月 10 日发布的示例里,用 656 次浏览、4 条讨论的篇幅,演示了从「选尾随类型」到「精炼止损位」的完整路径,核心就是先测试、再收窄。
「从改止损到接尾随:测试器里接着试」
前一篇里我们把真实账户成交搬进策略测试器,仅靠调整止损和止盈价位,就让原本实盘亏损的序列在回测里翻成与亏损规模相当的盈利。这说明同一套信号,换一组止损参数就可能扭转概率倾向,但外汇和贵金属杠杆高,实盘滑点会吃掉部分回测优势。 当时只动了静态止损,我想知道若把尾随停止接进去,曲线会怎么变。尾随停止在 MT5 里不改变开仓逻辑,只动态移动止损线,理论上能锁利也可能被震荡洗掉。 今天直接把几种不同尾随停止逻辑挂到这个 EA 上,在策略测试器跑同一批成交。你打开 MT5 测试器载入前篇 EA,就能照着加模块验证差异。
◍ 把SAR和均线改造成持仓尾随止损
抛物线转向(SAR)从设计上就是为趋势市服务的离场工具:看涨时它趴在价格下方,看跌时浮在价格上方,一旦价格反穿该线,指标翻转到另一侧并以前一周期极值重启。把它当尾随停止线用,逻辑很直接——多头持仓只要价格高于SAR就持住,SAR自身随价格上行而抬升,价格跌破即触发平仓,概率上能吃掉一段趋势但也可能在震荡里被反复扫损。外汇与贵金属杠杆高,这类跟踪止损遇横盘修正阶段磨损会放大,实盘前务必在MT5策略测试器跑品种历史。 移动平均线因滞后性同样适合做尾随线:多头时以低于价格的均线值为停止位,空头则反之。但原尾随类里 Run() 对每个品种都重跑一遍全持仓循环,品种一多就冗余。我们细化出按票据执行的新 Run() 方法,从主循环只传需要尾随的持仓单,避免重复遍历。 枚举尾随模式时常量从2起步而非0,是因为EA已占0(原始交易)、1(指定止损单交易),后续尾随类型顺延才不会撞值。CSymbolTradeExt 继承原品种交易类并挂入尾随类指针,通过 MqlParam 结构把指标周期等参数透传,不同尾随类型各取所需字段。 下面这段是无参 Run() 原码,内嵌了遍历所有持仓的循环,我们后续会抽掉它改调按票据的新方法:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Launch simple trailing with StopLoss offset from the price | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSimpleTrailing::Run(class="type">void) { class=class="str">"cmt">//--- if disabled, leave if(!this.m_active) class="kw">return false; class=class="str">"cmt">//--- trailing variables class="type">MqlTick tick = {}; class=class="str">"cmt">// price structure class="type">bool res = true; class=class="str">"cmt">// result of modification of all positions class=class="str">"cmt">//--- check the correctness of the data by symbol if(this.m_point==class="num">0) { class=class="str">"cmt">//--- let&class="macro">#x27;s try to get the data again ::ResetLastError(); this.SetSymbol(this.m_symbol); if(this.m_point==class="num">0) { ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError()); class="kw">return false; } } 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">ulong pos_ticket =::PositionGetTicket(i); if(pos_ticket == class="num">0) class="kw">continue;
按品种与魔术码过滤持仓再算止损
遍历持仓时先抓当前仓的品种名与魔术码,用 PositionGetString(POSITION_SYMBOL) 和 PositionGetInteger(POSITION_MAGIC) 取回。若实例里设了 m_magic != -1 且仓的魔术码对不上,或品种不等于 m_symbol,直接 continue 跳过——这步能避免 EA 去动别人的仓或别的品种。
跳过不匹配的仓后,用 SymbolInfoTick 抓最新报价,失败也跳。接着取仓类型、开仓价、原 SL:分别是 POSITION_TYPE、POSITION_PRICE_OPEN、POSITION_SL。
算出新止损 value_sl = GetStopLossValue(...),再丢进 CheckCriterion(...) 判断是否满足移动条件;满足就 ModifySL(pos_ticket, value_sl) 并累加进 res 返回标志。外汇与贵金属杠杆高,SL 改动若踩到 broker 的 StopLevel 限制会失败,实盘前应在 MT5 用 SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) 核对最小止损点数。
下面这段是过滤与取值的核心循环节选,逐行拆给你看逻辑落点。
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); }
「把品种精度塞进追踪止损类」
写一个可复用的追踪止损模块,第一件事是把当前品种的报价精度、点值、止损偏移这些参数先固化到成员变量里。下面这段类内声明和设置函数,就是干这个活的。 m_digits 存报价小数位,m_offset 是止损相对价格的固定距离(点),m_trail_start 是触发追踪所需盈利点数,m_trail_step 为每次跟进步长,m_spread_mlt 用来把点差放大后当作回撤容忍度,m_active 控制该实例是否生效。 SetSymbol() 里如果传入空串或 NULL,就退回当前图表品种;随后立刻用 SymbolInfoDouble 取 SYMBOL_POINT、用 SymbolInfoInteger 取 SYMBOL_DIGITS。开 MT5 按 F4 建个 EA 把这段抄进去,改 m_offset=50、m_trail_start=100,EURUSD 上跑回测,能看到 5 位报价下止损距离约 0.00050。外汇与贵金属杠杆高,参数错配可能瞬间放大回撤。
class="type">int m_digits; class=class="str">"cmt">// Symbol digits class="type">int m_offset; class=class="str">"cmt">// stop distance from price class="type">int m_trail_start; class=class="str">"cmt">// profit in points for launching trailing class="type">uint m_trail_step; class=class="str">"cmt">// trailing step class="type">uint m_spread_mlt; class=class="str">"cmt">// spread multiplier for returning StopLevel value class="type">bool m_active; class=class="str">"cmt">// trailing activity flag class=class="str">"cmt">//--- calculate and class="kw">return the StopLoss level of the selected position class="kw">virtual class="type">class="kw">double GetStopLossValue(class="type">ENUM_POSITION_TYPE pos_type, class="type">MqlTick &tick); class="kw">public: class=class="str">"cmt">//--- set trailing parameters class="type">void SetSymbol(class="kw">const class="type">class="kw">string symbol) { 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="type">void SetMagicNumber(class="kw">const class="type">long magic) { this.m_magic = magic; } class="type">void SetStopLossOffset(class="kw">const class="type">int offset) { this.m_offset = offset; } class="type">void SetTrailingStart(class="kw">const class="type">int start) { this.m_trail_start = start; } class="type">void SetTrailingStep(class="kw">const class="type">uint step) { this.m_trail_step = step; } class="type">void SetSpreadMultiplier(class="kw">const class="type">uint value) { this.m_spread_mlt = value; } class="type">void SetActive(class="kw">const class="type">bool flag) { this.m_active = flag; } class=class="str">"cmt">//--- class="kw">return trailing parameters
◍ 把移损参数暴露给外部调用
这个类把移损逻辑所需的全部状态做成了只读访问器,外部程序不必碰私有成员就能拿到当前设置。Symbol() 返回挂钩品种,MagicNumber() 给出订单识别码,StopLossOffset() 是止损相对价格的偏移点数,TrailingStart() 与 TrailingStep() 控制启动距离和跟进步长,SpreadMultiplier() 决定止损外扩几倍点差,IsActive() 标记开关是否开启。 默认构造里 m_spread_mlt 写死为 2,意味着止损默认放在现价外加 2 倍点差位置;m_magic 初值 -1 表示不绑定特定 EA 单。带参数的构造函数则要求调用方显式传入 symbol、magic、trailing_start、trailing_step、offset,否则对象处于未配置状态。 Run() 有两个重载:无参版对账户全部持仓跑移损,带 pos_ticket 版只处理指定单。函数开头先判 !m_active 或票据为 0 就直接退出,说明实盘里若没打开 m_active,再调 Run 也不会动止损。外汇与贵金属杠杆高,这类自动移损若 offset 设太小,遇跳空可能被秒扫,参数务必在 MT5 策略测试器里先跑一遍。
class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; } class="type">long MagicNumber(class="type">void) class="kw">const { class="kw">return this.m_magic; } class="type">int StopLossOffset(class="type">void) class="kw">const { class="kw">return this.m_offset; } class="type">int TrailingStart(class="type">void) class="kw">const { class="kw">return this.m_trail_start; } class="type">uint TrailingStep(class="type">void) class="kw">const { class="kw">return this.m_trail_step; } class="type">uint SpreadMultiplier(class="type">void) class="kw">const { class="kw">return this.m_spread_mlt; } class="type">bool IsActive(class="type">void) class="kw">const { class="kw">return this.m_active; } class=class="str">"cmt">//--- launch trailing with StopLoss offset from the price class="type">bool Run(class="type">void); class="type">bool Run(class="kw">const class="type">ulong pos_ticket); class=class="str">"cmt">//--- constructors CSimpleTrailing() : m_symbol(::Symbol()), m_point(::Point()), m_digits(::Digits()), m_magic(-class="num">1), m_trail_start(class="num">0), m_trail_step(class="num">0), m_offset(class="num">0), m_spread_mlt(class="num">2) {} CSimpleTrailing(class="kw">const class="type">class="kw">string symbol, class="kw">const class="type">long magic, class="kw">const class="type">int trailing_start, class="kw">const class="type">uint trailing_step, class="kw">const class="type">int offset); class=class="str">"cmt">//--- destructor ~CSimpleTrailing() {} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Launch simple trailing with StopLoss offset from the price | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSimpleTrailing::Run(class="kw">const class="type">ulong pos_ticket) { class=class="str">"cmt">//--- if trailing is disabled, or the ticket is invalid, we leave if(!this.m_active || pos_ticket==class="num">0)