将 MQL5 与数据处理包集成 (第 2 部分):机器学习和预测分析·进阶篇
(2/3)·从 MT5 历史数据到 Python 训练再到 MQL5 部署,跳过这套流程你只能停留在规则型 EA
◍ 用多周期滚动均值重构预测特征
把单一预测因子扩展成多周期组合,核心是为每个周期 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 验证分布。
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 接口加载。外汇与贵金属杠杆高,这类信号仅作概率参考,实盘前请在策略测试器用真实点差重跑。
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 模型,第一步是把模型文件作为二进制资源嵌进程序,而不是运行时去硬盘找路径。用 #resource 把 random_forest_model.onnx 打进 ex5,全局作用域里用 ExtModelDouble[] 承接,初始化时 OnnxCreate 拿到句柄,之后 OnTick 检测新柱再调 RunModel。
RunModel 的核心是二分类:输出张量形状 {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.1、stoploss=20、takeprofit=50 这几个 input 参数原样挂上,开 MT5 策略测试器用 EURUSD 15 分钟周期回测,看信号翻转频率是否和训练集一致——外汇和贵金属杠杆高,模型误判会快速放大亏损,参数先小仓验证。
下面这段是可直接编译进 EA 的骨架,注意 OnnxSetInputShape 的 {batch_size, 5} 必须和训练时特征数对齐,否则 OnnxRun 会返回 false:
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。外汇与贵金属杠杆高,模型加载失败绝不可硬跑,否则后续推理用的是空句柄。
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