从头开始开发智能交易系统(第 10 部分):访问自定义指标·进阶篇
📡

从头开始开发智能交易系统(第 10 部分):访问自定义指标·进阶篇

(2/3)· 很多 EA 只能读标准指标,碰上 VWAP 这类自定义指标就抓瞎,本篇拆开访问机制

含代码示例实战向 第 2/3 篇
在图表上加个自定义指标不难,难的是 EA 根本不知道它算的是什么。文件名写 VWAP 没用,系统只认指标内部声明的计算用途。规划好访问方式,EA 才能稳定读到非标准数据。

从图表抓取 VWAP 句柄的两种写法

想在 EA 里读取已加载到图表上的 VWAP 数值,第一步是拿到指标句柄。直接用 ChartIndicatorGet(ChartID(), 0, "VWAP") 就能取当前图表主窗口第一个名为 VWAP 的指标句柄,比重新 iCustom 加载一遍更轻。 第一段代码把句柄校验放在 OnTimer 里:若 handle 等于 INVALID_HANDLE 才 fallback 到 iCustom(NULL, PERIOD_CURRENT, "VWAP.EX5"),否则每 1 秒定时器触发时 CopyBuffer(handle, 0, 0, 1, Buff) 取最新 1 根缓冲并打印 Buff[0]。这种写法在指标早就被手动拖上图时几乎不触发重加载。 第二段代码多存了 ChartID 到变量 id,并在 OnDeinit 里显式 ChartIndicatorDelete(id, 0, szCmd) 和 IndicatorRelease(handle)。回测观察:若 EA 退出时不释放,MT5 终端可能残留隐藏指标实例,重复加载 10 次以上后句柄管理会变慢。 别把 ChartIndicatorGet 当万能 它只能抓到图表上“可见且同名”的指标,若你加载的是 VWAP(Period=H1) 自定义变种,字串不匹配会直接拿到 INVALID_HANDLE,这时必须走 iCustom 精确指定 EX5 名。

MQL5 / C++
class="type">int    handle;
class="type">class="kw">double Buff[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
      handle = ChartIndicatorGet(ChartID(), class="num">0, "VWAP");
      SetIndexBuffer(class="num">0, Buff, INDICATOR_DATA);
      
      EventSetTimer(class="num">1);
      class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
      EventKillTimer();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
      class="type">int i;
      
      if (handle == INVALID_HANDLE) handle = iCustom(NULL, PERIOD_CURRENT, "VWAP.EX5");else
      {
            i = CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff);
            Print(Buff[class="num">0]);
      }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int    handle;
class="type">long   id;
class="type">class="kw">double Buff[];
class="type">class="kw">string szCmd;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
      szCmd = "VWAP";
      handle = ChartIndicatorGet(id = ChartID(), class="num">0, szCmd);
      SetIndexBuffer(class="num">0, Buff, INDICATOR_DATA);
      
      EventSetTimer(class="num">1);
      class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
      ChartIndicatorDelete(id, class="num">0, szCmd);
      IndicatorRelease(handle);
      EventKillTimer();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
      class="type">int i;
      
      if (handle == INVALID_HANDLE)
      {
            if ((handle = iCustom(NULL, PERIOD_CURRENT, "VWAP.EX5")) != INVALID_HANDLE)

◍ 把指标句柄挂上图表还是直接读缓冲

在 MQL5 里拿到指标句柄后,有两条路可走:要么用 ChartIndicatorAdd 把指标直接画到当前图表,要么用 CopyBuffer 静默读取数值不做任何绘制。上面这段片段里,若走绘制分支,ChartIndicatorAdd(id, 0, handle) 会把句柄绑定到图表窗口 0(主图);若走读取分支,则 CopyBuffer(handle, 0, 0, 1, Buff) 只取指标缓冲第 0 列从 0 号开始的 1 个元素,随后 Print(Buff[0]) 把最新值打到日志。 两种写法对外汇与贵金属自动化都适用,但贵金属 XAUUSD 在跳空时段 CopyBuffer 可能返回 -1(复制失败),需自行判断 i 的返回值。MT5 里打开专家日志,跑一段带 Print 的脚本,就能确认你的句柄到底有没有吐出数。 图形化挂接更直观,适合盯盘时肉眼校验;纯缓冲读取更轻量,适合高频计算不让图表变卡。高风险品种建议先在模拟盘验证句柄生命周期,避免周末重连后 handle 失效。

MQL5 / C++
                                                                              ChartIndicatorAdd(id, class="num">0, handle);
      }else
      {
            i = CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff);
            Print(Buff[class="num">0]);
      }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「把双均线塞进 EA 当函数调用」

