DoEasy 函数库中的时间序列(第五十六部分):自定义指标对象,从集合中的指标对象获取数据·进阶篇
(2/3)· 标准指标写死参数,自定义指标参数未知,这篇讲清对象化封装与取值路径
指标对象的排序键与属性读写接口
在 MT5 自建指标容器类里,排序枚举把空值、品种、名称、短名都拆成了独立键。SORT_BY_INDICATOR_EMPTY_VALUE 接在双精度属性首值后面,专门标记『不绘制』的缓冲空位;字符串类则从 SORT_BY_INDICATOR_SYMBOL 起,依次覆盖 symbol、name、shortname,方便多实例按字段归并。 类内用一组 Set 方法写指标元数据:SetGroup 落分组、SetEmptyValue 定缓冲空值、SetName 与 SetShortName 分别写全名和副名,高亮的 SetID 把自定义整型 ID 写进 INDICATOR_PROP_ID。每个方法本质都是对 this.SetProperty 的薄封装,调用一次只改一个属性。 读方向对称:Status 取运行状态枚举,Group 取分组,Timeframe 取周期,TypeIndicator 取指标类型,全部由 this.GetProperty 强制转回对应枚举。开 MT5 新建 EA 时,把这些方法套到自己的 CIndicator 包装类上,就能在策略里按 ID 或短名快速检索已加载指标。
SORT_BY_INDICATOR_EMPTY_VALUE = FIRST_INDICATOR_DBL_PROP, class=class="str">"cmt">// Sort by the empty value for plotting where nothing will be drawn class=class="str">"cmt">//--- Sort by class="type">class="kw">string properties SORT_BY_INDICATOR_SYMBOL = FIRST_INDICATOR_STR_PROP, class=class="str">"cmt">// Sort by indicator symbol SORT_BY_INDICATOR_NAME, class=class="str">"cmt">// Sort by indicator name SORT_BY_INDICATOR_SHORTNAME, class=class="str">"cmt">// Sort by indicator class="type">class="kw">short name }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Set indicator’s(class="num">1) group, (class="num">2) empty value of buffers, (class="num">3) name, (class="num">4) class="type">class="kw">short name, (class="num">5) indicator ID class="type">void SetGroup(const ENUM_INDICATOR_GROUP group) { this.SetProperty(INDICATOR_PROP_GROUP,group); } class="type">void SetEmptyValue(const class="type">class="kw">double value) { this.SetProperty(INDICATOR_PROP_EMPTY_VALUE,value); } class="type">void SetName(const class="type">class="kw">string name) { this.SetProperty(INDICATOR_PROP_NAME,name); } class="type">void SetShortName(const class="type">class="kw">string shortname) { this.SetProperty(INDICATOR_PROP_SHORTNAME,shortname); } class="type">void SetID(const class="type">int id) { this.SetProperty(INDICATOR_PROP_ID,id); } class=class="str">"cmt">//--- Return indicator’s(class="num">1) status, (class="num">2) group, (class="num">3) timeframe, (class="num">4) type, (class="num">5) handle, (class="num">6) ID, class=class="str">"cmt">//--- (class="num">7) empty value of buffers, (class="num">8) name, (class="num">9) class="type">class="kw">short name, (class="num">10) symbol ENUM_INDICATOR_STATUS Status(class="type">void) const { class="kw">return (ENUM_INDICATOR_STATUS)this.GetProperty(INDICATOR_PROP_STATUS);} ENUM_INDICATOR_GROUP Group(class="type">void) const { class="kw">return (ENUM_INDICATOR_GROUP)this.GetProperty(INDICATOR_PROP_GROUP); } ENUM_TIMEFRAMES Timeframe(class="type">void) const { class="kw">return (ENUM_TIMEFRAMES)this.GetProperty(INDICATOR_PROP_TIMEFRAME); } ENUM_INDICATOR TypeIndicator(class="type">void) const { class="kw">return (ENUM_INDICATOR)this.GetProperty(INDICATOR_PROP_TYPE); }
◍ 指标句柄与属性读取的封装方法
在自建指标管理类里,把 MT5 原生接口的只读属性做成内联 getter,能省掉反复调用 CiIndicator::GetProperty 的样板代码。下面这组方法直接返回句柄、ID、空值、名称等核心字段,调用层拿到对象就能用。 Handle() 返回 (int)this.GetProperty(INDICATOR_PROP_HANDLE),这是 MT5 里跨函数引用指标实例的唯一整数令牌;ID() 对应 INDICATOR_PROP_ID,用于区分同类指标的不同加载实例。EmptyValue() 拿空值画布填充位,Name() 和 ShortName() 分别取完整名与子窗口显示名,Symbol() 锁定品种不随当前图表漂移。 注释里点明的六类描述方法(类型、状态、分组、周期、空值、参数数组)是调试时快速定位指标异常的关键。GetTypeDescription 已内联返回成员 m_ind_type_description,其余四个留作类外实现,建议接小布日志输出,在 EURUSD 这类高波动品种上排查指标掉线时概率更高。 外汇与贵金属杠杆交易风险极高,上述封装仅降低代码耦合,不构成任何方向判断。
class="type">int Handle(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(INDICATOR_PROP_HANDLE); } class="type">int ID(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(INDICATOR_PROP_ID); } class="type">class="kw">double EmptyValue(class="type">void) const { class="kw">return this.GetProperty(INDICATOR_PROP_EMPTY_VALUE); } class="type">class="kw">string Name(class="type">void) const { class="kw">return this.GetProperty(INDICATOR_PROP_NAME); } class="type">class="kw">string ShortName(class="type">void) const { class="kw">return this.GetProperty(INDICATOR_PROP_SHORTNAME); } class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.GetProperty(INDICATOR_PROP_SYMBOL); } class=class="str">"cmt">//--- Return description of(class="num">1) type, (class="num">2) status, (class="num">3) group, (class="num">4) timeframe, (class="num">5) empty value of indicator, (class="num">6) parameter of m_mql_param array class="type">class="kw">string GetTypeDescription(class="type">void) const { class="kw">return m_ind_type_description; } class="type">class="kw">string GetStatusDescription(class="type">void) const; class="type">class="kw">string GetGroupDescription(class="type">void) const; class="type">class="kw">string GetTimeframeDescription(class="type">void) const; class="type">class="kw">string GetEmptyValueDescription(class="type">void) const;
「从指标对象里抠出缓冲区数据」
做价格行为分析时,常常要在 EA 里直接读已加载指标的某根 K 线数值,而不是重复计算。下面这段 CIndicatorDE 类的接口和实现,就是给指标对象封装了按索引或按时间取缓冲区的通道。 类里声明了两个 GetDataBuffer 重载:一个吃 int index(柱序号),一个吃 datetime time(柱时间),都返回 double。还有 GetMqlParamDescription 负责把 m_mql_param 数组里的参数打成可读字符串,Print 系列虚函数留给子类填具体日志。 实际取数靠 CopyBuffer 一次性拷 1 根柱到长度为 1 的数组。拷贝成功(copied==1)就返回 array[0],否则回落到对象的 EmptyValue()。这意味着调用方拿到 EMPTY_VALUE 时,要先怀疑是不是句柄失效或索引越界,而不是当成真实报价。 两个重载唯一差别在 CopyBuffer 的第三参:传 index 时从序号定位,传 time 时从时间定位。回测里若用 time 重载,注意历史合成柱的 open time 必须严丝合缝,否则 copied 会返回 0。外汇与贵金属杠杆高,这类底层读取错误可能让信号逻辑静默失效,建议开 MT5 用 Print 把 copied 打出来验证一次。
class="type">class="kw">string GetMqlParamDescription(const class="type">int index) const; class=class="str">"cmt">//--- Display the description of indicator object properties in the journal(full_prop=true - all properties, false - supported ones only) class="type">void Print(const class="type">bool full_prop=false); class=class="str">"cmt">//--- Display(class="num">1) a class="type">class="kw">short description, (class="num">2) description of indicator object parameters in the journal(implementation in the descendants) class="kw">virtual class="type">void PrintShort(class="type">void) {;} class="kw">virtual class="type">void PrintParameters(class="type">void) {;} class=class="str">"cmt">//--- Return data of specified buffer from specified bar(class="num">1) by index, (class="num">2) by bar time class="type">class="kw">double GetDataBuffer(const class="type">int buffer_num,const class="type">int index); class="type">class="kw">double GetDataBuffer(const class="type">int buffer_num,const class="type">class="kw">datetime time); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of parameter of m_mql_param array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CIndicatorDE::GetMqlParamDescription(const class="type">int index) const { class="kw">return "["+(class="type">class="kw">string)index+"] "+MqlParameterDescription(this.m_mql_param[index]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return data of specified buffer from specified bar by index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CIndicatorDE::GetDataBuffer(const class="type">int buffer_num,const class="type">int index) { class="type">class="kw">double array[class="num">1]={EMPTY_VALUE}; class="type">int copied=::CopyBuffer(this.Handle(),buffer_num,index,class="num">1,array); class="kw">return(copied==class="num">1 ? array[class="num">0] : this.EmptyValue()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return data of specified buffer from specified bar by time | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CIndicatorDE::GetDataBuffer(const class="type">int buffer_num,const class="type">class="kw">datetime time) { class="type">class="kw">double array[class="num">1]={EMPTY_VALUE}; class="type">int copied=::CopyBuffer(this.Handle(),buffer_num,time,class="num">1,array); class="kw">return(copied==class="num">1 ? array[class="num">0] : this.EmptyValue()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Closed parametric constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CIndicatorDE::CIndicatorDE(ENUM_INDICATOR ind_type,
指标对象构造时的句柄与属性落盘
下面这段构造函数把外部传入的参数数组原样搬进对象内部,并立刻调用 IndicatorCreate 去拿指标句柄。注意 ArrayResize 的返回值被直接用作循环上限,意味着若传入 mql_params 长度为 0,后续拷贝循环一次都不会跑,但句柄仍会以 count=0 去创建,可能返回无效句柄。 [CODE] string symbol, ENUM_TIMEFRAMES timeframe, ENUM_INDICATOR_STATUS status, ENUM_INDICATOR_GROUP group, string name, string shortname, MqlParam &mql_params[]) { //--- Set collection ID for the object this.m_type=COLLECTION_INDICATORS_ID; //--- Write description of indicator type this.m_ind_type_description=IndicatorTypeDescription(ind_type); //--- If parameter array size passed to constructor is more than zero //--- fill in the array of object parameters with data from the array passed to constructor int count=::ArrayResize(this.m_mql_param,::ArraySize(mql_params)); for(int i=0;i<count;i++) { this.m_mql_param[i].type = mql_params[i].type; this.m_mql_param[i].double_value = mql_params[i].double_value; this.m_mql_param[i].integer_value= mql_params[i].integer_value; this.m_mql_param[i].string_value = mql_params[i].string_value; } //--- Create indicator handle int handle=::IndicatorCreate(symbol,timeframe,ind_type,count,this.m_mql_param); //--- Save integer properties this.m_long_prop[INDICATOR_PROP_STATUS] = status; this.m_long_prop[INDICATOR_PROP_TYPE] = ind_type; this.m_long_prop[INDICATOR_PROP_GROUP] = group; this.m_long_prop[INDICATOR_PROP_TIMEFRAME] = timeframe; this.m_long_prop[INDICATOR_PROP_HANDLE] = handle; this.m_long_prop[INDICATOR_PROP_ID] = WRONG_VALUE; //--- Save real properties this.m_double_prop[this.IndexProp(INDICATOR_PROP_EMPTY_VALUE)]=EMPTY_VALUE; //--- Save string properties
| this.m_string_prop[this.IndexProp(INDICATOR_PROP_SYMBOL)] = (symbol==NULL | symbol=="" ? ::Symbol() : symbol); |
|---|
this.m_string_prop[this.IndexProp(INDICATOR_PROP_NAME)] = name; this.m_string_prop[this.IndexProp(INDICATOR_PROP_SHORTNAME)]= shortname; } [/CODE] 逐行拆解:symbol 与 shortname 若为空串,symbol 会自动退回到当前图表品种(::Symbol()),但 shortname 不会补默认值,界面上可能显示空白。INDICATOR_PROP_ID 被硬性写为 WRONG_VALUE,说明对象刚构造时还未与某类集合索引绑定,后续需靠别的流程回填。 外汇与贵金属市场杠杆高、滑点大,这类底层对象若句柄创建失败却未被上层检查,策略在实时行情里可能静默失效。开 MT5 把这段塞进自己的指标管理类,打印 handle 值验证在 EURUSD 与 XAUUSD 上是否都返回大于 0 的有效整数。
class="type">class="kw">string symbol, ENUM_TIMEFRAMES timeframe, ENUM_INDICATOR_STATUS status, ENUM_INDICATOR_GROUP group, class="type">class="kw">string name, class="type">class="kw">string shortname, class="type">MqlParam &mql_params[]) { class=class="str">"cmt">//--- Set collection ID for the object this.m_type=COLLECTION_INDICATORS_ID; class=class="str">"cmt">//--- Write description of indicator type this.m_ind_type_description=IndicatorTypeDescription(ind_type); class=class="str">"cmt">//--- If parameter array size passed to constructor is more than zero class=class="str">"cmt">//--- fill in the array of object parameters with data from the array passed to constructor class="type">int count=::ArrayResize(this.m_mql_param,::ArraySize(mql_params)); for(class="type">int i=class="num">0;i<count;i++) { this.m_mql_param[i].type = mql_params[i].type; this.m_mql_param[i].double_value = mql_params[i].double_value; this.m_mql_param[i].integer_value= mql_params[i].integer_value; this.m_mql_param[i].string_value = mql_params[i].string_value; } class=class="str">"cmt">//--- Create indicator handle class="type">int handle=::IndicatorCreate(symbol,timeframe,ind_type,count,this.m_mql_param); class=class="str">"cmt">//--- Save integer properties this.m_long_prop[INDICATOR_PROP_STATUS] = status; this.m_long_prop[INDICATOR_PROP_TYPE] = ind_type; this.m_long_prop[INDICATOR_PROP_GROUP] = group; this.m_long_prop[INDICATOR_PROP_TIMEFRAME] = timeframe; this.m_long_prop[INDICATOR_PROP_HANDLE] = handle; this.m_long_prop[INDICATOR_PROP_ID] = WRONG_VALUE; class=class="str">"cmt">//--- Save real properties this.m_double_prop[this.IndexProp(INDICATOR_PROP_EMPTY_VALUE)]=EMPTY_VALUE; class=class="str">"cmt">//--- Save class="type">class="kw">string properties this.m_string_prop[this.IndexProp(INDICATOR_PROP_SYMBOL)] = (symbol==NULL || symbol=="" ? ::Symbol() : symbol); this.m_string_prop[this.IndexProp(INDICATOR_PROP_NAME)] = name; this.m_string_prop[this.IndexProp(INDICATOR_PROP_SHORTNAME)]= shortname; }
◍ 指标对象相等性判定与属性描述输出
在封装指标对象的类里,比较两个实例是否等价不能只看句柄。先调用 IsEqualMqlParamArrays 比对参数数组,若不一致直接返回 false,因为参数不同就意味着加载的可能是不同周期或不同计算的同名指标。 随后分三段遍历整数、双精度、字符串三类属性枚举:整数段从 0 到 INDICATOR_PROP_INTEGER_TOTAL,但主动跳过 INDICATOR_PROP_HANDLE 与 INDICATOR_PROP_ID,这两个由系统分配,不参与逻辑等价判断;双精度与字符串段则全量逐属性比对 GetProperty 返回值,任一不等即返回 false,全过才返回 true。 属性描述方法 GetPropertyDescription 用三元运算符把枚举映射成可读文本。以 INDICATOR_PROP_ID 为例,若对象不支持该属性则返回「不支持」提示,否则把 GetProperty 的整型值强转 string 输出;STATUS、TYPE、GROUP、TIMEFRAME 也按同样结构各自挂接描述函数。 开 MT5 把这段逻辑塞进你自己的指标管理类,能直接排查「肉眼同参数但比对失败」的坑——多数情况就是 HANDLE 或 ID 被误纳入了相等性判断。外汇与贵金属指标加载受报价会话影响,这类比对失败概率在跨品种复用时会明显上升,属高风险调试场景。
if(!IsEqualMqlParamArrays(compared_obj.m_mql_param)) class="kw">return false; class="type">int beg=class="num">0, end=INDICATOR_PROP_INTEGER_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_INDICATOR_PROP_INTEGER prop=(ENUM_INDICATOR_PROP_INTEGER)i; if(prop==INDICATOR_PROP_HANDLE || prop==INDICATOR_PROP_ID) class="kw">continue; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return false; } beg=end; end+=INDICATOR_PROP_DOUBLE_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_INDICATOR_PROP_DOUBLE prop=(ENUM_INDICATOR_PROP_DOUBLE)i; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return false; } beg=end; end+=INDICATOR_PROP_STRING_TOTAL; for(class="type">int i=beg; i<end; i++) { ENUM_INDICATOR_PROP_STRING prop=(ENUM_INDICATOR_PROP_STRING)i; if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) class="kw">return false; } class="kw">return true; } class="type">class="kw">string CIndicatorDE::GetPropertyDescription(ENUM_INDICATOR_PROP_INTEGER class="kw">property) { class="kw">return ( class="kw">property==INDICATOR_PROP_STATUS ? CMessage::Text(MSG_LIB_TEXT_IND_TEXT_STATUS)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.GetStatusDescription() ) : class="kw">property==INDICATOR_PROP_TYPE ? CMessage::Text(MSG_LIB_TEXT_IND_TEXT_TYPE)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.GetTypeDescription() ) : class="kw">property==INDICATOR_PROP_GROUP ? CMessage::Text(MSG_LIB_TEXT_IND_TEXT_GROUP)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.GetGroupDescription() ) : class="kw">property==INDICATOR_PROP_ID ? CMessage::Text(MSG_LIB_TEXT_IND_TEXT_ID)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==INDICATOR_PROP_TIMEFRAME ? CMessage::Text(MSG_LIB_TEXT_IND_TEXT_TIMEFRAME)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.GetTimeframeDescription() ) :
「把 MqlParam 拆成可读字符串」
在 MT5 自定义指标库里,MqlParam 结构体的内容直接打印就是一堆内存值,肉眼难辨。下面这个函数按 type 分支把参数还原成文字描述,调试时往日志一丢就能看清调用方到底传了什么。 以实际指标参数为例,控制台输出过这样一组:第1个整型值 13(对应指标类型枚举)、第2~3个为 0、第4个为 2。如果不走类型化描述,你只能看到四个数字,无法判断哪个是时间帧、哪个是价位精度。 字符串、时间、颜色、布尔、整型、双精度各自走不同分支:TYPE_STRING 取 string_value,TYPE_DATETIME 用 TimeToString 带日期分秒,TYPE_COLOR 转成 ColorToString 的 Web 色值,其余整数区间用 integer_value 强转,浮点则 DoubleToString 保留 8 位小数。 CIndAC::PrintShort 里拼 id 的逻辑也顺带处理了:ID 大于 WRONG_VALUE 才追加 ', id #xx]' 后缀,否则只补 ']'。复制下面代码到你的 .mqh 里,调用一次就能在专家日志验证参数映射是否符合预期。外汇与贵金属品种波动剧烈,这类底层诊断只帮你排错,不预示任何方向。
class="type">class="kw">string MqlParameterDescription(const class="type">MqlParam &mql_param) { class="type">int type=mql_param.type; class=class="str">"cmt">// 取出参数类型枚举 class="type">class="kw">string res=CMessage::Text(MSG_ORD_TYPE)+" "+class="kw">typename(type)+": "; class=class="str">"cmt">// 拼前缀:类型名+ class=class="str">"cmt">//--- type of parameter class="type">class="kw">string if(type==TYPE_STRING) res+=mql_param.string_value; class=class="str">"cmt">// 字符串类型取字符串成员 class=class="str">"cmt">//--- type of parameter class="type">class="kw">datetime else if(type==TYPE_DATETIME) res+=TimeToString(mql_param.integer_value,TIME_DATE|TIME_MINUTES|TIME_SECONDS); class=class="str">"cmt">// 时间类型转可读时间 class=class="str">"cmt">//--- type of parameter class="type">class="kw">color else if(type==TYPE_COLOR) res+=ColorToString((class="type">class="kw">color)mql_param.integer_value,true); class=class="str">"cmt">// 颜色转字符串 class=class="str">"cmt">//--- type of parameter class="type">bool else if(type==TYPE_BOOL) res+=(class="type">class="kw">string)(class="type">bool)mql_param.integer_value; class=class="str">"cmt">// 布尔转字符串 class=class="str">"cmt">//--- integer types else if(type>TYPE_BOOL && type<TYPE_FLOAT) res+=(class="type">class="kw">string)mql_param.integer_value; class=class="str">"cmt">// 整型区间取整数成员 class=class="str">"cmt">//--- real types else res+=DoubleToString(mql_param.double_value,class="num">8); class=class="str">"cmt">// 浮点保留8位 class="kw">return res; }