构建K线图趋势约束模型(第一部分):针对EA和技术指标·进阶篇
📊

构建K线图趋势约束模型(第一部分):针对EA和技术指标·进阶篇

(2/3)· 当M1里的假突破淹没你的均线交叉,D1的阴阳才是那道该立的篱笆

案例拆解新手友好 第 2/3 篇
很多交易者把均线交叉EA直接跑在M1上,结果被微观波动里的假信号反复扫损。其实只要在更高周期K线方向上加一道约束,信号质量会明显不同。本篇接着基础概念,把这道栅栏用MQL5代码立起来。

「双均线交叉落点的缓冲区填充逻辑」

这段处理的核心是:先确认两条均线句柄都已算出足够柱数,再把它们的缓冲区和当前品种的高低价数组按时间倒序排好,避免后面索引错位。 BarsCalculated 返回小于等于 0 就直接 return(0),CopyBuffer / CopyLow / CopyHigh 失败则回退到 rates_total,这是防止指标在历史数据不足时崩掉的基础防线。 主循环从 limit-1 往下跑到 0,并用 MathMin(PLOT_MAXIMUM_BARS_BACK-1, rates_total-1-OMIT_OLDEST_BARS) 跳过最老的一段 K 线——实测若 OMIT_OLDEST_BARS 设得过小,在 EURUSD 的 M1 上加载容易触发 Array out of range。 交叉判定很直白:当 MA[i] 上穿 MA2[i](且前一柱是下穿状态),把 Buffer1[i] 标在当根 K 线最低价;反之下穿则把 Buffer2[i] 标在最高价。没触发时写 EMPTY_VALUE,MT5 上就不会画出散点。 把这段代码贴进 MetaEditor 编译后,你能在副图看到均线金叉/死叉对应的高低点标记,外汇与贵金属波动大,此类信号仅作概率参考,实盘请自担高风险。

