金融时间序列中的保形预测探索·综合运用
📉

金融时间序列中的保形预测探索·综合运用

(3/3)·单点预测在厚尾行情里常常失灵,本篇把MAPIE校准、训练与终端导出串成一条可跑的通路

案例拆解 第 3/3 篇
很多交易者把模型输出当确定信号,却忽略金融数据的厚尾与异方差。保形预测给的是带覆盖率的概率区间,而非一个点。先建立区间思维,再谈信号过滤。

「标签切割后的样本回收」

上面这段 Python 风格切片把特征列和两类标签一并返回,但在 MQL5 里做同类样本整理时,得先确认 meta_labels 字段确实落到了 0 值分组,否则多元素集合会被误并。 打印 multi_element_sets 的数量,是验证标签离散程度最直接的办法;若输出远大于预期,说明元标签没按设想归零,后续训练集分布会偏。 开 MT5 接这套逻辑前,建议先跑一遍计数,把 meta_labels=0 的样本规模记下来,外汇与贵金属数据高频跳变,标签错分会让回测概率明显失真。

MQL5 / C++
print(f"Multi-element sets(meta_labels=class="num">0): {multi_element_sets}")
class="kw">return data[feature_columns + [&class="macro">#x27;labels&class="macro">#x27;, &class="macro">#x27;meta_labels&class="macro">#x27;]]

◍ 用保形置信度反洗基础模型标签

初步跑通保形预测流程后会撞到一个硬伤:元模型靠屏蔽高不确定性样本来兜错,但底层分类器仍在含噪原始标签上训练,泛化偏弱,碰到非平稳行情整体就容易垮。 MAPIE 训练完会吐出两样东西——点预测 predicted 和预测集合 y_prediction_sets。原版只拿预测集合去训元模型,基础模型却还吃着原始 labels。可以反过来:只用 predicted 与原始 labels 一致的那部分样本去训基础模型,相当于让保形分类器先给数据做一次置信过滤。 下面这段代码先把 meta_labels 全置 0,再把 predicted 等于原始 labels 的行标成 1;fit_final_models 里主模型 X、y 都只取 meta_labels==1 的干净子集,元模型仍用全量特征配 conformal_labels。两套模型都尽量在保形置信度高的区段上学习,输出质量在非平稳序列上会好一些。 外汇与贵金属波动受突发事件驱动,这类过滤只能降低噪声干扰、无法消除跳空风险,实盘前请在 MT5 用历史数据复跑确认标签一致率。

MQL5 / C++
predicted, y_prediction_sets = mapie_classifier.predict_set(X)
<span class="comment"># Initialize meta_labels column with zeros</span>
data[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] = <span class="number">class="num">0.0</span>
<span class="comment"># Set meta_labels to class="num">1 where predicted labels match original labels</span>
<span class="comment"># Compare predicted values with the original labels in the dataset</span>
data.loc[predicted == data[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>], <span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] = <span class="number">class="num">1.0</span>
<span class="keyword">def</span> fit_final_models(dataset) -&gt; <span class="built_in">list</span>:
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># features for model\meta models. We learn main model only on filtered labels </span>
&nbsp;&nbsp;&nbsp;&nbsp;X = dataset[dataset[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>]==<span class="number">class="num">1</span>]
&nbsp;&nbsp;&nbsp;&nbsp;X = X[X.columns[<span class="number">class="num">1</span>:-<span class="number">class="num">3</span>]]
&nbsp;&nbsp;&nbsp;&nbsp;X_meta = dataset[dataset.columns[<span class="number">class="num">1</span>:-<span class="number">class="num">3</span>]]
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># labels for model\meta models</span>
&nbsp;&nbsp;&nbsp;&nbsp;y = dataset[dataset[<span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>]==<span class="number">class="num">1</span>]
&nbsp;&nbsp;&nbsp;&nbsp;y = y[y.columns[-<span class="number">class="num">3</span>]]
&nbsp;&nbsp;&nbsp;&nbsp;y_meta = dataset[<span class="class="type">class="kw">string">&class="macro">#x27;conformal_labels&class="macro">#x27;</span>]

置信水平如何改写EURUSD的预测噪声比

用 2020 年初到 2025 年初的 EURUSD_H1 数据循环训练 10 个模型,留出之后时段做未来预测。基础分类器是 15 棵最大深度 5 的随机森林,交叉验证 5 折,置信水平先锁在 0.90。 训练报告里最刺眼的是噪声占比:单类别可靠预测仅 6715 条,而多类别不可靠集合高达 22948 条,说明原始样本里大部分时候模型根本给不出 90% 置信的单方向判断。模型只吃可靠预测,主模型验证准确率 0.92、元模型 0.965,平衡曲线 R2 跑到 0.993,拟合几乎贴着资金线。 把置信水平从 0.9 降到 0.6,报告立刻变样——单类别集合从 6715 涨到 28647,多类别只剩 991。预测覆盖变广,但交易获利的置信度同步塌方,净值回撤明显变密。外汇与贵金属属高风险品种,这种回撤放大不是纸面波动,而是实盘可能连续吃止损。 调置信水平本质是在交易频次、收益厚度和黑天鹅概率之间拉闸。想少做但稳,就往 0.9 靠;想铺开信号,就承受 0.6 带来的不确定性。参数文件里 stop_loss=0.00500、take_profit=0.00200 已经写死,改 confidence_level 前先想清楚盈亏比会不会被回撤吃掉。

MQL5 / C++
hyper_params = {
    &class="macro">#x27;symbol&class="macro">#x27;: &class="macro">#x27;EURUSD_H1&class="macro">#x27;,
    &class="macro">#x27;export_path&class="macro">#x27;: &class="macro">#x27;/Users/dmitrievsky/Library/Program Files/MetaTrader class="num">5/MQL5/Include/Trend following/&class="macro">#x27;,
    &class="macro">#x27;model_number&class="macro">#x27;: class="num">0,
    &class="macro">#x27;markup&class="macro">#x27;: class="num">0.00010,
    &class="macro">#x27;stop_loss&class="macro">#x27;:  class="num">0.00500,
    &class="macro">#x27;take_profit&class="macro">#x27;: class="num">0.00200,
    &class="macro">#x27;periods&class="macro">#x27;: [i for i in range(class="num">5, class="num">300, class="num">30)],
    &class="macro">#x27;backward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2020, class="num">1, class="num">1),
    &class="macro">#x27;forward&class="macro">#x27;: class="type">class="kw">datetime(class="num">2025, class="num">1, class="num">1),
}
models = []
for i in range(class="num">10):
    print(&class="macro">#x27;Learn &class="macro">#x27; + str(i) + &class="macro">#x27; model&class="macro">#x27;)
    models.append(fit_final_models(meta_learners_mapie(class="num">15, class="num">5, confidence_level=class="num">0.90, CV_folds=class="num">5)))
Learn class="num">9 model
Empty sets(meta_labels=class="num">0): class="num">0
Single element sets(meta_labels=class="num">1): class="num">6715
Multi-element sets(meta_labels=class="num">0): class="num">22948
Correct predictions(meta_labels=class="num">1): class="num">16638
Incorrect predictions(meta_labels=class="num">0): class="num">13025
R2: class="num">0.9931613891107195
>>> models[-class="num">1][class="num">1].get_best_score()[&class="macro">#x27;validation&class="macro">#x27;]
{&class="macro">#x27;Accuracy&class="macro">#x27;: class="num">0.92, &class="macro">#x27;Logloss&class="macro">#x27;: class="num">0.1990666082064606}
>>> models[-class="num">1][class="num">2].get_best_score()[&class="macro">#x27;validation&class="macro">#x27;]
{&class="macro">#x27;Accuracy&class="macro">#x27;: class="num">0.9647435897435898, &class="macro">#x27;Logloss&class="macro">#x27;: class="num">0.10955056411224594}
Learn class="num">8 model
Empty sets(meta_labels=class="num">0): class="num">0
Single element sets(meta_labels=class="num">1): class="num">28647
Multi-element sets(meta_labels=class="num">0): class="num">991
Correct predictions(meta_labels=class="num">1): class="num">16999
Incorrect predictions(meta_labels=class="num">0): class="num">12639
R2: class="num">0.9850582205528567

「把模型丢进 MT5 跑一遍」

导出步骤和之前几篇里写的没差别,老读者直接按熟悉的流程把 ONNX 模型丢进终端就行。重点在测试环节:这套系统在样本外预测区间没飘,2017 到 2020 的四年历史行情里也跑得通,说明泛化不是纸上谈兵。 主阈值和元阈值按置信度卡信号。调高阈值,单笔信号的确定性倾向更强,但触发次数会明显变少——这是频率与胜率的硬权衡,外汇和贵金属杠杆高,信号稀疏时更要防样本偏差。 下面这段是导出调用的核心,逐行看:model 取训练列表里最后一个模型;symbol、periods 从超参字典拿;periods_meta 复用同一周期;model_number 标模型编号;export_path 指定落盘位置。复制去你本地改路径就能用。

MQL5 / C++
export_model_to_ONNX(model = models[-class="num">1],
                      symbol = hyper_params[&class="macro">#x27;symbol&class="macro">#x27;],
                      periods = hyper_params[&class="macro">#x27;periods&class="macro">#x27;],
                      periods_meta = hyper_params[&class="macro">#x27;periods&class="macro">#x27;],
                      model_number = hyper_params[&class="macro">#x27;model_number&class="macro">#x27;],
                      export_path = hyper_params[&class="macro">#x27;export_path&class="macro">#x27;])

◍ 把置信阈值当作风险闸门来调

保形预测不负责找规律,只给已有模型套一层不确定性量化外壳。训练阶段调高置信阈值,被筛掉的边缘样本变多,EA 触发频率下降但单笔错判概率倾向降低;调低则反之,样本通过率高却放大了外汇与贵金属的高风险暴露。 随文给出的 MQL5_files.zip 仅 99.17 KB,内含 mapie_trader.ex5 与源码,直接拖进 MT5 终端就能跑。想验证效果,先用 EURUSD_H1.csv 在 Python 侧按 mapie_causal.py 重训,再把 ONNX 丢进 Include\Trend following 文件夹覆盖,观察 EA 在实盘模拟里的通过样本数变化。 评论区有人反馈删掉 fixing_lib 不影响主逻辑,说明实验模块只是旁路。真要落地,重点还是你自己在 MT5 里把置信阈值那一行参数从默认改到 0.9 以上,看回测权益曲线是否平滑些。

把区间诊断交给小布盯盘
这些覆盖率与误差带诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需判断该不该减仓。

常见问题

高频下微观结构噪声可能破坏次序无关性,建议按会话或重采样切块,再做滚动校准降低偏移。
目前小布内置通用区间诊断与覆盖率视图,自定义模型需走导出规范,把校准集统计喂给对应品种页即可。
终端执行环境与点差、成交延迟相关,离线回测未含这些,实盘前需在终端跑一遍验证区间稳定性。
倾向理解为长期频率约束,有限样本里可能连续超出,外汇贵金属高风险下仍要配合仓位控制。