从基础到中级:指标(二)·进阶篇
📈

从基础到中级:指标(二)·进阶篇

(2/3)· 还在用冗长脚本硬算均线?本篇用更少的代码行在MT5里复刻常见移动平均系统

进阶 第 2/3 篇
不少交易者照着入门示例画完线就以为懂了指标开发,结果面对真实均线系统仍无从下手。把简单绘图当终点,会让你错过MT5事件机制里更优雅的计算资源。接上篇,我们继续深挖如何少写代码做对事。

◍ 让指标参数在图表上可调

写死周期数的指标没人爱用。前面那段指数均线代码里,平滑系数写的是 2/(1+9),9 就是 EMA 周期,改它就得重编译,实在笨拙。MQL5 给我们留了口子:把某些内部值声明成「对用户是变量、对程序是常量」的输入项,挂上图表后直接在设置页填数即可。 先只在原代码第 21 行位置补一行 input int Period = 9;,编译后 MT5 会在指标属性里多出一个「输入」选项卡——这正对应代码里声明的 input 常量。此时用户能改周期,但选项卡里只显示变量名 Period,堆多了根本分不清谁是谁。 诡异点在后头:想让选项卡显示中文说明,得在常量声明行尾接一段字符串注释,形如 input int Period = 9; // 指数均线周期。编译器会把这个注释文本直接透出到用户输入界面。加上去之后再开属性页,原本光秃秃的 Period 就变成了带字面解释的输入框,接手你代码的人也不会抓瞎。 外汇与贵金属波动剧烈、杠杆风险高,这类可调参数只解决交互便利,不预示任何方向。开 MT5 把上面两行改完拖到 EURUSD 图表,切到输入选项卡确认能看到注释文字,就算跑通了这一节。

MQL5 / C++
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,重载指标,肉眼能看出均线滞后明显加大;外汇与贵金属杠杆高,参数调大不代表信号更稳,回测前先想清楚周期适配的波动环境。

MQL5 / C++
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 援助变少,但指标可控性上了一个台阶。

MQL5 / C++
<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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>&nbsp;&nbsp; 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>.&nbsp;&nbsp;&nbsp;&nbsp;<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>.&nbsp;&nbsp;&nbsp;&nbsp;<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> &amp;Time[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;Open[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;High[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;Low[], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;Close[], <span class="keyword">const</span> <span class="keyword">class="type">long</span> &amp;TickVolume[], <span class="keyword">const</span> <span class="keyword">class="type">long</span> &amp;Volume[], <span class="keyword">const</span> <span class="keyword">class="type">int</span> &amp;Spread[])
<span class="number">class="num">22</span>. {
<span class="number">class="num">23</span>.&nbsp;&nbsp;&nbsp;&nbsp;<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 能直观比对两种参数下曲线的滞后差异。

MQL5 / C++
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
把重复劳动交给小布
这些诊断与指标逻辑梳理,小布盯盘的AIGC已内置,打开对应品种页即可看到均线偏离与结构提示,你专注决策就好。

常见问题

交易量加权更反映成交密集区的价格重心,时间加权则单纯按周期数衰减历史值;指数移动平均属于按时间或周期数加权,选择取决于你关注的偏好。
那版只是事件触发下的简单输出,没有接入标准计算缓冲与OnCalculate的报价序列,所以形态和常见均线不同,需要本篇的改写思路。
可以,小布盯盘的AIGC模块能读取品种页上的结构,把你的均线偏离、交叉概率做成简报,省去手动刷图。
当你需要基于已有指标缓冲区而非原始价格序列计算时,第二个版本减少重复取值,逻辑更清晰,本篇第3节会具体展开。