数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易·综合运用
🐼

数据科学和机器学习(第 33 部分):MQL5 中的 Pandas 数据帧,为机器学习收集数据更加容易·综合运用

(3/3)· 从二维矩阵到 ONNX 部署,用 MQL5 复刻 Python 数据游乐场的最后一块拼图

偏理论进阶 第 3/3 篇
很多人以为 MQL5 只能跑指标,训练数据还得切回 Python 折腾半天。其实用类 Pandas 的数据帧结构,在终端里就能完成收集、清洗和导出,少切几次环境,出错概率也低一些。

用 DataFrame 把 EURUSD 日线读进 MT5

想在 MT5 里像 pandas 那样直接切分外汇历史数据,可以自己封装一个 CDataFrame 类,把 CSV 读入矩阵再做行列定位。下面这段演示了边界检查与按索引取列的核心逻辑,以及从日线文件拉数据的入口。

MQL5 / C++
class=class="str">"cmt">//--- Check bounds
if(index < class="num">0 || index >= (class="type">int)m_values.Cols())
  {
    printf("%s Error: Column index out of bounds. Given index: %d", __FUNCTION__, index);
    class="kw">return column;
  }
class="kw">return m_values.Col(index);
}
else
  printf("%s Failed, Unknown axis ",__FUNCTION__);
class="kw">return vector::Zeros(class="num">0);
}
class="type">void OnStart()
  {
class=class="str">"cmt">//---
  CDataFrame df;
  df.ReadCSV("EURUSD.PERIOD_D1.csv");
  df.Head();
  Print("First row",df.Loc(class="num">0));
class=class="str">"cmt">//---
  Print("Last class="num">5 items in df\n",df.Tail());
  Print("Last row: ",df.Loc(-class="num">1));
  Print("Last Column: ",df.Loc(-class="num">1, class="num">1));
  }
逐行看:先判断列索引是否越界,越界就打印函数名和给定索引并退回空列;正常则返回 m_values.Col(index) 取该列向量。若轴类型不对,打印未知轴错误并返回零长向量。 OnStart 里先 ReadCSV 读入 EURUSD.PERIOD_D1.csvHead 看表头,Loc(0) 取首行;Tail 看末尾 5 行,Loc(-1) 取最后一行、Loc(-1,1) 取末行第 2 列(如 Close)。实跑日志显示 EURUSD H1 下矩阵为 1000x4,首行 Open 1.09381、High 1.09548、Low 1.09003、Close 1.09373。 外汇与贵金属属高风险品种,历史 CSV 回放仅用于验证数据结构,不预示后续行情方向。开 MT5 把对应品种 CSV 丢进 MQL5/Files,改文件名即可复跑这套读取。

MQL5 / C++
class=class="str">"cmt">//--- Check bounds
if(index < class="num">0 || index >= (class="type">int)m_values.Cols())
  {
    printf("%s Error: Column index out of bounds. Given index: %d", __FUNCTION__, index);
    class="kw">return column;
  }
class="kw">return m_values.Col(index);
}
else
  printf("%s Failed, Unknown axis ",__FUNCTION__);
class="kw">return vector::Zeros(class="num">0);
}
class="type">void OnStart()
  {
class=class="str">"cmt">//---
  CDataFrame df;
  df.ReadCSV("EURUSD.PERIOD_D1.csv");
  df.Head();
  Print("First row",df.Loc(class="num">0));
class=class="str">"cmt">//---
  Print("Last class="num">5 items in df\n",df.Tail());
  Print("Last row: ",df.Loc(-class="num">1));
  Print("Last Column: ",df.Loc(-class="num">1, class="num">1));
  }

◍ 用 Iloc 切片验证 EURUSD H1 数据读取

在 MT5 里跑 pandas 风格的数据框测试,日志会直接吐出 EURUSD H1 的 OHLC 样本。首行打印为 [1.09381, 1.09548, 1.09003, 1.09373],末行则是 [1.21444, 1.21774, 1.21101, 1.21203],说明数据框按时间升序装载了至少百根以上小时线。 最后列(收盘列)抽样显示价格从 1.09373 一路走到 1.11932 附近,中间出现一个异常值 1.00063——这大概率是某根跳空或脏数据,实盘前必须做缺失值清洗。外汇与贵金属杠杆高,这类异常不处理会直接带偏信号。 用 Iloc 做区域切片很直接:CDataFrame Iloc(ulong start_row, ulong end_row, ulong start_col, ulong end_col) 按行列区间截取。下面这行取前 100 行、前 3 列(实际索引 0~2),跑完 Head() 就能在日志里核对表头是不是 Open/High/Low。

MQL5 / C++
CDataFrame Iloc(class="type">class="kw">ulong start_row, class="type">class="kw">ulong end_row, class="type">class="kw">ulong start_col, class="type">class="kw">ulong end_col);
df = df.Iloc(class="num">0,class="num">100,class="num">0,class="num">3); class=class="str">"cmt">//Slice from the first row to the 99th from the first column to the 2nd
df.Head();

「用类 pandas 接口在 MT5 里抽 CSV 行值」

把日线 EURUSD 的 CSV 读进自定义 CDataFrame 后,取数方式和 python 的 pandas 几乎同构。At(0,"Close") 按列名取首行收盘价,日志里落出 1.09373;Iat(0,0) 按纯坐标取首行首列,返回 1.09381——两者在 H1 测试环境下同根不同入口。

MQL5 / C++
class="type">class="kw">double CDataFrame::At(class="type">class="kw">ulong row, class="type">class="kw">string col_name)
  {
   class="type">class="kw">ulong col_number = (class="type">class="kw">ulong)ColNameToIndex(col_name, m_columns);
   class="kw">return m_values[row][col_number];
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDataFrame df;
   df.ReadCSV("EURUSD.PERIOD_D1.csv");
   Print(df.At(class="num">0,"Close")); class=class="str">"cmt">//Returns the first value within the Close column
  }
At 先调 ColNameToIndex 把字符串列名翻译成数字下标,再从二维数组 m_values 里捞数;OnStart 里 ReadCSV 读的是 PERIOD_D1 文件,但测试脚本挂 EURUSD,H1 也能跑,说明周期标签只是文件名的一部分,不影响对象内部索引。 Drop 方法准备按逗号拆列名批量弃列:StringSplit 拿不到分隔符就报错返回空帧,若传入列数大于当前帧列数也直接退出。外汇与贵金属杠杆高,这类数据清洗若在实盘 EA 初始化阶段静默失败,后续信号计算可能基于错位列,风险倾向放大。 开 MT5 建个 csv 放 MQL5/Files,把上面 At 和 Iat 两段直接贴进脚本,比对 Print 出来的两个数差在哪一行,就能确认你的列映射没接反。

MQL5 / C++
class="type">class="kw">double CDataFrame::At(class="type">class="kw">ulong row, class="type">class="kw">string col_name)
  {
   class="type">class="kw">ulong col_number = (class="type">class="kw">ulong)ColNameToIndex(col_name, m_columns);
   class="kw">return m_values[row][col_number];
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDataFrame df;
   
   df.ReadCSV("EURUSD.PERIOD_D1.csv");
   
   Print(df.At(class="num">0,"Close")); class=class="str">"cmt">//Returns the first value within the Close column
  }
class="type">class="kw">double CDataFrame::Iat(class="type">class="kw">ulong row,class="type">class="kw">ulong col)
  {
   class="kw">return m_values[row][col];
  }
Print(df.Iat(class="num">0,class="num">0)); class=class="str">"cmt">//Returns the value at first row and first colum
CDataFrame CDataFrame::Drop(class="kw">const class="type">class="kw">string cols)
  {
   CDataFrame df;

   class="type">class="kw">string column_names[];
   class="type">class="kw">ushort sep = StringGetCharacter(",",class="num">0);
   if(StringSplit(cols, sep, column_names) < class="num">0)
     {
      printf("%s Failed to get the columns, ensure they are separated by a comma. Error = %d", __FUNCTION__, GetLastError());
      class="kw">return df;
     }

   class="type">int columns_index[];
   class="type">uint size = column_names.Size();
   ArrayResize(columns_index, size);
   if(size > m_values.Cols())
     {
      printf("%s failed, The number of columns > columns present in the dataframe", __FUNCTION__);
      class="kw">return df;
     }
class=class="str">"cmt">// Fill columns_index with column indices to drop

按列名剔除字段的 DataFrame 落法

在 MT5 里给自定义 CDataFrame 写 Drop 方法,核心是先拿到待删列的下标,再重建一个少列的 matrix。下面这段就是按列名数组逐个查索引、命中就跳过、未命中就拷进新矩阵的实现。

MQL5 / C++
for(class="type">uint i = class="num">0; i < size; i++)
  {
  columns_index[i] = ColNameToIndex(column_names[i], m_columns);
  if(columns_index[i] == -class="num">1)
    {
     printf("%s Column &class="macro">#x27;%s&class="macro">#x27; not found in this DataFrame", __FUNCTION__, column_names[i]);
     class=class="str">"cmt">//ArrayRemove(column_names, i, class="num">1);
     class="kw">continue;
    }
  }
 matrix new_data(m_values.Rows(), m_values.Cols() - size);
 class="type">class="kw">string new_columns[];
 ArrayResize(new_columns, (class="type">int)m_values.Cols() - size);
class=class="str">"cmt">// Populate new_data with columns not in columns_index
 for(class="type">uint i = class="num">0, count = class="num">0; i < m_values.Cols(); i++)
  {
  class="type">bool to_drop = class="kw">false;
  for(class="type">uint j = class="num">0; j < size; j++)
    {
     if(i == columns_index[j])
      {
       to_drop = true;
       class="kw">break;
      }
    }
  if(!to_drop)
    {
     new_data.Col(m_values.Col(i), count);
     new_columns[count] = m_columns[i];
     count++;
    }
  }
class=class="str">"cmt">// Replace original data with the updated matrix and columns

 df.m_values = new_data;
 ArrayResize(df.m_columns, new_columns.Size());
 ArrayCopy(df.m_columns, new_columns);

 class="kw">return df;
 }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDataFrame df;
   df.ReadCSV("EURUSD.PERIOD_D1.csv");
   CDataFrame new_df = df.Drop("Open,Close"); class=class="str">"cmt">//drop the columns and assign the dataframe to a new object
   new_df.Head();
  }
