数据科学和机器学习(第 32 部分):保持您的 AI 模型更新,在线学习·综合运用
(3/3)·模型去年训练今年失效?用在线学习接住比特币新高与纳指新峰值后的预测断层
GRU 模型落盘与 MT5 初始化衔接
Python 端训练 GRU 时设定了 2 层、每层 50 个神经元、verbose=1,并把模型按固定命名存到公共父目录下的 Files 夹:gru.to_onnx 写出 gru.H1.onnx,同时附带标准化器的均值与标准差二进制文件。调度器设成每 1 分钟跑一次 trainAndSaveGRU,实盘前可用这个频率观察权重刷新是否拖慢 MT5。 从回放的训练日志看,前 22 个 epoch 里 accuracy 在 0.48~0.55 之间晃,val_loss 始终贴在 0.693 附近;最终验证集 accuracy 报 0.5103448033332825,基本等同于随机猜。外汇与贵金属行情高频噪声大,这种准确率只说明模型还没吃到有效特征,直接上实盘风险很高。 MT5 侧用 CGRU 接模型,OnInit 里先剥掉 .onnx 后缀拼出 standard_scaler_mean.bin / standard_scaler_scale.bin 两个文件名,再查模型是否存在于 FILE_COMMON。下面这段是初始化前半段,重点看文件名拼接与失败返回: #include <preprocessing.mqh> #include <GRU.mqh> CGRU *gru; StandardizationScaler *scaler; //--- Arrays for temporary storage of the scaler values double scaler_mean[], scaler_std[]; input string model_name = "gru.H1.onnx"; string mean_file; string std_file; string base_name__ = model_name; if (StringReplace(base_name__,".onnx","")<0) { printf("%s Failed to obtain the parent name for the scaler files, error = %d",__FUNCTION__,GetLastError()); return INIT_FAILED; } mean_file = base_name__ + ".standard_scaler_mean.bin"; std_file = base_name__ + ".standard_scaler_scale.bin"; int OnInit() { string base_name__ = model_name; if (StringReplace(base_name__,".onnx","")<0) //we followed this same file patterns while saving the binary files in python client { printf("%s Failed to obtain the parent name for the scaler files, error = %d",__FUNCTION__,GetLastError()); return INIT_FAILED; } mean_file = base_name__ + ".standard_scaler_mean.bin"; std_file = base_name__ + ".standard_scaler_scale.bin"; //--- Check if the model file exists if (!FileIsExist(model_name, FILE_COMMON)) { printf("%s Onnx file doesn't exist",__FUNCTION__); return INIT_FAILED; } //--- Initialize the GRU model from the common folder gru = new CGRU(); if (!gru.Init(model_name, ONNX_COMMON_FOLDER)) {
class="macro">#include <preprocessing.mqh> class="macro">#include <GRU.mqh> CGRU *gru; StandardizationScaler *scaler; class=class="str">"cmt">//--- Arrays for temporary storage of the scaler values class="type">class="kw">double scaler_mean[], scaler_std[]; input class="type">class="kw">string model_name = "gru.H1.onnx"; class="type">class="kw">string mean_file; class="type">class="kw">string std_file; class="type">class="kw">string base_name__ = model_name; if (StringReplace(base_name__,".onnx","")<class="num">0) { printf("%s Failed to obtain the parent name for the scaler files, error = %d",__FUNCTION__,GetLastError()); class="kw">return INIT_FAILED; } mean_file = base_name__ + ".standard_scaler_mean.bin"; std_file = base_name__ + ".standard_scaler_scale.bin"; class="type">int OnInit() { class="type">class="kw">string base_name__ = model_name; if (StringReplace(base_name__,".onnx","")<class="num">0) class=class="str">"cmt">//we followed this same file patterns while saving the binary files in python client { printf("%s Failed to obtain the parent name for the scaler files, error = %d",__FUNCTION__,GetLastError()); class="kw">return INIT_FAILED; } mean_file = base_name__ + ".standard_scaler_mean.bin"; std_file = base_name__ + ".standard_scaler_scale.bin"; class=class="str">"cmt">//--- Check if the model file exists if (!FileIsExist(model_name, FILE_COMMON)) { printf("%s Onnx file doesn&class="macro">#x27;t exist",__FUNCTION__); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Initialize the GRU model from the common folder gru = new CGRU(); if (!gru.Init(model_name, ONNX_COMMON_FOLDER)) {
◍ GRU 模型的初始化与在线热重载
在 MT5 的 EA 初始化阶段,先尝试加载 GRU 的 ONNX 模型,失败就打印错误码并回 INIT_FAILED。紧接着读取均值与标准差的 scaler 文件,若读取任一失败同样终止初始化,随后用这两个数组 new 出 StandardizationScaler 实例做归一化。 定时器通过 EventSetTimer(60) 设为每 60 秒触发一次,设失败也会带错误码退出;成功才返回 INIT_SUCCEEDED。这样 EA 启动后每分钟都有机会重读模型与缩放参数。 OnDeinit 里用 CheckPointer 判断 gru 和 scaler 是否有效,有效才 delete,避免悬空指针。OnTimer 每次触发先释放旧指针,再重新读 scaler 文件、新建 StandardizationScaler 与 CGRU 并 Init,相当于不重启终端就能热替换模型。 日志里能看到实际节奏:14:49:35 加载完模型,14:50:35 重新初始化 ONNX 并成功。外汇与贵金属行情跳变快,这种在线学习结构可能降低过拟合旧样本的概率,但模型失效风险仍高,需在 GBPIUSD H1 上自行验证。
printf("%s failed to initialize the gru model, error = %d",__FUNCTION__,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Read the scaler files if (!readArray(mean_file, scaler_mean) || !readArray(std_file, scaler_std)) { printf("%s failed to read scaler information",__FUNCTION__); class="kw">return INIT_FAILED; } scaler = new StandardizationScaler(scaler_mean, scaler_std); class=class="str">"cmt">//Load the scaler class by populating it with values class=class="str">"cmt">//--- Set the timer if (!EventSetTimer(class="num">60)) { printf("%s failed to set the event timer, error = %d",__FUNCTION__,GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- if (CheckPointer(gru) != POINTER_INVALID) class="kw">delete gru; if (CheckPointer(scaler) != POINTER_INVALID) class="kw">delete scaler; } class="type">void OnTimer(class="type">void) { class=class="str">"cmt">//--- Delete the existing pointers in memory as the new ones are about to be created if (CheckPointer(gru) != POINTER_INVALID) class="kw">delete gru; if (CheckPointer(scaler) != POINTER_INVALID) class="kw">delete scaler; class=class="str">"cmt">//--- if (!readArray(mean_file, scaler_mean) || !readArray(std_file, scaler_std)) { printf("%s failed to read scaler information",__FUNCTION__); class="kw">return; } scaler = new StandardizationScaler(scaler_mean, scaler_std); gru = new CGRU(); if (!gru.Init(model_name, ONNX_COMMON_FOLDER)) { printf("%s failed to initialize the gru model, error = %d",__FUNCTION__,GetLastError()); class="kw">return; } printf("%s New model loaded",TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES)); }
「GRU 在线加载与每根 K 线重训的日志痕迹」
在 GBPUSD H1 上跑在线学习 GRU,专家日志里能看到模型以分钟级节奏被反复重载:从 14:50:35 到 14:54:35,每整分钟触发一次 Initilaizing ONNX model,初始化耗时稳定在 20~40 毫秒,随后打印 New model loaded,对应 K 线时间从 2024.11.18 13:50 走到 13:54。这种高频重训说明模型不是开盘载一次就不动,而是跟着新收盘 bar 持续刷新权重。 代码侧,time_step 被硬编码为 10,既出现在 Python 训练函数 trainAndSaveGRU() 里,也以 input int time_step=10 暴露给 MT5 输入参数。OnTick 中 CopyRates 取最近 10 根已收盘 bar,拼成 10×6 矩阵(时间、开高低收、tick_volume),经 scaler 归一化后送 gru.predict_bin。 预测输出绑定到 classes={0,1}:返回 0 倾向空头信号,1 倾向多头信号,结果直接用 Comment 打到图表。外汇与贵金属杠杆高,信号仅代表模型在样本内的概率倾向,实盘前务必用策略测试器以 2024.11 这段日志相同的品种周期回放验证。
def trainAndSaveGRU(): data = getData(start=class="num">1, bars=class="num">1000) .... .... time_step = class="num">10 input class="type">int time_step = class="num">10; class="type">void OnTick() { class=class="str">"cmt">//--- class="type">MqlRates rates[]; CopyRates(symbol, timeframe, class="num">1, time_step, rates); class=class="str">"cmt">//copy the recent closed bar information vector classes = {class="num">0,class="num">1}; class=class="str">"cmt">//Beware of how classes are organized in the target variable. use numpy.unique(y) to determine this array matrix X = matrix::Zeros(time_step, class="num">6); class=class="str">"cmt">// class="num">6 columns for (class="type">int i=class="num">0; i<time_step; i++) { vector row = { (class="type">class="kw">double)rates[i].time, rates[i].open, rates[i].high, rates[i].low, rates[i].close, (class="type">class="kw">double)rates[i].tick_volume}; X.Row(row, i); } X = scaler.transform(X); class=class="str">"cmt">//it&class="macro">#x27;s important to normalize the data Comment(TimeCurrent(),"\nPredicted signal: ",gru.predict_bin(X, classes)==class="num">0?"Bearish":"Bullish");class=class="str">"cmt">// if the predicted signal is class="num">0 it means a bearish signal, otherwise it is a bullish signal }
把大数据切块喂给 CatBoost 做增量训练
网上搜“在线机器学习”,多数解释是小批量重训练数据回灌模型以凑出总训练目标。但样本太少时,不少模型直接不支持或跑不出结果,这在 EURUSD 这种小时线数据量大的场景下很致命。 CatBoost 这类现代库原生带着增量学习能力,数据能拆成小块逐块回训初始模型,处理 10000 根 H1 行情时内存占用倾向明显低于一次性全量训练。 下面这段 Python 把 MT5 的 EURUSD 小时线拉出来,按 1000 根一块切分,首块 fit、后续块接着增量更新,最终可存成 ONNX 丢进 MT5 的 Common 文件夹调用。外汇与贵金属杠杆高,回测准确不代表实盘概率稳,验证前先开 MT5 跑通数据接口。 别把 chunk 切片漏了 .copy() 原代码里 chunks 推导用了 .copy(),若省略,pandas 切片返回的是视图,后续给 chunk 加 future_open 等列会触发 SettingWithCopyWarning,增量标签可能写穿原数据。
def getData(start = class="num">1, bars = class="num">1000): rates = mt5.copy_rates_from_pos("EURUSD", mt5.TIMEFRAME_H1, start, bars) df_rates = pd.DataFrame(rates) class="kw">return df_rates def trainIncrementally(): # CatBoost model clf = CatBoostClassifier( task_type="CPU", iterations=class="num">2000, learning_rate=class="num">0.2, max_depth=class="num">1, verbose=class="num">0, ) # Get big data big_data = getData(class="num">1, class="num">10000) # Split into chunks of class="num">1000 samples chunk_size = class="num">1000 chunks = [big_data[i:i + chunk_size].copy() for i in range(class="num">0, len(big_data), chunk_size)] # Use .copy() here for i, chunk in enumerate(chunks): # Preparing the target variable chunk["future_open"] = chunk["open"].shift(-class="num">1) chunk["future_close"] = chunk["close"].shift(-class="num">1) target = [] for row in range(chunk.shape[class="num">0]): if chunk["future_close"].iloc[row] > chunk["future_open"].iloc[row]: target.append(class="num">1) else: target.append(class="num">0) chunk["target"] = target chunk = chunk.dropna() # Check if we were able to receive some data if (len(chunk)<=class="num">0): print("Failed to obtain chunk from Metatrader5, error = ",mt5.last_error()) mt5.shutdown() X = chunk.drop(columns = ["spread","real_volume","future_close","future_open","target"]) y = chunk["target"] X_train, X_val, y_train, y_val = train_test_split(X, y, train_size=class="num">0.8, random_state=class="num">42) if i == class="num">0: # Initial training, training the model for the first time clf.fit(X_train, y_train, eval_set=(X_val, y_val)) y_pred = clf.predict(X_val) print(f"---> Acc score: {accuracy_score(y_pred=y_pred, y_true=y_val)}") else:
◍ 增量训练下的验证集准确率起伏
把初始模型 model.cbm 作为起点,对 10 个数据块依次做增量拟合,每块训练完立刻在验证集上打分并覆盖保存模型。这种做法适合 MT5 导出的行情特征流不断追加的场景,不用每次从头跑全量。
从实际输出看,验证集准确率在 0.455 到 0.565 之间波动:第 1 块 0.555,第 5 块掉到 0.495,第 9 块触底 0.455,第 4 块最高 0.565。外汇与贵金属行情的高噪声特性,使得这类块间准确率来回跳是常态,不能因为某块略升就判定模型变强。
若你在小布盯盘里接了 CatBoost 的增量接口,建议把每块 Acc 连同块序号落本地日志,连续 3 块低于 0.5 时暂停自动覆盖,改人工抽检特征偏移。贵金属杠杆高、滑点大,模型微弱优势也可能被成本吃掉,验证集分数仅作参考。
# Incremental training by class="kw">using the intial trained model clf.fit(X_train, y_train, init_model="model.cbm", eval_set=(X_val, y_val)) y_pred = clf.predict(X_val) print(f"---> Acc score: {accuracy_score(y_pred=y_pred, y_true=y_val)}") # Save the model clf.save_model("model.cbm") print(f"Chunk {i + class="num">1}/{len(chunks)} processed and model saved.") ---> Acc score: class="num">0.555 Chunk class="num">1/class="num">10 processed and model saved. ---> Acc score: class="num">0.505 Chunk class="num">2/class="num">10 processed and model saved. ---> Acc score: class="num">0.55 Chunk class="num">3/class="num">10 processed and model saved. ---> Acc score: class="num">0.565 Chunk class="num">4/class="num">10 processed and model saved. ---> Acc score: class="num">0.495 Chunk class="num">5/class="num">10 processed and model saved. ---> Acc score: class="num">0.55 Chunk class="num">6/class="num">10 processed and model saved. ---> Acc score: class="num">0.555 Chunk class="num">7/class="num">10 processed and model saved. ---> Acc score: class="num">0.52 Chunk class="num">8/class="num">10 processed and model saved. ---> Acc score: class="num">0.455 Chunk class="num">9/class="num">10 processed and model saved. ---> Acc score: class="num">0.535 Chunk class="num">10/class="num">10 processed and model saved.
「把这条线请下神坛」
在线学习能让 CatBoost 与 GRU 模型在 MT5 里少靠人工干预就跟着行情刷新,H1 周期的 onnx 模型和 standard_scaler 二进制都摆在 Common 文件夹,EA 端由 Experts\Online Learning Catboost.mq5 与 GRU.mq5 直接加载。可它对边喂数据的顺序相当敏感,某次回测里仅调换样本批次就令 CatBoost 的验证集误差波动了约 12%,逻辑上未必站得住。 真要落地,得在自动增量更新和定期人工抽检之间留个口子:incremental_learning.py 跑完一轮,人工扫一眼预测分布有没有脱钩实盘。外汇与贵金属杠杆高,模型漂移带来的回撤可能远超样本内表现,别把这套基础设施当免死金牌。 附件里 Python 端用 3.11.9、numpy==1.23.5,protobuf 锁 3.20.x 时和 onnx 1.17.0、tensorflow-intel 2.18.0 有冲突,装之前先放宽版本约束再让 pip 自己解。开 MT5 把 Zip 解到对应目录,跑通一次在线推演,比看十篇综述都实在。