重构经典策略(第十三部分):最小化均线交叉的滞后性·进阶篇
📘

重构经典策略(第十三部分):最小化均线交叉的滞后性·进阶篇

第 2/3 篇

◍ 双周期均线与ATR的入场触发逻辑

这段逻辑用两个时间框架协同:TF_2 负责判定K线是否换新(用 iTime 比对静态时间戳),TF_1 取当根K线的开高低收与买卖价。只有当前时间戳变化时才重算指标缓冲与行情数据,避免每 tick 无谓刷新,回测中这类写法能把 CPU 占用压到常规逐 tick 写法的约三成。 核心入场在 find_position():快线 ma_s[0] 上穿慢线 ma_f[0] 则市价买,下穿则市价卖,VOL 为手数、bid/ask 取实时报价、SL/TP 暂置 0。注意这里没有过滤,纯均线交叉在 EURUSD 15M 这类品种上假突破概率偏高,实盘前建议加 ATR 阈值过滤。 持仓管理从 manage_position() 起步:先 PositionSelect 锁定当前符号持仓,读出初始 SL/TP,再把 atr_handler 近 90 根缓冲拷进 vector 算均值。buy_sl 的算法是 ask 减去(ATR_MULTIPLE 倍即时 ATR 再加 90 根 ATR 均值),相当于用「动态波幅 + 历史平均波幅」双保险兜底止损。外汇与贵金属杠杆高,这类跟踪止损若 ATR_MULTIPLE 设得过小,遇数据行情可能被瞬间扫损。 下面把更新与触发段原文贴出,方便你直接塞进 MT5 策略测试器改参数验证。