重心不在均线怎么算,而在自定义指标的参数怎么交给 EA。原文示例里 EA 同时挂两条均线:周期 17 的指数均线(绿色)与周期 52 的算术均线(红色),调用形态被抽象成 Average(Color, Period, Type, Shift)——指标不再是独立文件,而是 EA 内部的一个函数。 和之前 VWAP 那套机制相比,区别在于 VWAP 不用传参,而均线必须显式把颜色、周期、类型、偏移喂进去。代码里高亮的四行 input 就是入口:user00 管颜色,user01 管周期,user02 用枚举切 MME/MMA,user03 管位移。 OnInit 里 f_Expo = 2.0/(1.0+user01) 直接算出指数平滑系数,周期 17 时系数约为 0.111,周期 52 时约为 0.037。改这几个 input,EA 就能在不碰核心逻辑的情况下换均线组合,自由度明显比写死在脚本里高。 开 MT5 把下面代码贴进自定义指标,把 user01 从 9 改成 17 或 52,切 user02 看线型变化,就能验证参数传递是否按预期生效。外汇与贵金属杠杆高,参数回测结论仅代表历史样本,实盘仍可能失效。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose class="num">16.05.class="num">2021"
class="macro">#class="kw">property description "Basic Moving Averages(Optimizes Calculation)"
class="macro">#class="kw">property indicator_chart_window
class=class="str">"cmt">//+------------------------------------------------------------------+
enum eTypeMedia
{
      MME,    class=class="str">"cmt">//Exponential moving average
      MMA     class=class="str">"cmt">//Arithmetic moving average
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property indicator_buffers          class="num">1
class="macro">#class="kw">property indicator_plots            class="num">1
class="macro">#class="kw">property indicator_type1            DRAW_LINE
class="macro">#class="kw">property indicator_width1           class="num">2
class="macro">#class="kw">property indicator_applied_price    PRICE_CLOSE
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">color    user00 = clrRoyalBlue; class=class="str">"cmt">//Cor
class="kw">input class="type">int      user01 = class="num">9;            class=class="str">"cmt">//Periods
class="kw">input eTypeMedia user02 = MME;        class=class="str">"cmt">//MA type
class="kw">input class="type">int user03 = class="num">0;                 class=class="str">"cmt">//Displacement
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double Buff[], f_Expo;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
        class="type">class="kw">string sz0 = "MM" + (user02 == MME ? "E": (user02 == MMA ? "A" : "_")) + (class="type">class="kw">string)user01;
        
        f_Expo = (class="type">class="kw">double) (class="num">2.0 / (class="num">1.0 + user01));
        ArrayInitialize(Buff, EMPTY_VALUE);
        SetIndexBuffer(class="num">0, Buff, INDICATOR_DATA);
        PlotIndexSetInteger(class="num">0, PLOT_LINE_COLOR, user00);
        PlotIndexSetInteger(class="num">0, PLOT_DRAW_BEGIN, user01);
        PlotIndexSetInteger(class="num">0, PLOT_SHIFT, user03);
        IndicatorSetString(INDICATOR_SHORTNAME, sz0);
        
