数据科学和机器学习(第 32 部分):保持您的 AI 模型更新,在线学习(基础篇)
让模型跟着行情跑:MT5 在线学习落地
传统批量训练把模型钉死在历史快照上,行情结构一变就失效。在线学习(online learning)让模型每来一条新样本就增量更新参数,省去全量重训的算力浪费,也避免模型在样本外快速退化。 对做外汇和贵金属的交易者来说,这种机制的价值在于「分布漂移」能被实时消化。EURUSD 在 2024 年 Q4 的波动率聚类特征和 2025 年上半年的低波动 regime 差异明显,离线模型若不及时更新,信号衰减会很快。 MT5 本身提供了定时任务与信号订阅通道,可以把新收盘数据持续喂给 Python 端的 scikit-learn 或 Keras 模型,再回写预测结果到指标缓冲区。高风险品种尤其需要这种闭环,否则一次宏观事件就能让静态模型产出噪音。 别把正态当圣经:在线学习只解决「参数跟进」,不解决「特征失效」。若你喂的还是旧特征工程,模型更新再快也只是精确错误。
「实时数据流里的增量训练」
在线机器学习让模型不从一次性历史样本里定型,而是随着实时点数据流逐笔增量更新权重。对 MT5 交易者而言,这意味着你挂上的预测模块可以在新 tick 进来时调整自身算法,而不是死守一段过去回测出来的参数。 在交易数据环境里,难点从来不是“能不能学”,而是“何时学、学多勤”。比如你拿去年一整年的比特币数据训好的模型,遇到上周刚刷出的历史新高,新样本极可能被旧分布判成异常值而遭到稀释——外汇品种多数在固定区间来回,但纳斯达克100、标普500这类权益指数长期倾向向上探新峰,旧训练集的均值会系统性落后于现实。 所以在线学习不只是防旧信息过期,更是让模型贴合当下盘口。外汇与贵金属杠杆高、跳空频繁,这类实时更新若参数过灵敏,反而容易在流动性稀薄时段被噪音带偏,实盘前务必在策略测试器里用真实点差跑一遍。
◍ 为什么交易者该盯住在线学习
在线机器学习像骑车人边走边学,能跟着数据里的新形态调整自己,性能随时间的推移有提高的潜力。对外汇和贵金属这类高波动、高风险的品种来说,模型不僵死才有活路。 部分在线算法一次只吃一条数据,对普通交易者那点紧张的计算资源反而更安全,也更容易把依赖大数据的模型跑起来。批量学习在实盘落地时往往已经滞后,而在线学习直接给实时洞察——这在剥头皮或事件驱动交易里是关键差异。 增量训练让模型持续更新,训练过程更快、也更省成本。想在 MT5 里验证这点,直接用下面的增量归一化示例看权重如何随 tick 滑动。
把 AI 模型从 Python 搬到 MT5 的三段链路
做在线学习基础设施时,MT5 里的链路和纯 Python 应用不一样:数据起点是终端,模型训练在 Python 客户端,推理却要落回 MQL5 的 OnTick。中间靠 Common\Files 这个共享目录做桥,缺一不可。 Python 侧先用 mt5.initialize() 拉起平台,再用 copy_rates_from_pos("EURUSD", TIMEFRAME_H1, 1, 1000) 从索引 1(最近已收盘柱)向后取千根 H1 数据。实测取 100 根时,首行时间截 1731351600、开盘 1.06520、收盘 1.06491,tick_volume 1688,说明副本不含实时未收杆,回测口径稳定。 模型文件建议拆成 catboost_models.py 与 main.py:前者管初始化、训练、保存,后者管取数、造 target、切训练/验证集,最后以 ONNX 落盘到 Common\Files。MQL5 侧在 OnInit 检查文件是否存在,用 ONNX_COMMON_FOLDER 标志加载;特征顺序必须和 Python 训练时完全一致,否则 predict_bin 给出的 0(看涨)/1(看跌)标签会错位。外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前务必在策略测试器跑多品种验证。
class="kw">import pandas as pd class="kw">import numpy as np class="kw">import MetaTrader5 as mt5 from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime if not mt5.initialize(): # Initialize the MetaTrader class="num">5 platform print("initialize() failed") mt5.shutdown() def getData(start = class="num">1, bars = class="num">1000): rates = mt5.copy_rates_from_pos("EURUSD", mt5.TIMEFRAME_H1, start, bars) if len(rates) < bars: # if the received information is less than specified print("Failed to copy rates from MetaTrader class="num">5, error = ",mt5.last_error()) # create a pnadas DataFrame out of the obtained data df_rates = pd.DataFrame(rates) class="kw">return df_rates print("Trading info:\n" ,getData(class="num">1, class="num">100)) # get class="num">100 bars starting at the recent closed bar time open high low close tick_volume spread real_volume class="num">0 class="num">1731351600 class="num">1.06520 class="num">1.06564 class="num">1.06451 class="num">1.06491 class="num">1688 class="num">0 class="num">0 class="num">1 class="num">1731355200 class="num">1.06491 class="num">1.06519 class="num">1.06460 class="num">1.06505 class="num">1607 class="num">0 class="num">0 class="num">2 class="num">1731358800 class="num">1.06505 class="num">1.06573 class="num">1.06495 class="num">1.06512 class="num">1157 class="num">0 class="num">0 class="num">3 class="num">1731362400 class="num">1.06512 class="num">1.06564 class="num">1.06512 class="num">1.06557 class="num">1112 class="num">0 class="num">0 class="num">4 class="num">1731366000 class="num">1.06557 class="num">1.06579 class="num">1.06553 class="num">1.06557 class="num">776 class="num">0 class="num">0 .. ... ... ... ... ... ... ... ... class="num">95 class="num">1731693600 class="num">1.05354 class="num">1.05516 class="num">1.05333 class="num">1.05513 class="num">5125 class="num">0 class="num">0
「把 EURUSD 小时 K 线喂给 CatBoost 并导出 ONNX」
上面那几行是某次从 MT5 导出的 EURUSD 小时 bar 片段:第 96~99 行对应的时间戳 1731697200–1731708000(即 2024-11-15 伦敦时段),开盘从 1.05513 一路走到 1.05372,四根 bar 的成交量分别为 3966、2919、2651、2977,且 spread/tick 字段全为 0,说明这是已清洗的离线样本而非实时 tick 流。 要把这类结构化行情直接拿去训分类模型,可以套一个 sklearn Pipeline 包住 CatBoostClassifier。下面这段 Python 不承担 MT5 取数,只负责训练与格式转换,默认迭代 100、深度 6、学习率 0.1、交叉熵损失,并在验证集上挑最优树。 训练完顺手打一行样本外准确率,再走 to_onnx 把模型固化成 ONNX,方便丢进小布这类推理端做低延迟预测。外汇与贵金属杠杆高,样本外准确率仅代表历史窗口概率,实盘仍可能显著回撤。 别在 MT5 里硬跑 Python 依赖 MT5 终端本身不替你装 catboost / skl2onnx,正确做法是本地或云端训好、导出 .onnx,再用 MT5 的 Python API 或 DLL 桥接只做推理,避免终端卡死。
from catboost class="kw">import CatBoostClassifier from sklearn.metrics class="kw">import accuracy_score from onnx.helper class="kw">import get_attribute_value from skl2onnx class="kw">import convert_sklearn, update_registered_converter from sklearn.pipeline class="kw">import Pipeline from skl2onnx.common.shape_calculator class="kw">import ( calculate_linear_classifier_output_shapes, ) # noqa from skl2onnx.common.data_types class="kw">import ( FloatTensorType, Int64TensorType, guess_tensor_type, ) from skl2onnx._parse class="kw">import _apply_zipmap, _get_sklearn_operator_name from catboost.utils class="kw">import convert_to_onnx_object # Example initial data(X_initial, y_initial are your initial feature matrix and target) class CatBoostClassifierModel(): def __init__(self, X_train, X_test, y_train, y_test): self.X_train = X_train self.X_test = X_test self.y_train = y_train self.y_test = y_test self.model = None def train(self, iterations=class="num">100, depth=class="num">6, learning_rate=class="num">0.1, loss_function="CrossEntropy", use_best_model=True): # Initialize the CatBoost model params = { "iterations": iterations, "depth": depth, "learning_rate": learning_rate, "loss_function": loss_function, "use_best_model": use_best_model } self.model = Pipeline([ # wrap a catboost classifier in sklearn pipeline | good practice(not necessary tho :)) ("catboost", CatBoostClassifier(**params)) ]) # Testing the model self.model.fit(X=self.X_train, y=self.y_train, catboost__eval_set=(self.X_test, self.y_test)) y_pred = self.model.predict(self.X_test) print("Model&class="macro">#x27;s accuracy on out-of-sample data = ",accuracy_score(self.y_test, y_pred)) # a function for saving the trained CatBoost model to ONNX format def to_onnx(self, model_name): update_registered_converter( CatBoostClassifier, "CatBoostCatBoostClassifier", calculate_linear_classifier_output_shapes, self.skl2onnx_convert_catboost, parser=self.skl2onnx_parser_castboost_classifier,
◍ 把 CatBoost 导成 ONNX 丢进 MT5 公共目录
模型训完不算完,得让 MT5 端的推理脚本能直接加载。上面这段把 sklearn 管道转成 ONNX:target_opset 里基础域锁 12、ai.onnx.ml 锁 2,输入张量按 X_train.shape[1] 列宽建 FloatTensorType,写盘时序列化进 catboost.H1.onnx。 落地路径走 mt5.terminal_info()._asdict() 里的 commondata_path,再拼 'Files' 子目录。若目录不存在就 os.makedirs 建出来——MT5 只读 commondata_path\Files 下的模型文件,放错地方会直接 load 失败。 训练标签构造很直白:取后一根 K 线的 open/close,close 高于 open 标 1(看涨倾向),否则标 0。getData(start=1, bars=1000) 拉 1000 根 bar,train_size=0.7、random_state=42 切分,外汇与贵金属波动受杠杆放大,这套标签在极端跳空夜可能失真,实盘前建议先回测 H1 周期。 trainAndSaveCatBoost 函数把取数、打标、训模型、导 ONNX 封到一起;取数为空时 print mt5.last_error() 并 shutdown,避免空 DataFrame 把 CatBoost 训崩。开 MT5 跑一遍这个函数,就能在公共 Files 里看到 catboost.H1.onnx。
options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)
model_onnx = convert_sklearn(
self.model,
"pipeline_catboost",
[("input", FloatTensorType([None, self.X_train.shape[class="num">1]]))],
target_opset={"": class="num">12, "ai.onnx.ml": class="num">2},
)
# And save.
with open(model_name, "wb") as f:
f.write(model_onnx.SerializeToString())
data = getData(start=class="num">1, bars=class="num">1000)
# Preparing the target variable
data["future_open"] = data["open"].shift(-class="num">1) # shift one bar into the future
data["future_close"] = data["close"].shift(-class="num">1)
target = []
for row in range(data.shape[class="num">0]):
if data["future_close"].iloc[row] > data["future_open"].iloc[row]: # bullish signal
target.append(class="num">1)
else: # bearish signal
target.append(class="num">0)
data["target"] = target # add the target variable to the dataframe
data = data.dropna() # drop empty rows
X = data.drop(columns = ["spread","real_volume","future_close","future_open","target"])
y = data["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=class="num">0.7, random_state=class="num">42)
catboost_model = catboost_models.CatBoostClassifierModel(X_train, X_test, y_train, y_test)
catboost_model.train()
terminal_info_dict = mt5.terminal_info()._asdict()
common_path = terminal_info_dict["commondata_path"]
# Save models in a specific location under the common parent folder
models_path = os.path.join(common_path, "Files")
if not os.path.exists(models_path): class="macro">#if the folder exists
os.makedirs(models_path) # Create the folder if it doesn&class="macro">#x27;t exist
catboost_model.to_onnx(model_name=os.path.join(models_path, "catboost.H1.onnx"))
def trainAndSaveCatBoost():
data = getData(start=class="num">1, bars=class="num">1000)
# Check if we were able to receive some data
if (len(data)<=class="num">0):
print("Failed to obtain data from Metatrader5, error = ",mt5.last_error())
mt5.shutdown()
# Preparing the target variable
data["future_open"] = data["open"].shift(-class="num">1) # shift one bar into the future
data["future_close"] = data["close"].shift(-class="num">1)
target = []
for row in range(data.shape[class="num">0]):
if data["future_close"].iloc[row] > data["future_open"].iloc[row]: # bullish signal
target.append(class="num">1)
else: # bearish signal
target.append(class="num">0)