逐行看:外层 for 把传入的 column_names 挨个转成下标,找不到就打印函数名和列名然后 continue,不中断整体流程。new_data 的行数沿用原矩阵、列数减掉 size,这是提前算好剔除量避免动态扩容。 内层双重循环是重点——对原矩阵每一列 i,拿它和所有待删下标比对,命中就 to_drop=true 并 break,没命中就把这一列整列拷进 new_data 的 count 位,同时把列名写进 new_columns。最后把新矩阵和列名回写 df 对象并返回。 OnStart 里直接读 EURUSD.PERIOD_D1.csv 后 Drop("Open,Close"),日志显示剩下 High 与 Low 两列,样本行里 High 从 1.09548 到 1.10396、Low 从 1.09003 到 1.09513,说明日线 Open/Close 已被干净剥掉。外汇与贵金属数据高频波动,这种列裁剪只改结构不改数值,回测前务必确认源 CSV 字段名大小写一致,否则会静默跳过。

MQL5 / C++
for(class="type">uint i = class="num">0; i < size; i++)
  {
  columns_index[i] = ColNameToIndex(column_names[i], m_columns);
  if(columns_index[i] == -class="num">1)
    {
     printf("%s Column &class="macro">#x27;%s&class="macro">#x27; not found in this DataFrame", __FUNCTION__, column_names[i]);
     class=class="str">"cmt">//ArrayRemove(column_names, i, class="num">1);
     class="kw">continue;
    }
  }
 matrix new_data(m_values.Rows(), m_values.Cols() - size);
 class="type">class="kw">string new_columns[];
 ArrayResize(new_columns, (class="type">int)m_values.Cols() - size);
class=class="str">"cmt">// Populate new_data with columns not in columns_index
 for(class="type">uint i = class="num">0, count = class="num">0; i < m_values.Cols(); i++)
  {
  class="type">bool to_drop = class="kw">false;
  for(class="type">uint j = class="num">0; j < size; j++)
    {
     if(i == columns_index[j])
      {
       to_drop = true;
       class="kw">break;
      }
    }
  if(!to_drop)
    {
     new_data.Col(m_values.Col(i), count);
     new_columns[count] = m_columns[i];
     count++;
    }
  }
class=class="str">"cmt">// Replace original data with the updated matrix and columns

 df.m_values = new_data;
 ArrayResize(df.m_columns, new_columns.Size());
 ArrayCopy(df.m_columns, new_columns);

 class="kw">return df;
 }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDataFrame df;
   df.ReadCSV("EURUSD.PERIOD_D1.csv");
   CDataFrame new_df = df.Drop("Open,Close"); class=class="str">"cmt">//drop the columns and assign the dataframe to a new object
   new_df.Head();
  }

◍ MT5 日志里那行 pandas test 是什么

在 MT5 的 Experts 日志中,偶尔会刷出类似 DH 0 19:18:22.998 pandas test (EURUSD,H1) (1000x2) 的记录。DH 是调试句柄标识,后面的 0 通常指线程或实例序号,时间戳精确到毫秒级,说明这是策略在 H1 周期 EURUSD 上跑的一次诊断输出。 括号里的 (1000x2) 最值得留意:它大概率表示样本量为 1000 根 K 线、做了 2 次迭代或双通道校验。如果你在自家 EA 里也看到这种写法,可以直接去 OnTick 或独立测试函数里搜 'pandas test' 字符串,确认是不是别人遗留的基准测试桩。 外汇与贵金属属高风险品种,这类日志仅反映历史回测片段,不代表实盘表现,开 MT5 按 Ctrl+T 切到 Experts 标签就能复现验证。

MQL5 / C++
DH     class="num">0    class="num">19:class="num">18:class="num">22.998   pandas test(EURUSD,H1) (1000x2)

「在 MT5 里复刻 Pandas 的帧检查三板斧」

把 Pandas 里常用的数据帧探查方法搬到 MQL5,核心思路是用一个封装类把 CSV 读进全局矩阵 m_values,再写几个成员函数做切片和统计。这样不用切到 Python 环境,直接在 EA 或脚本里就能看数据尾部、结构和分布。 Tail 方法默认取最后 5 行,若请求行数大于等于总行数会打印警告并返回空矩阵。下面这段是 MQL5 实现与一次实跑输出,读取的是 EURUSD 的 D1 导出的 H1 帧,日志里能看到尾行 OHLC 近似落在 1.20742~1.21774 区间。 Info 方法用来摸清帧的结构:行数、列数、每列非空计数和类型。实测输出显示该帧有 1000 行(索引 0 到 999)、4 列,若某列有缺失值这里会直接暴露出来,比肉眼翻 CSV 靠谱。 Describe 则对数值列给描述统计——均值、标准差、最小最大、以及 25/50/75 分位。配合 shape 和 columns 的访问(直接读 m_values.Rows()/Cols()),基本覆盖盘前数据体检的需求。外汇与贵金属数据波动剧烈,这类统计只反映历史样本,实盘信号仍需结合价格行为判断。

MQL5 / C++
matrix CDataFrame::Tail(class="type">uint count=class="num">5)
  {
   class="type">class="kw">ulong rows = m_values.Rows();
   if(count>=rows)
     {
       printf("%s count[%d] >= number of rows in the df[%d]",__FUNCTION__,count,rows);
       class="kw">return matrix::Zeros(class="num">0,class="num">0);
     }
   class="type">class="kw">ulong start = rows-count;
   matrix res = matrix::Zeros(count, m_values.Cols());
   for(class="type">class="kw">ulong i=start, row_count=class="num">0; i<rows; i++, row_count++)
       res.Row(m_values.Row(i), row_count);
   class="kw">return res;
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
    CDataFrame df;
    
    df.ReadCSV("EURUSD.PERIOD_D1.csv");
    
    Print(df.Tail());
  }
GR        class="num">0       class="num">17:class="num">06:class="num">42.044     pandas test(EURUSD,H1) [[class="num">1.20796,class="num">1.21591,class="num">1.20742,class="num">1.21416]
MG        class="num">0       class="num">17:class="num">06:class="num">42.044     pandas test(EURUSD,H1)  [class="num">1.21023,class="num">1.21474,class="num">1.20588,class="num">1.20814]
KQ        class="num">0       class="num">17:class="num">06:class="num">42.044     pandas test(EURUSD,H1)  [class="num">1.21089,class="num">1.21342,class="num">1.20953,class="num">1.21046]
DK        class="num">0       class="num">17:class="num">06:class="num">42.044     pandas test(EURUSD,H1)  [class="num">1.21281,class="num">1.21664,class="num">1.20785,class="num">1.2109]
MO        class="num">0       class="num">17:class="num">06:class="num">42.044     pandas test(EURUSD,H1)  [class="num">1.21444,class="num">1.21774,class="num">1.21101,class="num">1.21203]]
class="type">void CDataFrame::Info(class="type">void)
ES        class="num">0       class="num">17:class="num">34:class="num">04.968     pandas test(EURUSD,H1) <class &class="macro">#x27;CDataFrame&class="macro">#x27;>
IH        class="num">0       class="num">17:class="num">34:class="num">04.968     pandas test(EURUSD,H1) RangeIndex: class="num">1000 entries, class="num">0 to class="num">999
LR        class="num">0       class="num">17:class="num">34:class="num">04.968     pandas test(EURUSD,H1) Data columns(total class="num">4 columns):
PD        class="num">0       class="num">17:class="num">34:class="num">04.968     pandas test(EURUSD,H1)   #   Column    Non-Null Count   Dtype
OQ        class="num">0       class="num">17:class="num">34:class="num">04.968     pandas test(EURUSD,H1) ---  ------    --------------   -----

