构建K线趋势约束模型(第九部分):多策略EA(2)·进阶篇
📈

构建K线趋势约束模型(第九部分):多策略EA(2)·进阶篇

(2/3)·前文铺了框架,这篇把唐奇安通道拆成可编译模块,解决主EA漏掉突破行情的老问题

进阶 第 2/3 篇
很多人在趋势约束EA里只跑均线过滤,价格一根大阳捅穿区间却没单子进场。唐奇安通道本身就是为抓这种突破而生的工具,平台还自带源码,不拿来扩策略等于把现成弹药搁在仓库。

「唐奇安通道指标的代码结构拆解」

MT5 自带的标准唐奇安通道指标用三个缓冲区把通道画出来:上轨存指定周期最高价,下轨存同周期最低价,中轨取两者均值。默认输入周期是 20 根 K 线,可在参数里改 InpDonchianPeriod,也能用 InpShowLabel 控制是否在图上标价格。 初始化时 OnInit 里调 SetIndexBuffer 把 0/1/2 号绘图分别绑到 ExtUpBuffer、ExtMdBuffer、ExtDnBuffer,并给每条线设了 1 根偏移(PLOT_SHIFT=1),也就是线条右移一根便于看突破。OnCalculate 里逐根算最高、最低和平均,写进对应缓冲区。 突破逻辑很直接:收盘价站上 ExtUpBuffer 倾向触发买入,跌破 ExtDnBuffer 倾向触发卖出;中轨 ExtMdBuffer 能当趋势过滤器,价格在其上方时才允许做多,可能过滤掉弱势假突破。外汇和贵金属波动大、杠杆高,这套信号只是概率优势,实盘前务必在 MT5 用历史数据验证。 下面把原文指标头部和初始化片段逐行过一遍,方便你直接抄进 MetaEditor 改。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Donchian Channel.mq5 |
class=class="str">"cmt">//|                                         Copyright class="num">2009-class="num">2024, MetaQuotes Ltd |
class=class="str">"cmt">//|                                                            [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright   "class="num">2009-class="num">2024, MetaQuotes Ltd"
class="macro">#class="kw">property link        "[MQL5官方文档]
class="macro">#class="kw">property description "Donchian Channel"
class=class="str">"cmt">//---
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers class="num">3
class="macro">#class="kw">property indicator_plots   class="num">3
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1  clrBlue
class="macro">#class="kw">property indicator_type2   DRAW_LINE
class="macro">#class="kw">property indicator_color2  clrGray
class="macro">#class="kw">property indicator_type3   DRAW_LINE
class="macro">#class="kw">property indicator_color3  clrRed
class=class="str">"cmt">//--- labels
class="macro">#class="kw">property indicator_label1  "Upper Donchian"
class="macro">#class="kw">property indicator_label2  "Middle Donchian"
class="macro">#class="kw">property indicator_label3  "Lower Donchian"
class=class="str">"cmt">//--- input parameter
input class="type">int   InpDonchianPeriod=class="num">20;    class=class="str">"cmt">// period of the channel
input class="type">bool  InpShowLabel      =true; class=class="str">"cmt">// show price of the level
class=class="str">"cmt">//--- indicator buffers
class="type">class="kw">double      ExtUpBuffer[];
class="type">class="kw">double      ExtMdBuffer[];
class="type">class="kw">double      ExtDnBuffer[];
class=class="str">"cmt">//--- unique prefix to identify indicator objects
class="type">class="kw">string ExtPrefixUniq;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator initialization function                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- define buffers
   SetIndexBuffer(class="num">0, ExtUpBuffer);
   SetIndexBuffer(class="num">1, ExtMdBuffer);
   SetIndexBuffer(class="num">2, ExtDnBuffer);
class=class="str">"cmt">//--- set a class="num">1-bar offset for each line
   PlotIndexSetInteger(class="num">0, PLOT_SHIFT, class="num">1);

唐奇安通道的绘制偏移与循环取值

