探索新功能:MQL5 的自定义指标(基础篇)
◍ 在 MT5 里写第一个自定义指标
MQL5 把指标和计算逻辑彻底拆开,自定义指标不再像 MQL4 那样挤在脚本里,而是独立编译成 ex5 挂到图表。对做价格行为分析的人,这意味着你能把一套形态识别逻辑直接变成副图,不用每次手动标。 这套机制从 MT5 上线就开放,官方示例库里最早的一批自定义指标文档发布于 2013-08-21,至今在终端里仍能编译运行,说明接口相对稳定。 想在 MT5 验证很简单:打开 MetaEditor,新建 Custom Indicator,选空模板,编译后拖到 EURUSD 图表,就能看到副图容器。外汇和贵金属杠杆高、滑点大,指标只是概率参考,别当成进出场唯一依据。
「MT5 到底新在哪」
MetaTrader 5 这套终端值得老交易者专门切过去试一次,核心变化不在界面,而在底层的 MQL5 语言和执行环境。它从根上改了编程语言,既能走结构化老路,也正式放开了面向对象编程,这对写复杂策略的人是实打实的利好。 相比 MT4,MQL5 的代码执行速度快了一个量级,回测和实时计算的卡顿明显更少;同时终端显示必要信息的扩展潜力也比以前强太多,自定义面板和图形对象不再挤在有限接口里。 本文不堆砌所有新特性,也不在这节放 OOP 代码——那部分太重,得单开章节讲。接下来我们只拆指标:MQL5 指标的结构、图形、类型与编程细节,和 MQL4 的差异会落到具体代码上。 随附文件可以直接丢进终端编译运行,无论你是刚摸 MT5 还是写了几年 EA,都能当场验证文里的点,外汇和贵金属品种波动大、杠杆高,跑之前先在小账户或策略测试器里确认风险。
MT5指标骨架:三大函数与属性定义
从 MQL4 切到 MQL5,指标的总体结构并没有颠覆:依然由初始化、计算、去初始化三个核心函数撑起整个生命周期。许多显示层面的设定可以靠 #property 在全局上下文直接声明,输入参数也照旧用 input 标识符写在文件头部,和旧版写法一脉相承。 一个实用变化是,input 现在支持枚举类型,下拉框会列出所有合法值,能有效挡掉手误填错参数的情况。上面代码里 AppliedPrice 用 ENUM_APPLIED_PRICE,自定义的 DayOfWeek 枚举也会进同一个下拉列表,开 MT5 挂上指标就能看到效果。 OnInit 里做缓冲区绑定和句柄分配,OnCalculate 负责逐根 K 线算值,OnDeinit 一般只用来释放文件句柄这类资源,多数简单指标可以留空。下面这段删节自一个彩色 RSI 实例,重点看属性声明和输入参数怎么铺。 代码里 indicator_buffers 设了 2,但 indicator_plots 只设 1,说明第二个缓冲区是颜色索引而非独立画线;RSIPeriod 默认 5、SmoothPeriod 默认 5,都是可直接在外部面板改的。外汇与贵金属波动剧烈,指标仅作概率参考,实盘前务必在策略测试器跑一遍验证。
class=class="str">"cmt">//--- 数据属性部分 class="macro">#class="kw">property copyright "TheXpert" class="macro">#class="kw">property link "theforexpert@gmail.com" class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--- 指标的描述一共不能超过511个字符 class=class="str">"cmt">//--- 包括换行符在内 class="macro">#class="kw">property description " " class="macro">#class="kw">property description " 根据彩色相对强弱指数指标(RSI)实例" class="macro">#class="kw">property description " 的指标创建演示。" class=class="str">"cmt">//--- 指标属性 class="macro">#class="kw">property indicator_separate_window class=class="str">"cmt">// 指标在单独子窗口中显示 class="macro">#class="kw">property indicator_buffers class="num">2 class=class="str">"cmt">// 使用缓冲区的数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">// 显示缓冲区的数量 class=class="str">"cmt">//--- 绘图1 class="macro">#class="kw">property indicator_color1 clrDarkSalmon, clrDeepSkyBlue class=class="str">"cmt">// 使用两种颜色 class="macro">#class="kw">property indicator_type1 DRAW_COLOR_LINE class=class="str">"cmt">// 显示样式 class=class="str">"cmt">//---- 缓冲区 class="type">class="kw">double Values[]; class=class="str">"cmt">// 数值缓冲区 class="type">class="kw">double ValuesPainting[]; class=class="str">"cmt">// 颜色索引缓冲区 class=class="str">"cmt">//--- 指标输入参数 input class="type">class="kw">string _1 = "RSI parameters"; input class="type">int RSIPeriod = class="num">5; input class="type">int SmoothPeriod = class="num">5; input ENUM_APPLIED_PRICE AppliedPrice = PRICE_CLOSE; input class="type">class="kw">string _2 = "Color settings"; input class="type">color Down = clrDarkSalmon; input class="type">color Up = clrDeepSkyBlue; class=class="str">"cmt">//---存放指标句柄的变量 class="type">int RSIHandle; class=class="str">"cmt">//... enum DayOfWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; input DayOfWeek Day; class=class="str">"cmt">//... class="type">int OnInit() { class=class="str">"cmt">//--- 绑定缓冲区 class=class="str">"cmt">//--- 用于做显示缓冲区的数值 SetIndexBuffer(class="num">0,Values,INDICATOR_DATA); class=class="str">"cmt">//--- 于做保存颜色缓冲区的数值 SetIndexBuffer(class="num">1,ValuesPainting,INDICATOR_COLOR_INDEX); class=class="str">"cmt">//--- 设置绘图缓冲区起点 PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,RSIPeriod); class=class="str">"cmt">//--- 设置指标名称 IndicatorSetString(INDICATOR_SHORTNAME,"Color RSI("+class="type">class="kw">string(RSIPeriod)+")"); class=class="str">"cmt">//--- 设置绘图的空值
◍ RSI双色绘制与增量计算内核
在 MT5 自定义指标里,用 PlotIndexSetInteger 给 0 号绘图索引分别绑定索引 0(Down)与索引 1(Up)两种线色,就能让 RSI 曲线随涨跌自动换色。空值先交给 PLOT_EMPTY_VALUE 接 EMPTY_VALUE,避免历史缺口被误绘成零轴横线。 初始化阶段用 iRSI(NULL,0,RSIPeriod,AppliedPrice) 拿句柄,当前图表、当前周期由 NULL 与 0 指定;两个数组 ArraySetAsSeries(...,true) 改成时间倒序,保证 Values[0] 永远是最新一根 K。 OnCalculate 里 toCount 取 rates_total 与 rates_total-prev_calculated+1 的较小值,这是典型的增量计算写法——只处理新增 K 线,老数据不重复跑。CopyBuffer 拉 RSI 缓冲若返回 -1,直接 Print 错误码并 return(0) 等下一 tick 重算。 颜色判定循环从 toCount-2 倒退到 0:当 Values[i] > Values[i+1] 且前值非 EMPTY_VALUE 时,ValuesPainting[i] 置 1(涨色),否则置 0(跌色)。最后 return(rates_total),把总量回传成下次的 prev_calculated,外汇与贵金属波动大,这种写法在 1 分钟图负载会明显低于全量重算。
PlotIndexSetDouble(class="num">0,PLOT_EMPTY_VALUE,EMPTY_VALUE); class=class="str">"cmt">//--- 设置绘图颜色 PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,class="num">0,Down); PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,class="num">1,Up); class=class="str">"cmt">//--- 获取指标句柄 RSIHandle=iRSI(NULL,class="num">0,RSIPeriod,AppliedPrice); class=class="str">"cmt">//--- 设置缓冲区索引方向 ArraySetAsSeries(Values,true); ArraySetAsSeries(ValuesPainting,true); class=class="str">"cmt">//--- 成功执行 class="kw">return(class="num">0); } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- 用于计算的柱数 class="type">int toCount=(class="type">int)MathMin(rates_total,rates_total-prev_calculated+class="num">1); class=class="str">"cmt">//--- 尝试复制iRSI指标数据 if(CopyBuffer(RSIHandle,class="num">0,class="num">0,toCount,Values)==-class="num">1) { Print("数据复制错误, 错误码为",GetLastError()); class=class="str">"cmt">//--- 返回重新计算指标数据 class="kw">return(class="num">0); } class=class="str">"cmt">//--- 计算颜色部分 for(class="type">int i=toCount-class="num">2;i>=class="num">0;--i) { class=class="str">"cmt">//--- 计算第一条线的颜色 if(Values[i+class="num">1]!=EMPTY_VALUE && Values[i]>Values[i+class="num">1]) ValuesPainting[i]=class="num">1; else ValuesPainting[i]=class="num">0; } class=class="str">"cmt">//--- 返回值为下一次调用的 prev_calculated 值 class="kw">return(rates_total); } class=class="str">"cmt">//--- 此函数不是必需的 class=class="str">"cmt">/* class="type">void OnDeinit() { } */
「MQL5 指标的两套数据接法」
从 MQL4 切到 MQL5,指标骨架先变一件事:原来靠 Start() 跑循环,现在统一交给 OnCalculate()。图表上的 OHLC、成交量、点差直接以数组引用塞进参数,不用再手写 CopyBuffer 去拉数据,代码量肉眼可见地降下来。 OnCalculate 第一个参数 rates_total 是数组总长度,第二个 prev_calculated 就是老版 IndicatorCounted() 的替代物——首次加载或补历史数据时被传 0,之后只传新增柱数。这个设计让增量计算变顺,但刚转过来的老手容易在 prev_calculated=0 的分支里漏掉初始化。 另一套概念是 i<…>OnArray 的扩展:指标不只能吃图表价格,还能吃别的指标句柄。参数 applied_price_or_handle 里传句柄,就能把 RSI 的输出喂给 MA 再平滑一层。终端自带的 Custom Moving Average 就是这么干的,三层嵌套能拿到三倍平滑度。 把零缓冲区当主输出,还能写「服务指标」——比如 Direction_Brush.mq5 把方向着色单独拆出来,挂到别的指标上就用。零缓冲区通用性有限,但写自定义指标时把核心信息放零缓冲,外围统计、信号、着色全丢给外部服务指标,主代码能干净不少。外汇和贵金属波动大、杠杆高,这类堆叠指标在跳空时可能延迟刷新,实盘前务必在 MT5 用历史数据跑一遍句柄链。
class="type">int OnCalculate(const class="type">int rates_total, class=class="str">"cmt">// 数组大小 const class="type">int prev_calculated, class=class="str">"cmt">// 上一次调用计算的柱数 const class="type">class="kw">datetime& time[], class=class="str">"cmt">// 当前图表和周期下的数据 const class="type">class="kw">double& open[], const class="type">class="kw">double& high[], const class="type">class="kw">double& low[], const class="type">class="kw">double& close[], const class="type">long& tick_volume[], const class="type">long& volume[], const class="type">int& spread[]) { class="kw">return rates_total; } class="type">int OnCalculate(const class="type">int rates_total, class=class="str">"cmt">// price[] 数组的大小 const class="type">int prev_calculated, class=class="str">"cmt">// 上一次调用计算的柱数 const class="type">int begin, class=class="str">"cmt">// 数据计算起点 const class="type">class="kw">double& price[] class=class="str">"cmt">// 用于计算的数据数组 ) { class="kw">return rates_total; } { class=class="str">"cmt">//... RSIHandle = iRSI(NULL, class="num">0, RSIPeriod, AppliedPrice); SmoothHandle = iMA(NULL, class="num">0, SmoothPeriod, class="num">0, MODE_EMA, RSIHandle); class=class="str">"cmt">//... }