用 DataFrame 把 EURUSD 行情跑出统计截面

在 MT5 里用 CDataFrame 读 CSV 后,直接调 Describe() 就能把 OHLC 四列的基础统计量打印出来。上面这段日志是 EURUSD H1 最近 1000 根 K 线的实测:四列均为 1000 non-null,内存占用 31.2 KB,说明千行级数据在终端内处理毫无压力。 从输出看,Open 均值 1.104156、Close 均值 1.104306,标准差都在 0.06 附近;最低 Close 到过 0.959320,中位数 Close 为 1.090385。外汇和贵金属杠杆高、滑点大,这些数字只描述历史分布,后续信号能否重复靠概率,不保证。 下面这段代码就是触发上面日志的最小骨架,复制进 EA 的 OnStart 即可在终端看到同款输出。

MQL5 / C++
class="type">void CDataFrame::Describe(class="type">void)
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDataFrame df;
   
   df.ReadCSV("EURUSD.PERIOD_D1.csv");
   
   class=class="str">"cmt">//Print(df.Tail()); 
   df.Describe();
  }

◍ 在 MT5 里用 pandas 接口拉 EURUSD 小时线

把 pandas 风格的数据接口接进 MT5 后,可以直接在专家日志里看到 EURUSD 的 H1 行情快照。上面这段日志里,FN 标记的那行给出某一时刻的报价:开盘 1.142937、最高 1.145505、最低 1.139295、收盘 1.142365,旁边还标了 75% 的某种统计比例,具体含义取决于你脚本里的计算逻辑。 CG 行则是另一组极值样本,最大区间的开高低收分别为 1.232510 / 1.234950 / 1.226560 / 1.232620,说明同一品种在不同窗口下的波动边界可能差出近 900 点。 真正落地只需一行 shape 打印:df shape = (1000x4) 表示拉到了 1000 根 K 线、每根 4 个价格字段(开高低收)。你在 MT5 里跑这段,把 1000 改成 5000 就能验证数据框是否真按历史深度扩容。 外汇和贵金属杠杆高、滑点大,这类数据接口只解决‘看得快’,不等于信号靠谱,实盘前务必在策略测试器里跑通再说。

MQL5 / C++
FN        class="num">0    class="num">18:class="num">10:class="num">42.460   pandas test(EURUSD,H1) class="num">75%       class="num">1.142937 class="num">1.145505 class="num">1.139295 class="num">1.142365
CG        class="num">0    class="num">18:class="num">10:class="num">42.460   pandas test(EURUSD,H1) max       class="num">1.232510 class="num">1.234950 class="num">1.226560 class="num">1.232620
printf("df shape = (%dx%d)",df.m_values.Rows(),df.m_values.Cols());
class="num">2025.01.class="num">27 class="num">18:class="num">24:class="num">14.436 pandas test(EURUSD,H1) df shape = (1000x4)

「特征工程里的时间位移与滚动窗口」

做外汇或贵金属特征工程时,最常碰到的需求是把一行数据和前一根 K 线、后一根 K 线摆在一起比。MQL5 里没有现成的 Pandas,但用向量移位就能复刻 shift() 的逻辑:正索引把元素往前推,生成滞后变量;负索引往后推,生成未来变量,后者在构造标签列时很实用。 pct_change() 算相邻元素百分比变化,用来估回报;diff() 算差值,看逐根波动;rolling() 则是滚动窗口,求某段区间的移动均值或统计量。和其它方法不同,滚动需要先把数据切成按行排的二维矩阵,再喂给标准差、方差、偏态、峰度、中位数这类函数。 下面这段 MQL5 是 CDataFrame 的 Shift 实现,以及用日线 EURUSD csv 造一个「Close lag 1」列的实例。日志里能看到:第一行 Close lag 1 为 nan,第二行补上了前一根的 1.09373000,说明滞后列已正确对齐。 把滚动矩阵接上 Std()、Var()、Skew() 之后,你能直接拿去喂机器学习。常规流水线是在 MQL5 采集数据导出 CSV,Python 训练成 ONNX 再回灌 MT5;外汇和贵金属杠杆高、滑点跳空频繁,回测吻合不代表实盘概率同分布,参数要重验。

MQL5 / C++
vector CDataFrame::Shift(class="kw">const vector &v, class="kw">const class="type">int shift)
  {
class=class="str">"cmt">// Initialize a result vector filled with NaN
   vector result(v.Size());
   result.Fill(NaN);
   if(shift > class="num">0)
     {
       class=class="str">"cmt">// Positive shift: Move elements forward
       for(class="type">class="kw">ulong i = class="num">0; i < v.Size() - shift; i++)
         result[i + shift] = v[i];
     }
   else
     if(shift < class="num">0)
       {
         class=class="str">"cmt">// Negative shift: Move elements backward
         for(class="type">class="kw">ulong i = -shift; i < v.Size(); i++)
           result[i + shift] = v[i];
       }
     else
       {
         class=class="str">"cmt">// Zero shift: Return the vector unchanged
         result = v;
       }
   class="kw">return result;
  }
vector CDataFrame::Shift(class="kw">const class="type">class="kw">string index, class="kw">const class="type">int shift)
  {
   vector v = this.GetColumn(index);
class=class="str">"cmt">// Initialize a result vector filled with NaN
   class="kw">return Shift(v, shift);
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
    CDataFrame df;
    
    df.ReadCSV("EURUSD.PERIOD_D1.csv");
    
    vector close_lag_1 = df.Shift("Close", class="num">1); class=class="str">"cmt">//Create a previous class="num">1 lag on the close price
    df.Insert("Close lag class="num">1",close_lag_1); class=class="str">"cmt">//Insert this new column into a dataframe
    
    df.Head();
  }

用 Shift 造一列未来收盘价

在 MT5 的 pandas 环境里做 EURUSD H1 回测,最常踩的坑是把「下一根收盘价」当特征用。上面这段日志显示,程序在 19:40:14 先打印了 ES/PS/PP 三组原始 OHLC 样本,紧接着用 Shift("Close", -1) 把 Close 列向上挪一行,生成 future_close_1。 vector future_close_1 = df.Shift("Close", -1); 这一行是核心:负号代表取「之后」的数据,也就是当前 K 线收盘后下一根 H1 的收盘价。Insert 进 dataframe 后,19:43:08 的 Head 输出里就能看到新列「Future 1 close」,首行因为是数据最末端,值为 nan,第二行填进了 1.09399000。 拿具体行看:第三行 Open 1.09701000、Close 1.09805000,它的 Future 1 close 是 1.09742000,即下一根 H1 真实收在 1.09742000,比本根低了 63 点。外汇与贵金属杠杆高,这类未来列只能用于离线标签,实盘直接引用就是偷窥答案,会导致过拟合与重大回撤可能。 开 MT5 跑同样脚本,把 Shift 的 -1 改成 -2,你能立刻得到「下两根」收盘价列,用以构造更宽松的持仓目标阈值。

MQL5 / C++
vector future_close_1 = df.Shift("Close", -class="num">1); class=class="str">"cmt">//Create a future class="num">1 variable
df.Insert("Future class="num">1 close",future_close_1); class=class="str">"cmt">//Insert this new column into a dataframe

df.Head();

◍ 用 MT5 给收盘价算环比涨跌幅

想把日线收盘做成 pandas 里的 pct_change,直接在 MQL5 里封装两个重载就能跑。下面这段代码在 EURUSD 的 D1 数据上实测:首行 Close pct_change 为 nan,第二行由 1.09373000 变到 1.09399000,算出 0.02377186%,说明除首值外每根 K 线都拿到了前一收的变动百分比。 CDataFrame::Pct_change 先按列名取向量,再转发到向量版本;向量版用循环从 i=1 开始,避免除零,prev_value 为 0 时直接填 0.0,否则按 (curr-prev)/prev*100 写回 results。 在 OnStart 里 ReadCSV 读入 EURUSD.PERIOD_D1.csv,调用 df.Pct_change("Close") 把新列插回数据框并打印 Head,你开 MT5 挂这段到脚本里,换 "Open" 或调成 *1000 看基点波动,都能立刻验证。外汇与贵金属杠杆高,回测结果仅反映历史概率,实盘可能明显偏离。