在 MT5 自定义指标初始化尾部,用 PlotIndexSetInteger 给第 1、2 条绘图线设 PLOT_SHIFT=1,通道线会整体右移一根 K 线,视觉上和价格收线位置错开半步,避免和最新 bar 黏连。 IndicatorSetString 把短名钉成 "Donchian Channel",IndicatorSetInteger 接 _Digits 保证小数位和品种一致;再用 GetTickCount64 取毫秒数截后 4 位拼 "_DN" 当对象前缀,多实例加载不会互相覆盖图形。 OnCalculate 里 prev_calculated 非零就从上一根重算,首次则从 InpDonchianPeriod+1 起步。循环中对每根 bar 用 ArrayMaximum / ArrayMinimum 在最近 N 根 high、low 里抓极值,分别写进上轨、下轨缓冲,中轨取两者均值——这就是通道核心。 若开了 InpShowLabel,会在末 bar 时间位画价格标签并 ChartRedraw 强制重绘;外汇与贵金属杠杆高,通道突破信号仅代表概率倾向,实盘前请在 MT5 策略测试器跑一遍周期参数。

MQL5 / C++
  PlotIndexSetInteger(class="num">1, PLOT_SHIFT, class="num">1);
  PlotIndexSetInteger(class="num">2, PLOT_SHIFT, class="num">1);
class=class="str">"cmt">//--- indicator name
  IndicatorSetString(INDICATOR_SHORTNAME, "Donchian Channel");
class=class="str">"cmt">//--- number of digits of indicator value
  IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
class=class="str">"cmt">//--- prepare prefix for objects
  class="type">class="kw">string number=StringFormat("%I64d", GetTickCount64());
  ExtPrefixUniq=StringSubstr(number, StringLen(number)-class="num">4);
  ExtPrefixUniq=ExtPrefixUniq+"_DN";
  Print("Indicator \"Donchian Channels\" started, prefix=", ExtPrefixUniq);
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
class=class="str">"cmt">//--- if the indicator has previously been calculated, start from the bar preceding the last one
  class="type">int start=prev_calculated-class="num">1;
class=class="str">"cmt">//--- if this is the first calculation of the indicator, then move by InpDonchianPeriod bars form the beginning
  if(prev_calculated==class="num">0)
     start=InpDonchianPeriod+class="num">1;
class=class="str">"cmt">//--- calculate levels for all bars in a loop
  for(class="type">int i=start; i<rates_total; i++)
    {
     class=class="str">"cmt">//--- get max/min values for the last InpDonchianPeriod bars
     class="type">int    highest_bar_index=ArrayMaximum(high, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod);
     class="type">int    lowest_bar_index=ArrayMinimum(low, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod);;
     class="type">class="kw">double highest=high[highest_bar_index];
     class="type">class="kw">double lowest=low[lowest_bar_index];
     class=class="str">"cmt">//--- write values into buffers
     ExtUpBuffer[i]=highest;
     ExtDnBuffer[i]=lowest;
     ExtMdBuffer[i]=(highest+lowest)/class="num">2;
    }
class=class="str">"cmt">//--- draw labels on levels
  if(InpShowLabel)
    {
     ShowPriceLevels(time[rates_total-class="num">1], rates_total-class="num">1);
     ChartRedraw();
    }

◍ 退出时清场与右侧价标绘制

指标卸载时若不清图形对象,MT5 子窗口会残留带前缀的箭头标签。OnDeinit 里用 ObjectsDeleteAll(0, ExtPrefixUniq, 0, OBJ_ARROW_RIGHT_PRICE) 按前缀精确删除,再 ChartRedraw(0) 强制重绘,避免下次加载叠影。 ShowPriceLevels 把上轨、中轨、下轨三个缓冲区的最新值分别用蓝、灰、红三色画成右向价格箭头。注意它取的是 last_index 位置的 ExtUpBuffer / ExtMdBuffer / ExtDnBuffer,也就是当前 K 线的通道边界。 ShowRightPrice 先尝试 ObjectCreate,失败才 ObjectMove,说明对象已存在就只更新位置不重建。标签宽度跟着图表缩放走:ChartGetInteger 取 CHART_SCALE,scale>1 时线宽设 2,否则 1;同时把对象设为不可选中、隐藏于对象列表(OBJPROP_HIDDEN=true)、Z序为 0,减少视觉干扰。 外汇与贵金属波动剧烈,通道边界仅反映历史区间,突破后回撤概率不低,实盘前请在 MT5 策略测试器用不同品种验证标签刷新是否卡顿。

