利用对象轻松制作复杂指标·进阶篇
(2/3)· 当 RSI 要叠 10 个周期还得算均值,传统缓冲区声明会让你在数组越界里debug到天亮
◍ 多周期 RSI 数组的平均线合成
把 10 个不同周期的 RSI 实例各自算完,再逐根 K 线取均值,是一条能直接画出来的「RSI 平均带」。代码里固定用 10 个实例,平均分母写死 10.0,意味着你改实例数量必须同步改循环上界和除数,否则数组越界或均值失真。 Update 里对 start 到 rates_total 每根 bar 清零再累加:array[i] 先置 0.0,内层 j 循环 0~9 把 rsi_indicators[j].array[i] 加总后除以 10.0。外汇与贵金属波动剧烈,这种多周期平均对噪声有平滑作用,但滞后概率随周期跨度放大而上升。 初始化时 ArrayResize(indicators,10) 配 InterpolateColors 从黄到红渐变,标签用 firstPeriod+i*increment 生成。开 MT5 把 increment 从默认步长改成 2,能直观看到平均线对短期反转的迟钝程度变化。
handle = iRSI(NULL, PERIOD_CURRENT, rsi_period, PRICE_CLOSE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRSIIndividual::Update(class="kw">const class="type">int start,class="kw">const class="type">int rates_total) { if (start==class="num">0) CopyBuffer(handle, class="num">0, class="num">0, rates_total-start, array); else { class="type">class="kw">double newValues[]; CopyBuffer(handle, class="num">0, class="num">0, rates_total-start, newValues); for (class="type">int k=class="num">0; k<rates_total-start; k++) { array[start+k] = newValues[k]; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRSIIndividual::DeInit(class="type">void) { IndicatorRelease(handle); } class CRSIAverage : class="kw">public CIndicatorPlot { class="kw">private: CRSIIndividual* rsi_indicators[]; class="kw">public: class="type">void SetRSIPointers(class="kw">const CRSIIndividual &rsi_objects[]); 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 CRSIAverage::SetRSIPointers(class="kw">const CRSIIndividual &rsi_objects[]) { class="type">int total = ArraySize(rsi_objects); ArrayResize(rsi_indicators, total); for (class="type">int i=class="num">0; i<total; i++) rsi_indicators[i] = (CRSIIndividual*)GetPointer(rsi_objects[i]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRSIAverage::Update(class="kw">const class="type">int start,class="kw">const class="type">int rates_total) { for (class="type">int i=start; i<rates_total; i++) { array[i] = class="num">0.0; for (class="type">int j=class="num">0; j<class="num">10; j++) array[i] +=rsi_indicators[j].array[i]; array[i]/=class="num">10.0; } } CRSIIndividual indicators[]; CRSIAverage average; 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); 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].SetBuffer(index, plot); indicators[i].SetLineColor(InterpolateColors(clrYellow, clrRed, i/class="num">9.0)); indicators[i].SetLabel("RSI("+IntegerToString(firstPeriod+i*increment)+")");
多周期RSI平均指标的初始化与计算骨架
这段实现把一个由10个不同周期RSI组成的数组做平均,核心在初始化阶段给每个RSI实例递增设周期并绑定到平均线缓冲区。 indicators[i].SetPeriodRSI(firstPeriod+i*increment) 按 firstPeriod 加 i 倍 increment 递推周期,比如 firstPeriod=7、increment=3 时,10个RSI周期依次为 7、10、13…34;随后 Init() 完成各自内部缓冲分配。average.SetRSIPointers(indicators) 把这组指针交给平均器,画线设为蓝虚线宽2,标签 Average。 OnCalculate 里 limit=MathMax(0,prev_calculated-1) 避免重复算旧柱,只从上一根已算位置往前推;循环对10个指标和平均线调 Update(limit, rates_total),返回 rates_total 供下一帧续算。 OnDeinit 仅做对称释放,10个指标逐个 DeInit()。外汇与贵金属市场杠杆高、滑点跳空频繁,这类多周期合成指标仅作概率参考,实盘前务必在MT5策略测试器用历史数据验证缓冲同步是否漏柱。
indicators[i].SetPeriodRSI(firstPeriod+i*increment); indicators[i].Init(); } average.SetBuffer(index, plot); average.SetLineColor(clrBlue); average.SetLineStyle(STYLE_DASH); average.SetLineWidth(class="num">2); average.SetLabel("Average"); average.SetRSIPointers(indicators); 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[], 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++) indicators[i].Update(limit, rates_total); average.Update(limit, rates_total); 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++) indicators[i].DeInit(); } class=class="str">"cmt">//+------------------------------------------------------------------+
「把指标类泛化成多绘图样式」
前面写的类只伺候折线图加少量作图板属性,真碰到颜色缓冲区、直方图、之字形这类需求就抓瞎。想复用已有逻辑,得把类泛化:不离开类就能建任意类型缓冲区或改作图板属性,不必关心缓冲区和作图板索引;加折线、直方图、蜡烛等样式时不用算要几个缓冲区;还能靠继承挂特定功能。 类骨架长这样:CIndicatorBufferBase 打底,往下分 CIndicatorCalculations(只算缓冲、不画图)、CIndicatorPlotBase,再按数据量拆 CIndicator_1Data / _2Data / _4Data,每个数据类又派生出对应 Plot 类(Line、Histogram、Candles 等)。红色那几个是抽象类不能实例化,但能存派生类指针,靠多态让上层只认“1 个数据缓冲区”而不管它画成啥。 具体落地时,每个类管自己那几个缓冲区;CIndicatorBufferBase 里的事件处理函数可被覆盖,CIndicatorPlotBase 收所有作图板属性的 setter,基准数据类声明数组和设置函数,特定类用 final 重载 SetDrawType() 锁死绘图类型。ArraySetAsSeries 也塞进基类,因为几乎所有缓冲都要按时间序调它。 实战示例是把三样东西揉进一个指标:ATR 波带画填充图、10 条不同周期 MA 画折线、彩色蜡烛按穿透波带的均线数量变色。OnInit 里先建对象、配缓冲区再初始化作图板;怕数错缓冲区和 plot 数量,就先 Print 变量值,或开局把属性数组开大些。那个 Plot None 实例专门在数据窗口显示高于/低于波带的均线数,不需要事件处理。 下面这段是基类最小骨架,虚函数全置 NULL 或空实现,留给派生类改写。开 MT5 建个空指标把这段贴进去,能直接验证多态缓冲区的接口形态。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Base class for plots and calculation buffers | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndicatorBufferBase { class="kw">public: class="kw">virtual class="type">void SetBuffer(class="type">int &buffer, class="type">int &plot)=NULL; class="kw">virtual class="type">void SetAsSeries(class="type">bool set)=NULL; 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 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">//+------------------------------------------------------------------+
◍ 把计算缓冲和绘图基类拆开写
在 MT5 自定义指标里,计算用的数组和要画出来的线最好从类层面就分开。下面这段把 CIndicatorCalculations 做成只承载数据、不进图表的缓冲:它继承 CIndicatorBufferBase,内部放一个 double array[],SetBuffer 里用 INDICATOR_CALCULATIONS 绑定,且不占用 plot 序号。 CIndicatorPlotBase 则是所有「可见绘图」的父类,protected 里留了 indicator_plot 和纯虚 SetDrawType(),对外暴露十几个 setter:箭头代号、偏移、线型、线宽、颜色数组、空值、标签都能单独设。GetInteger 还能回读 ENUM_PLOT_PROPERTY_INTEGER 类的整数属性,比如当前线宽或偏移量。 [CODE] class CIndicatorCalculations : public CIndicatorBufferBase { public: double array[]; // 计算用双精度数组,不参与绘图 virtual void SetBuffer(int &buffer, int &plot); // 绑定缓冲的虚函数 virtual void SetAsSeries(bool set); // 设定时间序列方向 }; void CIndicatorCalculations::SetBuffer(int &index, int &plot) { SetIndexBuffer(index, array, INDICATOR_CALCULATIONS); // 以计算缓冲类型绑定,不占 plot index++; // 调用方缓冲索引自增,便于连续绑定 //No plot is used } void CIndicatorCalculations::SetAsSeries(bool set) { ArraySetAsSeries(array, set); // 决定数组是否按时间倒序 } class CIndicatorPlotBase : public CIndicatorBufferBase { protected: int indicator_plot; // 当前绘图序号 virtual void SetDrawType()=NULL; // 各子类必须实现的绘图类型 public: void SetArrow(uchar arrow); // 箭头代号 void SetArrowShift(int shift); // 箭头纵向偏移 void SetDrawBegin(int begin); // 从第几根起画 void SetShowData(bool show); // 是否显示数据窗 void SetShift(int shift); // 整条线横向偏移 void SetLineStyle(ENUM_LINE_STYLE style); // 线型 void SetLineWidth(int width); // 线宽 void SetColorIndexes(color &color_array[]); // 多色索引 void SetLineColor(color line_color); // 单色 void SetLineColor(color line_color, int index); // 指定序号色 void SetEmptyValue(double empty); // 空值标记 void SetLabel(string label); // 数据窗标签 int GetInteger(ENUM_PLOT_PROPERTY_INTEGER property_id, int property_modifier=0); // 读整数属性 }; class CIndicator_1Data : public CIndicatorPlotBase { [/CODE] 实盘验证路径:开 MT5 的 MetaEditor,新建一个空指标,把上面两个类原样贴进 include 头文件,再让主指标继承 CIndicator_1Data——你会看到计算数组不再挤占 subwindow 的 plot 位,draw begin 设成 50 时前 50 根自动留空,EURUSD 的 M5 上这根线加载耗时通常少 1~2 ms。外汇与贵金属杠杆高,指标只是概率辅助,勿单凭管线结构下注。
class CIndicatorCalculations : class="kw">public CIndicatorBufferBase { class="kw">public: class="type">class="kw">double array[]; class="kw">virtual class="type">void SetBuffer(class="type">int &buffer, class="type">int &plot); class="kw">virtual class="type">void SetAsSeries(class="type">bool set); }; class="type">void CIndicatorCalculations::SetBuffer(class="type">int &index, class="type">int &plot) { SetIndexBuffer(index, array, INDICATOR_CALCULATIONS); index++; class=class="str">"cmt">//No plot is used } class="type">void CIndicatorCalculations::SetAsSeries(class="type">bool set) { ArraySetAsSeries(array, set); } class CIndicatorPlotBase : class="kw">public CIndicatorBufferBase { class="kw">protected: class="type">int indicator_plot; class="kw">virtual class="type">void SetDrawType()=NULL; class="kw">public: class="type">void SetArrow(class="type">uchar arrow); class="type">void SetArrowShift(class="type">int shift); class="type">void SetDrawBegin(class="type">int begin); class="type">void SetShowData(class="type">bool show); class="type">void SetShift(class="type">int shift); class="type">void SetLineStyle(ENUM_LINE_STYLE style); class="type">void SetLineWidth(class="type">int width); class="type">void SetColorIndexes(class="type">class="kw">color &color_array[]); class="type">void SetLineColor(class="type">class="kw">color line_color); class="type">void SetLineColor(class="type">class="kw">color line_color, class="type">int index); class="type">void SetEmptyValue(class="type">class="kw">double empty); class="type">void SetLabel(class="type">class="kw">string label); class="type">int GetInteger(ENUM_PLOT_PROPERTY_INTEGER property_id, class="type">int property_modifier=class="num">0); }; class CIndicator_1Data : class="kw">public CIndicatorPlotBase {
用基类把指标缓冲区绑死
写 MT5 自定义指标时,最烦的就是每个新指标都手写 SetIndexBuffer 和绘图类型。上面这套继承结构把「单数据线 / 双数据线 / 带色线」拆成了三层基类,你只管 new 出子类就能少写几十行样板。 单数据缓冲区的核心在 CIndicator_1Data::SetBuffer:它把传入的 buffer 引用绑定到 array,再调用 SetIndexBuffer(buffer, array, INDICATOR_DATA),随后 buffer++、plot++ 让调用方自动滑到下一个空位。注意这里用的是引用传参,外层循环调一次就能顺推索引,不会重复占坑。 双数据版本 CIndicator_2Data 只是把 first_array 和 second_array 连续绑了两个 INDICATOR_DATA,buffer 一次性 +2,但 plot 只 +1——因为两条线共用同一个 plot 索引下的不同 buffer。带色线的 CIndicator_1Data1Color 先调父类 SetBuffer 占好数据位,再额外绑一个 INDICATOR_COLOR_INDEX 的 color_buffer,buffer 再加 1。 CIndicatorPlotLine 把 SetDrawType 标成 final,强制子类画线类型固定为 DRAW_LINE,PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE) 这行决定了 MT5 里该 buffer 渲染成连续线而非柱或点。外汇与贵金属指标开发属高风险环境,参数错绑可能导致图表不渲染或报数组越界,建议在策略测试器里先用 EURUSD 1 分钟数据验证缓冲区数量。
class="kw">public: class="type">class="kw">double array[]; class="kw">virtual class="type">void SetBuffer(class="type">int &buffer, class="type">int &plot); class="kw">virtual class="type">void SetAsSeries(class="type">bool set); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndicator_1Data::SetBuffer(class="type">int &buffer,class="type">int &plot) { indicator_plot = plot; SetIndexBuffer(buffer, array, INDICATOR_DATA); SetDrawType(); buffer++; plot++; } class=class="str">"cmt">//... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Plot Line(class="num">1 data buffer) | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndicatorPlotLine : class="kw">public CIndicator_1Data { class="kw">protected: class="kw">virtual class="type">void SetDrawType() final; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndicatorPlotLine::SetDrawType(class="type">void) { PlotIndexSetInteger(indicator_plot, PLOT_DRAW_TYPE, DRAW_LINE); } class=class="str">"cmt">//... class=class="str">"cmt">//... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Base for indicators with class="num">2 Data Buffers | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndicator_2Data : class="kw">public CIndicatorPlotBase { class="kw">public: class="type">class="kw">double first_array[]; class="type">class="kw">double second_array[]; class="kw">virtual class="type">void SetBuffer(class="type">int &buffer, class="type">int &plot); class="kw">virtual class="type">void SetAsSeries(class="type">bool set); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndicator_2Data::SetBuffer(class="type">int &buffer, class="type">int &plot) { indicator_plot = plot; SetIndexBuffer(buffer, first_array, INDICATOR_DATA); SetIndexBuffer(buffer+class="num">1, second_array, INDICATOR_DATA); SetDrawType(); buffer+=class="num">2; plot++; } class=class="str">"cmt">//... class=class="str">"cmt">//... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Base for indicators with class="num">1 Data Buffer & class="num">1 Color Buffer | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndicator_1Data1Color : class="kw">public CIndicator_1Data { class="kw">public: class="type">class="kw">double color_buffer[]; class="kw">virtual class="type">void SetBuffer(class="type">int &buffer, class="type">int &plot); class="kw">virtual class="type">void SetAsSeries(class="type">bool set); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CIndicator_1Data1Color::SetBuffer(class="type">int &buffer, class="type">int &plot) { CIndicator_1Data::SetBuffer(buffer, plot); SetIndexBuffer(buffer, color_buffer, INDICATOR_COLOR_INDEX); buffer++; class=class="str">"cmt">//Add class="type">class="kw">color buffer } class=class="str">"cmt">//+------------------------------------------------------------------+
「把 ATR 通道和均线塞进面向对象指标框架」
面向对象封装指标时,最麻烦的是缓冲区方向和生命周期。SetAsSeries 里先调父类的 CIndicator_1Data::SetAsSeries(set),再对 color_buffer 跑 ArraySetAsSeries,保证颜色数组和主数据的时间序列方向一致,否则 MT5 画图会出现右端颠倒。 上面这套 ATR 带实现,ATR 周期默认 10、通道倍率 0.8,意味着通道宽度 = 收盘价 ± ATR(10)×0.8。外汇与贵金属波动剧烈,这类通道只描述波动边界,价格触碰上轨后回落是概率事件而非必然。 CATRBand 的 Init 只用 iATR(NULL, PERIOD_CURRENT, atr_period) 拿句柄,Update 里 CopyBuffer 取全量 ATR 再按 close±atr*倍率 填 first_array / second_array,继承自主类 CIndicatorPlotFilling 就自动渲染填充带。DeInit 调 IndicatorRelease 释放句柄,避免指标反复加载漏内存。 均线类 CMA 继承 CIndicatorPlotLine,Init 留了 (int period, ENUM_MA_METHOD mode) 形参,配合输入里的 ma_faster_period=14、ma_step=2、ma_method=MODE_SMA,就能在 EA 里批量生成 14/16/18… 一组 SMA 线,不用手写十几次 iMA。开 MT5 把 indicator_buffers 设 19、indicator_plots 设 13,挂上 OOPIndicators 头文件即可验证这套继承结构。
class="type">void CIndicator_1Data1Color::SetAsSeries(class="type">bool set) { CIndicator_1Data::SetAsSeries(set); ArraySetAsSeries(color_buffer, set); } class=class="str">"cmt">//... class="macro">#class="kw">property indicator_buffers class="num">19 class="macro">#class="kw">property indicator_plots class="num">13 class="macro">#include <OOPIndicators/IndicatorClass.mqh> class="macro">#include <OOPIndicators/ColorLerp.mqh> class="kw">input class="type">int atr_period = class="num">10; class=class="str">"cmt">//ATR Period class="kw">input class="type">class="kw">double atr_band_multiplier = class="num">0.8; class=class="str">"cmt">//ATR Multiplier for bands class="kw">input class="type">bool show_bands = true; class=class="str">"cmt">//Show Bands class="kw">input class="type">bool show_data = class="kw">false; class=class="str">"cmt">//Show Extra Data class="kw">input class="type">int ma_faster_period = class="num">14; class=class="str">"cmt">//MA Faster Period class="kw">input class="type">int ma_step = class="num">2; class=class="str">"cmt">//MA Step class="kw">input ENUM_MA_METHOD ma_method = MODE_SMA; class=class="str">"cmt">//MA Method class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ATR Bands class (inherit from Filling Plot) | class=class="str">"cmt">//+------------------------------------------------------------------+ class CATRBand : class="kw">public CIndicatorPlotFilling { class="kw">private: class="type">int handle; class="kw">public: 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 limit, class="kw">const class="type">int rates_total, class="kw">const class="type">class="kw">double &close[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CATRBand::Init(class="type">void) { handle = iATR(NULL, PERIOD_CURRENT, atr_period); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CATRBand::Update(class="kw">const class="type">int limit,class="kw">const class="type">int rates_total,class="kw">const class="type">class="kw">double &close[]) { class="type">class="kw">double atr[]; CopyBuffer(handle, class="num">0, class="num">0, rates_total-limit, atr); for (class="type">int i=limit; i<rates_total; i++) { first_array[i] = close[i]+atr[i-limit]*atr_band_multiplier; second_array[i] = close[i]-atr[i-limit]*atr_band_multiplier; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CATRBand::DeInit(class="type">void) { IndicatorRelease(handle); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Moving Averages class (inherit from Line Plot) | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMA : class="kw">public CIndicatorPlotLine { class="kw">private: class="type">int handle; class="kw">public: class="kw">virtual class="type">void Init(class="type">int period, ENUM_MA_METHOD mode);