创建多交易品种、多周期指标·进阶篇
📘

创建多交易品种、多周期指标·进阶篇

第 2/3 篇

多周期多品种指标的基类骨架

想在 MT5 里同时盯多个品种、多个周期的指标,第一步是把底层结构搭清楚。下面这段基类 CIndMSTF 直接继承 CObject,本身不画任何线,只负责给后续具体指标提供统一的对象容器与生命周期管理。 构造函数与析构函数留空实现,意味着子类要自己接管指标句柄的创建和释放。真正决定运行节奏的是两个定时器宏:TIMER_COUNT_1 设为 90,注释里写明不能超过 120(即两分钟);TIMER_COUNT_2 设为 20,原注释提示值太小会在活跃行情里频繁触发分时模拟,实战中黄金或欧元美元跳动快时容易拖慢回测。 类别与状态用枚举固化下来。ENUM_IND_CATEGORY 把趋势、震荡、成交量、比尔·威廉姆斯及自定义分成 6 类;ENUM_LINE_STATE 则定义了 14 种线态,从单纯向上/向下到穿越、触碰、等于某值都单列,方便后续做状态机判断而不是写一堆 if 比较价格。 比较模式枚举 ENUM_COMPARE_MODE 默认零值比全部属性,也可只按句柄、品种、周期、ID、描述或类别来比。开 MT5 把这段头文件塞进自己的 include 目录,先改 TIMER_COUNT_2 到 30~50 之间跑一晚欧美盘,感受下定时模拟频率对 CPU 占用的差异。

MQL5 / C++
class=class="str">"cmt">//| Base class of the multi-symbol multi-period indicator                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CIndMSTF : class="kw">public CObject
  {
class="kw">private:
class="kw">protected:
class="kw">public:
class=class="str">"cmt">//--- Constructor/destructor
                     CIndMSTF();
                    ~CIndMSTF();
  };
class=class="str">"cmt">//--- includes
class="macro">#include <Object.mqh>
class=class="str">"cmt">//--- defines
class="macro">#define TIMER_COUNT_1(class="num">90)   class=class="str">"cmt">// Timer class="num">1 size. Must not be more than two minutes(class="num">120)
class="macro">#define TIMER_COUNT_2(class="num">20)   class=class="str">"cmt">// Timer class="num">2 size. Too small values quickly trigger tick emulation, which is not desirable in an active market
class=class="str">"cmt">//--- enums
enum ENUM_IND_CATEGORY       class=class="str">"cmt">// Indicator categories
  {
   IND_CATEGORY_NONE,        class=class="str">"cmt">// Not set
   IND_CATEGORY_TREND,       class=class="str">"cmt">// Trend
   IND_CATEGORY_OSCILLATOR,  class=class="str">"cmt">// Oscillators
   IND_CATEGORY_VOLUME,      class=class="str">"cmt">// Volume
   IND_CATEGORY_WILLIAMS,    class=class="str">"cmt">// Bill Williams
   IND_CATEGORY_CUSTOM,      class=class="str">"cmt">// Custom
  };
enum ENUM_COMPARE_MODE       class=class="str">"cmt">// Comparison mode
  {
   class=class="str">"cmt">// By class="kw">default, the comparison mode is set to zero, which compares all properties
   COMPARE_MODE_HANDLE=class="num">1,     class=class="str">"cmt">// Compare by handle
   COMPARE_MODE_SYMBOL,       class=class="str">"cmt">// Compare by symbol
   COMPARE_MODE_TIMEFRAME,    class=class="str">"cmt">// Compare by chart period
   COMPARE_MODE_ID,           class=class="str">"cmt">// Compare by ID
   COMPARE_MODE_DESCRIPTION,  class=class="str">"cmt">// Compare by custom description
   COMPARE_MODE_CATEGORY,     class=class="str">"cmt">// Compare by category
  };
enum ENUM_LINE_STATE          class=class="str">"cmt">// Indicator line state
  {
   LINE_STATE_NONE,           class=class="str">"cmt">// Undefined
   LINE_STATE_UP,             class=class="str">"cmt">// Upward
   LINE_STATE_DOWN,           class=class="str">"cmt">// Downward
   LINE_STATE_TURN_UP,        class=class="str">"cmt">// Upward reversal
   LINE_STATE_TURN_DOWN,      class=class="str">"cmt">// Downward reversal
   LINE_STATE_STOP_UP,        class=class="str">"cmt">// Upward stop
   LINE_STATE_STOP_DOWN,      class=class="str">"cmt">// Downward stop
   LINE_STATE_ABOVE,          class=class="str">"cmt">// Above value
   LINE_STATE_BELOW,          class=class="str">"cmt">// Below value
   LINE_STATE_CROSS_UP,       class=class="str">"cmt">// Upward value crossing
   LINE_STATE_CROSS_DOWN,     class=class="str">"cmt">// Downward value crossing
   LINE_STATE_TOUCH_BELOW,    class=class="str">"cmt">// Touching value from below
   LINE_STATE_TOUCH_ABOVE,    class=class="str">"cmt">// Touching value from above
   LINE_STATE_EQUALS,         class=class="str">"cmt">// Equal to value
  };