MQL5 / C++
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- class="kw">delete all our graphical objects after use
   Print("Indicator \"Donchian Channels\" stopped, class="kw">delete all objects with prefix=", ExtPrefixUniq);
   ObjectsDeleteAll(class="num">0, ExtPrefixUniq, class="num">0, OBJ_ARROW_RIGHT_PRICE);
   ChartRedraw(class="num">0);
  }
class="type">void ShowPriceLevels(class="type">class="kw">datetime time, class="type">int last_index)
  {
   ShowRightPrice(ExtPrefixUniq+"_UP", time, ExtUpBuffer[last_index], clrBlue);
   ShowRightPrice(ExtPrefixUniq+"_MD", time, ExtMdBuffer[last_index], clrGray);
   ShowRightPrice(ExtPrefixUniq+"_Dn", time, ExtDnBuffer[last_index], clrRed);
  }
class="type">bool ShowRightPrice(const class="type">class="kw">string name, class="type">class="kw">datetime time, class="type">class="kw">double price, class="type">color clr)
  {
   if(!ObjectCreate(class="num">0, name, OBJ_ARROW_RIGHT_PRICE, class="num">0, time, price))
     {
       ObjectMove(class="num">0, name, class="num">0, time, price);
       class="kw">return(class="kw">false);
     }
   class="type">long scale=class="num">2;
   if(!ChartGetInteger(class="num">0, CHART_SCALE, class="num">0, scale))
     {
       Print(__FUNCTION__+", ChartGetInteger(CHART_SCALE) failed, error = ", GetLastError());
     }
   class="type">int width=scale>class="num">1 ? class="num">2:class="num">1;
   ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(class="num">0, name, OBJPROP_STYLE, STYLE_SOLID);
   ObjectSetInteger(class="num">0, name, OBJPROP_WIDTH, width);
   ObjectSetInteger(class="num">0, name, OBJPROP_BACK, class="kw">false);
   ObjectSetInteger(class="num">0, name, OBJPROP_SELECTABLE, class="kw">false);
   ObjectSetInteger(class="num">0, name, OBJPROP_SELECTED, class="kw">false);
   ObjectSetInteger(class="num">0, name, OBJPROP_HIDDEN, true);
   ObjectSetInteger(class="num">0, name, OBJPROP_ZORDER, class="num">0);
   class="kw">return(true);
  }

「唐奇安通道的三线缓冲填充」

在 MT5 自定义指标里,唐奇安通道的上中下三线靠三个动态数组承接。先声明 ExtUpBuffer、ExtMdBuffer、ExtDnBuffer,再用 SetIndexBuffer 分别绑定到索引 0、1、2,这样绘图引擎才知道每根 K 线对应哪条线。 核心循环从 start 跑到 rates_total。每一根 i 上,用 ArrayMaximum 在 high 数组的 [i-InpDonchianPeriod+1, InpDonchianPeriod] 窗口里找最高价所在 bar 索引,ArrayMinimum 同理找最低价索引——比如 InpDonchianPeriod=20,就是回看含当前 bar 在内的 20 根。 拿到 highest 与 lowest 后,上轨缓冲写最高、下轨缓冲写最低、中轨写两者均值。外汇与贵金属波动剧烈,通道参数错配会频繁假突破,实盘前务必在 MT5 策略测试器用历史数据核对缓冲赋值是否正确。 别把缓冲索引写反 上中下三线绑定到 0/1/2 是约定俗成,若把 ExtDnBuffer 绑到 0,图表会画出上下颠倒的通道,肉眼很难第一时间察觉。

