从零开始在MQL5中实现移动平均线:简单明了(基础篇)
📘

从零开始在MQL5中实现移动平均线:简单明了(基础篇)

第 1/3 篇

◍ 用 MQL5 画出第一条均线

在 MT5 里手写一条移动平均线,比很多人想的更直接。核心只用 OnCalculate 把价格序列喂给 iMA 的底层逻辑,或者干脆用内置指标句柄拿缓冲数组,省去自己写滑动窗口。 下面这段是最精简的自定义指标骨架:声明一个双精度缓冲,绑定到图表,周期里用 iMA 取得句柄后 CopyBuffer 把值搬进缓冲。注意第 3 参数 0 代表主缓冲,改 1 就是信号线缓冲(若存在)。 实盘里把周期设 14、平滑用 MODE_SMA,加载到 EURUSD 的 H1,你能立刻看到 745 次社区下载的同款基础逻辑跑出来的线——外汇与贵金属杠杆高,验证时先用策略测试器历史数据,别直接上真实账户。

MQL5 / C++
class="type">int handle;
class="type">class="kw">double buffer[];
class="type">int OnInit()
{
   handle = iMA(_Symbol, _Period, class="num">14, class="num">0, MODE_SMA, PRICE_CLOSE);
   SetIndexBuffer(class="num">0, 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">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[])
{
   CopyBuffer(handle, class="num">0, class="num">0, rates_total, buffer);
   class="kw">return rates_total;
}

「四种均线的算法与提速写法」

MT5 里常见的均线分四类:SMA 简单平均、EMA 指数加权、SMMA 平滑、LWMA 线性加权。它们核心差异在权重分配,SMA 对所有样本等权,EMA 给近期更高权重,SMMA 近似 EMA 但初值处理不同,LWMA 按距离线性给权。 以 EMA 为例,标准递推式为 MA[i] = Price[i]*k + MA[i-1]*(1-k),其中 k=2/(N+1)。在逐根 K 线计算时,若每 tick 都从头重算全序列,开销随历史长度线性增长。 计算优化通常做法是缓存上一根结果的 MA[i-1],新 tick 只更新最后若干项;对 SMA 可用滑动窗口和,避免每点重加 N 个价格。这样在 1 分钟图加载 5000 根、周期 50 时,重绘耗时可能从毫秒级降到微秒级。 打开 MT5 的「导航—指标—自定义」新建脚本,把下面 EMA 递推片段贴入 OnCalculate,改 Period 参数即可对比优化前后耗时。外汇与贵金属杠杆高,均线仅是概率参考,实盘须控仓。

均线到底是什么,为什么绕不开

移动平均线算的是某一货币对在设定周期内的平均价,用一串K线把价格轨迹压平来看。它是技术指标里资历最老的一支,大量派生指标(如MACD、布林带)都建立在它的计算逻辑之上。 在 MT5 里写指标、脚本或 EA,几乎都会碰到均线计算。以简单移动平均线(SMA)为例,它就是对一段数值序列取算术平均,数学上毫无门槛,但用来判趋势方向、滤掉小幅噪音非常实用。外汇和贵金属波动快、噪音多,均线的高风险环境里只是辅助,不是信号本身。 有意思的是它不止用在盘面上:气象学做气温平滑、经济学看景气均值、连一些机器学习的时间序列特征工程也会塞进移动平均。对交易者来说,它是一把基础但锋利的刀,用得好可能显著改善你的数据分析与进出场逻辑。

◍ SMA 的算术本质与 MT5 循环坑

简单移动平均线就是最近 N 根 K 线收盘价的算术平均。公式写成 SMA = SUM(CLOSE(i), N) / N,每根 K 线上的值等于它左边 N 根收盘价加总再除以周期。周期拉到 10 以上时,它对价格拐头的反应明显滞后,这是它和内嵌 EMA 类指标最大的体感差异。 下面这段是最直白的裸写循环,能跑但极耗资源:

MQL5 / C++
class="type">class="kw">double result=class="num">0;
for(class="type">int j=class="num">0; j<period; j++)
  result+=close[i-j];
result/=period;
逐行看:result 先清零;j 从 0 跑到 period-1,把当前 K 线 i 及其左侧共 period 根收盘价累加进 result;最后除以 period 得到均值。问题在主循环每根 K 线都嵌一个 j 循环,历史一长,每 tick 重算全量,MT5 日志很快弹「计算速度过慢」。 优化第一步是用 OnCalculate 的 prev_calculated 控起点。首次启动它等于 0,主循环从 period-1 那根开始(保证左边有够数 K 线);之后 prev_calculated 存了已算根数,新 K 线来了只从 prev_calculated-1 算起。这样避免了不变 K 线的重复计算,但当前 K 线每来一个 tick 仍跑一遍内循环求和。 更狠的剪枝是利用滑动窗口:首根 SMA 照常循环求和,之后每右移一根,只需「上一根 SMA 值 - 最左那根收盘价/period + 当前收盘价/period」。以周期 5、价格 2/4/6/8/10 为例,首值 6;下一根若收盘 12,则新值 = 6 - 2/5 + 12/5 = 8。省掉内循环,外汇与贵金属行情跳空多,这种写法在 M15 以上周期回测更稳,但高频 tick 下仍需注意点差滑点带来的高风险。

MQL5 / C++
class="type">class="kw">double result=class="num">0;
for(class="type">int j=class="num">0; j<period; j++)
  result+=close[i-j];
result/=period;

「手搓一个会在主图拖慢 MT5 的 SMA 指标」

上面这段自定义指标把 10 周期简单移动平均画在主图上,逻辑直白:从 InpPeriod 根 K 线开始,每根都现场把前 10 根 close 加一遍再除以 10,写进 MABuffer。 问题在实测——挂在 EURUSD 这类品种跑完整历史,终端报 indicator is too slow,耗时 66141 ms(约 66 秒)。根因是 OnCalculate 里外层遍历全部 bars、内层又重算窗口,复杂度 O(n·period),历史越长越卡。外汇与贵金属杠杆高、波动剧烈,指标卡顿可能让你漏掉入场信号,属高风险场景。 逐行看编译指令与计算骨架: #property version "1.00" → 指标版本号 #property indicator_chart_window → 画在主图而非独立子窗口 #property indicator_buffers 1 → 只声明 1 个数据缓冲区 #property indicator_plots 1 → 只画 1 条图线 #property indicator_label1 "SMA" → 图例名 SMA #property indicator_type1 DRAW_LINE → 线型绘制 #property indicator_color1 clrRed → 红色 #property indicator_style1 STYLE_SOLID → 实线 #property indicator_width1 1 → 线宽 1 input int InpPeriod = 10; → 外部参数,SMA 周期默认 10 double MABuffer[]; → 指标数据数组 int OnInit() → 初始化:SetIndexBuffer(0,MABuffer,INDICATOR_DATA) 绑定缓冲,return(INIT_SUCCEEDED) int OnCalculate(...) → 主循环 for(i=InpPeriod;i<rates_total;i++) 内层 for(j=0;j<InpPeriod;j++) result+=close[i-j]; 求平均后 MABuffer[i]=result,最后 return(rates_total) 开 MT5 把这段代码贴进自定义指标,用 Profile 看耗时;若你也跑出 6 万毫秒级别,就证明该重写成增量计算——下一节直接给改写版。

MQL5 / C++
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">//--- plot MA
class="macro">#class="kw">property indicator_label1  "SMA"
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">//--- input parameters
input class="type">int      InpPeriod  =  class="num">10;   class=class="str">"cmt">// SMA Period
class=class="str">"cmt">//--- indicator buffers
class="type">class="kw">double        MABuffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator initialization function                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- indicator buffers mapping
   SetIndexBuffer(class="num">0,MABuffer,INDICATOR_DATA);

class=class="str">"cmt">//--- successful initialization
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
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">//--- in the loop from the value of InpPeriod to the current history bar
   for(class="type">int i=InpPeriod; i<rates_total; i++)
     {
      class=class="str">"cmt">//--- calculate average close prices for InpPeriod bars
      class="type">class="kw">double result=class="num">0;
      for(class="type">int j=class="num">0; j<InpPeriod; j++)
         result+=close[i-j];
      result/=InpPeriod;

      class=class="str">"cmt">//--- Write the calculation result to the buffer
      MABuffer[i]=result;
     }
class=class="str">"cmt">//--- class="kw">return value of prev_calculated for the next call
   class="kw">return(rates_total);
  }

OnCalculate 的两种重载与首跑判定

自定义指标在 MT5 里必须实现 OnCalculate,系统提供两种重载:单数组版(只给 price[])和 OHLCV 全量版(time/open/high/low/close/tick_volume/volume/spread 全传)。前者常用于只盯收盘价序列的轻量指标,后者在需要实体高低或成交分布时更直接。 首跑与增量计算的分水岭是 prev_calculated:等于 0 代表首次加载或历史被改写,这时用 ArrayInitialize(MABuffer,EMPTY_VALUE) 清空缓冲,并把 start 设为 Period,跳过前 Period 根不足量的 bar。 若 prev_calculated 非 0,说明已有历史结果,start 回退一根(prev_calculated-1)即可从当前 bar 续算,避免每 tick 重跑全序列——外汇与贵金属 tick 密集,这个写法直接决定 CPU 占用与刷新延迟。 下面这段是两种函数签名与首跑分支的原文骨架,逐行对应上面逻辑:

MQL5 / C++
class="type">int OnCalculate(const class="type">int rates_total,      class=class="str">"cmt">// price[] array size
                  const class="type">int prev_calculated,   class=class="str">"cmt">// handled bars at the previous call
                  const class="type">int begin,             class=class="str">"cmt">// where significant data starts
                  const class="type">class="kw">double& price[]        class=class="str">"cmt">// array for calculation
   );
class="type">int OnCalculate(const class="type">int rates_total,      class=class="str">"cmt">// size of input time series
                  const class="type">int prev_calculated,   class=class="str">"cmt">// handled bars at the previous call
                  const class="type">class="kw">datetime& time[],      class=class="str">"cmt">// Time
                  const class="type">class="kw">double& open[],        class=class="str">"cmt">// Open
                  const class="type">class="kw">double& high[],        class=class="str">"cmt">// High
                  const class="type">class="kw">double& low[],         class=class="str">"cmt">// Low
                  const class="type">class="kw">double& close[],       class=class="str">"cmt">// Close
                  const class="type">long& tick_volume[],   class=class="str">"cmt">// Tick Volume
                  const class="type">long& volume[],        class=class="str">"cmt">// Real Volume
                  const class="type">int& spread[]          class=class="str">"cmt">// Spread
   );
class=class="str">"cmt">//--- If this is the first run or a change in historical data
   if(prev_calculated==class="num">0)
     {
      class=class="str">"cmt">//--- initialize the indicator buffer with an empty value and
      class=class="str">"cmt">//--- set the start of the calculation to the Period value
      ArrayInitialize(MABuffer,EMPTY_VALUE);
      start=Period;
     }
class=class="str">"cmt">//--- If this is not the first run, the calculation will start at the current bar
   else
      start=prev_calculated-class="num">1;

常见问题

新建自定义指标,在 OnCalculate 里用 iMA 取均线句柄并拷贝到缓冲数组,设好画图和颜色就能在主图显示。
常见坑是每根 K 线都从头算一遍均值,应只算新增一根的增减量,或改用内置 iMA 句柄避免重复循环。
可以,小布能读取你的指标逻辑并提示循环冗余、首跑判定缺失等拖慢问题,给出改写建议。
SMA 是算术平均对所有样本等权,EMA 给近期更高权重,所以 EMA 对价格转向反应更快、滞后更小。
用带 rates_total 与 prev_calculated 的重载,靠 prev_calculated==0 做首跑全量初始化,之后只补算增量。