将 MQL5 与数据处理包集成 (第 2 部分):机器学习和预测分析·进阶篇
🤖

将 MQL5 与数据处理包集成 (第 2 部分):机器学习和预测分析·进阶篇

(2/3)·从 MT5 历史数据到 Python 训练再到 MQL5 部署,跳过这套流程你只能停留在规则型 EA

含代码示例实战向 第 2/3 篇
很多人把 EA 写完就以为自动化到位了,其实还在用固定阈值硬扛变来变去的市场。不接预测模型,回测漂亮实盘照样被晃出去。本篇接上篇,继续把数据处理包的预测能力真正落进 MQL5。

◍ 用多周期滚动均值重构预测特征

把单一预测因子扩展成多周期组合,核心是为每个周期 i 计算收盘价与滚动均值的比值,以及目标变量在窗内的累加趋势。原文选取的 horizons = [2, 5, 55, 125, 750],意味着最短看 2 根 K 线、最长拉到 750 根,跨度从日内微结构到周线级背景。 代码先筛出数值列再做 rolling,避免对象列进运算炸错;ratio_column 用 Close / rolling_mean(Close) 表达价格相对均值的偏离,trend_column 用目标变量 shift(1) 后滚动求和,刻画前期方向上的连续强度。 重构后重新跑 backtestor,new_predictors 里每组周期贡献两个特征,模型阈值卡在 predict_proba >= 0.6 才判 1。外汇与贵金属属高风险品种,这套概率切割方式只提高信号置信,不保证方向正确,实盘前请在 MT5 历史数据上复算 precision_score 验证分布。

MQL5 / C++
class="kw">return pd.concat(all_predictions)
predictions = backtestor(data, model, predictors)
predictions["Predictions"].value_counts()
precision_score(predictions["<TRGT>"], predictions["Predictions"])
predictions["<TRGT>"].value_counts() / predictions.shape[class="num">0]
horizons = [class="num">2, class="num">5, class="num">55, class="num">125, class="num">750]
new_predictors = []
# Ensure only numeric columns are used for rolling calculations
numeric_columns = data.select_dtypes(include=[class="type">class="kw">float, class="type">int]).columns
for i in horizons:
    # Calculate rolling averages for numeric columns only
    rolling_averages = data[numeric_columns].rolling(i).mean()
    
    # Generate the ratio column
    ratio_column = f"Close_Ratio_{i}"
    data[ratio_column] = data["<CLOSE>"] / rolling_averages["<CLOSE>"]
    
    # Generate the trend column
    trend_column = f"Trend_{i}"
    data[trend_column] = data["<TRGT>"].shift(class="num">1).rolling(i).sum()
    
    new_predictors += [ratio_column, trend_column]
data
data = data.dropna()
def predict(train, test, predictors, model):
    model.fit(train[predictors], train["<TRGT>"])
    prcsn = model.predict_proba(test[predictors])[:class="num">1]
    prcsn[prcsn >= .class="num">6] = class="num">1
    prcsn[prcsn < .class="num">6] = class="num">0
    prcsn = pd.Series(prcsn, index = test.index, name = "Predictions")
    cmbnd = pd.concat([test["<TRGT>"], prcsn], axis = class="num">1)
    class="kw">return cmbnd
predictions = backtestor(data, model, new_predictors)
predictions["Predictions"].value_counts()
precision_score(predictions["<TRGT>"], predictions["Predictions"])

把随机森林压成 ONNX 丢进 MT5

用 Python 侧训练好分类器后,真正能在 MT5 里跑推理的前提是把模型序列化成一个独立文件。skl2onnx 干的就是这个桥接活:它把 sklearn 对象转成跨平台的 ONNX 字节流,不依赖原训练环境。 上面这段脚本里,RandomForestClassifier 只用了 50 棵树、min_samples_split=50,属于刻意压薄复杂度,避免在 5 分钟外汇品种上过拟合。train_test_split 设了 shuffle=False,因为行情序列不能打乱时间顺序,否则回测会偷看未来。 导出的 random_forest_model.onnx 必须手动拷到 MT5 终端的 Files 目录(通常 MQL5/Files 下),EA 才能用 COpenCL 或 OnnxRuntime 接口加载。外汇与贵金属杠杆高,这类信号仅作概率参考,实盘前请在策略测试器用真实点差重跑。

