数据科学和机器学习(第 32 部分):保持您的 AI 模型更新,在线学习(基础篇)
📘

数据科学和机器学习(第 32 部分):保持您的 AI 模型更新,在线学习(基础篇)

第 1/3 篇

让模型跟着行情跑: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(看跌)标签会错位。外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前务必在策略测试器跑多品种验证。

MQL5 / C++
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 桥接只做推理,避免终端卡死。

MQL5 / C++
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。

MQL5 / C++
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)

常见问题

没有固定周期,但当样本外准确率连续下滑、信号胜率明显衰减时就该更新。建议用滚动窗口观察近1~2周表现决定是否重训。
三段链路:Python训模型并导成ONNX、把模型文件丢进公共目录、在图表代码里加载并接实时K线数据做推断。核心是把数据格式对齐。
小布可定时拉取你的模型近期信号表现,在准确率跌破阈值时弹提醒,并给出当前行情适配度的诊断结论,你只需看一眼决定是否重训。
按时间严格切分、去掉未来函数、对缺失K线做前向填充;导出ONNX前确认特征顺序和软件内推断时完全一致。
在线学习要稳定数据流和防过拟合机制,落地链路长、调试成本高;静态模型周更省事,但风格切换期容易连续吃亏。