MQL5 中的电子表格·进阶篇
📊

MQL5 中的电子表格·进阶篇

(2/3)· 直接裸写二维数组常陷类型混乱,这篇把 CTable 的方法与数据模型一次讲清

实战向进阶 第 2/3 篇
在 MQL5 里用普通二维数组存多类型行情数据,很多人靠结构体和强制转换硬撑,结果调试时类型对不上直接崩。CTable 类把异构单元格封装进统一接口,但多数交易者只知声明不会用 FirstResize 规划列。不先定列类型就写数据,后面 SecondResize 也救不了你。

◍ 从日志抓出表格单元格的真实值

在 MT5 里用 CTable 这类自定义表结构时,最直接的验证方式就是往专家日志里打点。下面这句把 table1 第 2 列第 0 行的内容推到终端,能立刻看到该格存了什么。

MQL5 / C++
  Print("table1 ",table1.sGet(class="num">2,class="num">0));   class=class="str">"cmt">// print the cell located in the class="num">2-nd column, class="num">0 row of the &class="macro">#x27;table1&class="macro">#x27; table.
}
class="num">2011.02.class="num">09 class="num">14:class="num">18:class="num">37     Table Script(EURUSD,H1)        table1 class="num">321.01232000
class="num">2011.02.class="num">09 class="num">14:class="num">18:class="num">37     Table Script(EURUSD,H1)        table class="num">321.01232
日志里 table1 那行输出是 321.01232000,而另一个 table 打印出来是 321.01232,差别只在小数尾部的零被截断显示。EURUSD 在 H1 周期下跑这段脚本,说明表内数值精度保留和字符串转换方式不一致,可能让你在比对两表时误判相等。 开 MT5 把这段打印逻辑塞进你自己的表读写函数后面,重点看 sGet 返回的字符串格式,别只看肉眼四舍五入后的数。

MQL5 / C++
  Print("table1 ",table1.sGet(class="num">2,class="num">0));   class=class="str">"cmt">// print the cell located in the class="num">2-nd column, class="num">0 row of the &class="macro">#x27;table1&class="macro">#x27; table.
}
class="num">2011.02.class="num">09 class="num">14:class="num">18:class="num">37     Table Script(EURUSD,H1)        table1 class="num">321.01232000
class="num">2011.02.class="num">09 class="num">14:class="num">18:class="num">37     Table Script(EURUSD,H1)        table class="num">321.01232

「列模型还是行模型,存储结构怎么选」

在 MT5 里组织表格数据,常见有连接列和连接行两种方案。本文落地用的是列模型:通过一个中间物引用信息,上下范围的实施差异不大,但列模型允许把数据方法直接放在存储数据的下范围对象里。 若改用行模型,往往要覆盖上类 CTable 中处理信息的方法,类一旦需要增强就会变复杂。两种都能跑,列模型移动数据快,行模型逐行追加和取行更快——因为实盘数据本就是按行进表的。 还有种最省事的做法:直接用结构数组。它实现成本最低,但结构必须由程序表硬编码说明,等于放弃了用自定义参数动态配置表格属性的可能,对外汇/贵金属这种多变品种是高风险的僵化设计。

用继承把异类数据塞进同一个动态数组

想在一个动态数组里同时存 int、double 甚至自定义对象,难点在于数组单元格只能接受单一类型声明。标准库靠连接列表 + CObject 基类做这件事,但实测要对基类动大量手术,不如自己写一套轻量继承结构。 核心思路是:基类只留虚函数空壳,派生类各自覆盖。这样声明 CBase* 数组后,既能装 CA 也能装 CB,调用 set/get 时由虚表派发到真实类型。若不用继承而在基类把函数主体全写出来,二进制里每个对象都会背一份完整大函数代码;用继承后只占几个空函数,内存开销明显更低。 下面这段可直接贴进 MT5 跑。基类 CBase 构造函数打印自身,四个虚函数 set(int)/set(double)/get(int)/get(double) 都是空壳或返回 0;CA 私有 int temp,覆盖 set(int) 与 get(int);CB 私有 double temp,覆盖 set(double) 与 get(double)。OnStart 里 a 指向 CA、b 指向 CB,分别写入 15 和 13.3,打印出 a=15 b=13.3,最后 delete 释放。 别把虚函数当免费午餐 虚表派发省了内存,但每次调用多一次间接跳转;在高频 OnTick 里批量操作几万条异构数据时才谈得上划算,平时小数组用 union 或结构体更直白。外汇贵金属波动剧烈,任何数据结构回测结论都只是概率倾向,实盘前务必在策略测试器跑足样本。

