halo 的技术博客

返回

先说结论:在线 RL 要靠真金白银去环境里试错,实盘交易不给你这个机会——你只有一堆历史成交日志。把标准 Q 学习直接搬到这份固定数据集上会灾难性失败:Bellman 目标里的 maxₐ Q(s’,a’) 会专挑数据没覆盖过的动作、把它们的价值越推越高,部署就崩。CQL(保守 Q 学习)加一项正则,专门压低这些「没见过的动作」的 Q 值,给价值估计上一道保守下限。 在一个「趋势为主、偶发反转」的合成交易 MDP 上,我用一份只覆盖 40% 状态-动作格子的窄日志训练,40 个样本外新市场平均实测:无约束的 Naive-FQI 总收益 −61%、最大回撤 65%、胜率 0%;CQL 总收益 +11.5%、Sharpe +0.43、最大回撤压到 7.5%、胜率 72%。但也要诚实:α 调过头 CQL 就退化成行为克隆、收益封顶;数据一旦充足,保守反而成了拖累。

离线 RL 策略迁移:CQL vs 无约束 FQI 在样本外新市场的净值曲线

一、为什么实盘 RL 不能用「在线试错」#

强化学习的经典设定是在线的:智能体在环境里动作、拿反馈、更新策略,再动作。棋类、游戏可以这么玩——环境免费、试错无害。

但把它搬到实盘交易,前提就塌了:

  • 探索有成本:为了”学习”随机下一笔仓位,赔的是真钱。
  • 环境不可回退:你不能把市场重置到昨天再试一次。
  • 数据是既成事实:你手里往往只有一份历史成交/信号日志,而且是**别人(某个旧策略、某套人工规则)**跑出来的。

这就是**离线强化学习(Offline RL / Batch RL)**要解决的问题:只用一份固定的历史数据集 D,学出一个比生成这份数据的行为策略更好的新策略,且全程不与环境交互。 它天然贴合量化场景——把旧策略的成交记录当数据,学一个改进版,回测验证后再上线。

听起来像监督学习:数据给定、拟合就好。但这里有个致命陷阱。

二、致命陷阱:自举高估(bootstrapping overestimation)#

标准 Q 学习的更新是:

Q(s,a)Q(s,a)+η[r+γmaxaQ(s,a)Bellman 目标Q(s,a)]Q(s,a) \leftarrow Q(s,a) + \eta\Big[\underbrace{r + \gamma \max_{a'} Q(s',a')}_{\text{Bellman 目标}} - Q(s,a)\Big]

关键在目标里的 maxaQ(s,a)\max_{a'} Q(s',a')。在在线设定里,如果某个动作 aa' 的 Q 值被高估,智能体会真的去试它、发现回报没那么高、于是自我修正。

但在离线设定里,数据集 D 是死的。如果 aa' 从没在 D 里出现过(out-of-distribution,OOD 动作),它的 Q 值纯粹靠自举外推——没有任何真实回报来纠偏。更糟的是,max\max 算子天然偏爱被高估的动作:哪个动作的 Q 被(错误地)推高了,max\max 就选哪个当目标,然后这个虚高的目标又反过来抬高别的状态的 Q。

结果就是一个正反馈的高估螺旋:OOD 动作的 Q 值越滚越大,学出来的策略疯狂偏爱这些”看起来很美但从没被验证过”的动作。部署到真实环境——立刻崩。

这不是理论恐吓。看下面这张覆盖热图:我的行为策略日志只访问了 40% 的状态-动作格子,剩下 60% 全是 OOD 区域。Naive-FQI 在这些格子上的 Q 值完全是自举幻觉。

左:CQL 把 OOD 动作的平均 Q 压到负值防高估;右:离线数据集只覆盖 40% 的状态-动作格子

三、CQL:给价值估计上一道保守下限#

CQL(Conservative Q-Learning, Kumar et al., NeurIPS 2020)的思路极其干净:既然 OOD 动作的 Q 值会被高估,那就主动把「所有动作的 Q」往下压,同时把「数据里真实出现过的动作的 Q」拉回来。 一压一拉,净效果就是:数据支持的动作 Q 值保住,没数据支撑的 OOD 动作 Q 值被系统性打低。

在标准 Bellman 误差之外,CQL 加这么一项保守正则(对每个状态):

LCQL=α(logaeQ(s,a)压低所有动作EaD[Q(s,a)]抬高数据动作)\mathcal{L}_{\text{CQL}} = \alpha \Big(\underbrace{\log\sum_{a} e^{Q(s,a)}}_{\text{压低所有动作}} - \underbrace{\mathbb{E}_{a\sim D}[Q(s,a)]}_{\text{抬高数据动作}}\Big)

第一项 logaeQ(s,a)\log\sum_a e^{Q(s,a)}(logsumexp)是 maxaQ\max_a Q 的光滑上界,最小化它等于把整行 Q 往下拽;第二项抬高数据里真实动作的 Q。可以证明,这一项让学到的 QQ 成为真实 QπQ^\pi下界——即”宁可低估,不可高估”。α\alpha 是保守强度旋钮。

它对 Q(s,a)Q(s,a) 的梯度出奇地简单:

LCQLQ(s,a)=softmaxaQ(s,a)p^D(as)\frac{\partial \mathcal{L}_{\text{CQL}}}{\partial Q(s,a)} = \underbrace{\text{softmax}_a Q(s,a)}_{\text{压}} - \underbrace{\hat{p}_D(a\mid s)}_{\text{拉}}

其中 p^D(as)\hat p_D(a\mid s) 是数据集里状态 ss 下动作 aa 的经验频率。对 OOD 动作,p^D=0\hat p_D = 0,梯度纯是正的 softmax → 只压不拉 → Q 被打低。这正是我们要的。

四、纯 numpy 实现:表格版 CQL#

交易 MDP 设定:单资产,状态 =(当前仓位 ∈ {−1, 0, +1} × 动量信号分箱),动作 = {做空, 空仓, 做多},奖励 = 持仓收益 − 换手成本。行为策略是个只在强信号时动作、大部分时间空仓的”窄”策略,故意留下大片 OOD 区域。

两处工程细节值得说:

  1. logsumexp 的数值稳定:直接 exp(Q) 在 Q 值稍大时就溢出。减去每行最大值 Q - Q.max(axis=1) 再指数,softmax 结果不变但不会爆。
  2. 奖励缩放:交易单步奖励量级 ~1e-2,而 CQL 惩罚项量级 ~1。两者不在一个数量级,α\alpha 就没法调。对奖励做正的常数缩放(策略对此不变,但净值评估仍用原始奖励),让 Bellman 信号和保守项可比——这一步不做,α\alpha 扫描会退化成一条平线。

五、实测:40 个样本外新市场的策略迁移#

训练数据:只有 800 步的行为策略日志(现实里历史数据总是不够),覆盖率 40%。训练完,把 greedy 策略部署到 40 个全新生成的样本外市场,取平均:

策略总收益Sharpe最大回撤胜率
CQL(α=3)+11.5%+0.437.5%72%
Naive-FQI(α=0)−61.3%−1.1865.2%0%
始终做多(Buy&Hold)+4.0%−0.0245.0%40%
行为策略(生成数据的旧策略)+2.4%+0.0515.7%55%

结论很清楚:

  • Naive-FQI 灾难性失败:40 个市场里胜率 0%,平均亏 61%,回撤 65%。OOD 高估让它疯狂追逐幻觉动作,仓位被打飞。
  • CQL 稳住了迁移:不仅正收益,回撤只有 7.5%——保守约束让它不敢在没数据支撑的地方乱下重注,风险控制反而是它最大的优点。
  • CQL 超过了行为策略本身:+11.5% vs +2.4%,这才是离线 RL 的价值——从次优日志里学出更好的策略(policy improvement),而不只是模仿(那是行为克隆)。

机制证据:CQL 把 OOD 动作的平均 Q 从 Naive 的 0 压到 −0.44,数据内动作的 Q 则保住在合理水平。高估螺旋被掐断了。

六、α 不是越大越好:保守的代价#

CQL 的 α\alpha 是把双刃剑。我扫了一遍:

保守系数 α 的权衡:太小 OOD 高估失控,过大退化为行为克隆、收益封顶

  • α < 3:保守力度不够,压不住 OOD 高估,样本外 Sharpe 仍是负的(−1 到 −1.2),跟 Naive 差不多。
  • α ≈ 3–5:甜蜜区,Sharpe 从负转正,冲到 +0.4 ~ +0.64。
  • α > 5:Sharpe 封顶在 +0.64 不再上升。因为 α 太大时,保守项完全压过 Bellman 项,CQL 退化成行为克隆——只会照抄数据里的动作分布,policy improvement 消失,收益被行为策略的天花板锁死。

“保守”和”进取”是连续谱:α=0 是纯进取(高估崩盘),α=∞ 是纯保守(照抄旧策略)。价值在中间。

A. 实现细节#

  • 信号与执行:状态用 t 时刻的仓位 + 动量信号分箱;动作在 t 决定,收益用 t+1 的实现收益结算(signal-on-t / execute-on-t+1),无未来函数。
  • 奖励口径持仓方向 × 次期收益 − 0.0005 × 换手量,换手成本对每次仓位变动收取。训练时对奖励做 ×10 常数缩放使 α 可调,净值评估回到原始奖励口径
  • 数据集构造:行为策略在强动量(|信号|>0.45)时满仓做多/做空、否则大概率空仓 + 15% 随机探索,故意制造 40% 的窄覆盖。训练集仅 800 步,模拟”历史数据不足”。
  • 训练:表格 Q,批量 TD(np.add.at 按 (s,a) 聚合更新)+ CQL 保守梯度,50 轮,lr=0.2,γ=0.95。
  • 评估:40 个独立生成的样本外市场(每个 1500 步),greedy 策略部署,指标取跨市场平均;同一批市场同时评估 Naive/CQL/Buy&Hold/行为策略保证可比。

B. 已知偏差#

  • 合成 MDP,不是真实市场:数据生成过程是「趋势为主 + 偶发反转」的可控环境,动量在这里有真实净 edge。真实市场的 regime 切换更凶、信号更弱、成本更复杂,这里的 +11.5% 不可外推为实盘预期。
  • 表格离散化:仓位×动量只有 21 个状态。真实策略状态空间连续高维,需要函数逼近(神经网络 Q),届时 OOD 高估问题更严重(神经网络外推比查表更放飞),CQL 也更不可或缺,但实现和调参难度陡增。
  • 数据充足时 CQL 会输:这是最关键的边界。我另测过:当训练数据放大到 2 万步、覆盖率升到 52%,Naive-FQI 反而跑赢 CQL——因为 OOD 区域几乎被填满,高估无从发生,此时保守约束纯是拖累。CQL 的价值严格绑定在”数据窄、覆盖低”的场景
  • 单资产、无冲击成本:没有建模流动性冲击、滑点随仓位非线性放大等实盘摩擦。

C. 结果解读#

  1. 收益归因:CQL 的正收益主要来自”该动手时动手、没把握时空仓”的纪律性。它 72% 的市场胜率 + 7.5% 的低回撤说明收益不是靠少数几把豪赌,而是稳健的小赢累积——这正是保守约束的副产品。
  2. Naive 为什么归零:40 个市场胜率 0% 不是运气差,是结构性的。OOD 高估让它系统性偏好”从没被验证的动作”,一旦部署到没见过的状态就下错注,且错误方向一致(都往高估的幻觉动作偏),所以每个市场都亏。
  3. policy improvement vs behavior cloning:CQL +11.5% > 行为策略 +2.4% 证明它做到了”从次优数据学出更优策略”,这是离线 RL 区别于监督式行为克隆的核心。但 α 过大时这个优势消失(退化成克隆),说明 improvement 是”在保守约束下小心翼翼地偏离数据”换来的,不是免费的。
  4. 风险维度才是 CQL 的主场:即便某些设定下 CQL 收益不如 Naive,它的最大回撤(7.5%)永远远低于 Naive(65%)。对实盘而言,“不崩”比”收益高”优先级更高——离线 RL 上线前,CQL 这类保守方法几乎是必选项。
  5. 适用边界:数据窄、覆盖低、旧策略次优 → CQL 大幅受益;数据充足、覆盖高 → 保守成拖累,退回普通离线 Q 学习即可。上线前务必先估算数据集的状态-动作覆盖率,再决定 α。

拆穿五类陷阱(中阶):① 离线=免费午餐(错,OOD 高估让朴素方法比在线更危险);② 覆盖率不重要(错,40% vs 52% 覆盖率直接决定该用 CQL 还是普通 FQI);③ α 越大越稳(错,过大退化成行为克隆、收益封顶);④ 合成赢=实盘赢(错,合成环境动量有净 edge,真实市场信号弱得多);⑤ 保守必胜(错,数据充足时保守是纯拖累,Naive 反超)。

离线强化学习交易:用 CQL 保守 Q 学习做策略迁移
https://blog.halo26812.eu.org/blog/cql-offline-rl-trading
Author halo
Published at 2026年7月24日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