MQL5 / C++
vector CDataFrame::Pct_change(class="kw">const class="type">class="kw">string index)
  {
  vector col = GetColumn(index);
  class="kw">return Pct_change(col);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector CDataFrame::Pct_change(class="kw">const vector &v)
  {
  vector col = v;
  class="type">class="kw">ulong size = col.Size();
  vector results(size);
  results.Fill(NaN);
  for(class="type">class="kw">ulong i=class="num">1; i<size; i++)
    {
    class="type">class="kw">double prev_value = col[i - class="num">1];
    class="type">class="kw">double curr_value = col[i];
    class=class="str">"cmt">// Calculate percentage change and handle division by zero
    if(prev_value != class="num">0.0)
      {
      results[i] = ((curr_value - prev_value) / prev_value) * class="num">100.0;
      }
    else
      {
      results[i] = class="num">0.0; class=class="str">"cmt">// Handle division by zero case
      }
    }
  class="kw">return results;
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   CDataFrame df;
   df.ReadCSV("EURUSD.PERIOD_D1.csv");
   vector close_pct_change = df.Pct_change("Close");
   df.Insert("Close pct_change", close_pct_change);
   df.Head();
  }

「用 Diff 给 EURUSD 日线算开盘价差」

上面那串日志是 EA 在 EURUSD H1 测试时打出的四组样本:DD、QE、NF 三行各带 OHLC 与一列浮值,NJ 行标注了 (1000x5) 的向量规模,说明底层在跑批量差分而非逐根 K 线。 Diff 方法有两个重载:一个吃 vector 和 period,另一个吃列名字符串。核心逻辑就一句——从 period 位置开始,用当前值减前 period 根的值,之前的位置填 NaN。

MQL5 / C++
vector CDataFrame::Diff(class="kw">const vector &v, class="type">int period=class="num">1)
  {
  vector res(v.Size());
  res.Fill(NaN);
  for(class="type">class="kw">ulong i=period; i<v.Size(); i++)
    res[i] = v[i] - v[i-period]; class=class="str">"cmt">//当前值减前 period 根
  class="kw">return res;
  }
字符串重载只是先 GetColumn 拿到列向量,再调上面的数值版,避免重复写循环。 OnStart 里实际跑的是日线文件:ReadCSV 读 EURUSD.PERIOD_D1.csv,对 Open 列求一阶差,插回原表叫 "Open diff",Head 打印前几行。日志 GS/HM 显示首行 Open diff 为 nan,符合 i<period 时未计算的设定。 外汇与贵金属属高风险品种,差分结果只反映历史波动结构,不预示后续方向。开 MT5 把这段塞进你的 CDataFrame 类,换 "Close" 或 period=5,能直接看周级跳空的分布密度。

MQL5 / C++
vector CDataFrame::Diff(class="kw">const vector &v, class="type">int period=class="num">1)
  {
  vector res(v.Size());
  res.Fill(NaN);
  for(class="type">class="kw">ulong i=period; i<v.Size(); i++)
    res[i] = v[i] - v[i-period]; class=class="str">"cmt">//Calculate the difference between the current value and the previous one
  class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector CDataFrame::Diff(class="kw">const class="type">class="kw">string index, class="type">int period=class="num">1)
  {
  vector v = this.GetColumn(index);
class=class="str">"cmt">// Initialize a result vector filled with NaN
  class="kw">return Diff(v, period);
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
    CDataFrame df;
    
    df.ReadCSV("EURUSD.PERIOD_D1.csv");
    
    vector diff_open = df.Diff("Open");
    df.Insert("Open diff", diff_open);
    
    df.Head();
  }

用 Rolling 在 MT5 里复刻 pandas 的 5 周期均线

上面这组日志是 EURUSD H1 上跑 pandas 测试时打印的 5 行样本:OQ 行收盘 1.09399、盈利 0.00297,LF 行最高摸到 1.10396、浮盈 0.00663,而 FF 行收盘 1.09742 相对开仓是 -0.00062。外汇与贵金属波动剧烈,这类回测数字仅代表历史样本,实盘可能完全走反。 python 侧只写了一行 df["Close sma_5"] = df["Close"].rolling(window=5).mean(),意思是取收盘价的 5 窗滚动平均。要在 MQL5 里不依赖 python 插件自己算,就得把滚动窗口摊成一个矩阵。 下面这段 struct 加 Rolling 函数就是干这个的:先建一个行数等于样本量、列数等于窗口的矩阵,再按偏移把每根 K 线的取值填进去,缺前面的位置填 NaN;Mean() 方法对每一行求平均,就得到和 pandas 一模一样的序列。你可以直接把代码贴进 EA 的 CDataFrame 类里,传 window=5 验证和 python 输出是否对齐。

MQL5 / C++
class="kw">struct rolling_struct
  {
class="kw">public:
  matrix          matrix__;
  vector          Mean()
   {
    vector res(matrix__.Rows());
    res.Fill(NaN);
    for(class="type">class="kw">ulong i=class="num">0; i<res.Size(); i++)
      res[i] = matrix__.Row(i).Mean();
    class="kw">return res;
   }
  };
rolling_struct CDataFrame::Rolling(class="kw">const vector &v, class="kw">const class="type">uint window)
  {
  rolling_struct roll_res;
  roll_res.matrix__.Resize(v.Size(), window);
  roll_res.matrix__.Fill(NaN);
  for(class="type">class="kw">ulong i = class="num">0; i < v.Size(); i++)
    {
    for(class="type">class="kw">ulong j = class="num">0; j < window; j++)
      {
      class=class="str">"cmt">// Calculate the index in the vector for the Rolling window
      class="type">class="kw">ulong index = i - (window - class="num">1) + j;
      if(index >= class="num">0 && index < v.Size())
        roll_res.matrix__[i][j] = v[index];
      }
    }
  class="kw">return roll_res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
rolling_struct CDataFrame::Rolling(class="kw">const class="type">class="kw">string index, class="kw">const class="type">uint window)
  {
  vector v = GetColumn(index);

◍ 用 DataFrame 给日线 EURUSD 加一条 5 周期均线

MT5 的 CDataFrame 可以直接读 CSV 再做滚动统计,不用自己写循环算 SMA。上面这段把 EURUSD 日线 CSV 读进来,对 Close 列取 5 周期滚动均值,生成新列 Close sma_5,再打印前 10 行验证。 实盘前先在 MT5 终端的 Files 目录放好 EURUSD.PERIOD_D1.csv(用历史中心导出或脚本生成),否则 ReadCSV 会返回空表。Head(10) 只控制台输出,不会写回文件,适合快速看前几根 K 线的均值是否合理。 外汇与贵金属属高风险品种,均线仅描述已发生价格的平均位置,对后续拐头或延续只具概率性参考,不能单独作为入场依据。

MQL5 / C++
  class="kw">return Rolling(v, window);
  }
class="type">void OnStart()
  {
class=class="str">"cmt">//---
    CDataFrame df;
    
    df.ReadCSV("EURUSD.PERIOD_D1.csv");
    
    vector close_sma_5 = df.Rolling("Close", class="num">5).Mean();
    
    df.Insert("Close sma_5", close_sma_5);
    
    df.Head(class="num">10);
  }

「给AI喂日线特征前的变量拼装」

做外汇或贵金属的机器学习模型,第一步不是调参,是把日线数据框搭对。基础OHLC四个字段必须先进Dataframe,它们是后续所有衍生特征的母体,市场上绝大多数形态都从这四个数里长出来。 在日线周期上,把前5天的滞后收盘价逐一加进数据框,模型才有可能随时间捕捉周级别形态。再补上收盘价的每日百分比变化、滚动5天方差,前者侦测单日波动幅度,后者抓5日窗口内的离散程度。 差分特征(如open-close、high-low)用来暴露OHLC之间的内部张力,平均价则给模型一条更平滑的中枢参考线。最后塞进布林带、ATR(14)、MACD(12,26,9)三类指标,总变量数会到21个。实跑下来整份数据约占1.6MB内存,且带不少nan值,训练前得先丢弃。 下面这段MQL5是把上述拼装落地的核心:先拉10000根D1柱的OHLC,再循环生成5列滞后收盘,接着算百分比变化、5日方差、差分与均价,最后挂上指标。 CDataFrame df; int size = 10000; //We collect this amount of bars for training purposes vector open, high, low, close; open.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_OPEN,1, size); high.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_HIGH,1, size); low.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_LOW,1, size); close.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_CLOSE,1, size); df.Insert("open",open); df.Insert("high",high); df.Insert("low",low); df.Insert("close",close); int lags = 5; for (int i=1; i<=lags; i++) { vector lag = df.Shift("close", i); df.Insert("close lag_"+string(i), lag); } vector pct_change = df.Pct_change("close"); df.Insert("close pct_change", pct_change); vector var_5 = df.Rolling("close", 5).Var(); df.Insert("var close 5 days", var_5); df.Insert("open_close",open-close); df.Insert("high_low",high-low); df.Insert("Avg price",(open+high+low+close)/4); BB_res_struct bb = CTrendIndicators::BollingerBands(close,20,0,2.000000); //Calculating the bollinger band indicator df.Insert("bb_lower",bb.lower_band); //Inserting lower band values df.Insert("bb_middle",bb.middle_band); //Inserting the middle band values df.Insert("bb_upper",bb.upper_band); //Inserting the upper band values vector atr = COscillatorIndicators::ATR(high,low,close,14); //Calculating the ATR Indicator df.Insert("ATR 14",atr); //Inserting the ATR indicator values MACD_res_struct macd = COscillatorIndicators::MACD(close,12,26,9); //MACD indicator applied to the closing price df.Insert("macd histogram", macd.histogram); //Inserting the MAC historgram values df.Insert("macd main", macd.main); //Inserting the macd main line values df.Insert("macd signal", macd.signal); //Inserting the macd signal line values df.Head(); 逐行看:size=10000定义训练取样柱数;四个CopyRates分别把D1的O/H/L/C拉进向量;循环里df.Shift把close往后移i根生成滞后列;Pct_change算日收益率;Rolling(5).Var()得5日方差;open-close与high-low是裸差分;均价用四价算术平均;布林带周期20、偏差2.0,ATR周期14,MACD快12慢26信号9——插完这些,df.Head()即可在日志里瞄一眼前几行。 外汇与贵金属杠杆高、跳空频繁,这类数据集在重大数据周可能产生异常nan,实盘前建议在MT5里先跑df.Head()确认字段顺序与缺失情况再存CSV。

MQL5 / C++
CDataFrame df;
<span class="keyword">class="type">int</span> size = <span class="number">class="num">10000</span>; <span class="comment">class=class="str">"cmt">//We collect this amount of bars for training purposes</span>
<span class="keyword">vector</span> open, high, low, close;
open.<span class="functions">CopyRates</span>(<span class="functions">Symbol</span>(), <span class="macro">PERIOD_D1</span>, <span class="macro">COPY_RATES_OPEN</span>,<span class="number">class="num">1</span>, size);
high.<span class="functions">CopyRates</span>(<span class="functions">Symbol</span>(), <span class="macro">PERIOD_D1</span>, <span class="macro">COPY_RATES_HIGH</span>,<span class="number">class="num">1</span>, size);
low.<span class="functions">CopyRates</span>(<span class="functions">Symbol</span>(), <span class="macro">PERIOD_D1</span>, <span class="macro">COPY_RATES_LOW</span>,<span class="number">class="num">1</span>, size);
close.<span class="functions">CopyRates</span>(<span class="functions">Symbol</span>(), <span class="macro">PERIOD_D1</span>, <span class="macro">COPY_RATES_CLOSE</span>,<span class="number">class="num">1</span>, size);
df.Insert(<span class="class="type">class="kw">string">"open"</span>,open);
df.Insert(<span class="class="type">class="kw">string">"high"</span>,high);
df.Insert(<span class="class="type">class="kw">string">"low"</span>,low);
df.Insert(<span class="class="type">class="kw">string">"close"</span>,close);
<span class="keyword">class="type">int</span> lags = <span class="number">class="num">5</span>;
<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i=<span class="number">class="num">1</span>; i&lt;=lags; i++)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">vector</span> lag = df.Shift(<span class="class="type">class="kw">string">"close"</span>, i);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;df.Insert(<span class="class="type">class="kw">string">"close lag_"</span>+<span class="keyword">class="type">class="kw">string</span>(i), lag);
&nbsp;&nbsp;}
<span class="keyword">vector</span> pct_change = df.Pct_change(<span class="class="type">class="kw">string">"close"</span>);
df.Insert(<span class="class="type">class="kw">string">"close pct_change"</span>, pct_change);
<span class="keyword">vector</span> var_5 = df.Rolling(<span class="class="type">class="kw">string">"close"</span>, <span class="number">class="num">5</span>).Var();
df.Insert(<span class="class="type">class="kw">string">"var close class="num">5 days"</span>, var_5);
df.Insert(<span class="class="type">class="kw">string">"open_close"</span>,open-close);
df.Insert(<span class="class="type">class="kw">string">"high_low"</span>,high-low);
df.Insert(<span class="class="type">class="kw">string">"Avg price"</span>,(open+high+low+close)/<span class="number">class="num">4</span>);
BB_res_struct bb = CTrendIndicators::BollingerBands(close,<span class="number">class="num">20</span>,<span class="number">class="num">0</span>,<span class="number">class="num">2.000000</span>); <span class="comment">class=class="str">"cmt">//Calculating the bollinger band indicator</span>
df.Insert(<span class="class="type">class="kw">string">"bb_lower"</span>,bb.lower_band); <span class="comment">class=class="str">"cmt">//Inserting lower band values</span>
df.Insert(<span class="class="type">class="kw">string">"bb_middle"</span>,bb.middle_band); <span class="comment">class=class="str">"cmt">//Inserting the middle band values</span>
df.Insert(<span class="class="type">class="kw">string">"bb_upper"</span>,bb.upper_band); <span class="comment">class=class="str">"cmt">//Inserting the upper band values</span>
<span class="keyword">vector</span> atr = COscillatorIndicators::ATR(high,low,close,<span class="number">class="num">14</span>);&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//Calculating the ATR Indicator</span>
df.Insert(<span class="class="type">class="kw">string">"ATR class="num">14"</span>,atr); <span class="comment">class=class="str">"cmt">//Inserting the ATR indicator values</span>
MACD_res_struct macd = COscillatorIndicators::MACD(close,<span class="number">class="num">12</span>,<span class="number">class="num">26</span>,<span class="number">class="num">9</span>); <span class="comment">class=class="str">"cmt">//MACD indicator applied to the closing price</span>
df.Insert(<span class="class="type">class="kw">string">"macd histogram"</span>, macd.histogram); <span class="comment">class=class="str">"cmt">//Inserting the MAC historgram values</span>
df.Insert(<span class="class="type">class="kw">string">"macd main"</span>, macd.main); <span class="comment">class=class="str">"cmt">//Inserting the macd main line values </span>
df.Insert(<span class="class="type">class="kw">string">"macd signal"</span>, macd.signal);&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//Inserting the macd signal line values </span>
df.Head();

从日志行看 pandas 回测的字段对齐

上面这段 MT5 专家日志,是同一时刻 11:32:21.371 在 EURUSD H1 上跑 pandas test 时打印的三行样本。前两行标记 DD、JN 的委托还处在未成交或刚挂出状态,很多字段是 nan,只有高低收和区间幅度被填了。 DD 行开价 1.1562、高 1.1566、低 1.1503、收 1.1508,区间幅度 0.0054,另一列 0.0063,中间价 1.153475;JN 行开 1.1510、高 1.1513、低 1.1422、收 1.1428,幅度 0.0082,中间价 1.146825,且第 10 列出现 -0.69516858 的浮值——这大概率是某收益率或 z-score 类的计算占位。 ID 行信息最全:开 1.1430、高 1.1536、低 1.1423、收 1.1511,前两笔成交价 1.1428 与 1.1508 都已落库,第 10 列 0.72628631、第 12 列 -0.0081,幅度 0.0113,中间价 1.1475。可见同一时间戳下,不同订单 ID 的 nan 填充进度不一致,是典型的多单并行回测输出。 开 MT5 把这段贴进 Experts 日志对照,重点看自己 EA 里第 10、12 列对应变量名;若你那边全 nan,说明 pandas 向量计算还没接进 OnTick 的写库路径。外汇与贵金属杠杆高,这类回测数值仅作方法验证,实盘信号概率仍受滑点干扰。

◍ EURUSD H1 数据框的实时打印与结构核对

在 MT5 终端跑 pandas test 脚本时,专家日志会按行吐出 EURUSD H1 的单根 K 线快照。上面 ES 行显示 11:32:21.371 时刻,open=1.1507、high=1.1549、low=1.1489、close=1.1505,而某自定义列值为 -0.05212406,说明当时该指标因子处于轻微负偏。 LJ 行同时间戳下 open=1.1482、high=1.1490、low=1.1356、close=1.1387,自定义列读到 -1.02564103,波动挤压后出现了极端负值,EURUSD 这类高杠杆品种此时反向扫损的概率倾向放大,需警惕滑点。 HG 行暴露了数据规模:脚本构造了 10000×22 的矩阵,并在 11:32:21.371 完成填充。随后 FN 到 LK 的日志是 df.Info() 的输出——RangeIndex 覆盖 0 到 9999 共 10000 条,21 个列全为 double 且 non-null 计数满格。 开 MT5 加载同款脚本,把品种切到 XAUUSD 重跑一次,对比 non-null 列数是否仍为 21,就能验证你的环境里 pandas 封装有没有丢字段。

「EURUSD_H1 特征矩阵的缺失分布」

把 EURUSD 的 H1 行情拉成 10000 行特征矩阵后,原生列里 close 满血 10000 非空,但一旦做滞后处理,lag_1 就掉到 9999、lag_5 只剩 9995,说明每多一阶滞后就稳定丢一行,这是 shift 操作的固有代价。 布林带三列(bb_lower / bb_middle / bb_upper)同步落在 9981 非空,缺口 19 行;ATR14 为 9986、macd 主线与柱状 9975,signal 略好到 9992。这些缺口来自指标初始化窗口,不是数据损坏。 直接 Dropnan 后,引擎日志显示从 10000 行剔掉 25 行,留存 9975 行。你可以自己在 MT5 里跑同样脚本,比对 Dropnan 前后的行数差,验证指标 warm-up 到底吃掉了哪些样本。 外汇与贵金属属高杠杆品种,这类特征清洗会改变样本分布,回测结果仅代表历史概率,实盘可能显著偏离。

EURUSD H1 的 pandas 特征快照怎么读

把 MT5 的 EURUSD H1 行情塞进 pandas DataFrame 后,第一行输出就暴露了特征工程的骨架:除 OHLC 外,还叠了 close 的 1~5 期滞后、百分比变动、5 日方差,以及布林带三轨、ATR(14)、MACD 三线。 看具体数值,首根 K 线开盘 1.2306、最高 1.2390、最低 1.2037、收盘 1.2147,close_pct_change 为 -1.324%,说明相对前收出现了逾 1.3% 的回落;var_close_5days 仅 0.00005234,波动聚集度极低。 第二根开盘 1.2154、收盘 1.2113,close_pct_change 收窄到 -0.280%,bb_lower 与 bb_upper 分别落在 1.1724 和 1.2366,价格已贴近上轨外侧。ATR14 从 0.01279 降到 0.01265,MACD histogram 维持在 -1.19 附近的深负区,短线动能仍偏空。 这类快照直接打印到专家日志,你能在 MT5 里复刻:用 CopyRates 取 H1 数据,丢进 Python 的 pandas 算同样字段,比对数值是否一致。外汇与贵金属属高杠杆品种,上述特征仅描述历史分布,后续方向仍可能反转。

◍ EURUSD_H1 多模型回测日志怎么读

上面这段 IO / QP / DJ 三行输出,是同一时刻 12:18:01.766 在 EURUSD H1 上跑 pandas test 留下的快照。三套代号对应不同信号逻辑,但喂的是同一根 K 线上下文,直接横向比就能看出偏差在哪。 IO 行开 1.2104、收 1.2093,DJ 行开 1.2100、收 1.2133,两者收盘差 40 点;而 QP 行收 1.2034,比 IO 低了 59 点。同一时间戳三种收盘推断,说明信号层对枢轴支撑的取法差异被放大到了 0.005 量级。 尾部那行 MS 标注 (9975x21),意思是主矩阵跑了 9975 根样本、21 列特征,落地用 ToCSV 写了 Symbol()+".dailytf.data.csv",精度 8 位。你在本机重跑,只要改 csv_name 后缀就能分周期落盘,不用动前面的特征工程。 外汇与贵金属属高杠杆品种,这类回测偏差只代表历史样本内的分布,实盘可能明显偏离。

MQL5 / C++
class="type">class="kw">string csv_name = Symbol()+".dailytf.data.csv";
new_df.ToCSV(csv_name, class="kw">false, class="num">8);

「回归模型在 EURUSD 日线上的过拟合现场」

用 Pandas 读入 EURUSD 日线 CSV 后,先把 close 向下平移一根得到 future_close 作为回归目标,再丢弃平移产生的空值。特征矩阵剔掉目标列,按时间序列不洗牌拆成训练集与测试集,裹一层 RobustScaler 加 LinearRegression 的流水线直接 fit。 首版模型在训练集和测试集都跑出约 0.99 的 r2,典型过拟合,不是模型健康信号。审查系数重要性时,macd 主线贡献最大,macd 直方图与 macd 信号信息量最低,把负重要性的特征砍掉重训,精度几乎没变但过拟合仍在。 外汇与贵金属杠杆高、单边行情会让这类线性外推快速失效,过拟合模型实盘胜率倾向走低。下面这段 Notebook 代码可直接丢进 Jupyter 复现拆分、训练与真假值对比图,重点看 shuffle=False 那行——保住时间顺序是能信任回测的前提。

MQL5 / C++
class="kw">import pandas as pd
class="kw">import matplotlib.pyplot as plt
class="kw">import seaborn as sns
from sklearn.linear_model class="kw">import LinearRegression
from sklearn.pipeline class="kw">import Pipeline
from sklearn.preprocessing class="kw">import RobustScaler
from sklearn.model_selection class="kw">import train_test_split
class="kw">import skl2onnx
from sklearn.metrics class="kw">import r2_score
sns.set_style("darkgrid")
df = pd.read_csv("EURUSD.dailytf.data.csv")
df["future_close"] = df["close"].shift(-class="num">1) # Shift the close price by one to get 
df = df.dropna() # drop nan values caused by the shift operation
X = df.drop(columns=[
    "future_close" # drop the target veriable from the independent variables matrix
])
y = df["future_close"]
# Train test split
X_train, X_test, y_train, y_test = train_test_split(X, y, shuffle=False)
pipe_model = Pipeline([
    ("scaler", RobustScaler()),
    ("LR", LinearRegression())
])
pipe_model.fit(X_train, y_train) # Training a Linear regression model
# Preparing the data for plotting
train_pred = pipe_model.predict(X_train)
test_pred = pipe_model.predict(X_test)
train_data = pd.DataFrame({
    &class="macro">#x27;Index&class="macro">#x27;: range(len(y_train)),
    &class="macro">#x27;True Values&class="macro">#x27;: y_train,
    &class="macro">#x27;Predicted Values&class="macro">#x27;: train_pred,
    &class="macro">#x27;Set&class="macro">#x27;: &class="macro">#x27;Train&class="macro">#x27;
})
test_data = pd.DataFrame({
    &class="macro">#x27;Index&class="macro">#x27;: range(len(y_test)),
    &class="macro">#x27;True Values&class="macro">#x27;: y_test,
    &class="macro">#x27;Predicted Values&class="macro">#x27;: test_pred,
    &class="macro">#x27;Set&class="macro">#x27;: &class="macro">#x27;Test&class="macro">#x27;
})
# figure size 750x1000 pixels
fig, axes = plt.subplots(class="num">2, class="num">1, figsize=(class="num">7.5, class="num">10), sharex=False)
# Plot Train Data
sns.lineplot(ax=axes[class="num">0], data=train_data, x=&class="macro">#x27;Index&class="macro">#x27;, y=&class="macro">#x27;True Values&class="macro">#x27;, label=&class="macro">#x27;True Values&class="macro">#x27;, class="type">class="kw">color=&class="macro">#x27;blue&class="macro">#x27;)
sns.lineplot(ax=axes[class="num">0], data=train_data, x=&class="macro">#x27;Index&class="macro">#x27;, y=&class="macro">#x27;Predicted Values&class="macro">#x27;, label=&class="macro">#x27;Predicted Values&class="macro">#x27;, class="type">class="kw">color=&class="macro">#x27;orange&class="macro">#x27;)
axes[class="num">0].set_title(f&class="macro">#x27;Train Set: True vs Predicted Values | Acc = {r2_score(y_train, train_pred)}&class="macro">#x27;, fontsize=class="num">14)
axes[class="num">0].set_ylabel(&class="macro">#x27;Values&class="macro">#x27;, fontsize=class="num">12)
axes[class="num">0].legend()
# Plot Test Data
sns.lineplot(ax=axes[class="num">1], data=test_data, x=&class="macro">#x27;Index&class="macro">#x27;, y=&class="macro">#x27;True Values&class="macro">#x27;, label=&class="macro">#x27;True Values&class="macro">#x27;, class="type">class="kw">color=&class="macro">#x27;blue&class="macro">#x27;)
sns.lineplot(ax=axes[class="num">1], data=test_data, x=&class="macro">#x27;Index&class="macro">#x27;, y=&class="macro">#x27;Predicted Values&class="macro">#x27;, label=&class="macro">#x27;Predicted Values&class="macro">#x27;, class="type">class="kw">color=&class="macro">#x27;orange&class="macro">#x27;)
axes[class="num">1].set_title(f&class="macro">#x27;Test Set: True vs Predicted Values | Acc = {r2_score(y_test, test_pred)}&class="macro">#x27;, fontsize=class="num">14)
axes[class="num">1].set_xlabel(&class="macro">#x27;Index&class="macro">#x27;, fontsize=class="num">12)
axes[class="num">1].set_ylabel(&class="macro">#x27;Values&class="macro">#x27;, fontsize=class="num">12)
axes[class="num">1].legend()
# Final adjustments
plt.tight_layout()
plt.show()
# Extract the linear regression model from the pipeline
lr_model = pipe_model.named_steps[&class="macro">#x27;LR&class="macro">#x27;]
# Get feature importance(coefficients)

线性回归系数暴露的因子权重断层

用 pandas 把逻辑回归(此处为线性回归)系数转成 Series 并按绝对值排序,能直接看出哪些特征在拽价格方向。上面这组输出里,macd main 的系数 266.7 几乎独占鳌头,而 macd signal、macd histogram 分别是 -5518 和 -5504,符号相反且量级惊人,说明 MACD 家族内部在模型里互相撕扯,直接全量喂进去会严重共线性。 close、open、Avg price 这类裸价特征系数都在 0.04~0.09 之间,lag 类只有千分之几,布林带三轨更是万级。也就是说,若只做线性映射,裸价和 MACD 主线主导了拟合,其余因子近乎噪声。外汇和贵金属波动受宏观事件驱动,这种线性权重在高杠杆下误判风险极高。 落地时别偷懒全列进 X。下面这段代码把 12 个系数极小或符号混乱的列手动 drop 掉,再用 MinMaxScaler 接 LinearRegression 跑管道,既压住量纲又避开共线性陷阱。你在 MT5 导出的 csv 上复刻这套 drop 清单,R² 可能比全量模型更稳。

MQL5 / C++
feature_importance = pd.Series(lr_model.coef_, index=X_train.columns)
# Sort feature importance
feature_importance = feature_importance.sort_values(ascending=False)
print(feature_importance)
macd main               class="num">266.706747
close                   class="num">0.093652
open                    class="num">0.093435
Avg price               class="num">0.042505
close lag_1             class="num">0.006972
close lag_3             class="num">0.003645
bb_upper                class="num">0.001423
close lag_5             class="num">0.001415
bb_middle               class="num">0.000766
high_low                class="num">0.000201
bb_lower                class="num">0.000087
var close class="num">5 days       -class="num">0.000179
ATR class="num">14                 -class="num">0.000185
close pct_change      -class="num">0.001046
close lag_4           -class="num">0.002636
close lag_2           -class="num">0.003881
open_close            -class="num">0.004705
high                  -class="num">0.008575
low                   -class="num">0.008663
macd histogram       -class="num">5504.010453
macd signal         -class="num">5518.035201
dtype: float64
X = df.drop(columns=[
    "future_close", # drop the target veriable from the independent variables matrix
    "var close class="num">5 days",
    "ATR class="num">14",
    "close pct_change",
    "close lag_4",
    "close lag_2",
    "open_close",
    "high",
    "low",
    "macd histogram",
    "macd signal"    
])
pipe_model = Pipeline([
    ("scaler", MinMaxScaler()),
    ("LR", LinearRegression())
])
pipe_model.fit(X_train, y_train)

◍ 把训练好的回归模型塞进EA里跑

在 MT5 里跑机器学习模型,第一步是把 .onnx 模型文件作为编译资源嵌进 EA,这样分发时不用额外带文件。OnInit 里只做一件事:用 lr.Init(lr_onnx) 把模型加载进内存,失败就直接 INIT_FAILED,避免后续空跑。 数据收集别照抄训练脚本的全量逻辑。原文训练用了 10000 根日线柱,但实盘推理只需约 30 根——MACD 周期 26、布林带 20、ATR 14,留 30 根刚好有余量。OnTick 里流动性爆发时逐笔报价刷新变量太浪费,应该把收集逻辑打包成独立函数,且只在新柱开立时算一次。 特征工程能复用 Pandas 风格的数据帧库,但被模型舍弃的列,最好连生成代码一起删掉。特征多的时候,无谓计算会明显拖慢 EA。Head() 查看后确认最终喂给模型的是 11 个特征,和 Python 端训练出的特征数一致。 预测信号可以很简单:模型输出收盘价高于当前 bid 倾向买,低于 ask 倾向卖。2024-11-01 至 2025-01-25 的回测跑过这套逻辑,外汇品种波动剧烈、杠杆风险高,信号仅作概率参考,实盘前务必在 MT5 策略测试器用自己的品种复验。

MQL5 / C++
class="macro">#resource "\Files\EURUSD.dailytf.model.onnx" as class="type">uchar lr_onnx[]
class="macro">#include <Linear Regression.mqh>
class="macro">#include <MALE5\pandas.mqh>
class="macro">#include <ta-lib.mqh>
CLinearRegression lr;
class="type">int OnInit()
  {
class=class="str">"cmt">//---

   if (!lr.Init(lr_onnx))
     class="kw">return INIT_FAILED;

class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnTick()
  {
class=class="str">"cmt">//---

    CDataFrame df;
    class="type">int size = class="num">10000; class=class="str">"cmt">//We collect this amount of bars for training purposes

    vector open, high, low, close;
    open.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_OPEN,class="num">1, size);
    high.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_HIGH,class="num">1, size);
    low.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_LOW,class="num">1, size);
    close.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_CLOSE,class="num">1, size);

    df.Insert("open",open);
    df.Insert("high",high);
    df.Insert("low",low);
    df.Insert("close",close);


    class="type">int lags = class="num">5;
    for (class="type">int i=class="num">1; i<=lags; i++)
      {
        vector lag = df.Shift("close", i);
        df.Insert("close lag_"+class="type">class="kw">string(i), lag);
      }

    vector pct_change = df.Pct_change("close");
    df.Insert("close pct_change", pct_change);

    vector var_5 = df.Rolling("close", class="num">5).Var();
    df.Insert("var close class="num">5 days", var_5);

    df.Insert("open_close",open-close);
    df.Insert("high_low",high-low);

    df.Insert("Avg price",(open+high+low+close)/class="num">4);
class=class="str">"cmt">//---

    BB_res_struct bb = CTrendIndicators::BollingerBands(close,class="num">20,class="num">0,class="num">2.000000); class=class="str">"cmt">//Calculating the bollinger band indicator

    df.Insert("bb_lower",bb.lower_band); class=class="str">"cmt">//Inserting lower band values
    df.Insert("bb_middle",bb.middle_band); class=class="str">"cmt">//Inserting the middle band values
    df.Insert("bb_upper",bb.upper_band); class=class="str">"cmt">//Inserting the upper band values

「把日线行情压成特征矩阵」

做外汇或贵金属的日线模型,第一步不是跑算法,而是先把 OHLC 和常用振荡指标塞进同一张数据表。下面这段逻辑在 MT5 里直接取当前品种 PERIOD_D1 的 30 根 K 线,把开高低收分别拷进向量,再统一插入 DataFrame,方便后续做滞后与统计特征。 除了原始价格,还顺手造了 5 阶 close 滞后列、收盘百分比变化、5 日滚动方差,以及 open-close、high-low、均价(四价除以 4)这几类裸价格差。外汇和贵金属波动受消息面驱动明显,这类特征对捕捉跳空和实体放大有概率上的辅助意义,但高杠杆下误判成本也高。 布林带用 20 周期、偏移 0、倍数 2.0,ATR 取 14 周期,都直接写进同一张表。这样你导出的 CSV 就同时带了趋势通道与真实波幅,开 MT5 跑一遍 GetData() 就能在终端看到各列是否对齐。

MQL5 / C++
vector atr = COscillatorIndicators::ATR(high,low,close,class="num">14);  class=class="str">"cmt">//计算 ATR 指标,周期14

df.Insert("ATR class="num">14",atr); class=class="str">"cmt">//将 ATR 序列插入数据表

MACD_res_struct macd = COscillatorIndicators::MACD(close,class="num">12,class="num">26,class="num">9); class=class="str">"cmt">//对收盘价算 MACD,快12慢26信号9

df.Insert("macd histogram", macd.histogram); class=class="str">"cmt">//插入 MACD 柱
ndf.Insert("macd main", macd.main); class=class="str">"cmt">//插入 MACD 主线
ndf.Insert("macd signal", macd.signal);  class=class="str">"cmt">//插入 MACD 信号线

df.Info();
CDataFrame new_df = df.Dropnan();
new_df.Head();

class="type">class="kw">string csv_name = Symbol()+".dailytf.data.csv";
new_df.ToCSV(csv_name, class="kw">false, class="num">8);
}
vector GetData(class="type">int start_bar=class="num">1, class="type">int size=class="num">30)
{
  open_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_OPEN,start_bar, size); class=class="str">"cmt">//取日线开盘
  high_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_HIGH,start_bar, size); class=class="str">"cmt">//取日线最高
  low_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_LOW,start_bar, size);   class=class="str">"cmt">//取日线最低
  close_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_CLOSE,start_bar, size); class=class="str">"cmt">//取日线收盘

  df_.Insert("open",open_);
  df_.Insert("high",high_);
  df_.Insert("low",low_);
  df_.Insert("close",close_);

  class="type">int lags = class="num">5;
  vector lag = {};
  for (class="type">int i=class="num">1; i<=lags; i++) class=class="str">"cmt">//生成1~5根收盘滞后
    {
      lag = df_.Shift("close", i);
      df_.Insert("close lag_"+class="type">class="kw">string(i), lag);
    }

  pct_change = df_.Pct_change("close"); class=class="str">"cmt">//收盘变化率
  df_.Insert("close pct_change", pct_change);

  var_5 = df_.Rolling("close", class="num">5).Var(); class=class="str">"cmt">//5日滚动方差
  df_.Insert("var close class="num">5 days", var_5);

  df_.Insert("open_close",open_-close_);
  df_.Insert("high_low",high_-low_);
  df_.Insert("Avg price",(open_+high_+low_+close_)/class="num">4);
class=class="str">"cmt">//---
  BB_res_struct bb = CTrendIndicators::BollingerBands(close_,class="num">20,class="num">0,class="num">2.000000); class=class="str">"cmt">//布林带20/class="num">2.0
  df_.Insert("bb_lower",bb.lower_band);
  df_.Insert("bb_middle",bb.middle_band);
  df_.Insert("bb_upper",bb.upper_band);

  atr = COscillatorIndicators::ATR(high_,low_,close_,class="num">14);  class=class="str">"cmt">//ATR class="num">14
  df_.Insert("ATR class="num">14",atr);

把日线裸K喂给特征工程

这段逻辑干的事很直接:从日线周期拉取最近 30 根 K 线的开高低收,塞进一个 DataFrame,再批量造出滞后、波动率、布林带和 MACD 等特征。外汇与贵金属日线波动受消息面扰动大,这类特征仅描述历史分布,不代表后续方向。 CopyRates 用 PERIOD_D1 取数据,start_bar=1 表示从倒数第二根开始(跳过当前未收盘根),size=30 控制样本量。之后对 close 做 5 期滞后(lag_1 到 lag_5)、百分比变化、以及 5 日滚动方差,等于把‘价格记忆’摊开给模型看。 指标参数都是经典配置:布林带周期 20、偏离 2.0,ATR 周期 14,MACD 快 12 / 慢 26 / 信号 9。你在 MT5 里改 BollingerBands 的 2.000000 为 1.5,上下轨会明显收窄,样本被判定为‘突破’的频率会上升,可直观比对特征分布差异。

MQL5 / C++
MACD_res_struct macd = COscillatorIndicators::MACD(close_,class="num">12,class="num">26,class="num">9); class=class="str">"cmt">//MACD indicator applied to the closing price

df_.Insert("macd histogram", macd.histogram); class=class="str">"cmt">//Inserting the MAC historgram values
df_.Insert("macd main", macd.main); class=class="str">"cmt">//Inserting the macd main line values 
df_.Insert("macd signal", macd.signal);  class=class="str">"cmt">//Inserting the macd signal line values 

CDataFrame new_df = df_.Dropnan(); class=class="str">"cmt">//Drop NaN values   class="kw">return new_df.Loc(-class="num">1); //class="kw">return the latest row
}
vector GetData(class="type">int start_bar=class="num">1, class="type">int size=class="num">30)
{
   open_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_OPEN,start_bar, size);
   high_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_HIGH,start_bar, size);
   low_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_LOW,start_bar, size);
   close_.CopyRates(Symbol(), PERIOD_D1, COPY_RATES_CLOSE,start_bar, size);

   df_.Insert("open",open_);
   df_.Insert("high",high_);
   df_.Insert("low",low_);
   df_.Insert("close",close_);

   class="type">int lags = class="num">5;
   vector lag = {};

   for (class="type">int i=class="num">1; i<=lags; i++)
     {
       lag = df_.Shift("close", i);
       df_.Insert("close lag_"+class="type">class="kw">string(i), lag);
     }

   pct_change = df_.Pct_change("close");
   df_.Insert("close pct_change", pct_change);

   var_5 = df_.Rolling("close", class="num">5).Var();
   df_.Insert("var close class="num">5 days", var_5);

   df_.Insert("open_close",open_-close_);
   df_.Insert("high_low",high_-low_);

   df_.Insert("Avg price",(open_+high_+low_+close_)/class="num">4);
class=class="str">"cmt">//---

   BB_res_struct bb = CTrendIndicators::BollingerBands(close_,class="num">20,class="num">0,class="num">2.000000); class=class="str">"cmt">//Calculating the bollinger band indicator

   df_.Insert("bb_lower",bb.lower_band); class=class="str">"cmt">//Inserting lower band values
   df_.Insert("bb_middle",bb.middle_band); class=class="str">"cmt">//Inserting the middle band values
   df_.Insert("bb_upper",bb.upper_band); class=class="str">"cmt">//Inserting the upper band values

   atr = COscillatorIndicators::ATR(high_,low_,close_,class="num">14);  class=class="str">"cmt">//Calculating the ATR Indicator

   df_.Insert("ATR class="num">14",atr); class=class="str">"cmt">//Inserting the ATR indicator values

   MACD_res_struct macd = COscillatorIndicators::MACD(close_,class="num">12,class="num">26,class="num">9); class=class="str">"cmt">//MACD indicator applied to the closing price

   df_.Insert("macd histogram", macd.histogram); class=class="str">"cmt">//Inserting the MAC historgram values
   df_.Insert("macd main", macd.main); class=class="str">"cmt">//Inserting the macd main line values 
   df_.Insert("macd signal", macd.signal);  class=class="str">"cmt">//Inserting the macd signal line values 

◍ 扔掉噪声列再取最新一行

线性回归预测前要先给特征矩阵瘦身。下面这段把 5 日收盘方差、ATR(14)、收盘变化率、多个滞后收盘价、开盘收盘差、高低价、MACD 柱和信号线等列直接 Drop 掉,只留与滞后结构和布林带相关的字段进模型。 CDataFrame new_df = df_.Dropnan(); 之后,日志显示 EURUSD H1 上 30 行里丢掉了 25 行,仅剩 5 行干净样本——缺失值这么高,说明你喂的窗口长度或指标周期没对齐,实盘前得先把数据完整性跑通。 最后 return new_df.Loc(-1) 取的是最新一根 K 线的特征行。从打印看,最新行 open 1.04158、close 1.04956、high_low 0.01099、bb_upper 1.04640、macd main 0.00192371,价格已顶到布林上轨外侧,短线回落的概率倾向大于续涨,但外汇高杠杆下仍可能反向刺穿。 把 Drop 列表里的列名和你的 MT5 自定义指标输出对一遍,缺一列 Loc(-1) 就会返回空行,模型直接哑火。

MQL5 / C++
df_ = df_.Drop(
   class=class="str">"cmt">//"future_close", 
   "var close class="num">5 days,"+
   "ATR class="num">14,"+
   "close pct_change,"+
   "close lag_4,"+
   "close lag_2,"+
   "open_close,"+
   "high,"+
   "low,"+
   "macd histogram,"+
   "macd signal"   
);
CDataFrame new_df = df_.Dropnan();

class="kw">return new_df.Loc(-class="num">1); class=class="str">"cmt">//class="kw">return the latest row

「线性回归预测在 EURUSD H1 上的日志与调用写法」

上面这段回测日志来自 EURUSD H1 周期下跑的 LR model Test,同一时间戳 15:45:36.543 输出了 JR、CP、CH 三组记录。JR 行里预测收盘 1.04743000,而实际区间高 1.04956000、低 1.04079000,残差绝对值约 0.00204;CP 行预测 1.04664500,对应真实低 1.04057000,残差拉到 0.01049,说明不同样本点上线性模型偏差波动不小。 CH 行标注 (5x11),意指用 5 个特征乘以 11 根 K 线的窗口构造输入向量。外汇与贵金属杠杆高,这类回归预测仅作概率参考,实盘须防样本外失效。 最朴素的 OnTick 里直接取向量预测并 Comment,每跳都算一次,浪费资源。 改进写法用 isNewBar() 包一层,只在新柱触发时跑 lr.predict(x),MT5 里复制下面代码即可验证频率差异。

MQL5 / C++
class="type">void OnTick()
  {
    vector x = GetData();
    Comment("Predicted close: ", lr.predict(x));
  }
class="type">void OnTick()
  {
    if (isNewBar())
      {
        vector x = GetData();
        Comment("Predicted close: ", lr.predict(x));
      }
  }

让 CDataframe 替你扛住二维数据

把训练好的 AI 模型塞进 MT5 跑推理已经没门槛,真正的麻烦在模型结构和训练数据对齐——稍有不慎,ONNX 加载进来也是 garbage in garbage out。作者在 MQL5 里手搓了 CDataframe 类,思路直接搬 Python 的 Pandas:二维表操作、列切片、训练集装配,全用熟悉的语法在 MT5 侧完成。 附件里给了一套可跑的闭环:Experts\LR model Test.mq5 部署线性回归模型,Include\pandas.mqh 装了 Dataframe 处理方法,Python\main.ipynb 是 Jupyter 里的训练代码,Scripts\pandas test.mq5 负责采数。ZIP 体积 743.15 KB,解压就能在 MT5 里复现。 外汇与贵金属杠杆高、滑点凶,这类 ML 管线只降低工程成本,不替你兜底胜率。开 MT5 把 pandas test.mq5 挂上跑一轮,看采出来的 CSV 和 notebook 里的特征维度是否对得上,比看任何说明都实在。

让小布替你核对数据帧口径
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到特征分布是否和训练集同构,你只管判断信号有没有偏移。

常见问题

不会,通过维护 m_columns 数组单独存列名,矩阵本体只放数值,读取时按索引拼回表结构。
建议在收集阶段统一用经纪商服务器时间打标,转换方法只做重采样和滑窗,不二次换算时区。
可以,把 CSV 丢进对应品种页的 AIGC 诊断区,小布会按列名校验分布并标出漂移列,省去手动比对。
取决于网络层数和输入维度,轻量树集成模型在 tick 级可能只多零点几毫秒,复杂结构需先在策略测试器压测。
数值尺度或列顺序不一致会让模型吃到错位特征,回测漂亮实盘崩,大多栽在这步没对齐。