数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易(基础篇)
📘

数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易(基础篇)

第 1/3 篇

在 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:这种缺值占位在行情跳空或合成数据对齐时很常见,但二维表里才方便按时间戳做纵向回填。

MQL5 / C++
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 原型,对照看更清楚两边差异。

MQL5 / C++
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 的行为可复现。外汇与贵金属行情序列接进来前,先拿这种定长样本跑通,避免实盘数据维度错配。

MQL5 / C++
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 取值。

MQL5 / C++
  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);  
  }

常见问题

可以用类似 Pandas 的数据帧思路,在 MQL5 里自建带列名的结构体数组,把每根K线作为一行插入,比裸二维数组好维护。
先按分隔符拆成列再逐列 Insert 进自写数据帧,最后统一导出为矩阵,避免边解析边算导致索引错位。
可以,小布能读取你的导出文件并按列名装配成表格,直接给出可用于模型的样本矩阵,省去手写解析。
本质是插入列时没固定顺序,建议先定义空数据帧骨架再按列 Insert,每根Bar只追加一行就能保对齐。
单次 Insert 是线性追加,万行内无明显延迟;真要高频追加可预分配行数,避免反复扩容拖慢回测。