利用对象轻松制作复杂指标·综合运用
🧩

利用对象轻松制作复杂指标·综合运用

(3/3)·当 RSI 要叠 10 周期还要算均值,传统缓冲区写法让你在数组越界里debug到崩溃

偏理论 第 3/3 篇
把 10 个 RSI 的平均值画成一条线,老写法要声明 11 个双精度数组、配 11 个缓冲区、再手写循环累加——少一个属性就数组越界,多写一行就图 invisible。复杂指标最耗时的从来不是算法,而是和缓冲区声明搏斗。

把均线句柄和彩色K线塞进类里

在 MT5 自建指标里,把 iMA 句柄封装成 CMA 类,能避免每次 OnCalculate 重复申请指标句柄。Init 里用 iMA(NULL, PERIOD_CURRENT, period, 0, mode, PRICE_CLOSE) 建句柄,period 和 mode 由外部传入,比如 period=20、mode=MODE_SMA 就是常见的 20 周期平滑均线。 Update 的 limit 参数决定重算范围:limit==0 时一次性 CopyBuffer 拷贝全部 rates_total 根;否则只补算新来的 K 线,用 newVals[i-limit] 回填 array,省掉整段重拷。DeInit 调 IndicatorRelease(handle) 释放,防止退出时句柄泄漏。 CColorCandles 继承绘图基类,Update 接收 open/high/low/close 四数组引用,从 limit 开始只刷新增量写入自身 *_array。实盘跑 EURUSD 这种高波动品种时,增量更新比全量重算少占用约 30%–50% 的柱间 CPU 时间,但外汇和贵金属杠杆高,指标快不等于信号准,回测通过也只是概率优势。 别在 Init 之外反复调 iMA 句柄建一次就够了,重复建会拖慢 MT5 终端,尤其多周期加载时更容易掉帧。

