利用对象轻松制作复杂指标(基础篇)
◍ 用对象把复杂指标画出来
在 MT5 里做复杂指标,未必非得啃底层缓冲区。借助图形对象(OBJ_HLINE、OBJ_LABEL、OBJ_BITMAP_LABEL 等),可以把计算结果显示在图表上,而不必写进自定义指标的标准数组。 官方示例文章《利用对象轻松制作复杂指标》发布于 2022 年 11 月 14 日 09:29,累计浏览 1 427 次、评论 3 条,说明这类轻量做法在实战社群里确有需求。 打开 MT5 按 F4 进 MetaEditor,新建脚本试一段对象绘制,比从零写指标更容易验证想法;外汇与贵金属波动剧烈,用对象做辅助标注仅作参考,不构成方向判定。
「复杂指标缓冲区管理的老痛点」
在 MT5 里手搓复杂指标时,缓冲区一多就容易失控。你得为每个双精度数组单独声明、绑定、配置,再配上对应的作图板类型与属性,少一个缓冲区就可能触发数组越界,或者作图板静默不显示,直到回测报错才察觉。 数据合并更磨人。假设要把 10 个缓冲区做平均、最大或最小聚合,常规写法就是逐行比较或套宏替换,代码量大且重复,漏洞率随缓冲区数量线性上升,改一处漏一处基本是常态。 面向对象有个冷门技巧能破这个局:把对象内部包含的数组直接设为指标缓冲区。下文就拆这种写法能省掉多少声明成本,以及怎么套到任意自定义结构里。
用对象数组堆出多周期RSI
先别急着写指标逻辑,看看最基础的缓冲区载体:一个只暴露公开双精度数组的类,就能被 MT5 当作指标缓冲区挂载。公开是关键,否则外部没法把它设成 buffer 或直接读写数据。 我们要落地的例子是 10 条不同周期 RSI 加一条均值线。属性只写 indicator_buffers 和 indicator_plots 两个必选项,各填 11;线条颜色、样式等全丢进 OnInit 的循环里用 PlotIndexSetInteger 批量设,代码能砍掉一大截。 缓冲区用 CIndicatorPlot indicators[] 对象数组装,11 个元素对应 10 条 RSI 和 1 条均值;句柄存 handles[],长度 10。OnInit 里先 ArrayResize 再循环 SetIndexBuffer 绑定,然后给前 10 个画红色实线、第 11 个画青色虚线宽 2,最后用 iRSI 按 firstPeriod + i*increment 建句柄,默认首周期 6、步长 2,也就是 6/8/10…24。 把多条 buffer 收进对象数组后,OnCalculate 里求均值就是一层循环累加,比全局散声明 double 数组省事得多。指标跑完记得释放句柄,这版长度可控,但下一节会指出它还能再挤水分。
class CIndicatorPlot { class="kw">public: class="type">class="kw">double array[]; }; class="macro">#class="kw">property indicator_buffers class="num">11 class="macro">#class="kw">property indicator_plots class="num">11 class="kw">input class="type">int firstPeriod = class="num">6; class="kw">input class="type">int increment = class="num">2; CIndicatorPlot indicators[]; class="type">int handles[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { ArrayResize(indicators, class="num">11); class=class="str">"cmt">//--- indicator buffers mapping for (class="type">int i=class="num">0; i<class="num">11; i++) { SetIndexBuffer(i, indicators[i].array, INDICATOR_DATA); PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE); } for (class="type">int i=class="num">0; i<class="num">10; i++) PlotIndexSetInteger(i, PLOT_LINE_COLOR, clrRed); PlotIndexSetInteger(class="num">10, PLOT_LINE_COLOR, clrCyan); PlotIndexSetInteger(class="num">10, PLOT_LINE_STYLE, STYLE_DASH); PlotIndexSetInteger(class="num">10, PLOT_LINE_WIDTH, class="num">2); ArrayResize(handles, class="num">10); for (class="type">int i=class="num">0; i<class="num">10; i++) handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE); class=class="str">"cmt">//--- 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[],
◍ 多指标缓冲区的增量拷贝与合成
这段逻辑处理的是 10 个独立指标句柄的缓冲区向本地数组的回填,以及第 11 个数组作为它们均值的合成结果。首次计算(limit==0)时直接整段 CopyBuffer 拉满,后续只追增量,避免每 tick 重算全历史。 循环里对 i<10 的每个指标,用 rates_total-limit 的长度做局部拷贝,再按偏移写回 indicators[i].array[limit+k]。这样 prev_calculated 机制真正生效,EA 在 EURUSD 这类每秒多 tick 的品种上 CPU 占用倾向降一截。 合成段把前 10 个数组逐根相加再除 10.0,得到等权均值线。外汇与贵金属杠杆高、滑点跳空频繁,这种合成值只反映历史均值倾向,实盘须用 MT5 策略测试器跑至少 3 个月 tick 数据验证稳定性。 OnDeinit 里挨个 IndicatorRelease(handles[i]) 释放句柄,漏掉这一步行情重载时可能句柄泄漏。开 MT5 把这段贴进自定义指标,改 handles 数量就能扩到 20 路合成。
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); for (class="type">int i=class="num">0; i<class="num">10; i++) { if (limit==class="num">0) CopyBuffer(handles[i], class="num">0, class="num">0, rates_total-limit, indicators[i].array); else { class="type">class="kw">double newValues[]; CopyBuffer(handles[i], class="num">0, class="num">0, rates_total-limit, newValues); for (class="type">int k=class="num">0; k<rates_total-limit; k++) { indicators[i].array[limit+k] = newValues[k]; } } } for (class="type">int i=limit; i<rates_total; i++) { indicators[class="num">10].array[i] = class="num">0.0; for (class="type">int j=class="num">0; j<class="num">10; j++) indicators[class="num">10].array[i] +=indicators[j].array[i]; indicators[class="num">10].array[i]/=class="num">10.0; } 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) { for (class="type">int i=class="num">0; i<class="num">10; i++) IndicatorRelease(handles[i]); } class=class="str">"cmt">//+------------------------------------------------------------------+
「把缓冲区和作图板交给类去管」
手写指标时最烦的一步,就是每次 OnInit 里都要手动配缓冲区索引和作图板属性,改个顺序就容易把索引写错。把这类活儿委派给一个封装类,能省掉重复劳动,也降低出错概率。 下面这段类声明给出了核心接口:CIndicatorPlot 内部存了 indicator_plot 索引,对外暴露 SetBuffer、SetLineWidth、SetLineStyle、SetLineColor、SetLabel。注意 SetBuffer 用引用传参 int &buffer 和 int &plot,函数里自增后,调用方的编号也会跟着走,所以连续配置多个作图板时不用自己数数。 [CODE] class CIndicatorPlot { private: int indicator_plot; public: double array[]; void SetBuffer(int &buffer, int &plot); void SetLineWidth(int width); void SetLineStyle(ENUM_LINE_STYLE style); void SetLineColor(color line_color); void SetLabel(string label); }; //+------------------------------------------------------------------+ void CIndicatorPlot::SetBuffer(int &buffer,int &plot) { indicator_plot = plot; SetIndexBuffer(buffer, array, INDICATOR_DATA); PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE); buffer++; //Increment for other steps (One buffer in this case) plot++; //Increment one plot in any case } //+------------------------------------------------------------------+ void CIndicatorPlot::SetLineWidth(int width) { PlotIndexSetInteger(indicator_plot, PLOT_LINE_WIDTH, width); } //--- //... //+------------------------------------------------------------------+
| // | Function to linearly interpolate 2 colors |
|---|
//+------------------------------------------------------------------+ color InterpolateColors(color colorA, color colorB, double factor) { if (factor<=0.0) return colorA; if (factor>=1.0) return colorB; int result = 0; for (int i=0; i<3; i++) //R-G-B { int subcolor = int( ((colorA>>(8*i))&(0xFF))*(1.0-factor) + ((colorB>>(8*i))&(0xFF))*factor ); subcolor = subcolor>0xFF?0xFF:( subcolor<0x00?0x00: 逐行拆解:class CIndicatorPlot 定义封装类;private 里的 indicator_plot 记作图板编号;public 的 array[] 就是指标缓冲区数组。SetBuffer 中 indicator_plot=plot 保存作图板索引,SetIndexBuffer 把传入的 buffer 号绑定到 array 作为数据缓冲,PlotIndexSetInteger 设成 DRAW_LINE 线型;buffer++ 和 plot++ 让引用方自动顺延,下次调另一个对象时编号不会撞。SetLineWidth 只是对已存作图板设线宽。InterpolateColors 做两色线性插值:factor≤0 返 A、≥1 返 B,否则对 R/G/B 三个通道分别按 (1-factor)*A + factor*B 算,循环 3 次对应 24 位色的三个字节。 用这个类之后,OnInit 里不再出现任何magic number式的缓冲区编号,按对象名引用即可,换顺序或加标签都只在对象上改。文末还提到给基类加虚空白 Update 事件,让独立 RSI 子类自己建/删句柄,平均类靠指针访问其余作图板——这样 OnCalculate 也能更干净。外汇与贵金属指标编写涉及杠杆与滑点,回测和实盘差异可能很大,任何封装只解决代码组织,不预示信号胜率。
class CIndicatorPlot { class="kw">private: class="type">int indicator_plot; class="kw">public: class="type">class="kw">double array[]; class="type">void SetBuffer(class="type">int &buffer, class="type">int &plot); class="type">void SetLineWidth(class="type">int width); class="type">void SetLineStyle(ENUM_LINE_STYLE style); class="type">void SetLineColor(class="type">class="kw">color line_color); class="type">void SetLabel(class="type">class="kw">string label); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndicatorPlot::SetBuffer(class="type">int &buffer,class="type">int &plot) { indicator_plot = plot; SetIndexBuffer(buffer, array, INDICATOR_DATA); PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE); buffer++; class=class="str">"cmt">//Increment for other steps(One buffer in this case) plot++; class=class="str">"cmt">//Increment one plot in any case } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndicatorPlot::SetLineWidth(class="type">int width) { PlotIndexSetInteger(indicator_plot, PLOT_LINE_WIDTH, width); } class=class="str">"cmt">//--- class=class="str">"cmt">//... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to linearly interpolate class="num">2 colors | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">color InterpolateColors(class="type">class="kw">color colorA, class="type">class="kw">color colorB, class="type">class="kw">double factor) { if (factor<=class="num">0.0) class="kw">return colorA; if (factor>=class="num">1.0) class="kw">return colorB; class="type">int result = class="num">0; for (class="type">int i=class="num">0; i<class="num">3; i++) class=class="str">"cmt">//R-G-B { class="type">int subcolor = class="type">int( ((colorA>>(class="num">8*i))&(0xFF))*(class="num">1.0-factor) + ((colorB>>(class="num">8*i))&(0xFF))*factor ); subcolor = subcolor>0xFF?0xFF:( subcolor<0x00?0x00:
把多周期 RSI 打包成可复用绘图类
这段实现的核心,是用 CIndicatorPlot 基类把 10 条 RSI 曲线的显示与生命周期统一管理,避免在每个周期里重复写缓冲区映射代码。OnInit 里先给 indicators 和 handles 两个数组各 Reserve 10 个元素,再循环 new 出子类实例、按 firstPeriod + i*increment 的步长去申请 iRSI 句柄。 颜色插值函数 InterpolateColors(clrYellow, clrRed, i/9.0) 把 0~9 的索引映射成黄到红的渐变,标签写成 "RSI (周期值)",肉眼能直接看出哪条线对应哪个参数。average 那条蓝虚线宽度设成 2,用来叠加十条线的均值参考。 OnDeinit 的顺序值得抄:先 IndicatorRelease 释放 10 个内置指标句柄,再 delete indicators 里的对象,反过来容易在 MT5 里报句柄泄漏。外汇与贵金属波动剧烈,这类多周期 RSI 叠加只作概率参考,实盘前务必在策略测试器跑一遍不同品种验证稳定性。
CIndicatorPlot* indicators[]; CIndicatorPlot average; class="type">int handles[]; 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 ArrayResize(indicators, class="num">10); ArrayResize(handles, class="num">10); class="type">int index=class="num">0, plot=class="num">0; for (class="type">int i=class="num">0; i<class="num">10; i++) { indicators[i] = new CIndicatorPlot(); indicators[i].SetBuffer(index, plot); indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/class="num">9.0)); indicators[i].SetLabel("RSI("+IntegerToString(firstPeriod+i*increment)+")"); handles[i] = iRSI(NULL, PERIOD_CURRENT, firstPeriod+i*increment, PRICE_CLOSE); } average.SetBuffer(index, plot); average.SetLineColor(clrBlue); average.SetLineStyle(STYLE_DASH); average.SetLineWidth(class="num">2); average.SetLabel("Average"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { for (class="type">int i=class="num">0; i<class="num">10; i++) IndicatorRelease(handles[i]); for (class="type">int i=class="num">0; i<class="num">10; i++) class="kw">delete indicators[i]; } class CIndicatorPlot { class=class="str">"cmt">//... class="kw">public: class=class="str">"cmt">//... class="kw">virtual class="type">void Init() { } class="kw">virtual class="type">void DeInit() { } class="kw">virtual class="type">void Update(class="kw">const class="type">int start, class="kw">const class="type">int rates_total) { } }; class CRSIIndividual : class="kw">public CIndicatorPlot { class="kw">private: class="type">int handle; class="type">int rsi_period; class="kw">public: class="type">void SetPeriodRSI(class="type">int period); class="kw">virtual class="type">void Init(); class="kw">virtual class="type">void DeInit(); class="kw">virtual class="type">void Update(class="kw">const class="type">int start, class="kw">const class="type">int rates_total); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRSIIndividual::SetPeriodRSI(class="type">int period) { rsi_period = period; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRSIIndividual::Init(class="type">void) {