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

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

第 1/3 篇

「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 有效性并做重试,否则可能漏掉关键信号。

MQL5 / C++
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 误判指标缺失可能导致信号失效,实盘前务必在策略测试器里验证加载逻辑。

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">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" 不改,多周期叠加时容易认错线。外汇和贵金属杠杆高,指标只是参考,信号失效可能比预期快。

MQL5 / C++
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 策略测试器里先跑一遍验证句柄有效性。

MQL5 / C++
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">//+------------------------------------------------------------------+

常见问题

在EA的OnInit里用iCustom绑定指标文件名与缓冲索引,确认指标放在Indicators目录且缓冲序号与指标源码一致,否则返回空值。
先检查iCustom的参数顺序、指标短名、缓冲号;再用Print输出句柄和数值,确认指标已加载且当前品种有时间序列数据。
可以,小布能读取你的EA与指标配置,自动标出iCustom绑定异常和缓冲错位,省去你手动Print排查。
多为指标短名带空格或版本后缀导致不匹配,回测与实盘环境路径解析不同,统一用严格指标名并重启终端可解。
会,每多一个iCustom句柄就多一次跨模块调用;建议在OnInit统一缓存句柄,Tick里只CopyBuffer,别反复创建。