从零开始在MQL5中实现移动平均线:简单明了(基础篇)
◍ 用 MQL5 画出第一条均线
在 MT5 里手写一条移动平均线,比很多人想的更直接。核心只用 OnCalculate 把价格序列喂给 iMA 的底层逻辑,或者干脆用内置指标句柄拿缓冲数组,省去自己写滑动窗口。 下面这段是最精简的自定义指标骨架:声明一个双精度缓冲,绑定到图表,周期里用 iMA 取得句柄后 CopyBuffer 把值搬进缓冲。注意第 3 参数 0 代表主缓冲,改 1 就是信号线缓冲(若存在)。 实盘里把周期设 14、平滑用 MODE_SMA,加载到 EURUSD 的 H1,你能立刻看到 745 次社区下载的同款基础逻辑跑出来的线——外汇与贵金属杠杆高,验证时先用策略测试器历史数据,别直接上真实账户。
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 类指标最大的体感差异。 下面这段是最直白的裸写循环,能跑但极耗资源:
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;
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 万毫秒级别,就证明该重写成增量计算——下一节直接给改写版。
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 占用与刷新延迟。 下面这段是两种函数签名与首跑分支的原文骨架,逐行对应上面逻辑:
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;