MQL5 / C++
class="kw">import pandas as pd
from sklearn.ensemble class="kw">import RandomForestClassifier
from sklearn.model_selection class="kw">import train_test_split
class="kw">import onnx
class="kw">import skl2onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
# Load and preprocess your data(example)
# Replace this with your actual data loading process
class="macro">#data = pd.read_csv(&class="macro">#x27;your_data.csv&class="macro">#x27;)  # Replace with your actual data source
class="macro">#data = data.dropna()
# Define predictors and target
predictors = ["<CLOSE>", "<TICKVOL>", "<OPEN>", "<HIGH>", "<LOW>"]
target = "<TRGT>"
# Split data into train and test sets
train, test = train_test_split(data, test_size=class="num">0.2, shuffle=False)
# Define and train the model
model = RandomForestClassifier(n_estimators=class="num">50, min_samples_split=class="num">50, random_state=class="num">1)
model.fit(train[predictors], train[target])
# Export the trained model to ONNX format
initial_type = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([None, len(predictors)]))]
onnx_model = convert_sklearn(model, initial_types=initial_type)
# Save the ONNX model to a file
with open("random_forest_model.onnx", "wb") as f:
    f.write(onnx_model.SerializeToString())

「把训练好的模型塞进 MT5 跑起来」

在 MT5 里跑 ONNX 模型,第一步是把模型文件作为二进制资源嵌进程序,而不是运行时去硬盘找路径。用 #resourcerandom_forest_model.onnx 打进 ex5,全局作用域里用 ExtModelDouble[] 承接,初始化时 OnnxCreate 拿到句柄,之后 OnTick 检测新柱再调 RunModelRunModel 的核心是二分类:输出张量形状 {batch_size, 2},分别对应类 0(看跌)和类 1(看涨)的概率。代码里取 output_data[2*k]output_data[2*k+1] 比较,谁大就归谁,结果写回 output_vector,0 或 1 直接驱动后续下单。 输入侧假定每根 K 线提取 5 个特征,batch_size = input_vector.Size() / 5,若不足 5 个特征直接返回 false 不跑模型。实盘里你可以先把 lotsize=0.1stoploss=20takeprofit=50 这几个 input 参数原样挂上,开 MT5 策略测试器用 EURUSD 15 分钟周期回测,看信号翻转频率是否和训练集一致——外汇和贵金属杠杆高,模型误判会快速放大亏损,参数先小仓验证。 下面这段是可直接编译进 EA 的骨架,注意 OnnxSetInputShape{batch_size, 5} 必须和训练时特征数对齐,否则 OnnxRun 会返回 false:

MQL5 / C++
class="macro">#include <Trade/Trade.mqh>
class="macro">#define    ModelName          "RandomForestClassifier"
class="macro">#define    ONNXFilename       "random_forest_model.onnx"
class=class="str">"cmt">// Single ONNX model resource
class="macro">#resource "\Files\random_forest_model.onnx" as class="kw">const class="type">uchar ExtModelDouble[];
class="kw">input class="type">class="kw">double lotsize = class="num">0.1;    class=class="str">"cmt">// Trade lot size
class="kw">input class="type">class="kw">double stoploss = class="num">20;    class=class="str">"cmt">// Stop loss in points
class="kw">input class="type">class="kw">double takeprofit = class="num">50;  class=class="str">"cmt">// Take profit in points
class=class="str">"cmt">// Trading functions
CTrade m_trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Run classification class="kw">using class="type">class="kw">double values                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool RunModel(class="type">long model, vector &input_vector, vector &output_vector)
{
    class="type">ulong batch_size = input_vector.Size() / class="num">5; class=class="str">"cmt">// Assuming class="num">5 class="kw">input features
    if (batch_size == class="num">0)
        class="kw">return (false);
    output_vector.Resize((class="type">int)batch_size);
    class=class="str">"cmt">// Prepare class="kw">input tensor
    class="type">class="kw">double input_data[];
    ArrayResize(input_data, input_vector.Size());
    for (class="type">int k = class="num">0; k < input_vector.Size(); k++)
        input_data[k] = input_vector[k];
    class=class="str">"cmt">// Set class="kw">input shape
    class="type">ulong input_shape[] = {batch_size, class="num">5}; class=class="str">"cmt">// class="num">5 class="kw">input features for each prediction
    OnnxSetInputShape(model, class="num">0, input_shape);
    class=class="str">"cmt">// Prepare output tensor
    class="type">class="kw">double output_data[];
    ArrayResize(output_data, (class="type">int)batch_size);
    class=class="str">"cmt">// Set output shape(binary classification)
    class="type">ulong output_shape[] = {batch_size, class="num">2}; class=class="str">"cmt">// Output shape for probability(class="num">0 or class="num">1)
    OnnxSetOutputShape(model, class="num">0, output_shape);
    class=class="str">"cmt">// Run the model
    class="type">bool res = OnnxRun(model, ONNX_DEBUG_LOGS, input_data, output_data);
    if (res)
    {
        class=class="str">"cmt">// Copy output to vector(only keeping the class with highest probability)
        for (class="type">int k = class="num">0; k < batch_size; k++)
            output_vector[k] = (output_data[class="num">2 * k] < output_data[class="num">2 * k + class="num">1]) ? class="num">1.0 : class="num">0.0;
    }
    class="kw">return (res);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Generate class="kw">input data for prediction                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector input_data()
{
    vector input_vector;
    class="type">MqlRates rates[];

◍ 把K线喂给ONNX前的三道闸门

模型推理之前,得先解决三件琐事:取最近5根H1数据、判断新K线、确认当前品种有无持仓。这三块代码不华丽,但漏掉任意一处,实盘里的信号就会重复触发或错乱。 取数部分用 CopyRates 拉取 Symbol() 在 PERIOD_H1 下从偏移5往前数1根(即最近5根)的速率数组,成功才继续。随后把输入向量拉成 5×5=25 维,按每根K线的 open/high/low/close/tick_volume 顺序平铺——这是多数轻量ONNX行情模型约定的特征排布。 新K线检测靠静态变量 last_time 跟 iTime 取到的当前0号K线时间比对,不等就刷新并返回 true,避免每个 tick 都重算。持仓检查 PosExists 则倒序扫 PositionsTotal,用 PositionGetInteger(POSITION_TYPE) 比类型、PositionGetString(POSITION_SYMBOL) 锁本品种,命中即返回 true。 初始化里先 OnnxCreateFromBuffer 从嵌入的字节流建模型,句柄等于 INVALID_HANDLE 就打印错误并 INIT_FAILED。外汇与贵金属杠杆高,模型加载失败绝不可硬跑,否则后续推理用的是空句柄。

MQL5 / C++
  class=class="str">"cmt">// Get the last class="num">5 bars of data
  if (CopyRates(Symbol(), PERIOD_H1, class="num">5, class="num">1, rates) > class="num">0)
  {
      input_vector.Resize(class="num">5 * class="num">5); class=class="str">"cmt">// class="num">5 class="kw">input features for each bar
      for (class="type">int i = class="num">0; i < class="num">5; i++)
      {
          input_vector[i * class="num">5] = rates[i].open;
          input_vector[i * class="num">5 + class="num">1] = rates[i].high;
          input_vector[i * class="num">5 + class="num">2] = rates[i].low;
          input_vector[i * class="num">5 + class="num">3] = rates[i].close;
          input_vector[i * class="num">5 + class="num">4] = rates[i].tick_volume;
      }
  }
  class="kw">return (input_vector);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if there is a new bar                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool NewBar()
{
    class="kw">static class="type">class="kw">datetime last_time = class="num">0;
    class="type">class="kw">datetime current_time = iTime(Symbol(), Period(), class="num">0);
    if (current_time != last_time)
    {
        last_time = current_time;
        class="kw">return (true);
    }
    class="kw">return (false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if a position of a certain type exists                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool PosExists(class="type">int type)
{
    for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--)
    {
        if (PositionGetInteger(POSITION_TYPE) == type && PositionGetString(POSITION_SYMBOL) == Symbol())
            class="kw">return (true);
    }
    class="kw">return (false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program initialization                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
    Print("Initializing ONNX model...");
    class=class="str">"cmt">// Initialize the ONNX model
    class="type">long model = OnnxCreateFromBuffer(ExtModelDouble, ONNX_DEFAULT);
    if (model == INVALID_HANDLE)
    {
        Print("Error loading ONNX model: ", GetLastError());
        class="kw">return INIT_FAILED;
    }
    class=class="str">"cmt">// Store the model handle for further use
让小布替你跑这套
从 CSV 导出到 ONNX 转换的重复步骤,小布盯盘的 AIGC 已内置常用脚本模板,打开对应品种页即可调取,你只管调特征和看泛化表现。

常见问题

倾向选无限柱数再按日期截取,避免给定时段柱数不足导致样本断层,尤其跨多年周期训练时。
常见做法是转 ONNX,因为 MQL5 侧推理库对 ONNX 支持较直接;其他格式需额外封装,复杂度更高。
概率上存在,建议按市场阶段切分验证,并在部署前用近期样本做滚动回测确认泛化。
目前小布支持接入标准 ONNX 做品种页推理展示,你把导出文件挂到对应配置即可,不用自己写监听线程。
可以把 CSV 丢到共享盘或内网传,训练侧无 MT5 依赖;只须保证 MQL5 部署机读得到最终模型文件。