开发交易机器人:Python与MQL5结合(第二部分):模型选择、创建与训练,以及Python自定义测试器(基础篇)
◍ 把Python模型塞进MQL5测试器
在 MT5 里直接训练机器学习模型并不现实,更稳的做法是用 Python 完成模型选择与训练,再把推理逻辑通过 socket 或文件桥接回 MQL5 的自定义测试器。官方示例在 2024 年 12 月公开,单篇阅读量 1 649,说明这条工程路径已有可复用的社区基础。 MQL5 的 OnTester 钩子允许你在优化pass结束时跑一段自定义统计,这正是接入 Python 模型的入口。关键在于把每根 K 线的特征序列化传给外部进程,回收预测信号再计入权益曲线。 实盘前务必在策略测试器里用真实点差和延迟回放,外汇与贵金属杠杆品种跳空频繁,模型在样本外失效的概率偏高,任何信号都只能当作概率倾向而非确定性结论。
从特征工程切到可回测的模型
上一步已经把数据增强跑完,特征也筛出了最优子集,这一步的落点是真正让模型从特征里学出信号。要做的事很具体:搭一个能用历史特征回放的自定义 Python 测试器,把模型预测和图形输出都接进去。 这里有个实操点值得记:测试器必须能重放特征序列并画出 equity 曲线,否则只看准确率容易踩过拟合的坑。外汇与贵金属杠杆高、滑点跳空频繁,回测漂亮不等于实盘能跑,概率上盈利模型也常在小样本段翻车。 所有脚本走 Python,但会嵌 MQL5 库里取价和订单相关的部分,方便后续直接迁回 MT5 跑。下一步就是模型本体和这套测试器的代码对接。
「跑通这套模型要先备齐的Python环境」
我实测用的解释器是 Python 3.10.10,低于这个版本在导入 MetaTrader5 模块时可能踩到二进制兼容坑,建议直接对齐。 代码里封装了三块核心能力:用 sklearn 的 GMM 做特征聚类、用 RFECV 做递归特征消除筛选、以及训练 XGBoost 分类器,三者都依赖下面列出的第三方库。 需要预装的模块清单:pandas、numpy、sklearn、xgboost、matplotlib、seaborn、MetaTrader5、tqdm。外汇与贵金属数据接入 MT5 走的是官方 API,杠杆品种波动剧烈,回测结论仅代表历史样本,实盘存在显著回撤风险。 安装用 pip 一条命令即可,复制去终端执行就能把依赖拉齐,之后才能 import 成功跑后面的训练函数。
pip install pandas numpy sklearn xgboost matplotlib seaborn tqdm MetaTrader5
◍ 分类与回归的取舍,以及为什么选 XGBoost
做行情预测时先碰到的坑是:到底把问题当分类还是回归。分类适合要明确是非答案的场景,比如「下一根 K 线涨不涨」;回归则去猜一个连续值,比如未来收盘价具体多少。后者对价格序列看似更顺手,但标注几乎只能拿资产未来价格硬标,工程上很麻烦。 我更偏向分类,因为标注能压成是非条件,后面还能扩成多类去映射一整套手动交易逻辑。前面文章里那套数据标注代码本身就是二元分类用的,所以这一系就锁定在这个结构上了。 模型选型看特征量、数据类型和类别数。逻辑回归、随机森林、神经网络都能上,但我实测一圈下来,当下最扛打的是 boosting 类。最终拍板用 XGBoost:基于决策树、带正则化和并行、可调参极多,数据科学比赛里常因精度高拿名次,这也是我选它的硬标准。 外汇和贵金属波动受事件驱动,用这类模型做信号只是概率倾向,实盘前务必在 MT5 历史数据上自己回测一遍。
用XGBoost跑通分类器并做美元级回测
梯度提升树里,XGBoost靠二阶导数优化目标函数,比只用一阶梯度的老方法在收敛效率和分类边界上更省算力。把它接进MT5导出的标注数据,能训出一个判断「未来48根K线内价格能否拉开200点且不被100点止损扫掉」的二分类模型。 train_xgboost_classifier 接收带 label 的数据和提升轮数,内部做空值校验、特征数值化检查,再用 XGBClassifier 拟合。原文在训练集上跑完,测试集预测准确率落在 53%,也就是说模型判对的单子里约有一半对应可盈利结构;配合约 3 的盈利因子,这种准度在外汇品种上可能够用,但贵金属和交叉盘滑点更大,实际未必复现。 光看点数是不够的。下面这段自定义测试器把预测信号转成多空模拟:predict=1 做多、predict=0 做空,盈利超过 markup 才平仓,逐笔累加余额。它没算掉期,因为利率浮动,真要保守可以把 markup 多塞几个点。跑完会画出余额曲线,一眼看模型吃不吃手续费。 Windows 上跑这段前,先装好与 Python 版本匹配的 Visual C++ 可再发行包和 OpenMP 运行时,不然 xgboost 导入就崩。开 MT5 把历史数据丢进同一套流程,换 n_estimators 从 100 调到 500,看曲线会不会过拟合。
class="kw">import xgboost as xgb class="kw">import xgboost as xgb def train_xgboost_classifier(data, num_boost_rounds=class="num">500): # Check if data is not empty if data.empty: raise ValueError("Data should not be empty") # Check if all required columns are present in the data required_columns = [&class="macro">#x27;label&class="macro">#x27;, &class="macro">#x27;labels&class="macro">#x27;] if not all(column in data.columns for column in required_columns): raise ValueError(f"Data is missing required columns: {required_columns}") # Remove the &class="macro">#x27;label&class="macro">#x27; column as it is not a feature X = data.drop([&class="macro">#x27;label&class="macro">#x27;, &class="macro">#x27;labels&class="macro">#x27;], axis=class="num">1) y = data[&class="macro">#x27;labels&class="macro">#x27;] # Check if all features have numeric data type if not all(pd.api.types.is_numeric_dtype(X[column]) for column in X.columns): raise ValueError("All features should have numeric data type") # Create an XGBoostClassifier model clf = xgb.XGBClassifier(objective=&class="macro">#x27;binary:logistic&class="macro">#x27;, max_depth=class="num">10, learning_rate=class="num">0.3, n_estimators=num_boost_rounds, random_state=class="num">1) # Train the model on the data clf.fit(X, y) # Return the trained model class="kw">return clf labeled_data_engineered = feature_engineering(labeled_data_clustered, n_features_to_select=class="num">20) # Get all data raw_data = labeled_data_engineered # Test the model on all data train_data = raw_data[raw_data.index <= FORWARD] # Test the model on all data test_data = raw_data[raw_data.index >= FORWARD] # Train the XGBoost model on the filtered data xgb_clf = train_xgboost_classifier(train_data, num_boost_rounds=class="num">100) # Test the model on all data test_data = raw_data[raw_data.index >= FORWARD] X_test = test_data.drop([&class="macro">#x27;label&class="macro">#x27;, &class="macro">#x27;labels&class="macro">#x27;], axis=class="num">1) y_test = test_data[&class="macro">#x27;labels&class="macro">#x27;] predicted_labels = xgb_clf.predict(X_test) # Calculate prediction accuracy accuracy = (predicted_labels == y_test).mean() print(f"Prediction accuracy: {accuracy:.2f}") def test_model(model, X_test, y_test, markup, initial_balance=class="num">10000.0, point_cost=class="num">0.00001): balance = initial_balance trades = class="num">0 profits = [] # Test the model on the test data predicted_labels = model.predict(X_test) for i in range(len(predicted_labels) - class="num">48): if predicted_labels[i] == class="num">1: # Open a class="type">long position entry_price = X_test.iloc[i][&class="macro">#x27;close&class="macro">#x27;] exit_price = X_test.iloc[i+class="num">48][&class="macro">#x27;close&class="macro">#x27;] if exit_price > entry_price + markup: # Close the class="type">long position with profit