MQL5 / C++
class="type">class="kw">double ExtUpBuffer[];
class="type">class="kw">double ExtMdBuffer[];
class="type">class="kw">double ExtDnBuffer[];
SetIndexBuffer(class="num">0, ExtUpBuffer);
SetIndexBuffer(class="num">1, ExtMdBuffer);
SetIndexBuffer(class="num">2, ExtDnBuffer);
for(class="type">int i=start; i<rates_total; i++)
{
  class=class="str">"cmt">//--- calculate highest and lowest for the Donchian period
  class="type">int highest_bar_index = ArrayMaximum(high, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod);
  class="type">int lowest_bar_index  = ArrayMinimum(low, i-InpDonchianPeriod+class="num">1, InpDonchianPeriod);
  class="type">class="kw">double highest = high[highest_bar_index];
  class="type">class="kw">double lowest  = low[lowest_bar_index];
  class=class="str">"cmt">//--- assign values to buffers
  ExtUpBuffer[i] = highest;
  ExtDnBuffer[i] = lowest;
  ExtMdBuffer[i] = (highest + lowest) / class="num">2;
}

从零搭一个唐奇安突破EA

唐奇安通道是 MT5 内置指标,写突破逻辑时只需盯它的上下轨缓冲区。先在 MetaEditor 5 建个空 EA 项目,取名 BreakoutEA,初始模板只勾基本项、带 #property strict 指令,逼编译器做类型检查,顺手把交易库 include 进来,地基就算打好了。 初始化阶段用 iCustom() 加载放在 Free Indicators\\Donchian Channel 子目录的通道指标,返回 INVALID_HANDLE 就直接报错退出,避免后面空跑。输入参数先留四个:通道周期(默认 20)、风险回报比(默认 1.5)、手数、止损获利点数。 OnDeinit() 里调 IndicatorRelease() 释放句柄,防内存泄漏;OnTick() 每次报价触发,先用 PositionsTotal() 确认无持仓才去判信号,天然屏蔽同屏多开。信号本身极简:CopyBuffer() 取最新上下轨,收盘价破上轨做多、破下轨做空,止损获利按输入点数算。 下单走 CTrade 类封装的买卖函数,失败就打印错误。独立回测里这套裸突破能正常撮合订单,但外汇和贵金属波动无序,裸突破在震荡市亏钱概率偏高,下一阶段必须接趋势过滤。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 BreakoutEA.mq5 |
class=class="str">"cmt">//|                                         Copyright class="num">2024, Clemence Benjamin |
class=class="str">"cmt">//|                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, Clemence Benjamin"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---

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">//---

  }
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">// Input parameters
input class="type">int InpDonchianPeriod = class="num">20;       class=class="str">"cmt">// Period for Donchian Channel
input class="type">class="kw">double RiskRewardRatio = class="num">1.5;     class=class="str">"cmt">// Risk-to-reward ratio

◍ EA 初始化失败的典型日志与资源回收

把唐奇安通道 EA 挂上 MT5 前,先确认自定义指标路径是否为 Free Indicators\Donchian Channel。OnInit 里用 iCustom 加载,若返回 INVALID_HANDLE 会直接 INIT_FAILED,EA 连第一根 K 线都跑不了。 下面这段是实盘回测里真实打出来的 Journal:2024.10.20 08:49:04.117 报 cannot load custom indicator 'Donchian Channel' [4802],紧接着 OnInit critical error、tester stopped because OnInit failed。也就是说指标没进根目录的 indicators 存储,EA 在 2022.01.01 的起始点就被掐断。 外汇与贵金属杠杆高,EA 起不来就谈不上风控,先排环境再谈策略。OnDeinit 里调 IndicatorRelease(handle) 把句柄释放掉,避免重复加载拖慢终端;OnTick 则用 PositionsTotal()==0 做无持仓闸门,才进 CheckTradingConditions 读上下轨缓冲。 读通道值前需给 ExtUpBuffer / ExtDnBuffer 留 2 格空间,ArrayResize 成 2 后再取最新数据,否则可能越界报错。

