从头开始开发智能交易系统(第 10 部分):访问自定义指标(基础篇)
「EA 调用自定义指标的通道」
在 MT5 里让智能交易系统读取自建指标,核心是用 iCustom 拿到指标句柄,再用 CopyBuffer 把缓冲区数据搬进 EA。很多新手卡在『指标名和参数顺序必须和 ex5 文件完全一致』这一步,参数错一位就会返回空值或 INVALID_HANDLE。 以调用一个名为 'MyTrendFilter' 的自定义指标为例,它在客户端数据文件夹 MQL5/Indicators 下,接受三个输入:平滑周期 14、价格类型 PRICE_CLOSE、偏移 0。下面这段声明展示了标准取法。 [CODE] int handle = iCustom(_Symbol, _Period, "MyTrendFilter", 14, PRICE_CLOSE, 0); if(handle == INVALID_HANDLE) { Print("指标句柄获取失败"); return; } double buffer[1]; if(CopyBuffer(handle, 0, 0, 1, buffer) <= 0) { Print("缓冲区拷贝失败"); return; } double currentValue = buffer[0]; [/CODE] 逐行看:第一行用 iCustom 向当前品种当前周期申请指标实例,三个参数依次对应 ex5 暴露的输入变量;第二到四行做防御,句柄非法直接退出避免后续崩 EA。第六行声明长度为 1 的数组承接最新一根 K 线的数值;第七行从 0 号缓冲区拷贝 1 根bar,失败则打印并返回;末行把数组第 0 位赋给变量供逻辑判断。 实盘里外汇与贵金属波动剧烈、滑点和重连都会导致 CopyBuffer 偶发返回 0,建议每次 OnTick 前先判断 handle 有效性并做重试,否则可能漏掉关键信号。
class="type">int handle = iCustom(_Symbol, _Period, "MyTrendFilter", class="num">14, PRICE_CLOSE, class="num">0); if(handle == INVALID_HANDLE) { Print("指标句柄获取失败"); class="kw">return; } class="type">class="kw">double buffer[class="num">1]; if(CopyBuffer(handle, class="num">0, class="num">0, class="num">1, buffer) <= class="num">0) { Print("缓冲区拷贝失败"); class="kw">return; } class="type">class="kw">double currentValue = buffer[class="num">0];
◍ EA 吃不到自定义指标就废了
一套交易 EA 如果只能调标准指标,本质上就是半残。它能管仓位、能发单、逻辑写得再漂亮,只要拿不到自定义指标的数据,分析维度就被锁死在 MT5 自带那几个里。 一个典型例子是 VWAP(成交量加权平均价格)。在巴西期货交易所这是交易员盯盘的核心均线,但 MT5 根本没预装。你可以手搓一个自定义指标把 VWAP 画上屏,可一旦想在 EA 里直接读它的计算值,麻烦就来了——没规划好访问方式,几乎没法弄。 现实是:绝大多数自定义指标的数据接口,EA 默认够不着。缺乏这套桥接知识,你就只能继续用 MA、MACD 那套标准货。本文要拆的,就是怎么在 EA 里把自定义指标(比如 VWAP)的数据拽出来用。
让 EA 认得出你的 VWAP 指标
VWAP 的逐根 K 线计算并不复杂,核心就是典型价乘成交量再做累计除权。下面这段 OnCalculate 里高亮的三行就是全部算法:典型价 (high+low+close)/3 乘 volume 累加到 Price,volume 累加到 Volume,再相除写进缓冲。 [CODE] int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { double Price = 0; ulong Volume = 0; static int siPos = 0; if (macroGetDate(time[rates_total - 1]) != macroGetDate(time[siPos])) { for (int c0 = rates_total - 1; macroGetDate(time[siPos]) != macroGetDate(time[c0]); siPos++); ArrayInitialize(VWAP_Buff, EMPTY_VALUE); } for (int c0 = siPos; c0 < rates_total; c0++) { Price += ((high[c0] + low[c0] + close[c0]) / 3) * volume[c0]; Volume += volume[c0]; VWAP_Buff[c0] = Price / Volume; } return rates_total; } #property copyright "Daniel Jose - Indicador VWAP ( IntraDay )" #property version "1.01" #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 #property indicator_width1 2 #property indicator_type1 DRAW_LINE #property indicator_color1 clrBlack //+------------------------------------------------------------------+ [/CODE] 指标能在图上画出 VWAP 后,真正的坑才出现:EA 并不知道图表上有没有这个指标。文件名写 VWAP.mq5 对 MT5 运行时毫无意义,EA 靠的是指标内部声明的唯一名称去定位,而不是磁盘文件名。 解决方式是进自定义指标初始化代码,加一行能反映计算用途的标识名。MetaTrader 5 标准库里的自定义移动平均就是这么做的——它的名称示意写在指标内部,和文件名脱钩。加完这行,EA 才能用 iCustom 或类似机制确认指标已加载并读值。外汇与贵金属杠杆高,EA 误判指标缺失可能导致信号失效,实盘前务必在策略测试器里验证加载逻辑。
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[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double Price = class="num">0; class="type">class="kw">ulong Volume = class="num">0; class="kw">static class="type">int siPos = class="num">0; if (macroGetDate(time[rates_total - class="num">1]) != macroGetDate(time[siPos])) { for (class="type">int c0 = rates_total - class="num">1; macroGetDate(time[siPos]) != macroGetDate(time[c0]); siPos++); ArrayInitialize(VWAP_Buff, EMPTY_VALUE); } for (class="type">int c0 = siPos; c0 < rates_total; c0++) { Price += ((high[c0] + low[c0] + close[c0]) / class="num">3) * volume[c0]; Volume += volume[c0]; VWAP_Buff[c0] = Price / Volume; } class="kw">return rates_total; } class="macro">#class="kw">property copyright "Daniel Jose - Indicador VWAP( IntraDay )" class="macro">#class="kw">property version "class="num">1.01" class="macro">#class="kw">property indicator_chart_window 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_width1 class="num">2 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_color1 clrBlack class=class="str">"cmt">//+------------------------------------------------------------------+
「初始化里把指标名和缓冲绑对」
MT5 自定义指标跑不起来,十有八九是 OnInit 里缓冲没绑或短名没设。上面几段代码把同一个 VWAP / MA 指标的初始化写出了三种写法,有的只绑了缓冲,有的补了 IndicatorSetString 设短名,还有的顺手把位数、偏移、画法起止都配了。
宏 #define macroGetDate(A) (A - (A % 86400)) 把时间戳按 86400 秒(一天秒数)取整,用来切日线分界,VWAP 按日重置就靠它。
最完整的那段里,PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, InpMAPeriod) 让画线从第 InpMAPeriod 根 K 线才开始,避免前面数据不足画出畸值;PLOT_SHIFT 控制水平偏移。switch 按 InpMAMethod 拼出 SMA / EMA / LWMA / SMMA 短名,挂在图表上能直接看清参数。
开 MT5 新建指标把这段 OnInit 粘进去,编译后看导航树里的短名是否随参数变——若写死成 "VWAP" 不改,多周期叠加时容易认错线。外汇和贵金属杠杆高,指标只是参考,信号失效可能比预期快。
class="macro">#define macroGetDate(A) (A - (A % class="num">86400)) class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double VWAP_Buff[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { SetIndexBuffer(class="num">0, VWAP_Buff, INDICATOR_DATA); class="kw">return INIT_SUCCEEDED; } class="type">int OnInit() { SetIndexBuffer(class="num">0, VWAP_Buff, INDICATOR_DATA); IndicatorSetString(INDICATOR_SHORTNAME, "VWAP"); class="kw">return INIT_SUCCEEDED; } class="type">void OnInit() { SetIndexBuffer(class="num">0,ExtLineBuffer,INDICATOR_DATA); IndicatorSetInteger(INDICATOR_DIGITS,_Digits+class="num">1); PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,InpMAPeriod); PlotIndexSetInteger(class="num">0,PLOT_SHIFT,InpMAShift); class="type">class="kw">string short_name; class="kw">switch(InpMAMethod) { case MODE_EMA : short_name="EMA"; class="kw">break; case MODE_LWMA : short_name="LWMA"; class="kw">break; case MODE_SMA : short_name="SMA"; class="kw">break; case MODE_SMMA : short_name="SMMA"; class="kw">break; class="kw">default : short_name="unknown ma"; } IndicatorSetString(INDICATOR_SHORTNAME, short_name + "(" + class="type">class="kw">string(InpMAPeriod) + ")"); PlotIndexSetDouble(class="num">0,PLOT_EMPTY_VALUE,class="num">0.0); }
◍ EA 怎么把指标值抓进来计算
从零写 EA 时,最实用的第一步不是重写指标,而是让 EA 直接读图表上已有指标的最后计算结果。MQL5 里跨系统读写数据的核心函数是 CopyBuffer,它不挑指标——标准指标和自定义指标都能拉数据,所以你写的任意指标都可能被 EA 复用。 假设 VWAP 已经在图表上,EA 干净骨架里只需在 OnInit 用 ChartIndicatorGet 拿到指标句柄,再用 SetIndexBuffer 绑定接收数组;之后每秒钟靠 EventSetTimer(1) 触发的 OnTimer 里调用 CopyBuffer(handle, 0, 0, 1, Buff) 取最新一根值并打印。下面这段代码是最小可跑片段,去掉高亮部分就是纯空 EA: 如果图表上根本没有 VWAP,EA 也能自己加载:用 iCustom 把自定义指标挂上去,后续读取逻辑不变。更省心的是让 EA 把指标画出来——读取 VWAP 末值后显示在屏幕上,EA 删除时指标也跟着消失,图表上永远看得到 EA 在算什么。 把这套逻辑换到均线也一样通:iCustom 加载时把周期、偏移等参数写死,EA 内部照抄 VWAP 的 CopyBuffer 用法即可。外汇和贵金属行情跳动快、滑点随机,这类自动加载指标的做法只是工程便利,不预示任何方向,实盘前请在 MT5 策略测试器里先跑一遍验证句柄有效性。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { 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=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">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) { i = CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff); Print(Buff[class="num">0]); } } class=class="str">"cmt">//+------------------------------------------------------------------+