数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易(基础篇)
在 MQL5 里用 Pandas 数据帧攒训练样本
MQL5 原生没有 Pandas,但借助 Python 集成环境,交易员可以直接在 MT5 脚本里调用 pandas.DataFrame 来规整历史行情与指标序列,比用数组硬拼方便太多。 DataFrame 的本质是一张带列名、带索引的二维表。你可以按列塞入 Open、High、Low、Close、Volume,也可以把自定义指标缓冲区整列丢进去,行索引默认是 0、1、2… 的整数序列。 往类里加数据最常见的是用字典构造:把每列数组作为键值传入,pandas 会自动对齐长度。如果某列长度不一致,会触发 NaN 填充,这一点在回测数据拼接时要盯紧。 把 DataFrame 直接分配给 CSV 文件,意味着你可以用 pandas 的 read_csv 读取 MT5 导出的 tick 或 bar 文件,再用 to_csv 写回,省掉自己解析分隔符的麻烦。 实际做机器学习数据收集时,建议先把 2020—2025 年的 XAUUSD 日线用 CopyRatesFromServer 拉到 Python 侧,转成 DataFrame 后做滑窗特征工程,再导出 CSV 给 sklearn 训练。外汇与贵金属杠杆高、跳空频繁,样本外验证必须留足最近 3 个月数据,否则过拟合概率显著上升。
「把 Pandas 的数据玩法搬进 MQL5」
在 MT5 里跑 ONNX 模型时,一个常被忽略的坑是:训练环境和部署环境的数值结构必须对得上。MQL5 从支持开放神经网络交换(ONNX)格式后,外部训练的模型就能直接进 MetaTrader 5 做交易推断,但 Python 侧训练出的数据组织和 MQL5 侧读取方式往往有细微偏差,同一数据结构里的字段值都可能出现浮动。 现实里绝大多数模型是用 Python 训的,再借 MQL5 代码部署到终端,两套技术栈的差异会让数据预处理步骤很难一一对齐。为了不在部署时反复踩坑,我们直接仿照 Python 里最常用的大数据处理库 Pandas 来搭一套等价能力。 Pandas 本来就是数据科学家洗训练数据的标配,把它在 Python 里的数据操作逻辑用 MQL5 重写,等于在终端里复刻了一个相同的数据游乐场。你打开 MT5 的 MetaEditor,就能照着这个思路把熟悉的 dataframe 操作改成 MQL5 结构,先验证两边读出的样本数值是否一致。
◍ 二维表才是盯盘数据的落脚点
Pandas 给数据处理准备了两类容器:Series 是一维标记数组,能塞整数、字符串、对象等任意类型;Dataframe 则是带行列的二维表结构,本质就是一张二维数组。 从交易工程视角看,Series 更像 MT5 里的一维数组或向量,承载不了多品种多周期的横纵关系,所以没必要在它上面花功夫。真正值得啃的是 Dataframe——K线序列、指标矩阵、信号标记都能摊成行和列来算。 下面这行构造了一个含 6 个元素的 Series,其中第 4 位用 np.nan 显式置空,其余为 1、3、5、6、8:这种缺值占位在行情跳空或合成数据对齐时很常见,但二维表里才方便按时间戳做纵向回填。
s = pd.Series([class="num">1, class="num">3, class="num">5, np.nan, class="num">6, class="num">8])
在 MQL5 里搭一个 DataFrame 骨架
Pandas 的 DataFrame 本质是一张带列名的二维表,行和列的关系跟二维数组一致。MQL5 原生没有 DataFrame,但矩阵(matrix)是最贴近的二维对象,用它存数值、用字符串数组存列名,就能复刻基础结构。 和 NumPy 这类库不同,Pandas 靠跟踪列名让数据更可读。我们在 pandas.mqh 里放一个 m_columns 数组专门记列名,再配一个 matrix 类型的 m_values 装数据,类名叫 CDataFrame。 Python 端 DataFrame 支持整数、字符串、对象混装,但 MQL5 版没必要这么灵活——目标只是给机器学习模型喂数据,浮点和双精度最常用。所以整数、长整型都得先映射成 double,字符串要在进类之前编码,因为类里所有变量都会被强转成 double。 下面这段同时给了 MQL5 的类声明和 Python 原型,对照看更清楚两边差异。
class CDataFrame { class="kw">public: class="type">class="kw">string m_columns[]; class=class="str">"cmt">//An array of class="type">class="kw">string values for keeping track of the column names matrix m_values; class=class="str">"cmt">// A 2D matrix CDataFrame(); ~CDataFrame(class="type">void); } class="kw">import pandas as pd df = pd.DataFrame({ "Integers": [class="num">1,class="num">2,class="num">3,class="num">4,class="num">5], "Doubles": [class="num">0.1,class="num">0.2,class="num">0.3,class="num">0.4,class="num">0.5], "Strings": ["one","two","three","four","five"] })
「往自写数据框里塞列的 Insert 实现」
MQL5 没有 Python 那种 pd.DataFrame({...}) 一步建表并灌数据的语法,类实例也不能在声明时顺手接收矩阵和列名。要往自己写的 CDataFrame 里加数据,正路是单独写一个 Insert 方法,而不是在构造函数里硬塞各种杂活。
Insert 的核心逻辑是先查重:遍历 m_columns 数组,若列名已存在就走修改分支,直接把新向量写进对应列;不存在才扩列。下面这段是方法主体,建议直接拷进 pandas.mqh 验证。
维度校验很实在——若新向量长度大于矩阵已有行数,直接 printf 报错并 return,不会偷偷截断。反过来,若向量比行数短,剩余位置填 NaN,保证矩阵每列等长,这点跟 Python 里 pandas 的对齐行为一致。
测试时可以用两列向量 v1={1,2,3,4,5}、v2={10,20,30,40,50} 调两次 Insert,MT5 调试日志里能看到列数从 0 涨到 2,且短向量补 NaN 的行为可复现。外汇与贵金属行情序列接进来前,先拿这种定长样本跑通,避免实盘数据维度错配。
df = pd.DataFrame({ "first column": [class="num">1,class="num">2,class="num">3,class="num">4,class="num">5], "second column": [class="num">10,class="num">20,class="num">30,class="num">40,class="num">50] }) class="type">void CDataFrame::Insert(class="type">class="kw">string name, class="kw">const vector &values) { class=class="str">"cmt">//--- Check if the column exists in the m_columns array if it does exists, instead of creating a new column we modify an existing one class="type">int col_index = -class="num">1; for (class="type">int i=class="num">0; i<(class="type">int)m_columns.Size(); i++) if (name == m_columns[i]) { col_index = i; class="kw">break; } class=class="str">"cmt">//--- We check if the dimensiona are Ok if (m_values.Rows()==class="num">0) m_values.Resize(values.Size(), m_values.Cols()); if (values.Size() > m_values.Rows() && m_values.Rows()>class="num">0) class=class="str">"cmt">//Check if the new column has a bigger size than the number of rows present in the matrix { printf("%s new column &class="macro">#x27;%s&class="macro">#x27; size is bigger than the dataframe",__FUNCTION__,name); class="kw">return; } class=class="str">"cmt">//--- if (col_index != -class="num">1) { m_values.Col(values, col_index); if (MQLInfoInteger(MQL_DEBUG)) printf("%s column &class="macro">#x27;%s&class="macro">#x27; exists, It will be modified",__FUNCTION__,name); class="kw">return; } class=class="str">"cmt">//--- If a given vector to be added to the dataframe is smaller than the number of rows present in the matrix, we fill the remaining values with Not a Number(NaN) vector temp_vals = vector::Zeros(m_values.Rows()); temp_vals.Fill(NaN); class=class="str">"cmt">//to create NaN values when there was a dimensional mismatch for (class="type">class="kw">ulong i=class="num">0; i<values.Size(); i++) temp_vals[i] = values[i]; class=class="str">"cmt">//--- m_values.Resize(m_values.Rows(), m_values.Cols()+class="num">1); class=class="str">"cmt">//We resize the m_values matrix to accomodate the new column m_values.Col(temp_vals, m_values.Cols()-class="num">1); class=class="str">"cmt">//We insert the new column after the last column ArrayResize(m_columns, m_columns.Size()+class="num">1); class=class="str">"cmt">//We increase the sice of the column names to accomodate the new column name m_columns[m_columns.Size()-class="num">1] = name; class=class="str">"cmt">//we assign the new column to the last place in the array } class="macro">#include <MALE5\pandas.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- CDataFrame df; vector v1= {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5}; vector v2= {class="num">10,class="num">20,class="num">30,class="num">40,class="num">50};
◍ 从字符串到矩阵的表格装配
CDataFrame 的第二个构造函数走的是「列名字符串 + 矩阵」路线:用逗号把列名拆进临时数组,再和矩阵列数比对。若 StringSplit 返回小于 0,说明拆分失败,直接 printf 报错并 return,不会生成脏对象。 拆出 columns_names 后有一道硬校验——columns_names.Size() 必须等于 values.Cols()。例如传入 "first column,second column" 得到 2 个名字,矩阵就必须是 2 列;否则提示 dataframe's columns != columns present in the values matrix 并退出。这一步能挡掉大部分手滑传参。 校验通过后,ArrayCopy 把列名搬进 m_columns,矩阵整体赋给 m_values,构造完成。OnStart 里给的样例矩阵是 5 行 2 列:{1,10}、{2,20}、{3,30}、{4,40}、{5,50},配合 "first column,second column" 正好 2 列对齐,可直接编译跑通验证。 外汇与贵金属行情数据接入这类结构时波动剧烈,矩阵维度错配可能在回测中静默丢行,建议在 StringSplit 处加断点确认 sep 取值。
df.Insert("first column", v1); df.Insert("second column", v2); } CDataFrame::CDataFrame(class="kw">const class="type">class="kw">string &columns, class="kw">const matrix &values) { class="type">class="kw">string columns_names[]; class=class="str">"cmt">//A temporary array for obtaining column names from a class="type">class="kw">string class="type">class="kw">ushort sep = StringGetCharacter(",", class="num">0); if (StringSplit(columns, sep, columns_names)<class="num">0) { printf("%s failed to obtain column names",__FUNCTION__); class="kw">return; } if (columns_names.Size() != values.Cols()) class=class="str">"cmt">//Check if the given number of column names is equal to the number of columns present in a given matrix { printf("%s dataframe&class="macro">#x27;s columns != columns present in the values matrix",__FUNCTION__); class="kw">return; } ArrayCopy(m_columns, columns_names); class=class="str">"cmt">//We assign the columns to the m_columns array m_values = values; class=class="str">"cmt">//We assing the given matrix to the m_values matrix } class="type">void OnStart() { class=class="str">"cmt">//--- matrix data = { {class="num">1,class="num">10}, {class="num">2,class="num">20}, {class="num">3,class="num">30}, {class="num">4,class="num">40}, {class="num">5,class="num">50}, }; CDataFrame df("first column,second column",data); }