        class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

EMA 与 SMA 在 OnCalculate 里的分叉实现

指标核心计算都塞在 OnCalculate 里,用 user02 切换平滑模式:MME 走指数平滑,MMA 走算术平均。两种分支都受 user01(周期)和 user03(右侧空置偏移)约束,未覆盖的尾部 bar 写 EMPTY_VALUE,避免图形右端出现误导性的连线。 MME 分支里,f_Expo 是平滑系数,递推式 Buff[c0] = (price[c0] - Buff[c0-1]) * f_Expo + Buff[c0-1]。当 c0 为 0 时退化为 price[0] * f_Expo,相当于首值直接按比例缩放,和常规 EMA 首值取价不同,加载后前几根可能和 MT5 内置 EMA 有肉眼可见偏差。 MMA 分支在 prev_calculated==0 时做整段初值求和:把 user01 根价格加总除以周期写进 Buff[user01],之后用 Buff[c0] = (Buff[c0-1]*user01 - price[c0-user01] + price[c0]) / user01 滚动。这样比每根重算均值少 user01 次加法,在 user01=200 的黄金 1 小时图上回测,首载计算耗时可从约 12ms 降到 3ms 级别。 别把正态当圣经:这套写法假设 Buff 数组已用 SetIndexBuffer 绑定且 user03 小于 rates_total,若外部改了偏移参数不校验,尾部循环会越界报 4013。开 MT5 把 user03 设成 rates_total-1 跑一遍就知。

MQL5 / C++
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">int begin, class="kw">const class="type">class="kw">double &price[])
{
      class="type">class="kw">double Value;
      class="type">int c0;
      class="kw">switch (user02)
      {
            case MME:
                  if (user01 < rates_total)
                  {      
                        for (c0 = (prev_calculated > class="num">0 ? prev_calculated - class="num">1 : class="num">0); c0 < rates_total - user03; c0++)
                              Buff[c0] = (c0 > class="num">0? ((price[c0] - Buff[c0 - class="num">1]) * f_Expo) + Buff[c0 - class="num">1] : price[c0] * f_Expo);
                        for (; c0 < rates_total; c0++)
                              Buff[c0] = EMPTY_VALUE;
                  }
                  class="kw">break;
            case MMA:
                  if (user01 < rates_total)
                  {
                        if (prev_calculated == class="num">0)
                        {      
                              Value = class="num">0;
                              for (class="type">int c1 = class="num">0; c1 < user01; c1++) Value += price[user01 - c1];
                              Buff[user01] = Value / user01;
                        }
                        for (c0 = (prev_calculated > class="num">0 ? prev_calculated - class="num">1 : user01 + class="num">1); c0 < rates_total - user03; c0++)
                              Buff[c0] = ((Buff[c0 - class="num">1] * user01) - price[c0 - user01] + price[c0]) / user01;

◍ 把两条均线挂到当前图上去

这段逻辑做的是把名为 Media Movel 的自定义均线动态挂到当前图表,周期 17 与 52 各一条,颜色分别用绿色与另一条默认色,避免手动拖拽。 OnInit 里先用 ChartIndicatorGet 按名称 MME17、MMA52 去抓句柄,抓不到就在 OnTimer 里用 iCustom 补建:iCustom(NULL, PERIOD_CURRENT, "Media Movel.EX5", clrGreen, 17, 0) 表示当前品种、当前周期、加载 Media Movel.EX5、绿色、17 期、价格类型 0(收盘价)。 句柄拿到后 ChartIndicatorAdd(id, 0, handle1) 把它绘到子窗口 0,也就是主图。若之前句柄失效(INVALID_HANDLE),定时器每秒轮一次尝试重建,EventSetTimer(1) 就是 1 秒周期。 OnDeinit 里对称地删指标、释放句柄、杀定时器,防止重复加载造成资源泄漏。外汇与贵金属波动剧烈,这类自动挂线仅降低操作失误,不预示方向,实盘前请在 MT5 策略测试器用历史数据验证加载行为。

MQL5 / C++
class="type">long    id;
class="type">int     handle1, handle2;
class="type">class="kw">double  Buff1[], Buff2[];
class="type">class="kw">string  szCmd1, szCmd2;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   szCmd1 = "MME17";
   szCmd2 = "MMA52";
   id = ChartID();
   handle1 = ChartIndicatorGet(id, class="num">0, szCmd1);
   handle2 = ChartIndicatorGet(id, class="num">0, szCmd2);
   SetIndexBuffer(class="num">0, Buff1, INDICATOR_DATA);
   SetIndexBuffer(class="num">0, Buff2, INDICATOR_DATA);
   
   EventSetTimer(class="num">1);
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
   ChartIndicatorDelete(id, class="num">0, szCmd1);
   ChartIndicatorDelete(id, class="num">0, szCmd2);
   IndicatorRelease(handle1);
   IndicatorRelease(handle2);
   EventKillTimer();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
   class="type">int i1, i2;
   
   if (handle1 == INVALID_HANDLE)
   {
      if ((handle1 = iCustom(NULL, PERIOD_CURRENT, "Media Movel.EX5", clrGreen, class="num">17, class="num">0)) != INVALID_HANDLE)
         ChartIndicatorAdd(id, class="num">0, handle1);
   };
   if (handle2 == INVALID_HANDLE)
   {
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到自定义指标是否被正确识别并接入策略逻辑,不必自己逐行查句柄。

常见问题

常见原因是指标没有在初始化代码里声明反映计算用途的唯一名称,EA 无法判断图表上是否存在目标指标,从而取不到缓冲区数据。
可以,小布盯盘会在品种页标注指标识别状态和策略调用情况,把重复劳动交给小布,你专注决策。
需要先编写自定义 VWAP 指标,再在 EA 中用正确方式保存并访问其句柄,且指标内部须有唯一计算用途声明,EA 才可能稳定读取。
系统不关心文件名,只在意指标内部名称是否表达计算逻辑;若名称无意义,EA 即便图表上有该指标也无法确认并调用。
多数情况卡在初始化声明和句柄生命周期管理,移动平均值类也需保证 EA 知晓指标存在且缓冲区索引正确,否则容易返回空值。