MQL5 / C++
class="kw">virtual class="type">void      DeInit();
class="kw">virtual class="type">void      Update(class="kw">const class="type">int limit, class="kw">const class="type">int rates_total);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMA::Init(class="type">int period, ENUM_MA_METHOD mode)
{
  handle = iMA(NULL, PERIOD_CURRENT, period, class="num">0, mode, PRICE_CLOSE);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMA::Update(class="kw">const class="type">int limit,class="kw">const class="type">int rates_total)
{
  if (limit==class="num">0) CopyBuffer(handle, class="num">0, class="num">0, rates_total, array);
  else
  {
    class="type">class="kw">double newVals[];
    CopyBuffer(handle, class="num">0, class="num">0, rates_total-limit, newVals);
    for (class="type">int i=limit; i<rates_total; i++)
      array[i] = newVals[i-limit];
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMA::DeInit(class="type">void)
{
  IndicatorRelease(handle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Color Candles class (inherit from Color Candles Plot)             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CColorCandles : class="kw">public CIndicatorPlotColorCandles
{
class="kw">public:
  class="kw">virtual class="type">void      Update(class="kw">const class="type">int limit,
                           class="kw">const class="type">int rates_total,
                           class="kw">const class="type">class="kw">double &open[],
                           class="kw">const class="type">class="kw">double &high[],
                           class="kw">const class="type">class="kw">double &low[],
                           class="kw">const class="type">class="kw">double &close[]);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorCandles::Update(class="kw">const class="type">int limit,
                           class="kw">const class="type">int rates_total,
                           class="kw">const class="type">class="kw">double &open[],
                           class="kw">const class="type">class="kw">double &high[],
                           class="kw">const class="type">class="kw">double &low[],
                           class="kw">const class="type">class="kw">double &close[])
{
  for (class="type">int i=limit; i<rates_total; i++)
  {
    open_array[i] = open[i];
    high_array[i] = high[i];
    low_array[i] = low[i];
    close_array[i] = close[i];

◍ 用计数法给均线带染色

这段逻辑干的事很直接:拿 10 条均线(TOTAL_MA 宏定义为 10)逐一和通道上下轨比大小,高于上轨的记 +1,低于下轨的记 -1,最后把净计数写进 color_buffer。 int count_ma = TOTAL_MA; for (int m=0; m<TOTAL_MA; m++) { if (maIndicators[m].array[i] > bands.first_array[i]) count_ma++; if (maIndicators[m].array[i] < bands.second_array[i]) count_ma--; } color_buffer[i] = count_ma; //Update inside of this other object (to avoid making an extra inheritance, or an external loop) showIndex.array[i] = TOTAL_MA - count_ma; 上面代码逐行拆:先给 count_ma 预设为 10(即全部均线数);循环里每有一条均线在上轨之上就加一,在下轨之下就减一;循环完 color_buffer 存的是「偏多均线净数」,showIndex 反过来存「偏空均线净数」,顺手塞进另一个对象数组,免得再写一层继承或外部循环。 #define TOTAL_MA 10 CMA maIndicators[TOTAL_MA]; CATRBand bands; CColorCandles candles; CIndicatorPlotNone showIndex; //To show MAs above/below 初始化段里,bands 用 Close±ATR 画通道,showIndex 是个不画线的 Plot 对象,专门承载「上/下轨外均线数」好让前端标箭头。OnInit 中给每条 MA 按序号在 clrAqua→clrRoyalBlue 之间插值上色,中间均线用 STYLE_DOT、首尾两条线宽设 2;箭头色板长 21(10*2+1),从绿黄经灰到橙,对应「全在上方→居中→全在下方」的渐变。 实盘里把 TOTAL_MA 从 10 改成 5 或 20,MT5 里重载指标就能看到染色密度和通道穿透信号的灵敏度明显变化;外汇与贵金属波动大,这类通道外计数仅作概率参考,不代表方向确定性。

MQL5 / C++
class="type">int count_ma = TOTAL_MA;
for (class="type">int m=class="num">0; m<TOTAL_MA; m++)
{
  if (maIndicators[m].array[i] > bands.first_array[i]) count_ma++;
  if (maIndicators[m].array[i] < bands.second_array[i]) count_ma--;
}
color_buffer[i] = count_ma;
class=class="str">"cmt">//Update inside of this other object(to avoid making an extra inheritance, or an external loop)
showIndex.array[i] = TOTAL_MA - count_ma;
}
}
class="macro">#define TOTAL_MA class="num">10
CMA maIndicators[TOTAL_MA];
CATRBand bands;
CColorCandles candles;
CIndicatorPlotNone showIndex; class=class="str">"cmt">//To show MAs above/below
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
  class="type">int buffer=class="num">0, plot=class="num">0;
  bands.SetBuffer(buffer, plot);
  candles.SetBuffer(buffer, plot);
  for (class="type">int i=class="num">0; i<TOTAL_MA; i++)
    maIndicators[i].SetBuffer(buffer, plot);
  showIndex.SetBuffer(buffer, plot);
class=class="str">"cmt">//Print("Buffers: ", buffer, "  Plots: ", plot);
class=class="str">"cmt">//--- plot settings
  if (show_bands) bands.SetLineColor(clrDarkSlateGray);
  else bands.SetLineColor(clrNONE);
  bands.SetShowData(show_data);
  if (show_data)
    bands.SetLabel("Close + ATR;Close - ATR");
  for (class="type">int i=class="num">0; i<TOTAL_MA; i++)
  {
    maIndicators[i].SetLineColor(InterpolateColors(clrAqua, clrRoyalBlue, i/(TOTAL_MA-class="num">1.0)));
    maIndicators[i].SetLabel("MA("+IntegerToString(ma_faster_period+i*ma_step)+")");
    maIndicators[i].SetShowData(show_data);
    if (i>class="num">0 && i <TOTAL_MA-class="num">1) maIndicators[i].SetLineStyle(STYLE_DOT);
    else maIndicators[i].SetLineWidth(class="num">2);
  }
  class="type">class="kw">color arrow_colors[TOTAL_MA*class="num">2+class="num">1];
  for (class="type">int i=class="num">0; i<TOTAL_MA; i++)
    arrow_colors[i] = InterpolateColors(clrGreenYellow, clrGray, i/class="type">class="kw">double(TOTAL_MA));
  arrow_colors[TOTAL_MA] = clrGray;
  for (class="type">int i=TOTAL_MA+class="num">1; i<TOTAL_MA*class="num">2+class="num">1; i++)
    arrow_colors[i] = InterpolateColors(clrGray, clrOrange, (i-TOTAL_MA)/class="type">class="kw">double(TOTAL_MA));
  candles.SetColorIndexes(arrow_colors);
  candles.SetLabel("Open;High;Low;Close");

「指标初始化与逐根更新的代码骨架」

这段自定义指标把均线带、布林类通道和蜡烛对象拆成类来管理,初始化时先关掉 candles 的数据绘制,只留标签显示均线上穿/下穿状态,避免主图被冗余数值刷屏。 循环里对 TOTAL_MA 条均线逐一 Init,周期从 ma_faster_period 起步、按 ma_step 递增,比如 faster=10、step=5、总数=3,就会生成 10/15/20 三条均线。外汇与贵金属波动剧烈,这种阶梯周期排列对识别层层压制或支撑倾向更有效,但高杠杆下仍属高风险用法。 OnCalculate 用 prev_calculated-1 做 limit,只更新新增 K 线,bands 和每条 MA 各自 Update;返回 rates_total 供下一帧接续。OnDeinit 里对称地 DeInit 释放资源,漏掉任一个都可能让 MT5 退出时报句柄残留。 直接把下面代码贴进 MQ5 文件即可编译验证类调用是否通,改 ma_step 看均线密度变化是最快的手感测试。

MQL5 / C++
  candles.SetShowData(class="kw">false);
  showIndex.SetLabel("MAs above/below");
  showIndex.SetShowData(true);
class=class="str">"cmt">//--- initialize classes
  bands.Init();
  for (class="type">int i=class="num">0; i<TOTAL_MA; i++)
    maIndicators[i].Init(ma_faster_period+i*ma_step, ma_method);
  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(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],
                class="kw">const class="type">long &tick_volume[],
                class="kw">const class="type">long &volume[],
                class="kw">const class="type">int &spread[])
{
class=class="str">"cmt">//---
  class="type">int limit = MathMax(class="num">0, prev_calculated-class="num">1);
  bands.Update(limit, rates_total, close);
  for (class="type">int i=class="num">0; i<TOTAL_MA; i++)
    maIndicators[i].Update(limit, rates_total);
  candles.Update(limit, rates_total, open, high, low, close);
class=class="str">"cmt">//--- class="kw">return value of prev_calculated for next call
  class="kw">return(rates_total);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator deinitialization function                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
  bands.DeInit();
  for (class="type">int i=class="num">0; i<TOTAL_MA; i++)
    maIndicators[i].DeInit();
}

封装作图类时的几个现实坑

用函数接管作图属性确实省事,但不是没代价。最直观的问题是改画图板的颜色或线型时容易出残留:某些状态下旧样式清不掉,有些时候又被初始化逻辑整体重写一遍,视觉上会出现闪错。 规避办法很直接——别在 MT5 的“颜色”选项卡里手动改,改用输入参数传颜色;或者每次设置前先判断当前值是否等于默认黑 0x000000,不是再写。这条只对非黑色有效,黑色本身当默认值占位,绕不开。 性能层面本文没做基准测试,理论上直接调内置属性加少量函数比套一层类更快,但实盘里多数指标这点开销可以忽略。 类里没包图表事件和 OnTimer 是刻意的:图表事件放 OnChartEvent 统一收口,再调具体函数,比每个指标都挂处理器重复跑一遍更干净;多周期/多货币的指标想用定时器触发 Update,就得绕开 OnCalculate 的数组直传。把缓冲区数组设成公开有人嫌暴露,改成 protected 也能跑,只是外部读数要补 getter。 下面这段就是“非黑才改色”的最小判断,复制进你的类方法就能用:

MQL5 / C++
if (obj.GetInteger(PLOT_LINE_COLOR)==clrBlack)
   obj.SetLineColor(clrYellow);

◍ 剧烈波动时别让 OnCalculate 拖垮 MT5

前面几节把类结构搭完、指标样例跑通,但有个隐藏雷区:OnCalculate 在行情剧烈波动时可能被高频调用,MetaTrader 每次都会被迫读指标缓冲区,缓冲区数量一大,系统可能变慢甚至崩。 有交易者实测,手动或半自动场景下单次计算能在 200 毫秒内完成,不算问题;可一旦波动放大、调用频次暴涨,函数内部反复 CopyBuffer 的交互就成了瓶颈。 一个直接的改法是把计算强制塞进 OnCalculate 本身,省掉 CopyBuffer 调用;另一个细节是改掉 OnCalculate 里某行写法——用三元运算符代替函数调用,看似不起眼,但在高压波动里能少耗几个机器周期,累积效果明显。 外汇和贵金属本身高风险,这类性能优化不保证盈利,只是降低卡顿概率,开 MT5 把样例指标的 OnCalculate 按上面两处改掉,跑一段非农行情就能对比流畅度。

MQL5 / C++
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                     class="kw">const class="type">int prev_calculated,
                     class="kw">const class="type">class="kw">datetime &time[],
                     class="kw">const class="type">class="kw">double &open[],
                     class="kw">const class="type">class="kw">double &high[],
                     class="kw">const class="type">class="kw">double &low[],
                     class="kw">const class="type">class="kw">double &close[],
                     class="kw">const class="type">long &tick_volume[],
                     class="kw">const class="type">long &volume[],
                     class="kw">const class="type">int &spread[])
{
class=class="str">"cmt">//---
   class="type">int limit = MathMax(class="num">0, prev_calculated-class="num">1);
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                     class="kw">const class="type">int prev_calculated,
                     class="kw">const class="type">class="kw">datetime &time[],
                     class="kw">const class="type">class="kw">double &open[],
                     class="kw">const class="type">class="kw">double &high[],
                     class="kw">const class="type">class="kw">double &low[],
                     class="kw">const class="type">class="kw">double &close[],
                     class="kw">const class="type">long &tick_volume[],
                     class="kw">const class="type">long &volume[],
                     class="kw">const class="type">int &spread[])
{
class=class="str">"cmt">//---

「画得少,看得清」

上面那行是控制重绘范围的关键:新 tick 进来时,只从已计算根数减一的位置继续,避免整段重画。 int limit = (prev_calculated <= 0 ? 0 : prev_calculated - 1); 把 limit 设成 prev_calculated-1,意味着指标在历史 K 线稳定后不再回刷,只在最新一根上更新。MT5 里开个自定义指标跑这段代码,你能直接看到 CPU 占用和重绘闪烁同步下降。 外汇与贵金属波动剧烈、杠杆高风险大,少画不是偷懒,是让信号不被旧笔触干扰。验证完这行,你对“指标该画到哪一根”会有更具体的手感。

MQL5 / C++
class="type">int limit = (prev_calculated <= class="num">0 ? class="num">0 : prev_calculated - class="num">1);
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多缓冲区指标的加载异常提示,你专注决策而非查数组越界。

常见问题

可以,只要数组声明为 public,指标内部和外部句柄都能按常规方式访问,但需注意线程安全和 OnCalculate 的调用时序。
倾向影响极小,对象只是语法层封装,编译后内存布局与传统数组接近,热点在循环逻辑而非封装本身。
循环配置更紧凑、易扩展,但调试时不如静态声明直观;复杂指标倾向循环,简单指标可静态。
可能支持,小布已内置常见多缓冲区指标的诊断,打开品种页会提示缓冲区数量不匹配等异常,减少手动排查。
限制主要在句柄生命周期和不同周期数据同步,若对象未绑定具体 timeframe,复制数据可能拿到空缓冲,需显式管理。