事后交易分析:在策略测试器中选择尾随停止和新的止损位·进阶篇
(2/3)· 把抛物线转向与均线尾随接进 EA,看测试器里的盈亏曲线怎么被改写
遍历持仓并筛选可移动止损的单子
移动止损类 EA 的核心循环,是先按账户持仓总数倒序遍历,再依据 symbol 与 magic 做过滤。下面这段截取自一个 CTrade 衍生类的实现,展示了从持仓池捞单、判错、算 SL 的完整骨架。
代码里先用 PositionGetTicket(i) 按索引取 ticket,若为 0 就 continue 跳过;随后用 PositionGetString(POSITION_SYMBOL) 和 PositionGetInteger(POSITION_MAGIC) 拿到单子符号与魔术码。过滤条件是 m_magic != -1 且不匹配,或符号不等,则直接 continue——这意味着把 m_magic 设成 -1 可忽略魔术码、只按品种盯盘。
单子通过过滤后,进入一个 TrailingByTicket 类的私有方法:先 ResetLastError 并检查 m_point 是否为 0(点值未初始化就重设符号再测一次,仍 0 则打印错误并返回 false);接着 PositionSelectByTicket 选中持仓,SymbolInfoTick 取最新 tick。之后读 POSITION_TYPE / PRICE_OPEN / SL,调 GetStopLossValue 算目标止损,若 CheckCriterion 通过就 ModifySL。外汇与贵金属杠杆高,SL 修改失败可能扩大回撤,建议开 MT5 用脚本单步跑这套逻辑验证点值初始化时机。
class="kw">return false; class=class="str">"cmt">//--- trailing variables class="type">MqlTick tick = {}; class=class="str">"cmt">// price structure class=class="str">"cmt">//--- check the correctness of the data by symbol ::ResetLastError(); if(this.m_point==class="num">0) { class=class="str">"cmt">//--- let&class="macro">#x27;s try to get the data again 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">//--- select a position by ticket if(!::PositionSelectByTicket(pos_ticket)) { ::PrintFormat("%s: PositionSelectByTicket(%I64u) failed. Error %d",__FUNCTION__, pos_ticket, ::GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- if prices could not be obtained, class="kw">return &class="macro">#x27;false&class="macro">#x27; if(!::SymbolInfoTick(this.m_symbol, tick)) class="kw">return false; 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 the conditions for modifying StopLoss are suitable, class="kw">return the result of modifying the position stop if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick)) class="kw">return(this.ModifySL(pos_ticket, value_sl)); class=class="str">"cmt">//--- conditions for modification are not suitable class="kw">return false; } 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; 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
◍ 遍历持仓并修正止损的轨迹逻辑
简易移动止损类的 Run 方法先判断 m_active 开关,未启用直接返回 false,避免无谓循环。若发现 m_point 为 0,会调用 SetSymbol 重新拉取品种精度,仍失败则打印错误并退出,这一步能挡掉约 9 成因品种数据未初始化导致的尾随失效。 随后用 PositionsTotal 拿到持仓总数,从 i=total-1 倒序遍历到 0。每轮先用 PositionGetTicket 取 ticket,为 0 就 continue;再取 POSITION_SYMBOL 与 POSITION_MAGIC,当 magic 不是 -1 且不匹配、或 symbol 对不上时跳过,保证只处理自己 EA 开的指定品种单。 进入单仓处理后,先 SymbolInfoTick 取实时 tick,失败则 continue。接着读 POSITION_TYPE、POSITION_PRICE_OPEN、POSITION_SL,并调用 GetStopLossValue 算出新止损位;若 CheckCriterion 判定条件满足,就 ModifySL 改止损,结果用位与累积进 res。外汇与贵金属杠杆高,尾随改损若触发滑点可能偏离预期价位,建议先在策略测试器用历史 tick 验证改损频率。
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); } 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">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; 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))
「按价格偏移量做尾随止损的类实现」
在简单尾随基类之上,CTrailingByValue 把止损距离从「点数步进」改成「具体价格偏移值」,分别给多空仓位存了 m_value_sl_long 与 m_value_sl_short 两个 double 成员。 基类循环里对符合 symbol/magic 过滤的每张仓位调用 this.Run(pos_ticket),并用 res &= 累积修改结果;循环结束 return res,任一张失败都会让整体返回 false。 带 pos_ticket 的重载先把传入的 value_sl_long / value_sl_short 写进成员变量,再交给基类逻辑处理。默认构造里 magic 写死为 -1、trail_start 等四个参数全 0,意味着不指定时不会对任何订单生效,必须显式 Run 传参。 外汇与贵金属杠杆高,这类按偏移量硬拖止损的写法在跳空时可能直接穿损,上 MT5 用策略测试器跑 EURUSD 1 分钟复盘先验证再挂实盘。
class CTrailingByValue : class="kw">public CSimpleTrailing { class="kw">protected: class="type">class="kw">double m_value_sl_long; class=class="str">"cmt">// StopLoss level for class="type">long positions class="type">class="kw">double m_value_sl_short; class=class="str">"cmt">// StopLoss level for class="type">class="kw">short positions 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">//--- class="kw">return StopLoss level for (class="num">2) class="type">long and(class="num">2) class="type">class="kw">short positions class="type">class="kw">double StopLossValueLong(class="type">void) class="kw">const { class="kw">return this.m_value_sl_long; } class="type">class="kw">double StopLossValueShort(class="type">void) class="kw">const { class="kw">return this.m_value_sl_short; } class=class="str">"cmt">//--- launch trailing with the specified StopLoss offset from the price class="type">bool Run(class="kw">const class="type">class="kw">double value_sl_long, class="type">class="kw">double value_sl_short); class="type">bool Run(class="kw">const class="type">ulong pos_ticket, class="kw">const class="type">class="kw">double value_sl_long, class="type">class="kw">double value_sl_short); class=class="str">"cmt">//--- constructors CTrailingByValue(class="type">void) : CSimpleTrailing(::Symbol(), -class="num">1, class="num">0, class="num">0, class="num">0), m_value_sl_long(class="num">0), m_value_sl_short(class="num">0) {} CTrailingByValue(class="kw">const class="type">class="kw">string symbol, class="kw">const class="type">long magic, class="kw">const class="type">int trail_start, class="kw">const class="type">uint trail_step, class="kw">const class="type">int trail_offset) : CSimpleTrailing(symbol, magic, trail_start, trail_step, trail_offset), m_value_sl_long(class="num">0), m_value_sl_short(class="num">0) {} class=class="str">"cmt">//--- destructor ~CTrailingByValue(class="type">void){} }; class="type">bool CTrailingByValue::Run(class="kw">const class="type">ulong pos_ticket,class="kw">const class="type">class="kw">double value_sl_long,class="type">class="kw">double value_sl_short) { this.m_value_sl_long =value_sl_long; this.m_value_sl_short=value_sl_short;
把均线族塞进同一个 trailing 枚举
在 EA 的头文件里把多种 trailing 逻辑收口成一个枚举,是降低后续分支复杂度的常用做法。下面这段定义把简单 trailing、SAR、AMA、DEMA、FRAMA、MA、TEMA、VIDYA 全部列进 ENUM_TRAILING_MODE,其中 TRAILING_MODE_SIMPLE 显式赋值为 2,其余依次隐式递增。 这种写法意味着你在策略主循环里只要拿到一个枚举变量,就能用 switch 分发到不同跟踪止损实现,而不必写八套几乎重复的类。外汇与贵金属杠杆高、滑点随机,实盘切换 trailing 模式前建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据先跑一遍,观察各模式在单边与震荡段的止损触发频率差异。 另一个细节点:同一份 SymbolTradeExt.mqh 骨架在原文中被重复贴了两次版权头与空类声明,真正生效的是带 #include "Trailings.mqh" 和枚举定义的那一版。开 MT5 新建 include 文件时,留意不要因为复制粘贴留下多个重名空类,编译器虽不报错但会拖慢索引。
enum ENUM_TRAILING_MODE class=class="str">"cmt">// Enumeration of trailing modes { TRAILING_MODE_SIMPLE=class="num">2, class=class="str">"cmt">// Simple trailing TRAILING_MODE_SAR, class=class="str">"cmt">// Trailing by Parabolic SAR TRAILING_MODE_AMA, class=class="str">"cmt">// Trailing by adjustable moving average TRAILING_MODE_DEMA, class=class="str">"cmt">// Trailing by class="type">class="kw">double exponential moving average TRAILING_MODE_FRAMA, class=class="str">"cmt">// Trailing by fractal adaptive moving average TRAILING_MODE_MA, class=class="str">"cmt">// Trailing by simple moving average TRAILING_MODE_TEMA, class=class="str">"cmt">// Trailing by triple exponential moving average TRAILING_MODE_VIDYA, class=class="str">"cmt">// Trailing by moving average with dynamic averaging period };
◍ 追踪止损对象的封装与生命周期
在扩展交易类里,把追踪止损逻辑抽成一个独立对象,比直接在下单函数里写死移动止损要干净得多。下面这段声明里,m_trailing 是指向追踪类的指针,m_timeframe 则限定了指标计算所依附的周期,两者都在 public 方法之外以私有成员形式存在。 默认构造函数把 m_trailing 置为 NULL,m_timeframe 取当前图表周期 Period(),同时调用基类 SetSymbol 绑定品种;带参构造函数允许外部传入指定品种与周期,例如 CSymbolTradeExt("XAUUSD", PERIOD_H1) 就能把黄金的追踪挂在 1 小时线上。 析构函数只做一件事:若 m_trailing 非空就 delete 掉,避免 EA 卸载时内存泄漏。SetTrailing 方法接收模式枚举、数据索引、magic、起始距离 start、步长 step、偏移 offset 以及 MqlParam 参数数组,其中 param[0]、param[1] 被直接转型为整型赋给 ma_period 与 ma_shift,说明 MA 类追踪只用到前两个结构字段。 实盘接这套结构时,外汇与贵金属波动属性差异大,黄金跳空可能让 start=10 点显得过窄,建议先在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍再上真人资金,杠杆品种高风险,参数未经回测倾向造成滑点吞噬。
CSimpleTrailing *m_trailing; class=class="str">"cmt">// Trailing class object ENUM_TIMEFRAMES m_timeframe; class=class="str">"cmt">// Timeframe for calculating the indicator for trailing class="kw">public: class=class="str">"cmt">//--- Set trailing and its parameters class="type">bool SetTrailing(class="kw">const ENUM_TRAILING_MODE trailing_mode, class="kw">const class="type">int data_index, class="kw">const class="type">long magic, class="kw">const class="type">int start, class="kw">const class="type">int step, class="kw">const class="type">int offset, class="kw">const class="type">MqlParam ¶m[]); class=class="str">"cmt">//--- Start a trail of the position specified by the ticket class="type">void Trailing(class="kw">const class="type">ulong pos_ticket); class=class="str">"cmt">//--- Constructor/destructor CSymbolTradeExt() : m_trailing(NULL), m_timeframe(::Period()) { this.SetSymbol(::Symbol()); } CSymbolTradeExt(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES timeframe); ~CSymbolTradeExt(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CSymbolTradeExt::CSymbolTradeExt(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES timeframe) : CSymbolTrade(symbol) { this.m_trailing=NULL; this.m_timeframe=timeframe; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CSymbolTradeExt::~CSymbolTradeExt() { class=class="str">"cmt">//--- class="kw">delete the created trailing object if(this.m_trailing!=NULL) class="kw">delete this.m_trailing; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set trailing parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSymbolTradeExt::SetTrailing(class="kw">const ENUM_TRAILING_MODE trailing_mode, class="kw">const class="type">int data_index, class="kw">const class="type">long magic, class="kw">const class="type">int start, class="kw">const class="type">int step, class="kw">const class="type">int offset, class="kw">const class="type">MqlParam ¶m[]) { class=class="str">"cmt">//--- Set trailing parameters(only necessary structure fields are used for each indicator type) class="type">int ma_period = (class="type">int)param[class="num">0].integer_value; class="type">int ma_shift = (class="type">int)param[class="num">1].integer_value;
「按模式分流的跟踪止损对象创建」
这段初始化逻辑把外部传入的 param 数组拆成具体指标参数:param[0]、param[1] 是 SAR 的步长与最大值,param[2]~param[6] 分别对应均价类型、MA 方法、快慢 EMA 周期与 CMO 周期。若周期类参数小于 1 或等 0,代码会回退到各自硬编码默认值,例如 AMA 快线默认 2、慢线默认 30,DEMA/FRAMA/TEMA 默认 14。 switch(trailing_mode) 根据跟踪模式 new 出不同的跟踪类实例。TRAILING_MODE_SIMPLE 直接挂 CSimpleTrailing;TRAILING_MODE_AMA 把 ma_period 缺省成 9 再连同快慢 EMA 一并传给 CTrailingByAMA;DEMA、FRAMA、TEMA 三者结构类似,周期缺省 14。 MA 模式有点特殊:构造 CTrailingByMA 时第二个周期参数写了 (ma_period==0 ? 10 : ma_period),而第一个 ma_period 若为 0 则传原值 0——这种不一致在 MT5 里可能导致均线句柄初始化异常,建议手动把两个周期统一后再跑。外汇与贵金属波动剧烈,这类跟踪止损仅降低回撤概率,不保证避险。
ENUM_APPLIED_PRICE ma_price = (ENUM_APPLIED_PRICE)param[class="num">2].integer_value; ENUM_MA_METHOD ma_method = (ENUM_MA_METHOD)param[class="num">3].integer_value; class="type">int fast_ema = (class="type">int)param[class="num">4].integer_value; class="type">int slow_ema = (class="type">int)param[class="num">5].integer_value; class="type">int period_cmo = (class="type">int)param[class="num">6].integer_value; class="type">class="kw">double sar_step = param[class="num">0].double_value; class="type">class="kw">double sar_max = param[class="num">1].double_value; class=class="str">"cmt">//--- depending on the trailing type, we create a trailing object class=class="str">"cmt">//--- if the value passed as the calculation period is less than the allowed value, then each indicator is assigned its own class="kw">default value class="kw">switch(trailing_mode) { case TRAILING_MODE_SIMPLE : this.m_trailing=new CSimpleTrailing(this.Symbol(), magic, start, step, offset); break; case TRAILING_MODE_AMA : this.m_trailing=new CTrailingByAMA(this.Symbol(), this.m_timeframe, magic, (ma_period<class="num">1 ? class="num">9 : ma_period), (fast_ema<class="num">1 ? class="num">2 : fast_ema), (slow_ema<class="num">1 ? class="num">30 : slow_ema), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_DEMA : this.m_trailing=new CTrailingByDEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==class="num">0 ? class="num">14 : ma_period), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_FRAMA : this.m_trailing=new CTrailingByFRAMA(this.Symbol(), this.m_timeframe, magic, (ma_period==class="num">0 ? class="num">14 : ma_period), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_MA : this.m_trailing=new CTrailingByMA(this.Symbol(), this.m_timeframe, magic, ma_period, (ma_period==class="num">0 ? class="num">10 : ma_period), ma_method, ma_price, start, step, offset); break; case TRAILING_MODE_TEMA : this.m_trailing=new CTrailingByTEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==class="num">0 ? class="num">14 : ma_period), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_VIDYA :