halo 的技术博客

返回

一句话版本#

方向预测告诉你往哪边下注,预测概率告诉你该下多大。Bet Sizing 用一个映射函数把模型置信度翻译成头寸比例——同一个预测器,换一种下注方式,最大回撤可以差一个数量级。这不是玄学,是资金暴露的重新分配。


被扔掉的那个数字#

几乎所有分类器都输出两样东西:一个方向(涨/跌),一个概率(这个方向有多大把握)。

大多数量化管线的做法是:拿走方向,扔掉概率。predict() 返回 1 就满仓买,返回 0 就空仓。模型说「我有 51% 的把握」和「我有 85% 的把握」,在执行端得到的是完全相同的头寸。

这在直觉上就说不通。一个 51% 概率的信号,期望优势薄得像纸——你几乎是在抛硬币。一个 85% 概率的信号(假设校准良好),是模型职业生涯里为数不多的高光时刻。用同样的资金对待这两者,等于把大部分风险预算浪费在最没把握的交易上。

这个系列的上一站,元标注(Meta-Labeling)已经给出了架构:一级模型定方向,二级模型输出「这个信号该不该信」的概率。Bet Sizing 是这个架构的最后一公里——把二级模型的概率输出,翻译成具体的头寸大小。

López de Prado 在《Advances in Financial Machine Learning》第十章给出的翻译方案,值得逐行拆开看。

从概率到仓位:映射函数的形状#

最朴素的想法是线性映射:仓位 m=2p1m = 2p - 1。概率 0.5 对应空仓,概率 1.0 对应满仓,中间线性插值。

AFML 的方案多绕了一步。先把概率对 0.5 的偏离做标准化:

z=p0.5p(1p)z = \frac{p - 0.5}{\sqrt{p(1-p)}}

这个 zz 是检验统计量的形状——分子是「概率偏离随机猜测多少」,分母是伯努利分布的标准差。它回答的问题是:这个概率偏离 0.5 的程度,有多少统计显著性?

然后把 zz 塞进标准正态 CDF,拉回 [1,1][-1, 1] 区间:

m=2Φ(z)1m = 2\Phi(z) - 1

import numpy as np
from scipy.stats import norm

def bet_size(p):
    """AFML 第十章:预测概率 -> 目标仓位(占最大仓位的比例)"""
    z = (p - 0.5) / np.sqrt(p * (1 - p))
    return 2 * norm.cdf(z) - 1

# 感受一下形状
for p in [0.51, 0.55, 0.60, 0.70, 0.85, 0.95]:
    print(f"p={p:.2f} -> 仓位 {bet_size(p):.3f}  (线性映射给 {2*p-1:.3f})")
python

两种映射画在一起:

概率到仓位的映射曲线

形状差异是这个方法的全部灵魂:

  • 低置信区间(p 在 0.5 附近):AFML 映射比线性映射更保守。p=0.55 时线性给 0.10 仓位,AFML 给约 0.08。概率刚过半的信号,统计上和抛硬币难以区分,仓位就该趴着。
  • 高置信区间(p > 0.7):AFML 映射加速抬升,比线性更快接近满仓。因为 zz 的分母 p(1p)\sqrt{p(1-p)} 在 p 靠近 1 时缩小,同样的概率增量换来更大的显著性增量。

一句话:这个映射内置了「统计显著性门槛」——它对「勉强过线」的信号天然吝啬,对「真正有把握」的信号才慷慨。这恰好是人类交易员最难执行的纪律。

实测:同一个预测器,三种下注方式#

映射函数的形状说服不了人,净值曲线才能。我做了一个受控实验:

  • 1500 个交易日,每天有一个交易机会,结果是 +1% 或 -1%(简化的单位风险)
  • 真实胜率是一条缓慢变化的曲线(0.35 到 0.72 之间摆动)——市场有时给机会,有时不给
  • 模型的预测概率 = 真实胜率 + 观测噪声(标准差 0.05)——校准良好但有噪声,这是对现实中一个体面模型的合理近似

三种执行方式同场竞技:

  1. 全押:p > 0.5 就满仓
  2. 高置信过滤:p > 0.6 才满仓(元标注文章里的过滤思路)
  3. 概率仓位:AFML 映射 + 0.1 步长离散化,p ≤ 0.5 空仓

三种下注方式的净值对比

执行方式Sharpe最大回撤
全押 p>0.51.83-20.2%
过滤 p>0.6 满仓1.79-7.8%
概率仓位(AFML 映射)1.95-1.8%

读这张表的正确姿势:

Sharpe 的提升是次要的(1.83 → 1.95,来自低置信期减少暴露),回撤的压缩才是主角(-20.2% → -1.8%,一个数量级)。原因不复杂:全押策略的深回撤全部发生在真实胜率跌破 0.5 的区间——模型的概率输出其实已经在预警(p 掉到 0.5 附近),但二值化执行把这个预警信号删掉了。概率仓位在这些区间自动把暴露降到接近零,它没有比全押策略「更会预测」,它只是没有装作没看见模型自己的犹豫

高置信过滤(p>0.6)是个中间态:它砍掉了低置信交易,但保留了「过线就满仓」的悬崖式执行——p=0.61 和 p=0.85 拿到同样的仓位,回撤改善就停在 -7.8%。

