在 EA 交易代码中实现指标的计算(基础篇)
「在 EA 里直接算指标而不是反复调用 iCustom」
把指标逻辑搬进 EA 代码里自己算,而不是每次都靠 iCustom 去读外部指标,是降低延迟和依赖的一条实用路径。MQL5 里指标和 EA 是两套独立程序,跨程序取缓冲数据会有额外开销,高频或复杂策略下这个开销可能明显拖慢 tick 处理。 原文给出的迁移思路是先写一个负责指标计算的类,再把这个类实例挂到 EA 里。这样 EA 主循环里直接调类方法拿数值,不经过指标句柄。对一个 20 日均线类做这样的封装后,EA 在回测里每秒处理 tick 数倾向比 iCustom 方式高一些,具体幅度看品种波动和电脑性能。 要注意,外汇和贵金属杠杆高、滑点跳空频繁,自己算指标若写错缓冲索引,信号会 silently 错掉。开 MT5 用下面的骨架类验证一下,比只读文档来得实在。
EA 里塞指标还是外挂引用
写 EA 的人迟早要选一条路:是把指标逻辑直接搬进 EA 源码,还是只在 EA 里用 iCustom / iIndicator 去引用外部指标。两种做法背后动机不一样,有人想捂住策略细节,发一个 ex5 文件了事;也有人只是想少占点资源,毕竟不用的缓冲区和信号线在后台照样算。 MT4 时代 Nikolay Kositsin 就拆过类似命题,MT5 这边因为指标缓冲区和句柄机制变了,收益和坑都得重新量。先把这条岔路记下来,后面用代码实测两种写法在 tick 处理上的开销差异。 外汇和贵金属杠杆高,回测省下的毫秒不代表实盘稳,任何结构优化都只是概率上的执行改善。
◍ 把指标逻辑搬进EA前先补三处架构债
MT5里指标和EA的底层数据通道不一样。指标开头声明的缓冲区数组是终端原生支持的时间序列,元素自动跟K线柱绑定,新烛形来了终端替你平移和扩容;EA没有这套机制,把指标算法塞进EA时,你得自己建数组并手动维护柱索引映射。 除了可见的绘图缓冲,指标里常藏辅助缓冲做中间计算,迁移时也得一并建出来。若策略不依赖指标线变色,绘制色彩缓冲可以省掉,但数据缓冲和中间缓冲漏建一个,计算结果就会错位。 分时入口也不同:MT5指标用OnCalculate,参数直接给rates_total、prev_calculated和OHLC数组;EA只有无参的OnTick。意味着EA里要自己开时间序列句柄、自己比对prev_calculated来只重算新增深度,而不是每次从头跑历史。 下面这段空白指标模板能看清缓冲区声明到映射的最小骨架: //+------------------------------------------------------------------+
| // | Blanc.mq5 |
|---|---|
| // | Copyright 2018, DNG® |
| // | [MQL5官方文档] |
//+------------------------------------------------------------------+ #property copyright "Copyright 2018, DNG®" #property link "[MQL5官方文档] #property version "1.00" #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 //--- 绘图缓冲区 #property indicator_label1 "Buffer" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- 指标缓冲区 double BufferBuffer[]; //+------------------------------------------------------------------+
| // | 自定义指标初始化函数 |
|---|
//+------------------------------------------------------------------+ int OnInit() { //--- 指标缓冲区映射 SetIndexBuffer(0,BufferBuffer,INDICATOR_DATA); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
| // | 自定义指标迭代函数 |
|---|
//+------------------------------------------------------------------+ 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[], 逐行看:#property indicator_buffers 1 声明1个缓冲槽;double BufferBuffer[] 是实际数组;OnInit里SetIndexBuffer(0,BufferBuffer,INDICATOR_DATA)把数组0号槽映射成数据缓冲,终端此后自动按柱推送;OnCalculate的参数列里rates_total是当前总柱数、prev_calculated是上次已算柱数,靠这两数差值就能只算新柱。EA里这些参数都不会掉给你,要CopyBuffer或自建数组去追。 外汇和贵金属杠杆高、滑点跳空频繁,迁移后务必在策略测试器用真实点差跑一遍,确认重算深度和缓冲索引没偏。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Blanc.mq5 | class=class="str">"cmt">//| Copyright class="num">2018, DNG® | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2018, DNG®" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" 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=class="str">"cmt">//--- 绘图缓冲区 class="macro">#class="kw">property indicator_label1 "Buffer" class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_color1 clrRed class="macro">#class="kw">property indicator_style1 STYLE_SOLID class="macro">#class="kw">property indicator_width1 class="num">1 class=class="str">"cmt">//--- 指标缓冲区 class="type">class="kw">double BufferBuffer[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 指标缓冲区映射 SetIndexBuffer(class="num">0,BufferBuffer,INDICATOR_DATA); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ 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[],
「OnCalculate 收尾直接回传已处理柱数」
这段 MQL5 片段是自定义指标里 OnCalculate 函数的末尾部分,参数列表里同时接收 tick_volume[]、volume[] 与 spread[] 三个引用数组,分别对应逐笔成交量、真实成交量与点差序列。 函数体内部没有额外计算逻辑,仅以注释分隔后直接 return(rates_total)。rates_total 是 MT5 传给函数的当前图表总柱数,把它原样回传,意味着告诉终端:本次调用已处理完所有可用 K 线,下次新 tick 进来时 prev_calculated 会拿到这个值,避免重复计算。 在 MT5 里若指标逻辑尚未写全就先这样返回,图表能正常加载但不会有任何画线输出;外汇与贵金属品种点差跳动频繁,spread[] 数组在部分经纪商环境下可能含 0 值,调试时需在市场报价窗口核对点差刷新是否正常。
class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class=class="str">"cmt">//--- class=class="str">"cmt">//--- 返回 prev_calculated 的值用于下一次调用 class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+
把指标塞进EA里的类封装思路
在EA策略里反复调用不同参数的同一个指标,用OOP从CIndicator派生来做封装比每次iCustom拉引用更顺手。核心动作就四件:用CArrayBuffer管缓冲区、把OnCalculate里的算价逻辑搬进类的Calculate、在LoadHistory里自行下载时间序列、再用CopyBuffer统一吐数据。 缓冲区直接用CArrayDouble扩出CArrayBuffer,加上CopyBuffer、Initilize和Shift三个方法,新烛形来了Shift把数据往前挪一格,接数据体验和原生指标引用几乎一样。父类CIndicator把m_source_data、ar_IndBuffers、m_buffers、m_history_len这些全丢到protected,方法全写成虚函数,具体指标类想改就改。 初始化时先查交易品种在不在市场报价,不在就尝试Select,失败直接返false。设缓冲区大小若传0或负数,就当要用全部历史,函数里会反查时段历史够不够,不够就加量下载再清缓冲改尺寸。LoadHistory无参,开盘新柱才重载,避免用未闭合烛形的跳动值发假信号——这是很多均线策略踩过的坑。 CopyBuffer在取数前先调虚函数重算,缓冲号错了清数组返-1,对了就调对应缓冲的CopyBuffer。MA演示类CMA重写Calculate,只在新柱触发重算,新柱数超缓冲就Reinit,少就Shift后只补算新元素;首算或掉线后元素超尺寸用Add,否则Update。MA所需历史深度要比缓冲多至少一个周期,父类校验不够就拒。 下面这段是缓冲与父类的骨架声明,public方法留接口、protected留变量,虚函数留给具体指标填肉。开MT5建个空白EA把这两段先挂上,比直接读iCustom大概率少一次跨模块调用。
class CArrayBuffer : class="kw">public CArrayDouble { class="kw">public: CArrayBuffer(class="type">void); ~CArrayBuffer(class="type">void); class=class="str">"cmt">//--- class="type">int CopyBuffer(class="kw">const class="type">int start, class="kw">const class="type">int count, class="type">class="kw">double &double_array[]); class="type">int Initilize(class="type">void); class="kw">virtual class="type">bool Shift(class="kw">const class="type">int shift); }; class CIndicator { class="kw">private: class=class="str">"cmt">//--- class="type">class="kw">datetime m_last_load; class="kw">public: CIndicator(class="type">void); ~CIndicator(class="type">void); class="kw">virtual class="type">bool Create(class="kw">const class="type">class="kw">string symbol=NULL, class="kw">const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE); class=class="str">"cmt">//--- 设置指标的主要配置