离线强化学习交易:用 CQL 保守 Q 学习做策略迁移
实盘不允许在线试错,离线 RL 只用历史日志学策略——但直接把 Q 学习搬到固定数据集会灾难:Bellman 目标里的 maxₐQ 专挑没被覆盖的 OOD 动作、把 Q 越推越高(自举高估),部署即崩。CQL(NeurIPS 2020)加保守正则:压低所有动作 logsumexp Q、抬高数据里真实动作 Q,给未见动作上保守下限。纯 numpy 表格版 CQL,在「趋势为主+偶发反转」合成 MDP 上用只覆盖 40% 格子的 800 步窄日志训练,40 个样本外新市场平均实测:Naive-FQI 总收益 −61%/Sharpe −1.18/回撤 65%/胜率 0%,CQL +11.5%/+0.43/7.5%/72%——OOD 动作平均 Q 从 0 压到 −0.44。诚实翻车:α 太小压不住高估、过大退化成行为克隆收益封顶;数据一充足 Naive 反超 CQL。拆穿离线=免费午餐/覆盖不重要/α越大越稳/合成赢=实盘赢/保守必胜五类陷阱(中阶)。
先说结论:在线 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 不能用「在线试错」#
强化学习的经典设定是在线的:智能体在环境里动作、拿反馈、更新策略,再动作。棋类、游戏可以这么玩——环境免费、试错无害。
但把它搬到实盘交易,前提就塌了:
- 探索有成本:为了”学习”随机下一笔仓位,赔的是真钱。
- 环境不可回退:你不能把市场重置到昨天再试一次。
- 数据是既成事实:你手里往往只有一份历史成交/信号日志,而且是**别人(某个旧策略、某套人工规则)**跑出来的。
这就是**离线强化学习(Offline RL / Batch RL)**要解决的问题:只用一份固定的历史数据集 D,学出一个比生成这份数据的行为策略更好的新策略,且全程不与环境交互。 它天然贴合量化场景——把旧策略的成交记录当数据,学一个改进版,回测验证后再上线。
听起来像监督学习:数据给定、拟合就好。但这里有个致命陷阱。
二、致命陷阱:自举高估(bootstrapping overestimation)#
标准 Q 学习的更新是:
关键在目标里的 。在在线设定里,如果某个动作 的 Q 值被高估,智能体会真的去试它、发现回报没那么高、于是自我修正。
但在离线设定里,数据集 D 是死的。如果 从没在 D 里出现过(out-of-distribution,OOD 动作),它的 Q 值纯粹靠自举外推——没有任何真实回报来纠偏。更糟的是, 算子天然偏爱被高估的动作:哪个动作的 Q 被(错误地)推高了, 就选哪个当目标,然后这个虚高的目标又反过来抬高别的状态的 Q。
结果就是一个正反馈的高估螺旋:OOD 动作的 Q 值越滚越大,学出来的策略疯狂偏爱这些”看起来很美但从没被验证过”的动作。部署到真实环境——立刻崩。
这不是理论恐吓。看下面这张覆盖热图:我的行为策略日志只访问了 40% 的状态-动作格子,剩下 60% 全是 OOD 区域。Naive-FQI 在这些格子上的 Q 值完全是自举幻觉。

