Vasily's Blog

一个记录学习经历的站点

0%

因子化世界模型:一次彻底的失败

缘起

2025年底读到一篇论文,讲的是将图像分解为"语义token"和"细节token"进行分治建模。我觉得这个思路可以迁移到金融时间序列上:

价格运动可以被分解为两个物理过程——

  1. 趋势延续(Trend Continuation):价格沿着已形成的趋势惯性运行,这应该是一个低频、近乎确定性的过程。
  2. 均值回归(Mean Reversion):价格因短期过度偏离而出现向长期均值回归的倾向,这是一个高频、类似弹性恢复的过程。

传统的端到端模型试图同时逼近两个不同物理过程的复合。如果能拆开分别建模再组合,理论上应该更高效。

于是有了因子化世界模型(Factorized World Model, FWM)

架构设计

状态分解

从原始 OHLCV 数据中提取趋势状态序列和回归残差序列:

  1. HP滤波:用 Hodrick-Prescott 滤波从收盘价中提取一条平滑的趋势线 \(\tau_t\)。参数 \(\lambda = 5000\) 控制了趋势的光滑程度。

  2. 趋势Token:计算趋势线的滑动斜率,做 z-score 标准化后离散化为 5 类 Token——强下降、弱下降、横盘、弱上升、强上升。阈值用 \(\pm 0.3\sigma\)\(\pm 1.0\sigma\)

  3. 残差特征:定义残差 \(r_t = p_t - \tau_t\),提取 8 维特征向量:标准化残差、RSI(14)、布林带 %B 位置、ATR 比率、成交量比率、K线振幅比率、斜率 z-score、EMA20 偏离比率。

  4. 均值回归标签:当标准化残差超过 \(2\sigma\) 且未来 \(K\) 根 K 线内价格向趋势线方向回归超过一定阈值,标记为回归触发点。注意这是一个前视标签,仅用于训练。

模型

1
2
3
4
5
6
7
8
输入: 趋势Token + 8维残差特征

Token嵌入(5→d) + 残差投影(8→d) → 逐元素相加 → 可学习位置编码

4层Transformer Decoder (因果掩码, d=192, 6头)

趋势预测头: Linear(d, 5) → softmax → 下一时刻趋势状态分布
回归信号头: Linear(d, 128) → GELU → Linear(128, 1) → sigmoid → MR概率

决策映射

将趋势概率分布与 MR 概率结合生成交易信号:

最大概率趋势 MR < 30% MR > 70%
上升 做多 (+1) 平仓 (0)
横盘 观望 (0) 观望 (0)
下降 做空 (-1) 平仓 (0)

趋势概率分布的熵用作不确定性度量,熵过高时强制平仓。

实现

代码约 900 行,包括:

  • factorized_world_model.py — 核心模型:StateDecomposer、FactorizedEncoder、TemporalWorldModel(Transformer)、DecisionInterpreter
  • train_factorized_world_model.py — CLI 驱动的完整流水线:数据加载 → 状态分解 → 训练 → 回测 → 报告

训练过程中修了三个 Bug:

  1. HP滤波内存溢出\(48,000 \times 48,000\) 的稠密矩阵需要 17GB,改用 scipy.sparse 五对角矩阵
  2. 斜率z-score的O(T²)循环_compute_slope_z 对每个时间步都重算所有历史斜率,全量数据需要 \(\sum_{t=24}^{47999} (t-12) \approx 1.15\times 10^9\) 次 polyfit,预计运行数小时。改为一次卷积预计算所有斜率
  3. 回测索引越界:最后一个 bar 没有下一个 K 线数据

修复后全量 48,000 根 K 线的状态分解从数小时降至 19.6 秒。

初试:单次分割的假象

第一次训练用标准的 70/15/15 时序分割,结果看起来非常漂亮:

1
2
3
4
5
6
7
8
趋势准确率: 96.0%
Cohen's Kappa: 0.95
MR AUC: 0.89

回测(测试期 2025.08-2026.05,BTC跌了35%):
完整模型: +904% 夏普 8.26
趋势跟随: +4579% 夏普 12.11
买入持有: -35%

当时差点就被骗了。"夏普 12" 在真实市场中不存在。文艺复兴大奖章基金的历史最佳也才 3-4 的水平。这个数字完美得不像真的——因为它就是假的。