MQL5 / C++
   ArrayInitialize(Buffer2, EMPTY_VALUE);
   }
  else
   limit++;

  if(BarsCalculated(MA_handle) <= class="num">0)
   class="kw">return(class="num">0);
  if(CopyBuffer(MA_handle, class="num">0, class="num">0, rates_total, MA) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(MA, true);
  if(BarsCalculated(MA_handle2) <= class="num">0)
   class="kw">return(class="num">0);
  if(CopyBuffer(MA_handle2, class="num">0, class="num">0, rates_total, MA2) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(MA2, true);
  if(CopyLow(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, Low) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(Low, true);
  if(CopyHigh(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, High) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(High, true);
  class=class="str">"cmt">//--- main loop
  for(class="type">int i = limit-class="num">1; i >= class="num">0; i--)
   {
    if (i >= MathMin(PLOT_MAXIMUM_BARS_BACK-class="num">1, rates_total-class="num">1-OMIT_OLDEST_BARS)) class="kw">continue; class=class="str">"cmt">//omit some old rates to prevent "Array out of range" or slow calculation  
    
    class=class="str">"cmt">//Indicator Buffer class="num">1
    if(MA[i] > MA2[i]
    && MA[i+class="num">1] < MA2[i+class="num">1] class=class="str">"cmt">//Moving Average crosses above Moving Average
    )
     {
     Buffer1[i] = Low[i]; class=class="str">"cmt">//Set indicator value at Candlestick Low
     }
    else
     {
     Buffer1[i] = EMPTY_VALUE;
     }
    class=class="str">"cmt">//Indicator Buffer class="num">2
    if(MA[i] < MA2[i]
    && MA[i+class="num">1] > MA2[i+class="num">1] class=class="str">"cmt">//Moving Average crosses below Moving Average
    )
     {
     Buffer2[i] = High[i]; class=class="str">"cmt">//Set indicator value at Candlestick High
     }
    else
     {
     Buffer2[i] = EMPTY_VALUE;
     }
   }
  class="kw">return(rates_total);
  }
class=class="str">"cmt">//copy the code to meta editor to compile it

◍ 用日线当筛子过滤逆势信号

把高时间框架趋势当成筛子,逻辑和谷物分离一样:只放行与日线方向一致的信号,逆势噪音直接漏掉。具体落地时,用前一根 M1 收盘价去比前一根 D1 开盘价(D1 开盘近似前一天收盘),M1 收 ≥ D1 开判多头,≤ 则判空头;日线多头只给买信号,空头只给卖信号。 选 M1 收盘价而不是买价卖价或当日收盘,是因为按这套箭头风格,后者在图表上画不出所需提示。贴着 D1 开与低周期收的关系走,指标箭头和策略意图才能对齐,决策链路更短。 回测片段显示约束接入后信号分布明显收敛:总信号 28 个,卖出 27、买入 1,其中虚假逆势 3 个、正确 25 个。和未加约束的表比,真信号留住了,假信号被削掉,EA 实盘过滤更干净。外汇与贵金属波动剧烈,高杠杆下仍可能连续止损,须自测周期匹配度。 下面这段是 D1 多头条件的核心判断,顺带看缓冲赋值怎么画箭头: if(Close[1+barshift_M1[i]] >= Open[1+barshift_D1[i]]) // M1前根收 >= D1前根开 { Buffer1[i] = Low[i]; // 多头缓冲写在K线最低价处 } else { Buffer1[i] = EMPTY_VALUE; // 不满足就留空不画 } 把这段反过来写便是空头分支,再和均线交叉句柄 MA_handle / MA_handle2 串起来,约束就无缝嵌进指标了。

MQL5 / C++
if(Close[class="num">1+barshift_M1[i]] >= Open[class="num">1+barshift_D1[i]] class=class="str">"cmt">//Candlestick Close >= Candlestick Open
      )
        {
         Buffer1[i] = Low[i]; class=class="str">"cmt">//Set indicator value at Candlestick Low
        }
      else
        {
         Buffer1[i] = EMPTY_VALUE;
        }

双箭头指标初始化与均线句柄绑定

在 MT5 自定义指标里,OnInit 决定两件事:画图缓冲怎么挂,以及外部指标句柄怎么拿。下面这段把 Buffer1、Buffer2 分别绑到索引 0 和 1,并用 PlotIndexSetInteger 把箭头符号设成 241 和 242——也就是向上 / 向下小三角,肉眼区分多空信号比纯线形更直接。

MQL5 / C++
class=class="str">"cmt">// Custom indicator initialization function
class="type">int OnInit()
  {
   SetIndexBuffer(class="num">0, Buffer1);
   PlotIndexSetDouble(class="num">0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(class="num">0, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1));
   PlotIndexSetInteger(class="num">0, PLOT_ARROW, class="num">241);
   SetIndexBuffer(class="num">1, Buffer2);
   PlotIndexSetDouble(class="num">1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(class="num">1, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1));
   PlotIndexSetInteger(class="num">1, PLOT_ARROW, class="num">242);
   class=class="str">"cmt">//initialize myPoint
   myPoint = Point();
   if(Digits() == class="num">5 || Digits() == class="num">3)
     {
      myPoint *= class="num">10;
     }
   MA_handle = iMA(NULL, PERIOD_CURRENT, class="num">7, class="num">0, MODE_EMA, PRICE_CLOSE);
   if(MA_handle < class="num">0)
     {
      Print("The creation of iMA has failed: MA_handle=", INVALID_HANDLE);
      Print("Runtime error = ", GetLastError());
      class="kw">return(INIT_FAILED);
     }
   MA_handle2 = iMA(NULL, PERIOD_CURRENT, class="num">21, class="num">0, MODE_EMA, PRICE_CLOSE);
   if(MA_handle2 < class="num">0)
     {
      Print("The creation of iMA has failed: MA_handle2=", INVALID_HANDLE);
      Print("Runtime error = ", GetLastError());
      class="kw">return(INIT_FAILED);
     }
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">// Custom indicator iteration function
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime& time[],
                class="kw">const class="type">class="kw">double& open[],
                class="kw">const class="type">class="kw">double& high[],
                class="kw">const class="type">class="kw">double& low[],
                class="kw">const class="type">class="kw">double& close[],
逐行拆一下关键点:SetIndexBuffer 把数组交给图表引擎;PLOT_EMPTY_VALUE 设成 EMPTY_VALUE 让无信号处不画;PLOT_DRAW_BEGIN 用 MathMax 卡住历史深度,避免老数据刷屏。myPoint 那段处理 5 位 / 3 位报价的小点倍数,EURUSD 这类 5 位数对报价要乘 10 才等价 4 位的 1 点。 均线句柄是信号核心:MA_handle 取 7 周期 EMA 收盘,MA_handle2 取 21 周期 EMA 收盘,快线慢线交叉构成后续箭头触发条件。若 iMA 返回负值,直接 Print 错误码并 INIT_FAILED,指标加载失败能在日志看到具体 runtime error。外汇与贵金属波动剧烈,这类双均线箭头仅作概率参考,实盘前务必在策略测试器跑历史数据验证。 OnCalculate 的参数列表只截到 close[] 末尾,真正的循环计算在原文后续节。你可以现在开 MT5 新建指标,把上面 OnInit 粘进去,编译后若箭头不显示,先查 PLOT_ARROW 的 241/242 是否被终端字体支持。

MQL5 / C++
class="type">int OnInit()
  {
   SetIndexBuffer(class="num">0, Buffer1);
   PlotIndexSetDouble(class="num">0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(class="num">0, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1));
   PlotIndexSetInteger(class="num">0, PLOT_ARROW, class="num">241);
   SetIndexBuffer(class="num">1, Buffer2);
   PlotIndexSetDouble(class="num">1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetInteger(class="num">1, PLOT_DRAW_BEGIN, MathMax(Bars(Symbol(), PERIOD_CURRENT)-PLOT_MAXIMUM_BARS_BACK+class="num">1, OMIT_OLDEST_BARS+class="num">1));
   PlotIndexSetInteger(class="num">1, PLOT_ARROW, class="num">242);
   myPoint = Point();
   if(Digits() == class="num">5 || Digits() == class="num">3)
     {
      myPoint *= class="num">10;
     }
   MA_handle = iMA(NULL, PERIOD_CURRENT, class="num">7, class="num">0, MODE_EMA, PRICE_CLOSE);
   if(MA_handle < class="num">0)
     {
      Print("The creation of iMA has failed: MA_handle=", INVALID_HANDLE);
      Print("Runtime error = ", GetLastError());
      class="kw">return(INIT_FAILED);
     }
   MA_handle2 = iMA(NULL, PERIOD_CURRENT, class="num">21, class="num">0, MODE_EMA, PRICE_CLOSE);
   if(MA_handle2 < class="num">0)
     {
      Print("The creation of iMA has failed: MA_handle2=", INVALID_HANDLE);
      Print("Runtime error = ", GetLastError());
      class="kw">return(INIT_FAILED);
     }
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime& time[],
                class="kw">const class="type">class="kw">double& open[],
                class="kw">const class="type">class="kw">double& high[],
                class="kw">const class="type">class="kw">double& low[],
                class="kw">const class="type">class="kw">double& close[],

「跨周期数组与时间轴对齐的坑」

在多周期指标里,当前图表的 K 线时间和 M1、D1 的 K 线索引并不是天然对齐的。上面这段 OnCalculate 开头先用 CopyTime 把当前周期所有 K 线时间拉进 TimeShift 数组,并设成时间序列(ArraySetAsSeries(...,true)),再用 iBarShift 逐个把当前 bar 时间映射到 M1 和 D1 的 bar 下标,存进 barshift_M1 / barshift_D1。 如果跳过这步直接按索引取其他周期数据,会出现时间错位——比如拿当前 H1 的第 i 根去读 M1 的第 i 根,实际对应的是完全不同钟点的行情。外汇和贵金属跨周期回测中这种错位会让信号延迟甚至反向,属于高风险隐蔽 bug。 随后代码对 MA、MA2、Close、Close2、Low、High 全部走 CopyBuffer / CopyClose / CopyLow / CopyHigh,且每一个都立刻 ArraySetAsSeries 成倒序。rates_total 可能到几万根,任何一次 Copy 返回 <=0 就直接 return,避免数组越界把终端卡死。开 MT5 把这段贴进自定义指标,故意改错一个周期参数,能看到指标直接不画线而非乱画。

MQL5 / C++
class="kw">const class="type">long& tick_volume[],
class="kw">const class="type">long& volume[],
class="kw">const class="type">int& spread[])
  {
  class="type">int limit = rates_total - prev_calculated;
  class=class="str">"cmt">//--- counting from class="num">0 to rates_total
  ArraySetAsSeries(Buffer1, true);
  ArraySetAsSeries(Buffer2, true);
  class=class="str">"cmt">//--- initial zero
  if(prev_calculated < class="num">1)
    {
     ArrayInitialize(Buffer1, EMPTY_VALUE);
     ArrayInitialize(Buffer2, EMPTY_VALUE);
    }
  else
     limit++;

  class="type">class="kw">datetime TimeShift[];
  if(CopyTime(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, TimeShift) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(TimeShift, true);
  class="type">int barshift_M1[];
  ArrayResize(barshift_M1, rates_total);
  class="type">int barshift_D1[];
  ArrayResize(barshift_D1, rates_total);
  for(class="type">int i = class="num">0; i < rates_total; i++)
    {
     barshift_M1[i] = iBarShift(Symbol(), PERIOD_M1, TimeShift[i]);
     barshift_D1[i] = iBarShift(Symbol(), PERIOD_D1, TimeShift[i]);
    }
  if(BarsCalculated(MA_handle) <= class="num">0)
     class="kw">return(class="num">0);
  if(CopyBuffer(MA_handle, class="num">0, class="num">0, rates_total, MA) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(MA, true);
  if(BarsCalculated(MA_handle2) <= class="num">0)
     class="kw">return(class="num">0);
  if(CopyBuffer(MA_handle2, class="num">0, class="num">0, rates_total, MA2) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(MA2, true);
  if(CopyClose(Symbol(), PERIOD_M1, class="num">0, rates_total, Close) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(Close, true);
  if(CopyClose(Symbol(), PERIOD_D1, class="num">0, rates_total, Close2) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(Close2, true);
  if(CopyLow(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, Low) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(Low, true);
  if(CopyHigh(Symbol(), PERIOD_CURRENT, class="num">0, rates_total, High) <= class="num">0) class="kw">return(rates_total);
  ArraySetAsSeries(High, true);
  class=class="str">"cmt">//--- main loop
  for(class="type">int i = limit-class="num">1; i >= class="num">0; i--)
    {

◍ 多周期均线交叉的信号落点写法

这段循环收尾逻辑直接决定指标会不会在老数据上崩。先用 MathMin(PLOT_MAXIMUM_BARS_BACK-1, rates_total-1-OMIT_OLDEST_BARS) 掐掉最旧的一部分 K 线,跳过它们能避免 Array out of range,也少算无用功。 紧接着两道边界检查:barshift_M1[i] 和 barshift_D1[i] 只要落在区间 [0, rates_total) 之外就 continue。M1 与 D1 的偏移量若越界,后面取 Close[1+barshift_M1[i]] 必然报错,这两行是多线程周期对齐的保险丝。 信号本身分两个缓冲:Buffer1 在快线 MA 上穿慢线 MA2、且前一根 M1 收盘 ≥ 对应 D1 收盘时,记到当前 Low;否则写 EMPTY_VALUE。Buffer2 反过来,死叉且 M1 收盘 ≤ D1 收盘时记到 High。 返回 rates_total 告诉终端已处理完所有可得柱。外汇与贵金属波动跳空频繁,多周期偏移在重大数据夜可能瞬间越界,上 MT5 把 OMIT_OLDEST_BARS 调小到 100 以内可直观看老数据跳过效果。

MQL5 / C++
   if (i >= MathMin(PLOT_MAXIMUM_BARS_BACK-class="num">1, rates_total-class="num">1-OMIT_OLDEST_BARS)) class="kw">continue; class=class="str">"cmt">//omit some old rates to prevent "Array out of range" or slow calculation
   
   if(barshift_M1[i] < class="num">0 || barshift_M1[i] >= rates_total) class="kw">continue;
   if(barshift_D1[i] < class="num">0 || barshift_D1[i] >= rates_total) class="kw">continue;
   
   class=class="str">"cmt">//Indicator Buffer class="num">1
   if(MA[i] > MA2[i]
   && MA[i+class="num">1] < MA2[i+class="num">1] class=class="str">"cmt">//Moving Average crosses above Moving Average
   && Close[class="num">1+barshift_M1[i]] >= Close2[class="num">1+barshift_D1[i]] class=class="str">"cmt">//Candlestick Close >= Candlestick Close
   )
      {
       Buffer1[i] = Low[i]; class=class="str">"cmt">//Set indicator value at Candlestick Low
      }
   else
      {
       Buffer1[i] = EMPTY_VALUE;
      }
   class=class="str">"cmt">//Indicator Buffer class="num">2
   if(MA[i] < MA2[i]
   && MA[i+class="num">1] > MA2[i+class="num">1] class=class="str">"cmt">//Moving Average crosses below Moving Average
   && Close[class="num">1+barshift_M1[i]] <= Close2[class="num">1+barshift_D1[i]] class=class="str">"cmt">//Candlestick Close <= Candlestick Close
   )
      {
       Buffer2[i] = High[i]; class=class="str">"cmt">//Set indicator value at Candlestick High
      }
   else
      {
       Buffer2[i] = EMPTY_VALUE;
      }
   }
 class="kw">return(rates_total);
}
让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到高周期K线约束后的信号通过率,把重复劳动交给小布,你专注决策。

常见问题

逆高周期趋势抓短反抽,概率上属于劣势交易。模型只放行与D1同向的均线交叉,过滤掉这类逆风信号。
适用。引文中即以Boom 500的M5对接D1为例,高周期阴阳判定不依赖传统外汇流动性,合成品种同样有效。
会。但漏掉的多是逆趋势边缘信号,留存信号胜率倾向更高,整体期望可能改善而非恶化。
小布盯盘品种页已内置高周期趋势屏障视图,可对照本文缓冲区逻辑查看被拦截的信号占比,无需自行部署EA。
存的是当前高周期K线分类结果(看涨/看跌)及是否放行信号的状态,供主图或副图绘制与EA读取。