enum ENUM_ERR_TYPE            class=class="str">"cmt">// Indicator calculation error type
  {

「指标缓冲区的错误态与结构体封装」

在自研 MT5 指标里,先把缓冲区可能出的岔子归类,比事后查 bug 更省时间。下面这组枚举把四种典型异常列清楚:0 号是无错误,1 号代表数据未同步(多发生在跨周期读取刚切换品种时),2 号是数据根本没加载,3 号是计算没跑完——实盘里若缓冲区返回 1 或 3,硬画线会出幽灵信号。 真正管缓冲区的是 SBuffer 结构。它把数组、初值、水平偏移、描述塞进一个包里:array[] 存指标值,init_value 决定空值填充什么,shift 控制整条线左右挪几根 K,descript 方便在数据窗口里辨认。 结构里顺手写了几个内联方法。SetInitValue / InitValue 管初值读写,SetShift / Shift 管偏移,BuffResize 用 ArrayResize 改大小且校验返回值是否等于目标尺寸,BufferSize 直接取 array.Size(),InitBuffer 拿 ArrayInitialize 把整段填成 init_value。这样在 OnCalculate 里调一句 InitBuffer() 就能复位,不用反复写循环。 别把缓冲区的空值当零处理 init_value 若设成 0,在 RSI 这类有真实 0 值的指标里会和「未初始化」混淆;贵金属与外汇波动快,建议设成 EMPTY_VALUE 之外的明显异常数(如 -1)以便肉眼排错。

MQL5 / C++
  ERR_TYPE_NO_ERROR,           class=class="str">"cmt">// No error
  ERR_TYPE_NO_CYNC,             class=class="str">"cmt">// Data not synchronized
  ERR_TYPE_NO_DATA,             class=class="str">"cmt">// Data not loaded
  ERR_TYPE_NO_CALC,             class=class="str">"cmt">// Calculation not completed
  };
class=class="str">"cmt">//--- class="kw">struct
class="kw">struct SBuffer                  class=class="str">"cmt">// Structure of the indicator buffer
  {
  class="type">class="kw">double              array[];     class=class="str">"cmt">// Indicator buffer array
  class="type">class="kw">double              init_value;  class=class="str">"cmt">// Initializing value
  class="type">int                 shift;       class=class="str">"cmt">// Horizontal shift of the buffer
  class="type">class="kw">string              descript;    class=class="str">"cmt">// Buffer description
  class=class="str">"cmt">//--- (class="num">1) Sets, (class="num">2) returns the initializing value,
  class="type">void                SetInitValue(const class="type">class="kw">double value) { init_value=value;                                                    }
  class="type">class="kw">double              InitValue(class="type">void)                                                    { class="kw">return init_value;                                            }
  class=class="str">"cmt">//--- (class="num">1) Sets, (class="num">2) returns the buffer offset
  class="type">void                SetShift(const class="type">int value)                                          { shift=value;                                                                }
  class="type">int                 Shift(class="type">void)                                                        { class="kw">return shift;                                                          }
class=class="str">"cmt">//--- (class="num">1) Resizes the buffer array, (class="num">2) returns the size of the buffer array,
class=class="str">"cmt">//--- (class="num">3) initializes the array with the set "empty" value
  class="type">bool                BuffResize(const class="type">int new_size)   { class="kw">return(ArrayResize(array,new_size)==new_size);}
  class="type">uint                BufferSize(class="type">void)                                                    { class="kw">return array.Size();                                            }
  class="type">int                 InitBuffer(class="type">void)                                                    { class="kw">return ArrayInitialize(array,init_value);    }
  };

◍ 线状态与错误码的枚举映射