MQL5 / C++
class CBase
  {
class="kw">public:
                    CBase(){Print(__FUNCTION__);};
                    ~CBase(){Print(__FUNCTION__);};
   class="kw">virtual class="type">void      set(class="type">int sor){};
   class="kw">virtual class="type">void      set(class="type">class="kw">double sor){};
   class="kw">virtual class="type">int       get(class="type">int k){class="kw">return(class="num">0);};
   class="kw">virtual class="type">class="kw">double    get(class="type">class="kw">double k){class="kw">return(class="num">0);};
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class CA: class="kw">public CBase
  {
class="kw">private:
   class="type">int               temp;
class="kw">public:
                    CA(){Print(__FUNCTION__);};
                    ~CA(){Print(__FUNCTION__);};
   class="type">void              set(class="type">int sor){temp=sor;};
   class="type">int               get(class="type">int k){class="kw">return(temp);};
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class CB: class="kw">public CBase
  {
class="kw">private:
   class="type">class="kw">double            temp;
class="kw">public:
                    CB(){Print(__FUNCTION__);};
                   ~CB(){Print(__FUNCTION__);};
   class="type">void              set(class="type">class="kw">double sor){temp=sor;};
   class="type">class="kw">double            get(class="type">class="kw">double k){class="kw">return(temp);};
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
  CBase *a;   CBase *b;
  a=new CA(); b=new CB();
  a.set(class="num">15);  b.set(class="num">13.3);
  Print("a=",a.get(class="num">0)," b=",b.get(class="num">0.));
  class="kw">delete a;
  class="kw">delete b;
  }

◍ 二维数组排序靠索引数组保结构

处理 MT5 里的数组时,我刻意避开标准类,主要卡点在引用方式:用中间索引数组做间接寻址,比直接用变量指向单元格要慢,因为数组单元格得先在内存里定位。但换来的是能按行整体搬动,不破坏一条记录里混装的多种类型数据。 一维数组排序是把乱序元素排顺;二维数组不能整体重排,只需按某一列排序,同时让所有行跟着换位置。行本身是绑定结构,所以排序时要另存一个初始索引数组,知道哪一行对应原单元格,就能整行调出来。 标准类的快速排序是不稳定算法,经典实现里不适合这种用法;即便改成稳定版(多一次数据复制+索引数组排序),速度仍可能优于冒泡,但递归吃栈。字符串数组更占堆栈,所以我用鸡尾酒排序(双向冒泡)兜底。下面这段演示按第 2 列排序后,索引数组 r 把原行 1、2、0 重排,数据结构原样不动。 外汇与贵金属行情跳变频繁,这类自定义排序多用于本地持仓/品种扫描面板,回测或实盘前请在 MT5 策略测试器里用真实 tick 验证栈占用。

MQL5 / C++
before sorting by the class="num">2-nd column
class="num">4 class="num">2 class="num">3
class="num">1 class="num">5 class="num">3
class="num">3 class="num">3 class="num">6
after sorting
class="num">1 class="num">5 class="num">3
class="num">3 class="num">3 class="num">6
class="num">4 class="num">2 class="num">3
Initial array looks as following:
a[class="num">0][class="num">0]= class="num">4; a[class="num">0][class="num">1]= class="num">2; a[class="num">0][class="num">2]= class="num">3;
a[class="num">1][class="num">0]= class="num">1; a[class="num">1][class="num">1]= class="num">5; a[class="num">1][class="num">2]= class="num">3;
a[class="num">2][class="num">0]= class="num">3; a[class="num">2][class="num">1]= class="num">3; a[class="num">2][class="num">2]= class="num">6;
And the array of indexes of sorting by the class="num">2-nd column looks as:
r[class="num">0]=class="num">1;
r[class="num">1]=class="num">2;
r[class="num">2]=class="num">0;
Sorted values are returned according to the following scheme:
a[r[class="num">0]][class="num">0]-> class="num">1; a[r[class="num">0]][class="num">1]-> class="num">5; a[r[class="num">0]][class="num">2]-> class="num">3;
a[r[class="num">1]][class="num">0]-> class="num">3; a[r[class="num">1]][class="num">1]-> class="num">3; a[r[class="num">1]][class="num">2]-> class="num">6;
a[r[class="num">2]][class="num">0]-> class="num">4; a[r[class="num">2]][class="num">1]-> class="num">2; a[r[class="num">2]][class="num">2]-> class="num">3;
把表结构诊断交给小布
这些 CTable 列类型与维度的初始化检查,小布盯盘的 AIGC 已内置,打开对应品种页即可看到结构是否匹配你的数据流,你只需专注策略逻辑。

常见问题

FirstResize 本质是用 ENUM_DATATYPE 数组描述列类型并顺带确定行长度,它解决构造函数内传参不便的问题,也支持把对象引用传进去再划分内存。
目前小布内置的是表结构诊断与维度匹配提示,代码仍由你写;它帮你确认列类型数组和 SecondResize 行数是否合理。
不会。它只改记录行长的变量,在实际分配内存大小范围内虚拟截断,常用于复制表格时去掉无用行。
因为方法接收的是 CTable 类型对象的引用,MQL5 中对象引用必须靠 GetPointer 获取,否则无法在类间传递并复制列类型与数据。
CTable 内部按列存的 ENUM_DATATYPE 决定读取方式,调用对应 Get 方法即可,不用自己记每列偏移,详见本篇第 3 节。