从基础到中级:指标(二)·进阶篇
(2/3)· 还在用冗长脚本硬算均线?本篇用更少的代码行在MT5里复刻常见移动平均系统
◍ 让指标参数在图表上可调
写死周期数的指标没人爱用。前面那段指数均线代码里,平滑系数写的是 2/(1+9),9 就是 EMA 周期,改它就得重编译,实在笨拙。MQL5 给我们留了口子:把某些内部值声明成「对用户是变量、对程序是常量」的输入项,挂上图表后直接在设置页填数即可。 先只在原代码第 21 行位置补一行 input int Period = 9;,编译后 MT5 会在指标属性里多出一个「输入」选项卡——这正对应代码里声明的 input 常量。此时用户能改周期,但选项卡里只显示变量名 Period,堆多了根本分不清谁是谁。 诡异点在后头:想让选项卡显示中文说明,得在常量声明行尾接一段字符串注释,形如 input int Period = 9; // 指数均线周期。编译器会把这个注释文本直接透出到用户输入界面。加上去之后再开属性页,原本光秃秃的 Period 就变成了带字面解释的输入框,接手你代码的人也不会抓瞎。 外汇与贵金属波动剧烈、杠杆风险高,这类可调参数只解决交互便利,不预示任何方向。开 MT5 把上面两行改完拖到 EURUSD 图表,切到输入选项卡确认能看到注释文字,就算跑通了这一节。
class=class="str">"cmt">// 原代码 class="num">04 关键片段(仅示意 Period 声明位置) class="type">int OnInit() { SetIndexBuffer(class="num">0, gl_buffer, INDICATOR_DATA); class="kw">return INIT_SUCCEEDED; } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { class="type">class="kw">double k = class="num">2. / (class="num">1 + class="num">9); for(class="type">int c = (prev_calculated > class="num">0 ? prev_calculated - class="num">1 : class="num">0); c < rates_total; c++) gl_buffer[c] = (c ? ((price[c] - gl_buffer[c-class="num">1]) * k) + gl_buffer[c-class="num">1] : price[c] * k); class="kw">return rates_total; } class=class="str">"cmt">// 代码 class="num">05:加入用户输入 input class="type">int Period = class="num">9; class=class="str">"cmt">// 指数均线周期 class=class="str">"cmt">// 代码 class="num">06 第 class="num">10 行实际写法: input class="type">int Period = class="num">9; class=class="str">"cmt">// 指数均线周期
「用 9 行核心代码手搓一条指数均线」
MT5 自定义指标里,一条指数移动平均(EMA)并不需要依赖内置 iMA 函数,上面这段源码把最小可用骨架拆得很干净:1 个绘图缓冲、1 个 plot、周期参数 user01 默认 9。 平滑系数 k 在第 23 行算出来:k = 2 / (1 + user01),当 user01=9 时 k=0.2,意味着最新价权重占 20%,前一期均值占 80%,这是标准 EMA 递推关系。 OnCalculate 里从第 25 行起做增量计算:若 prev_calculated>0 则从上一根已算位置减 1 续算,避免每 tick 重跑全样本;第 26 行用三元表达式处理首根(c=0 时直接 price[0]*k)与后续递推。 把 user01 改成 21 或 55,重载指标,肉眼能看出均线滞后明显加大;外汇与贵金属杠杆高,参数调大不代表信号更稳,回测前先想清楚周期适配的波动环境。
class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_color1 clrBlack 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=class="str">"cmt">//+----------------+ input class="type">uchar user01 = class="num">9; class=class="str">"cmt">//+----------------+ class="type">class="kw">double gl_buffer[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { SetIndexBuffer(class="num">0, gl_buffer, INDICATOR_DATA); class="kw">return INIT_SUCCEEDED; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { class="type">class="kw">double k = class="num">2. / (class="num">1 + user01); for (class="type">int c = (prev_calculated > class="num">0 ? prev_calculated - class="num">1 : class="num">0); c < rates_total; c++) gl_buffer[c] = (c ? ((price[c] - gl_buffer[c - class="num">1]) * k) + gl_buffer[c - class="num">1] : price[c] * k); class="kw">return rates_total; }; class=class="str">"cmt">//+------------------------------------------------------------------+
OnCalculate 第二版里把数据源焊死在收盘价上
MQL5 里 OnCalculate 是唯一被重载的标准库函数。比起第一版只拿到已经算好的指标缓冲,第二版直接把 Open、High、Low、Close、TickVolume、Volume、Spread 这些原始序列全塞进参数里(见代码第 21 行)。好处是数据更全,坑是 MT5 不再替你兜底——界面选项卡会丢掉「适用价格」那一项,因为你在代码里把计算锚死了。 代码 07 那版直接拿 Close[] 算指数平均,用户再也没法在属性里换最高价或最低价。实测拖上图后,指标属性窗比原来少一个「应用于」下拉页(原文图 06)。这不是 bug,是你自己把输入类型写死造成的。 想把这权利交还用户,又不想退回第一版,最干净的做法是上枚举。先声明一个 enum 列出 PRICE_CLOSE、PRICE_OPEN 等项,再把 input 参数从 uchar 改成 const enum 类型(代码 09 第 18 行)。编译能过,终端里下拉框就回来了(图 08)。 但 MT5 不帮你转发用户选择,得自己在 OnCalculate 里接。代码 10 第 32 行建 price 变量,第 36 行用 switch 把枚举映射到具体数组,第 51 行把原本写死的 Close[i] 换成 price[i]。这样外汇、贵金属图表上跑的指标就能按用户选的价型重算,不过这类品种点差跳变频繁,切换数据源后信号可能明显漂移,属于高风险操作。 最后给枚举每项加字符串注释(代码 11),用户看到的是「收盘价」「开盘价」而非数字 0、1。对你而言规则变多、MT5 援助变少,但指标可控性上了一个台阶。
<span class="number">class="num">01</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">02</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span> <span class="number">class="num">03</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">04</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_type1</span> <span class="macro">DRAW_LINE</span> <span class="number">class="num">05</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_color1</span> <span class="macro">clrBlack</span> <span class="number">class="num">06</span>. <span class="comment">class=class="str">"cmt">//+----------------+</span> <span class="number">class="num">07</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_buffers</span> <span class="number">class="num">1</span> <span class="number">class="num">08</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">indicator_plots</span> <span class="number">class="num">1</span> <span class="number">class="num">09</span>. <span class="comment">class=class="str">"cmt">//+----------------+</span> <span class="number">class="num">10</span>. <span class="keyword">input</span> <span class="keyword">class="type">uchar</span> user01 = <span class="number">class="num">9</span>; <span class="comment">class=class="str">"cmt">//Exponential average periods</span> <span class="number">class="num">11</span>. <span class="comment">class=class="str">"cmt">//+----------------+</span> <span class="number">class="num">12</span>. <span class="keyword">class="type">class="kw">double</span> gl_buffer[]; <span class="number">class="num">13</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">14</span>. <span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() <span class="number">class="num">15</span>. { <span class="number">class="num">16</span>. <span class="functions">SetIndexBuffer</span>(<span class="number">class="num">0</span>, gl_buffer, <span class="macro">INDICATOR_DATA</span>); <span class="number">class="num">17</span>. <span class="number">class="num">18</span>. <span class="keyword">class="kw">return</span> <span class="macro">INIT_SUCCEEDED</span>; <span class="number">class="num">19</span>. }; <span class="number">class="num">20</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">21</span>. <span class="keyword">class="type">int</span> <span class="functions">OnCalculate</span>(<span class="keyword">const</span> <span class="keyword">class="type">int</span> rates_total, <span class="keyword">const</span> <span class="keyword">class="type">int</span> prev_calculated, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">datetime</span> &Time[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &Open[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &High[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &Low[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &Close[], <span class="keyword">const</span> <span class="keyword">class="type">long</span> &TickVolume[], <span class="keyword">const</span> <span class="keyword">class="type">long</span> &Volume[], <span class="keyword">const</span> <span class="keyword">class="type">int</span> &Spread[]) <span class="number">class="num">22</span>. { <span class="number">class="num">23</span>. <span class="keyword">class="type">class="kw">double</span> k = <span class="number">class="num">2</span>. / (<span class="number">class="num">1</span> + user01); <span class="number">class="num">24</span>.
◍ 枚举切换与逐根递推的落地写法
把平滑数据源做成可切换项,比写死 Close 更实用。下面这段枚举定义允许在 HIGH / OPEN / CLOSE / LOW 之间挑计算基准,外部参数 user02 直接绑定到枚举类型,MT5 输入栏会下拉呈现,不用手填数字。 初始版本里 user02 默认 HIGH,后续修订改成了 CLOSE,说明作者回测后倾向用收盘价算指数平滑,避免影线噪声干扰。外汇与贵金属波动受数据影线影响大,用 HIGH 算出的线可能在上影线处过度抖动,切换 CLOSE 后曲线更贴合实体走势。 核心递推在 OnCalculate 的循环里:prev_calculated 大于 0 时从倒数第二根续算,否则从 0 开始;每根新值 = 上一根缓冲值 + (当前 Close - 上一根缓冲值) * k,k 由 user01 周期推导(原文默认 9 期)。这种写法只补算新增 K 线,历史重载时 CPU 占用明显低于全量重算。 return rates_total 告诉终端已处理完所有柱,缓冲区 gl_buffer 直接被绘图属性 DRAW_LINE 接管显示。把 user01 从 9 改成 21,再切 user02 到 LOW,开 EURUSD 的 M15 能直观比对两种参数下曲线的滞后差异。
for(class="type">int c=(prev_calculated>class="num">0?prev_calculated-class="num">1:class="num">0);c<rates_total;c++) gl_buffer[c]=(c?((Close[c]-gl_buffer[c-class="num">1])*k)+gl_buffer[c-class="num">1]:Close[c]*k); class="kw">return rates_total; }; class=class="str">"cmt">//+------------------------------------------------------------------+ enum Enum_TypeInput { HIGH, OPEN, CLOSE, LOW }; input class="type">uchar user01 = class="num">9; class=class="str">"cmt">//Exponential average periods input Enum_TypeInput user02 = CLOSE; class=class="str">"cmt">//Data type for calculation