这段 switch 把指标线在多维分析里可能处的状态全部翻译成可读字符串,方便在日志或面板上直接看,而不是对着数字发懵。从 NONE 到 EQUALS 一共 14 个分支,覆盖了方向(UP/DOWN)、停顿(STOP_UP/STOP_DOWN)、反转(TURN_UP/TURN_DOWN)、相对基准线的穿越与触碰(CROSS_UP/TOUCH_BELOW 等),default 兜底返回 Unknown。 另一个 TypeErrorcDescription 函数只处理 3 类计算期错误:ERR_TYPE_NO_CYNC 对应数据未同步,ERR_TYPE_NO_DATA 是数据没加载,ERR_TYPE_NO_CALC 是计算没跑完,其余情况归为 No error。实盘里若多品种多周期指标频繁吐出 Data is not synchronized,往往意味着跨品种历史数据还没补齐,外汇与贵金属品种尤其容易在跳空后触发,属于高概率但非必然的同步风险。 下面这段是基类 CIndMSTF 的私有成员声明,它继承自 CObject,专门用来承载多符号多周期指标的基础属性。m_program 记程序类型,m_type 记指标类型,m_timeframe 和 m_symbol 分别锁定时段与品种,后续扩展都从这四个字段起步。

MQL5 / C++
   case LINE_STATE_NONE        :  class="kw">return "None";
   case LINE_STATE_UP          :  class="kw">return "Up";
   case LINE_STATE_STOP_UP     :  class="kw">return "Stop Up";
   case LINE_STATE_TURN_UP     :  class="kw">return "Turn Up";
   case LINE_STATE_DOWN        :  class="kw">return "Down";
   case LINE_STATE_STOP_DOWN   :  class="kw">return "Stop Down";
   case LINE_STATE_TURN_DOWN   :  class="kw">return "Turn Down";
   case LINE_STATE_ABOVE       :  class="kw">return "Above level";
   case LINE_STATE_BELOW       :  class="kw">return "Below level";
   case LINE_STATE_CROSS_UP    :  class="kw">return "Crossing Up";
   case LINE_STATE_CROSS_DOWN  :  class="kw">return "Crossing Down";
   case LINE_STATE_TOUCH_BELOW :  class="kw">return "Touch from Below";
   case LINE_STATE_TOUCH_ABOVE :  class="kw">return "Touch from Above";
   case LINE_STATE_EQUALS      :  class="kw">return "Equals";
   class="kw">default                     :  class="kw">return "Unknown";
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return error description in indicator calculation                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string TypeErrorcDescription(ENUM_ERR_TYPE error_type)
  {
   class="kw">switch(error_type)
     {
      case ERR_TYPE_NO_CYNC   :  class="kw">return "Data is not synchronized";
      case ERR_TYPE_NO_DATA   :  class="kw">return "Data not loaded";
      case ERR_TYPE_NO_CALC   :  class="kw">return "Calculation not completed";
      class="kw">default                 :  class="kw">return "No error";
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base class of the multi-symbol multi-period indicator            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CIndMSTF : class="kw">public CObject
  {
class="kw">private:
   ENUM_PROGRAM_TYPE m_program;          class=class="str">"cmt">// Program type
   ENUM_INDICATOR    m_type;             class=class="str">"cmt">// Indicator type
   ENUM_TIMEFRAMES   m_timeframe;        class=class="str">"cmt">// Chart timeframe
   class="type">class="kw">string            m_symbol;           class=class="str">"cmt">// Chart symbol

指标基类里的成员与缓冲区接口

在 MQL5 自建指标框架里,通常会先放一个基类把通用字段收拢。下面这段声明里,公开部分存了句柄、标识、成败标记和错误类型,也带名称与参数描述,方便后续调试时直接打印。 m_handle 是 EA 或脚本向终端申请指标实例后拿到的整数句柄;m_success 与 m_type_err 配合,能在每 tick 计算后判断这次取值是否可靠,避免拿到的缓冲里混着空值。 保护段里的 m_param[] 用 MqlParam 结构装载参数,m_buffers[] 才是真正存序列数据的地方。m_limit 表示本 tick 最少要多少根 bar 才能算,m_rates_total 是可用总柱数,m_prev_calculated 记录上一次已算柱数——这三个 int 直接决定你的主循环从哪根 bar 开始写,省得每 tick 全量重算。 SetName 与 SetParameters 只是薄封装,把名字和参数字符串写进成员;真正吃算力的是 BufferResize / BuffersResize 与 BufferInitialize / BuffersInitialize,前者改指定或全缓冲长度,后者顺带填初值。开 MT5 新建一个空指标,把这段声明原样贴进头文件,编译后能在调试窗口看到 m_name 与 m_parameters 随 OnInit 赋值变化。

MQL5 / C++
  class="type">int                m_handle;             class=class="str">"cmt">// Indicator handle
  class="type">int                m_id;                 class=class="str">"cmt">// Identifier
  class="type">bool               m_success;            class=class="str">"cmt">// Successful calculation flag
  ENUM_ERR_TYPE      m_type_err;           class=class="str">"cmt">// Calculation error type
  class="type">class="kw">string             m_description;        class=class="str">"cmt">// Custom description of the indicator
  class="type">class="kw">string             m_name;               class=class="str">"cmt">// Indicator name
  class="type">class="kw">string             m_parameters;        class=class="str">"cmt">// Description of indicator parameters
class="kw">protected:
  ENUM_IND_CATEGORY m_category;            class=class="str">"cmt">// Indicator category
  class="type">MqlParam           m_param[];            class=class="str">"cmt">// Array of indicator parameters
  class="type">class="kw">string             m_title;              class=class="str">"cmt">// Title(indicator name + description of parameters)
  SBuffer            m_buffers[];          class=class="str">"cmt">// Indicator buffers
  class="type">int                m_digits;             class=class="str">"cmt">// Digits in indicator values
  class="type">int                m_limit;              class=class="str">"cmt">// Number of bars required to calculate the indicator on the current tick
  class="type">int                m_rates_total;        class=class="str">"cmt">// Number of available bars for indicator calculation
  class="type">int                m_prev_calculated;    class=class="str">"cmt">// Number of calculated bars on the previous indicator call

class=class="str">"cmt">//--- (class="num">1) Sets indicator name, (class="num">2) description of parameters
  class="type">void               SetName(const class="type">class="kw">string name)                    { this.m_name=name;        }
  class="type">void               SetParameters(const class="type">class="kw">string str)               { this.m_parameters=str;   }

class=class="str">"cmt">//--- Resizes the(class="num">1) specified, (class="num">2) all indicator buffers
  class="type">bool               BufferResize(const class="type">uint buffer_num,const class="type">int new_buff_size);
  class="type">bool               BuffersResize(const class="type">int new_buff_size);
class=class="str">"cmt">//--- Initializes the(class="num">1) specified, (class="num">2) all indicator buffers
  class="type">bool               BufferInitialize(const class="type">uint buffer_num,const class="type">int new_buff_size);
  class="type">bool               BuffersInitialize(const class="type">int new_buff_size);

「MqlParam 结构比对的两个底层函数」

在 MT5 的 MqlParam 封装里,判断两个输入参数是否「同构」靠的是 IsEqualParameters。它不只比 type,还同时比对 integer_value、string_value,以及用 NormalizeDouble 把 double_value 差值精确到 8 位小数后是否归零——这意味着两个浮点参数只要误差小于 1e-8 就会被判相等。 CompareParams 则在这之上给出排序语义:相等返回 0;若当前参数在 type、整型、字符串或双精度任一维度上「大于」对比参数就返回 1,反之返回 -1。注意它是按字段顺序短路判断的,type 优先于数值和字符串。 实盘写 EA 参数校验时,这套逻辑可以直接复用:比如检测用户新载入的指标体系跟模板是否参数一致,调一下 IsEqualParameters 就能拦掉 8 位精度内的微小漂移,避免重复初始化指标句柄。外汇与贵金属杠杆高,参数误判可能引发异常下单,建议先在策略测试器里跑通再上真仓。

MQL5 / C++
class=class="str">"cmt">//--- Returns the flag indicating equality of the structure of one parameter of two objects
class="type">bool                IsEqualParameters(const class="type">MqlParam &this_param,const class="type">MqlParam &compared_param) const
                    {
                    if(this_param.type==compared_param.type            &&
                       this_param.integer_value==compared_param.integer_value  &&
                       this_param.string_value==compared_param.string_value    &&
                       ::NormalizeDouble(this_param.double_value-compared_param.double_value,class="num">8)==class="num">0
                       ) class="kw">return true;
                    class="kw">return false;
                    }
class=class="str">"cmt">//--- Return the result of comparison on one parameter of two objects
   class="type">int              CompareParams(const class="type">MqlParam &this_param,const class="type">MqlParam &compared_param)
                    {
                    if(this.IsEqualParameters(this_param,compared_param))
                       class="kw">return class="num">0;
                    else if(this_param.type>compared_param.type                 ||
                       this_param.integer_value>compared_param.integer_value    ||
                       this_param.string_value>compared_param.string_value      ||
                       this_param.double_value>compared_param.double_value
                       ) class="kw">return class="num">1;
                    else if(this_param.type<compared_param.type                 ||
                       this_param.integer_value<compared_param.integer_value    ||
                       this_param.string_value<compared_param.string_value      ||
                       this_param.double_value<compared_param.double_value
                       ) class="kw">return -class="num">1;

◍ 封装类的对外接口怎么用

上面那段代码是这个指标封装类 public 区的方法声明,真正在 EA 或脚本里调用的就是这些接口,而不是去碰内部的指标句柄。 CreateIndicator(void) 只负责建计算部分并返回句柄;Calculate(void) 跑一次计算,DataToBuffer(...) 把结果按目标品种和周期填进外部数组,limit 参数控制填充根数。实盘里若 limit 设得过大,可能拖慢 OnTick 节奏,外汇和贵金属波动快,这种开销要自己压到最小。 GetData 与 GetDataTo 的区别在于后者带 symbol_to / timeframe_to,能跨周期取数;BufferLineState 系列返回 ENUM_LINE_STATE,可判断线态而不必自己写阈值比较。SetBufferInitValue 和 SetBufferShift 分别管缓冲初值与偏移,调参时改这两个比改核心算法安全。 直接把这套声明拷进你的类头文件,MT5 里建个测试指标调用 Calculate + DataToBuffer,看不同 limit 下的耗时差异,比读文档直观。

MQL5 / C++
      else
            class="kw">return -class="num">1;
          }

class="kw">public:
class=class="str">"cmt">//--- Creates the calculation part of the indicator, returns the handle
   class="type">int            CreateIndicator(class="type">void);
class=class="str">"cmt">//--- (class="num">1) Calculates the indicator, (class="num">2) fills the passed buffer array(taking into account the chart period/symbol) with data from the indicator calculation buffer of this class
   class="type">bool           Calculate(class="type">void);
   class="type">bool           DataToBuffer(const class="type">class="kw">string symbol_to,const ENUM_TIMEFRAMES timeframe_to,const class="type">uint buffer_num,const class="type">int limit,class="type">class="kw">double &buffer[]);
class=class="str">"cmt">//--- (class="num">1) Sets(class="num">2) returns the initializing value for the specified buffer
   class="type">void           SetBufferInitValue(const class="type">uint buffer_num,const class="type">class="kw">double value);
   class="type">class="kw">double         BufferInitValue(const class="type">uint buffer_num) const;
class=class="str">"cmt">//--- (class="num">1) Sets(class="num">2) returns the offset value for the specified buffer
   class="type">void           SetBufferShift(const class="type">uint buffer_num,const class="type">int value);
   class="type">class="kw">double         BufferShift(const class="type">uint buffer_num) const;
class=class="str">"cmt">//--- Returns data of the specified buffer(class="num">1) as is, (class="num">2) relative to the specified symbol/timeframe,
class=class="str">"cmt">//--- (class="num">3) amount of data in the specified buffer, (class="num">4) the state of the indicator line as it is in the calculation part buffer,
class=class="str">"cmt">//--- (class="num">5) indicator line state taking into account the chart symbol/period, description of the line state(class="num">6) as is in the buffer(class="num">7) taking into account the chart symbol/period
   class="type">class="kw">double         GetData(const class="type">uint buffer_num,const class="type">int index)                const;
   class="type">class="kw">double         GetDataTo(const class="type">class="kw">string symbol_to,const ENUM_TIMEFRAMES timeframe_to,const class="type">uint buffer_num,const class="type">int index) const;
   class="type">uint           DataTotal(const class="type">uint buffer_num)                              const;
   ENUM_LINE_STATE   BufferLineState(const class="type">uint buffer_num,const class="type">int index)    const;
   ENUM_LINE_STATE   BufferLineState(const class="type">class="kw">string symbol_from,const ENUM_TIMEFRAMES timeframes_from,const class="type">uint buffer_num,const class="type">int index) const;
   ENUM_LINE_STATE   BufferLineStateRelative(const class="type">int buffer_num,const class="type">int index,const class="type">class="kw">double level0,const class="type">class="kw">double level1=EMPTY_VALUE);

常见问题

基类应预留缓冲区接口、周期/品种管理成员和错误态结构体,方便子类只填计算逻辑。先把骨架搭稳,再往里塞具体指标公式。
用结构体封装缓冲区的错误态,把无效数据标记为错误码而非硬写0或空值。渲染前先查状态枚举,避免把异常当正常价格画出来。
可以。小布能按你贴的代码梳一遍枚举映射和缓冲区封装,标出可能漏掉的错误态分支,省去你人工逐行核对。
主要比对参数类型、双精度值和字符串句柄,确认调用端传参与指标声明一致。不一致就直接返回错误态,不进计算。
对外只暴露初始化、更新和取值三个接口,内部周期切换和缓冲分配全藏起来。调用方传品种和周期就能拿线,不用碰底层。