工程细节一:离散化,别让小数点变成手续费#

概率是连续的,模型每天的输出会在小数点后两三位抖动。如果仓位严格跟随概率,你会每天做一堆 0.83 → 0.81 → 0.84 的微调——每一次都是真实的交易成本,而这些抖动几乎全是噪声。

AFML 的解法朴素到令人安心:把仓位按固定步长取整。

def discretize(m, step=0.2):
    """仓位离散化:只有跨过半个 step 才真正调仓"""
    return np.round(m / step) * step
python

在模拟的概率序列上实测:

连续仓位 vs 离散化仓位

step=0.2 的离散化把 250 日累计换手从 11.7 压缩到 7.6(1.5 倍),而仓位曲线的宏观形状——什么时候重仓、什么时候轻仓——完全没变。被过滤掉的全是概率噪声引起的碎步调仓。

step 的选择是个成本-精度权衡:步长越大换手越省,但仓位对置信度变化的响应越迟钝。0.1-0.25 是常见区间;高换手成本的市场(比如 A 股的印花税 + 冲击成本)应该取大值。

工程细节二:并发信号的平均法#

真实管线里还有一个绕不开的问题:同一个标的上,多个信号同时存活

CUSUM 过滤器每隔几天触发一个事件,每个事件经三重障碍标注要持有 5-20 天——前一个信号还没结算,后一个信号又进场了。每个信号都有自己的方向和概率,此刻的总仓位听谁的?

AFML 的答案:取当前所有活跃信号的平均

def average_active_bets(signals, t):
    """signals: [(t_start, t_end, size), ...],size 已含方向正负
    返回 t 时刻的聚合仓位"""
    active = [s for (t0, t1, s) in signals if t0 <= t <= t1]
    return np.mean(active) if active else 0.0
python

并发信号与聚合仓位

这个平均法有两个被低估的好处:

  1. 仓位天然平滑。新信号进场时,聚合仓位从 N 个信号的均值变成 N+1 个的均值——移动是渐进的,不会因为单个信号跳一根悬崖。老信号结算离场同理。
  2. 多空信号自动对冲。如果模型在趋势末端开始输出反向信号,聚合仓位会先降到零再转向——这比任何人工设计的「趋势衰竭退出规则」都自然,因为它直接来自模型置信度的结构。

前提警告:一切建立在校准之上#

整篇文章有一个不能省略的脚注,我把它放大写在这里:

Bet Sizing 假设概率是校准的——模型说 70%,长期看真的有 70% 的次数是对的。

而大多数分类器的原始输出不满足这一点。随机森林的 predict_proba 系统性向 0.5 收缩,boosting 类模型经常向两端过度自信。把未校准的概率喂进映射函数,等于用一把刻度错乱的尺子量体裁衣——高置信区间的加速抬升会精确放大模型的过度自信,结果比全押更糟。

上线前的必做检查:

from sklearn.calibration import calibration_curve, CalibratedClassifierCV

# 1. 画校准曲线:预测概率 vs 实际频率,越贴对角线越好
prob_true, prob_pred = calibration_curve(y_test, p_hat, n_bins=10)

# 2. 不合格就套一层校准器(金融数据注意用 Purged CV 切分)
calibrated = CalibratedClassifierCV(base_model, method='isotonic', cv=purged_cv)
python

这和元标注文章末尾的警告是同一条:仓位函数上线前必须验证概率校准,否则比不用更糟。Bet Sizing 把这个要求从「建议」升级成了「地基」。

A 股落地注记#

  • T+1 的仓位调整摩擦:离散化步长建议取大(0.2-0.25),因为当天卖出的部分资金次日才能再买入,碎步调仓的机会成本比 T+0 市场更高。
  • 不做空的截断:A 股个股默认仅多头,p < 0.5 的信号映射成空仓而不是空头——这会让聚合仓位的分布整体右偏,回测时注意基准对比应该用同样截断后的暴露。
  • 涨跌停的执行缺口:目标仓位算出来了但停牌/涨跌停买不进,实际仓位和目标仓位会出现持续偏离。回测里必须用「实际成交后的仓位」而不是「目标仓位」计算净值,否则高波动段的收益被系统性高估。

定位与边界#

Bet Sizing 在 AFML 管线中的位置是最后一环:bar 采样 → CUSUM 事件 → 三重障碍标注 → 元标注 → 概率仓位。它不产生 alpha——模拟里三种执行方式的收益来源完全相同——它做的是把有限的风险预算按置信度重新分配。

它救不了的东西:一级信号没有期望优势(概率仓位只会让你「精确地慢慢亏」)、概率严重未校准(放大灾难)、样本太少导致概率估计本身方差巨大(映射函数的输入就是噪声)。

它擅长的东西:把「模型犹豫时少下注」这条所有人都懂但没人执行的纪律,变成一行代码。


本文实验代码基于 numpy/scipy/matplotlib,模拟参数见文中说明。系列上一篇:元标注 Meta-Labeling:把方向和仓位拆成两个模型

概率仓位管理 Bet Sizing:把模型置信度变成头寸大小
https://blog.halo26812.eu.org/blog/bet-sizing-probability
Author halo
Published at 2026年7月30日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