从头开始开发智能交易系统(第 10 部分):访问自定义指标·进阶篇
(2/3)· 很多 EA 只能读标准指标,碰上 VWAP 这类自定义指标就抓瞎,本篇拆开访问机制
从图表抓取 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 名。
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 失效。
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 看线型变化,就能验证参数传递是否按预期生效。外汇与贵金属杠杆高,参数回测结论仅代表历史样本,实盘仍可能失效。
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 跑一遍就知。
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 策略测试器用历史数据验证加载行为。
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) {