问题:单次分割只测了最后 15% 的数据,而那段时间 BTC 处于单边下跌。模型只要无脑做空就能赚钱。MR 预警全程负贡献:

1
2
趋势跟随(无MR): +4579%
完整模型(有MR): +904% ← MR信号全错了

在 BTC 1h 上,均值回归预警的所有触发都是错误的。

滚动训练:真相大白

构建了累积窗口的 walk-forward 训练模式,18 个窗口覆盖 2022-2026 全年:

1
2
3
4
5
6
7
窗口1: 训练 [0-10000]  测试 [10000-12000]  → -37.7%
窗口2: 训练 [0-12000] 测试 [12000-14000] → -15.6%
窗口3: 训练 [0-14000] 测试 [14000-16000] → -12.7%
窗口4: 训练 [0-16000] 测试 [16000-18000] → -43.9%
...
窗口18: 训练 [0-44000] 测试 [44000-46000] → -48.5%
累计: -99.9%

18 个窗口,全部亏损,无一例外。

这不是运气问题,这是系统性问题。在上涨的窗口、下跌的窗口、震荡的窗口——模型都在稳定亏钱。

换成山寨币试试

也许只是 BTC 太有效了?我用同样的配置测了三个波动等级不同的山寨币:

币种 等级 累计收益 平均夏普
SOLUSDT 高波主流 -100.0% -2.28
DOGEUSDT 中高波meme -100.0% -4.07
ZECUSDT 中波老币 -100.0% -2.18

结果完全一致。全部 -100%,无一幸免。

独立实验的交叉验证

FWM 的失败不是孤立的。在之前的项目中:

  • DeepSeek-LLM 交易回测:从 1h K 线挖掘 50+ 技术指标,通过 LLM 获取交易决策。方向预测准确率 44%,接近随机。
  • XGBoost/随机森林:在 BTC 1h 上反复尝试,无法产生稳定正收益。
  • LSTM 集成模型:三个模型的集成,同样没有正预期。

这些独立实验用完全不同的方法得出了一致结论:BTC 1h 的价格变化在统计意义上接近随机游走

失败的根本原因

现在复盘,有两个致命的设计错误:

1. HP 滤波的前视偏差

StateDecomposer 对整个训练集做了一次性双向 HP 滤波:

1
trend = spsolve(A, series)  # 一次性求解整个序列

这意味着训练集里时间 \(t\) 的趋势是拿 \(t\) 以后的数据算出来的。模型学到的"规律"本质上包含了未来信息,到真没见过的新数据上自然崩溃。

这解释了为什么单次分割(70/15/15)的测试结果看起来那么漂亮——验证集和测试集的数据也被同样方式分解了,信息泄漏是一体的。

2. OHLCV 的单向预测信噪比太低

本质上我在做的是:用 \([t-47, t]\) 的 OHLCV 数据预测 \(t+1\) 时刻的方向。这个任务的信噪比在加密货币 1h 级别上极低。无论是浅层模型(XGBoost)、深层模型(LSTM)、还是大型模型(Transformer/LLM),都无法从中提取出能稳定盈利的信号。


教训

好的教训

  • Walk-forward 测试是必需的:单次分割的夏普 12 是假象,滚动训练的 -99.9% 是真相。
  • M 个独立实验得到同样的负结论,这个结论值得信任:LLM、XGBoost、LSTM、Transformer 都不行,那就是这条路行不通。
  • 在开始之前先估计上限:如果 \(R^2\) 的天花板是 0.01,任何模型都不可能赚钱。

坏的教训

花了整整一天,写了 900 行代码,跑了几小时的 GPU 训练,得到了一个字:不行。

但其实这个结果的价值比一次"成功的回测"更大——它排除了一个看起来很有前景的方向,让精力可以集中在真正有效的事情上。

代码归档

FWM 项目代码留在仓库中,作为一次诚实的失败记录:

  • ml_models/factorized_world_model.py
  • ml_models/train_factorized_world_model.py
  • notes/factorized_world_model_TECH.md

之前验证有效的策略(低波山寨币暴跌抄底,胜率 57.5%,夏普 4.01)继续实盘模拟。