MQL5 / C++
class=class="str">"cmt">//--- Update the system
  class="kw">static class="type">class="kw">datetime time_stamp;
  class="type">class="kw">datetime current_time = iTime(Symbol(),TF_2,class="num">0);
  if(current_time != time_stamp)
    {
      time_stamp = current_time;
      CopyBuffer(atr_handler,class="num">0,class="num">0,class="num">1,atr);
      CopyBuffer(ma_s_handler,class="num">0,class="num">0,class="num">1,ma_s);
      CopyBuffer(ma_f_handler,class="num">0,class="num">0,class="num">1,ma_f);
      o  = iOpen(Symbol(),TF_1,class="num">0);
      h  = iHigh(Symbol(),TF_1,class="num">0);
      l  = iLow(Symbol(),TF_1,class="num">0);
      c  = iClose(Symbol(),TF_1,class="num">0);
      bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
      ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      if(PositionsTotal() == class="num">0)
        find_position();
      if(PositionsTotal() > class="num">0)
        manage_position();
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find a position                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void find_position(class="type">void)
  {
   if((ma_s[class="num">0] > ma_f[class="num">0]))
     {
      Trade.Sell(VOL,Symbol(),bid,class="num">0,class="num">0,"");
      trade = -class="num">1;
     }
   if((ma_s[class="num">0] < ma_f[class="num">0]))
     {
      Trade.Buy(VOL,Symbol(),ask,class="num">0,class="num">0,"");
      trade = class="num">1;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Manage our positions                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void manage_position(class="type">void)
  {
class=class="str">"cmt">//--- Select the position
   if(PositionSelect(Symbol()))
     {
      class=class="str">"cmt">//--- Get ready to update the SL/TP
      class="type">class="kw">double initial_sl   = PositionGetDouble(POSITION_SL);
      class="type">class="kw">double initial_tp   = PositionGetDouble(POSITION_TP);
      class=class="str">"cmt">//--- Calculate the average ATR move
      vector atr_mean;
      atr_mean.CopyIndicatorBuffer(atr_handler,class="num">0,class="num">0,class="num">90);
      class="type">class="kw">double buy_sl       = (ask - ((ATR_MULTIPLE * atr[class="num">0]) + atr_mean.Mean()));

用ATR均值重算止损止盈的改仓逻辑

这段改仓代码的核心,是把止损止盈锚定在 ATR 倍数与 ATR 均值之和上,而不是固定点数。买方向的止损取 bid 加上(ATR_MULTIPLE 倍当前 ATR 再加 atr_mean.Mean()),卖方向则反过来用 bid 减;止盈只取 0.5 倍 ATR 加均值,意味着盈利目标比止损空间更收敛。 double sell_sl = (bid + ((ATR_MULTIPLE * atr[0]) + atr_mean.Mean())); double buy_tp = (ask + ((ATR_MULTIPLE * 0.5 * atr[0]) + atr_mean.Mean())); double sell_tp = (bid - ((ATR_MULTIPLE * 0.5 * atr[0]) + atr_mean.Mean())); double new_sl = ((trade == 1) && (initial_sl < buy_sl)) ? (buy_sl) : ((trade == -1) && (initial_sl > sell_sl)) ? (sell_sl) : (initial_sl); double new_tp = ((trade == 1) && (initial_tp < buy_tp)) ? (buy_tp) : ((trade == -1) && (initial_tp > sell_tp)) ? (sell_tp) : (initial_tp); 第1行:卖单止损价 = 卖价 +(ATR倍数×当前ATR + ATR均值),用于空单的止损外扩。 第2行:买单止盈价 = 买价 +(ATR倍数×0.5×当前ATR + ATR均值),盈利目标用半倍ATR。 第3行:卖单止盈价 = 卖价 -(ATR倍数×0.5×当前ATR + ATR均值),与买单调对称。 第4行:新止损判定,多单且原止损小于买损则上提为buy_sl,空单且原止损大于卖损则下移至sell_sl,否则不动。 第5行:新止盈同理,只在更优一侧覆盖原TP。 当 initial_sl 与 initial_tp 都是 0,即新开仓未设防护时,代码直接按 trade 方向用 buy_sl/sell_sl 配合对应 TP 调用 PositionModify 写入。若两端都不为 0,则走 else 分支用 new_sl/new_tp 做调仓更新。 在 MT5 里把 ATR_MULTIPLE 从 1.0 调到 1.5,止损距离会明显拉宽,回测 EURUSD 日线可能看到被扫概率下降但回撤变大,外汇与贵金属属高风险品种,参数改动前先用策略测试器跑一遍。

MQL5 / C++
class="type">class="kw">double sell_sl      = (bid + ((ATR_MULTIPLE * atr[class="num">0]) + atr_mean.Mean()));
class="type">class="kw">double buy_tp       = (ask + ((ATR_MULTIPLE * class="num">0.5 * atr[class="num">0]) + atr_mean.Mean()));
class="type">class="kw">double sell_tp      = (bid - ((ATR_MULTIPLE * class="num">0.5 * atr[class="num">0]) + atr_mean.Mean()));
class="type">class="kw">double new_sl       = ((trade == class="num">1) && (initial_sl <  buy_sl)) ? (buy_sl) : ((trade == -class="num">1) && (initial_sl > sell_sl)) ? (sell_sl) : (initial_sl);
class="type">class="kw">double new_tp       = ((trade == class="num">1) && (initial_tp <  buy_tp)) ? (buy_tp) : ((trade == -class="num">1) && (initial_tp > sell_tp)) ? (sell_tp) : (initial_tp);
if(initial_sl == class="num">0 && initial_tp == class="num">0)
  {
   if(trade == class="num">1)
     {
      original_sl = buy_sl;
      Trade.PositionModify(Symbol(),buy_sl,buy_tp);
     }
   if(trade == -class="num">1)
     {
      original_sl = sell_sl;
      Trade.PositionModify(Symbol(),sell_sl,sell_tp);
     }
  }
class=class="str">"cmt">//--- Update the position
else
   if((initial_sl * initial_tp) != class="num">0)
     {
      Trade.PositionModify(Symbol(),new_sl,new_tp);
     }

「把双均线系统的骨架搭起来」

做跨周期双均线策略,第一步是把时间框架和风控参数钉死。下面这段预处理宏定义了下层周期用 H1,ATR 取 14 周期、止损宽度为 ATR 的 3 倍,单笔手数锁在 0.01——外汇和贵金属波动剧烈,小仓试错是高风险的必要前提,别一上来就放大体积。 全局变量里挂了两条均线句柄、一个 ATR 句柄,以及 bid/ask 和 OHLC 缓存数组。CTrade 对象直接引自标准库 Trade.mqh,省掉自己写下单逻辑的麻烦。 初始化只调一个 setup(),退出时 release() 释放指标句柄防止内存泄漏;OnTick 里目前只跑 update() 刷新行情变量,真正的信号判断留到后面函数补。你可以直接把这段抄进 MT5 的 EA 模板,编译后先确认句柄不为空再往下写。 别把宏当摆设 参数写死在 #define 里,回测时改 ATR_MULTIPLE 从 3 调到 2,止损会明显变窄、被扫概率可能上升,动手改一次比看十遍直观。

MQL5 / C++
class="macro">#define TF_2                PERIOD_H1 class=class="str">"cmt">//--- Our lower time frame
class="macro">#define ATR_PERIOD      class="num">14          class=class="str">"cmt">//--- The period for our ATR
class="macro">#define ATR_MULTIPLE    class="num">3           class=class="str">"cmt">//--- How wide should our stops be?
class="macro">#define VOL              class="num">0.01       class=class="str">"cmt">//--- Trading volume
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int    trade;
class="type">int    ma_f_handler,ma_s_handler,atr_handler;
class="type">class="kw">double ma_f[],ma_s[],atr[];
class="type">class="kw">double bid,ask;
class="type">class="kw">double o,h,l,c;
class="type">class="kw">double original_sl;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Setup
   setup();
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- Release our indicators
   release();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Update system variables
   update();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom functions                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Setup our system                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void setup(class="type">void)
  {
   atr_handler  = iATR(Symbol(),TF_1,ATR_PERIOD);

◍ 指标句柄释放与跨周期刷新逻辑

双均线句柄在初始化时按 TF_1 周期挂上:快线为 10 周期 EMA 收盘价,慢线为 60 周期 EMA 收盘价。若 EA 退出或重载,release() 里用 IndicatorRelease() 依次释放 atr、ma_f、ma_s 三个句柄,避免 MT5 终端里指标句柄泄漏导致的资源占用。 update() 用 TF_2 的 K 线时间戳做触发闸门:仅当 iTime(Symbol(),TF_2,0) 变化时才重拷缓冲区。此时 CopyBuffer 各取 1 根最新值进 atr / ma_s / ma_f 数组,并同步刷入当前 TF_1 的 OHL C 与买卖价。 无持仓时调 find_position(),规则极简:慢线在快线之上则市价 Sell、trade 标 -1;慢线在快线之下则市价 Buy、trade 标 1。外汇与贵金属杠杆高,这种裸趋势交叉在震荡市可能连续止损,上 MT5 用策略测试器跑 EURUSD 的 H1 便能直接看权益曲线。

MQL5 / C++
  ma_f_handler = iMA(Symbol(),TF_1,class="num">10,class="num">0,MODE_EMA,PRICE_CLOSE);
  ma_s_handler = iMA(Symbol(),TF_1,class="num">60,class="num">0,MODE_EMA,PRICE_CLOSE);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Release variables we do not need                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void release(class="type">void)
  {
   IndicatorRelease(atr_handler);
   IndicatorRelease(ma_f_handler);
   IndicatorRelease(ma_s_handler);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update system variables                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update(class="type">void)
  {
class=class="str">"cmt">//--- Update the system
   class="kw">static class="type">class="kw">datetime time_stamp;
   class="type">class="kw">datetime current_time = iTime(Symbol(),TF_2,class="num">0);
   if(current_time != time_stamp)
     {
      time_stamp = current_time;
      CopyBuffer(atr_handler,class="num">0,class="num">0,class="num">1,atr);
      CopyBuffer(ma_s_handler,class="num">0,class="num">0,class="num">1,ma_s);
      CopyBuffer(ma_f_handler,class="num">0,class="num">0,class="num">1,ma_f);
      o   = iOpen(Symbol(),TF_1,class="num">0);
      h   = iHigh(Symbol(),TF_1,class="num">0);
      l   = iLow(Symbol(),TF_1,class="num">0);
      c   = iClose(Symbol(),TF_1,class="num">0);
      bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
      ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      if(PositionsTotal() == class="num">0)
         find_position();
      if(PositionsTotal() > class="num">0)
         manage_position();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find a position                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void find_position(class="type">void)
  {
   if((ma_s[class="num">0] > ma_f[class="num">0]))
     {
      Trade.Sell(VOL,Symbol(),bid,class="num">0,class="num">0,"");
      trade = -class="num">1;
     }
   if((ma_s[class="num">0] < ma_f[class="num">0]))
     {
      Trade.Buy(VOL,Symbol(),ask,class="num">0,class="num">0,"");
      trade = class="num">1;
     }
  }

常见问题

当快线慢线刚交叉且当前ATR高于近20根均值时,才视为有效触发,避免窄幅震荡假信号。
以ATR均值乘1.5作止损距离、乘3作止盈距离,价格每越过一个ATR均值单位就重算一次仓位边界。
小布可加载对应品种页,实时标出均线交叉且ATR超均值的节点,并推送改仓提醒,省去手动刷图。
会造成内存泄漏与跨周期刷新卡顿,建议在Init退出和周期切换时显式释放指标句柄。
多数是句柄未重绑或刷新频率过低所致,并非均线计算慢,需检查OnTimer间隔与重算触发条件。