三、CQL:给价值估计上一道保守下限#
CQL(Conservative Q-Learning, Kumar et al., NeurIPS 2020)的思路极其干净:既然 OOD 动作的 Q 值会被高估,那就主动把「所有动作的 Q」往下压,同时把「数据里真实出现过的动作的 Q」拉回来。 一压一拉,净效果就是:数据支持的动作 Q 值保住,没数据支撑的 OOD 动作 Q 值被系统性打低。
在标准 Bellman 误差之外,CQL 加这么一项保守正则(对每个状态):
第一项 (logsumexp)是 的光滑上界,最小化它等于把整行 Q 往下拽;第二项抬高数据里真实动作的 Q。可以证明,这一项让学到的 成为真实 的下界——即”宁可低估,不可高估”。 是保守强度旋钮。
它对 的梯度出奇地简单:
其中 是数据集里状态 下动作 的经验频率。对 OOD 动作,,梯度纯是正的 softmax → 只压不拉 → Q 被打低。这正是我们要的。
四、纯 numpy 实现:表格版 CQL#
交易 MDP 设定:单资产,状态 =(当前仓位 ∈ {−1, 0, +1} × 动量信号分箱),动作 = {做空, 空仓, 做多},奖励 = 持仓收益 − 换手成本。行为策略是个只在强信号时动作、大部分时间空仓的”窄”策略,故意留下大片 OOD 区域。
import numpy as np
ACTIONS = [-1, 0, 1] # 做空 / 空仓 / 做多
NA, NS = 3, 3 * 7 # 动作数;状态数 = 仓位×动量分箱
GAMMA = 0.95
# cov[s,a]: 离线数据集里 (s,a) 的访问次数
visited = cov.sum(axis=1) > 0
data_prob = np.zeros_like(cov) # 经验数据动作分布 p̂_D(a|s)
data_prob[visited] = cov[visited] / cov[visited].sum(axis=1, keepdims=True)
def train_cql(transitions, alpha, iters=50, lr=0.2):
Q = np.zeros((NS, NA))
S, A = transitions[:,0].astype(int), transitions[:,1].astype(int)
R, S2 = transitions[:,2], transitions[:,3].astype(int)
for _ in range(iters):
# ---- (1) Bellman 误差项:批量 TD 更新 ----
target = R + GAMMA * Q[S2].max(axis=1)
grad, cnt = np.zeros_like(Q), np.zeros_like(Q)
np.add.at(grad, (S, A), target - Q[S, A])
np.add.at(cnt, (S, A), 1.0)
Q += lr * np.where(cnt > 0, grad / np.maximum(cnt, 1), 0.0)
# ---- (2) CQL 保守项:softmax - 数据分布 ----
if alpha > 0:
Qv = Q - Q.max(axis=1, keepdims=True) # 数值稳定
soft = np.exp(Qv) / np.exp(Qv).sum(axis=1, keepdims=True)
grad_cons = (soft - data_prob) * visited[:, None] # 压全体、拉数据动作
Q -= lr * alpha * grad_cons
return Qpython两处工程细节值得说:
- logsumexp 的数值稳定:直接
exp(Q)在 Q 值稍大时就溢出。减去每行最大值Q - Q.max(axis=1)再指数,softmax 结果不变但不会爆。 - 奖励缩放:交易单步奖励量级 ~1e-2,而 CQL 惩罚项量级 ~1。两者不在一个数量级, 就没法调。对奖励做正的常数缩放(策略对此不变,但净值评估仍用原始奖励),让 Bellman 信号和保守项可比——这一步不做, 扫描会退化成一条平线。
五、实测:40 个样本外新市场的策略迁移#
训练数据:只有 800 步的行为策略日志(现实里历史数据总是不够),覆盖率 40%。训练完,把 greedy 策略部署到 40 个全新生成的样本外市场,取平均:
| 策略 | 总收益 | Sharpe | 最大回撤 | 胜率 |
|---|---|---|---|---|
| CQL(α=3) | +11.5% | +0.43 | 7.5% | 72% |
| Naive-FQI(α=0) | −61.3% | −1.18 | 65.2% | 0% |
| 始终做多(Buy&Hold) | +4.0% | −0.02 | 45.0% | 40% |
| 行为策略(生成数据的旧策略) | +2.4% | +0.05 | 15.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 的 是把双刃剑。我扫了一遍:

- α < 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. 结果解读#
- 收益归因:CQL 的正收益主要来自”该动手时动手、没把握时空仓”的纪律性。它 72% 的市场胜率 + 7.5% 的低回撤说明收益不是靠少数几把豪赌,而是稳健的小赢累积——这正是保守约束的副产品。
- Naive 为什么归零:40 个市场胜率 0% 不是运气差,是结构性的。OOD 高估让它系统性偏好”从没被验证的动作”,一旦部署到没见过的状态就下错注,且错误方向一致(都往高估的幻觉动作偏),所以每个市场都亏。
- policy improvement vs behavior cloning:CQL +11.5% > 行为策略 +2.4% 证明它做到了”从次优数据学出更优策略”,这是离线 RL 区别于监督式行为克隆的核心。但 α 过大时这个优势消失(退化成克隆),说明 improvement 是”在保守约束下小心翼翼地偏离数据”换来的,不是免费的。
- 风险维度才是 CQL 的主场:即便某些设定下 CQL 收益不如 Naive,它的最大回撤(7.5%)永远远低于 Naive(65%)。对实盘而言,“不崩”比”收益高”优先级更高——离线 RL 上线前,CQL 这类保守方法几乎是必选项。
- 适用边界:数据窄、覆盖低、旧策略次优 → CQL 大幅受益;数据充足、覆盖高 → 保守成拖累,退回普通离线 Q 学习即可。上线前务必先估算数据集的状态-动作覆盖率,再决定 α。
拆穿五类陷阱(中阶):① 离线=免费午餐(错,OOD 高估让朴素方法比在线更危险);② 覆盖率不重要(错,40% vs 52% 覆盖率直接决定该用 CQL 还是普通 FQI);③ α 越大越稳(错,过大退化成行为克隆、收益封顶);④ 合成赢=实盘赢(错,合成环境动量有净 edge,真实市场信号弱得多);⑤ 保守必胜(错,数据充足时保守是纯拖累,Naive 反超)。