MQL5 / C++
input class="type">class="kw">double LotSize = class="num">0.1;              class=class="str">"cmt">// Default lot size for trading
input class="type">class="kw">double pipsToStopLoss = class="num">15;       class=class="str">"cmt">// Stop loss in pips
input class="type">class="kw">double pipsToTakeProfit = class="num">30;     class=class="str">"cmt">// Take profit in pips
class=class="str">"cmt">// Indicator handle storage
class="type">int handle;
class="type">class="kw">string indicatorKey;
class=class="str">"cmt">// Expert initialization function
class="type">int OnInit() {
    class=class="str">"cmt">// Create a unique key for the indicator based on the symbol and period
    indicatorKey = StringFormat("%s_%d", Symbol(), InpDonchianPeriod);
    
    class=class="str">"cmt">// Load the Donchian Channel indicator
    handle = iCustom(Symbol(), Period(), "Free Indicators\\Donchian Channel", InpDonchianPeriod);
    
    class=class="str">"cmt">// Check if the indicator loaded successfully
    if (handle == INVALID_HANDLE) {
        Print("Failed to load the indicator. Error: ", GetLastError());
        class="kw">return INIT_FAILED;
    }
    class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//Typical Journal log when the EA fails to locate an indicator in the root indicators storage.
class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.117 class="num">2022.01.class="num">01 class="num">00:class="num">00:class="num">00   cannot load custom indicator &class="macro">#x27;Donchian Channel&class="macro">#x27; [class="num">4802]
class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.118 class="num">2022.01.class="num">01 class="num">00:class="num">00:class="num">00   indicator create error in &class="macro">#x27;DonchianEA.mq5&class="macro">#x27; (class="num">1,class="num">1)
class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.118 OnInit critical error
class="num">2024.10.class="num">20 class="num">08:class="num">49:class="num">04.118 tester stopped because OnInit failed
class=class="str">"cmt">// Expert deinitialization function
class="type">void OnDeinit(const class="type">int reason) {
    class=class="str">"cmt">// Release the indicator handle to free up resources
    IndicatorRelease(handle);
}
class=class="str">"cmt">// Main execution function with block-based control
class="type">void OnTick() {
    class=class="str">"cmt">// Check if any positions are currently open
    if (PositionsTotal() == class="num">0) {
        CheckTradingConditions();
    }
}
class=class="str">"cmt">// Check trading conditions based on indicator buffers
class="type">void CheckTradingConditions() {
    class="type">class="kw">double ExtUpBuffer[], ExtDnBuffer[];  class=class="str">"cmt">// Buffers for upper and lower Donchian bands
    class=class="str">"cmt">// Resize buffers to hold the latest data
    ArrayResize(ExtUpBuffer, class="num">2);
    ArrayResize(ExtDnBuffer, class="num">2);
    class=class="str">"cmt">// Get the latest values from the Donchian Channel
让小布替你跑这套通道扫描
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到通道突破预警,把重复劳动交给小布,你专注决策。

常见问题

常见默认是20根,但波动率高的贵金属可缩到10~15根,低波动货币对可拉到30根,具体看品种节奏,没有统一圣经。
导航窗口指标分类下右键该指标选修改,即可跳进MetaEditor对应mq5,结构比自己重写省事很多。
可以,小布盯盘品种页已内置通道类预警,不需要你自己写EA也能先观察价格与轨道的互动关系。
建议先独立开发新策略模块再做融合,代码复杂度上来后这样排错更快,也方便单独回测。
概率上会出现,所以本文把它挂在趋势约束模型下,用原过滤条件压低噪音,但外汇贵金属高风险仍可能连续止损。