回测结果再漂亮,落地之前还要回答一个非常具体的问题:这一笔到底下多少。两个研究员可能对同一个信号给出几乎一样的 alpha 估计、一样的协方差矩阵、一样的组合权重,但只要一个用全 Kelly、一个用四分之一 Kelly,他们的最大回撤可能差出一倍以上、净值曲线完全不像同一个策略。组合构建(portfolio construction)回答了「相对权重如何分配」,头寸管理(position sizing)回答的是「绝对规模到底多大」——前者决定方向,后者决定幅度,两者乘起来才是真正进场的金额。
把这件事写成数学,是 Kelly 公式的几何增长率最大化、是波动率目标(vol-targeting)的杠杆倒推、是 CVaR 风险预算的一组约束。把这件事做成工程,则要在保证金(margin)规则、监管杠杆约束、客户资金条款、回撤断路器、再平衡频率、交易成本之间反复折中。本文不写「头寸管理就是 Kelly 公式」这种简化叙事,因为单纯按全 Kelly 上线的策略几乎一定会被现实打脸:Kelly 假设的是已知概率分布、无限可分的资本、忽略冲击成本;现实里这三件事一件都不成立。本文按从理论到工程的递进顺序铺开:先把 Kelly、Vol-Targeting、风险预算这三件「逻辑工具」讲清楚,再把回撤管理、保证金、心理偏差、再平衡这四件「工程现实」叠加进来,最后给出可以贴在墙上的上线 checklist。
代码示例使用 Python 3.11、numpy 1.26、pandas 2.2、scipy 1.11,所有数据为本地合成或可复现仿真。
风险提示:本文出现的所有头寸管理方法和公式只用于阐释方法论本身,不构成任何投资建议。现实中按 Kelly、Vol-Targeting 或风险预算公式直接出场的策略都假设了极强的统计平稳性,这种假设在真实市场中常常被打破。把任何示例代码搬到生产前,请独立校核胜率与赔率估计、波动率估计窗口、杠杆上限、保证金规则、回撤断路器与硬止损。
一、头寸管理在交易系统中的位置
把整条量化流水线展开看,头寸管理位于「组合构建 → 执行算法」之间的那一段窄缝里。上游是组合优化器输出的相对权重 w,下游是 OMS / EMS 的具体下单指令。它承担的任务有三个:把相对权重 w 乘上一个杠杆系数 L,得到名义敞口 Lw;按账户净值 NAV 把名义敞口翻成具体金额或股数;把这一组金额按风险预算、保证金规则、断路器一一过滤之后再下单。
这三个动作合起来才叫头寸管理。把任何一个单拎出来都不完整:只算 L 不看 NAV,会在大账户和小账户上得到完全不同的滑点;只看金额不看断路器,回撤期会被推到清盘线以下;只看断路器不看再平衡频率,账户会在动态杠杆和现实成交之间反复抽搐。
一点一、为什么相对权重不够
组合优化器的标准输出是 ∑|w_i| = 1 或 ∑w_i = 1 的相对权重。把它直接乘上账户净值就开仓,会面对几个非常具体的问题:
第一,风险尺度未定。同一组相对权重 w,在不同协方差矩阵 Σ 下的组合波动率 σ_p = √(wᵀΣw) 完全不同。不同时段、不同 regime、不同行业暴露下的 σ_p 可能从年化 6% 跳到年化 22%,相同账户下名义敞口完全相同,但波动率三倍差距,对应的尾部风险天差地别。
第二,Kelly 比例未定。组合的预期超额收益 μ_p 与方差 σ_p² 决定了几何增长意义下的最优杠杆 L* = μ_p / σ_p²。这个比例和 w 是分开求的:w 解决「方向」,L* 解决「幅度」。把幅度写进 w 里,会让组合优化和资金管理纠缠成一坨,调参时无从下手。
第三,约束分层。监管杠杆、券商保证金、内部风险预算、单策略上限,是层层叠加的硬约束。组合优化器一般只处理「行业、风格、单只」这一层,杠杆与保证金属于资金管理层。混在一起优化既不优雅,也很难调参。
把头寸管理抽出来作为独立一层,是工程上几乎所有成熟基金的默认做法。研究员只看 w,PM(portfolio manager,组合经理)和 risk team 决定 L 与一系列断路器;这种分层在 ops 上才能让责任边界清楚。
一点二、头寸管理的工程合同
把头寸管理写成一份函数签名,它的输入是:
- 上游相对权重 w_t(来自组合优化器);
- 当前账户净值 NAV_t;
- 当前协方差估计 Σ_t(来自风险模型);
- 当前累计盈亏曲线、回撤指标 DD_t;
- 当前可用保证金、监管杠杆约束 L_max;
- 风险预算配置 B(按因子、品种、时段切分);
- 信号的 Kelly 估计 f_t(来自策略元信息)。
输出是一份绝对金额向量 dollar_pos_t,可以直接转成股数、合约数或币数。中间需要执行的逻辑链条至少包括:
- 用 Vol-Targeting 或 Kelly 计算目标杠杆 L_t;
- 用风险预算 B 对 L_t·w_t 做按因子/品种/时段的硬上限裁剪;
- 用回撤断路器对 L_t 做向下打折;
- 用保证金与监管杠杆约束对 dollar_pos_t 做最后一次裁剪;
- 按再平衡频率与成本阈值,把 dollar_pos_t 与现有持仓 pos_{t−1} 合并成下单指令。
这五步没有先后顺序的争议——上游永远是「更软的目标」,下游永远是「更硬的约束」。把这五步写成五个独立函数、串成一条管道,是工程上唯一不容易翻车的写法。任何把 Kelly、Vol-Target、风险预算、断路器混在一个大循环里的实现,半年内一定会出 bug。
一点三、信号 → 头寸的耦合点
头寸管理对信号本身有要求,至少有两点必须事先沟通:
其一,信号必须能给出胜率与赔率估计,否则 Kelly 没法算。哪怕只是粗略的二项化估计(比如把「信号大于阈值」对应到一个胜率),也比直接拍一个杠杆要强。
其二,信号必须能给出预期持有期。日内信号、日频信号、周频信号,对应的协方差估计窗口、Vol-Target 平滑参数、再平衡频率完全不同。一个 60 天 EWMA 协方差估计在日频策略上是基本配置,在日内策略上则慢得离谱。
这两个耦合点要在策略立项时就讲清楚,不要等到上线前才补。研究员不愿意给的「胜率/赔率/持有期」估计,会在头寸管理这一层被一个粗暴的常数替代——而这个常数往往就是上线后表现衰减的源头。
二、Kelly 公式
John Kelly 在 1956 年的 A New Interpretation of Information Rate 把信号下注问题写成了几何增长率最大化。原文不长,结论简单:在已知胜率 p、赔率 b 的离散下注问题里,使账户对数收益期望最大的下注比例为
f* = (p·b − q) / b = p − q/b (其中 q = 1 − p)
这个公式被后来无数书写成「凯利公式」「最优下注比例」「凯利准则」,但它的核心只有一句:几何增长意义下的最优。算术增长意义下,下注越多期望越大;几何增长意义下,下注超过 2f* 长期一定破产。这两个意义的差别,是 Kelly 公式所有工程含义的源头。
二点一、离散 Kelly 的推导
考虑反复独立同分布的二项下注:每次以概率 p 赢得 b 倍下注金额,以概率 q = 1 − p 输掉本金。把每期对数收益的期望写出来:
G(f) = p · ln(1 + b·f) + q · ln(1 − f)
对 f 求导并令其为零:
dG/df = p·b / (1 + b·f) − q / (1 − f) = 0
⇒ p·b·(1 − f) = q·(1 + b·f)
⇒ f* = (p·b − q) / b
二阶导数为负,所以 f* 是最大值。代入得到对数增长率上限 G(f) = p·ln(1 + b·f) + q·ln(1 − f*),这就是「在当前赔率下的可持续年化增长率」。
下面这张图把算术期望和几何增长率画在一起。
图里要看的有四个点。其一,橙色直线是算术期望 E[ΔW] = f·(p·b − q),它是单调上升的——下注越多期望越大,没有内部最优。其二,绿色曲线是几何增长 G(f),存在严格的内部最优 f* = 0.10。其三,蓝色虚线标出的 ½ Kelly 比 f* 增长率略低,但回撤显著更小,是工程上的默认选择。其四,红色点标出的 f ≈ 2f* 是「破产临界」——超过这个比例,G(f) < 0,长期一定破产,哪怕算术期望仍然为正。
第四点是 Kelly 的灵魂。算术期望和几何增长不是同一回事,长期复利不看算术,只看几何。这就是为什么「赌的越大期望越大」是错的——它把算术混淆成了几何。
二点二、连续 Kelly 与 GBM
把离散 Kelly 推广到连续时间下的几何布朗运动(geometric Brownian motion,GBM),给出更常用的「连续 Kelly」形式。设资产对数收益服从
dS_t / S_t = μ dt + σ dW_t
把杠杆 L 放在资产上,组合的对数收益变成
d(ln V_t) = (L·μ − ½·L²·σ²) dt + L·σ dW_t
期望对数增长率 g(L) = L·μ − ½·L²·σ²,对 L 求导得
L* = μ / σ²
最大对数增长率为 g* = μ² / (2σ²) = ½ · SR²,其中 SR = μ/σ 是夏普率(Sharpe ratio)。这个结果非常干净:全 Kelly 杠杆等于夏普率除以年化波动率,最大对数增长率是夏普率平方的一半。
把它换成更直观的数字:年化夏普 1.0、波动率 15% 的策略,全 Kelly 杠杆是 1/0.15 = 6.67 倍。年化夏普 2.0、波动率 10% 的策略,全 Kelly 杠杆是 2/0.10 = 20 倍。这两个数字立刻能让人理解为什么没人真的跑全 Kelly——杠杆 6 倍以上,单日 3σ 跌幅就能造成接近 30% 的回撤。
二点三、多资产 Kelly
多资产情形下,Kelly 公式推广为
L* = Σ⁻¹ μ
这里 μ 是 N 维超额收益向量,Σ 是 N×N 协方差矩阵,L* 是 N 维「杠杆向量」(每只资产的全 Kelly 头寸占比)。它和均值方差最优组合的形式几乎一样:
w_MV(λ) = (1 / 2λ) · Σ⁻¹ μ
只要把风险厌恶系数 λ 设为 ½,最优 MV 组合就等于全 Kelly 组合。这是 Kelly 与 Markowitz 的内在等价:对数效用函数的二阶 Taylor 展开就是均值方差。
工程上,这给了一个非常实用的桥接:组合优化器输出的相对权重 w,可以乘上一个全 Kelly 总杠杆 ‖Σ⁻¹μ‖,得到全 Kelly 头寸;也可以乘上分数 Kelly 系数(如 0.25 或 0.5),得到分数 Kelly 头寸。这就把组合方向和资金幅度自然解耦。
二点四、分数 Kelly 的工程动机
理论上 f* 给出最大几何增长率,工程上几乎没有人用 f*。常见的做法是用 ¼ Kelly 到 ½ Kelly。这个看似「保守」的选择背后有四个非常硬的工程理由。
第一,μ 与 Σ 估计有误差。Kelly 公式假设 p、b、μ、σ 已知,但工程上这些都是估计量,且估计误差结构性偏向乐观——研究员选择上线的策略往往是「样本内表现最好的」,对应的 μ 估计有强烈的选择性偏差(selection bias)。MacLean、Thorp、Ziemba 在 The Kelly Capital Growth Investment Criterion 里有专门一章讨论这件事,结论是 ½ Kelly 在估计误差为 ±50% 时仍能获得 75% 以上的最优增长率,但回撤减少一半以上。
第二,回撤幅度对 f 极其敏感。在 GBM 下,杠杆 L 时的最大回撤期望大致正比于 L²。也就是说从 ½ Kelly 升到全 Kelly,回撤期望会扩大 4 倍。从工程角度,4 倍的回撤意味着完全不同的客户体验、完全不同的赎回压力、完全不同的清盘风险。
第三,Kelly 假设无限可分资本。现实里有最小手数、保证金、合约单位、整数约束,下注比例 f 不是连续的。在小账户上,f* = 0.10 可能根本下不了一手。
第四,Kelly 假设忽略冲击成本。Kelly 把交易成本设为零,现实里杠杆越高换手越多,冲击成本随杠杆超线性增长。这一项让 f* 在工程上必须再次向下打折。
把这四条合起来:½ Kelly 不是保守,而是对模型不确定性、估计误差、可分性、成本结构的综合定价。它是工程上 Kelly 公式的「正确实现版本」。
二点五、Python:Kelly 计算器
下面这段代码实现一个统一的 Kelly 计算器,覆盖离散、连续、多资产三种形态。
import numpy as np
def kelly_discrete(p: float, b: float) -> float:
"""离散二项 Kelly:p 胜率,b 赔率(赢得 b 倍本金)。"""
if not (0 < p < 1):
raise ValueError("p must be in (0, 1)")
if b <= 0:
raise ValueError("b must be positive")
f_star = (p * b - (1 - p)) / b
return max(0.0, f_star)
def kelly_continuous(mu: float, sigma: float) -> float:
"""连续 Kelly:mu 年化超额收益,sigma 年化波动率。"""
if sigma <= 0:
raise ValueError("sigma must be positive")
return mu / (sigma ** 2)
def kelly_multivariate(mu: np.ndarray, cov: np.ndarray,
ridge: float = 1e-6) -> np.ndarray:
"""多资产 Kelly:mu 超额收益向量,cov 协方差矩阵。"""
n = len(mu)
cov_reg = cov + ridge * np.eye(n)
return np.linalg.solve(cov_reg, mu)
def fractional_kelly(f_full: float | np.ndarray,
fraction: float = 0.5,
leverage_cap: float = 3.0) -> float | np.ndarray:
"""分数 Kelly:把全 Kelly 头寸按 fraction 缩放并裁剪到上限。"""
f_scaled = fraction * np.asarray(f_full)
return np.clip(f_scaled, -leverage_cap, leverage_cap)
if __name__ == "__main__":
print("Discrete: f* =", kelly_discrete(p=0.55, b=1.0))
print("Continuous: L* =", kelly_continuous(mu=0.10, sigma=0.15))
rng = np.random.default_rng(42)
n = 5
mu = np.array([0.08, 0.06, 0.05, 0.04, 0.03])
A = rng.standard_normal((n, n))
cov = A @ A.T / n + 0.1 * np.eye(n)
L = kelly_multivariate(mu, cov)
print("Multivariate full Kelly:", np.round(L, 3))
print("Half Kelly capped:", np.round(fractional_kelly(L, 0.5, 1.0), 3))代码里有三个细节值得停一下。其一,kelly_multivariate
用 np.linalg.solve 而不是
np.linalg.inv,并加了一个 ridge 项
1e-6 * I:这是工程上对协方差矩阵病态的最小防御,避免
condition number
极大时方向被噪声放大。其二,fractional_kelly
默认 fraction = 0.5、leverage_cap =
3.0:这两个数字不是公理,要根据策略波动率特征自定义,但「先打折再裁剪」的顺序是固定的。其三,kelly_discrete
在 f_star < 0 时返回 0:负 Kelly
意味着应该反向下注,这种语义在工程实现里通常用「不下注」表达,反向头寸由信号方向直接给出,不在
Kelly 计算器内部处理。
二点六、Kelly 在量化里的真实位置
总结这一节的工程结论:Kelly 公式不是「最优下注规则」,而是「头寸幅度上限的理论参考」。实际上线的策略,几乎不会按 Kelly 直接下注,而是把 Kelly 作为「最大可能杠杆」的上限警戒线——任何让实际杠杆超过 50% Kelly 的决策,都需要风控团队签字。这种用法把 Kelly 从一个「最优解公式」变成了一个「红线检查工具」,是它在量化里最稳健的存在方式。
三、波动率目标
Vol-Targeting 是另一个广为流行的头寸管理方法,思路比 Kelly 更朴素:把组合的实现波动率(realized volatility)锚定在一个目标值 σ* 上,让杠杆 L 反向跟随波动率变化。
三点一、目标波动率倒推杠杆
设组合的当前波动率估计为 σ̂_t,目标年化波动率为 σ*,则目标杠杆为
L_t = σ* / σ̂_t
逻辑很简单:当市场平静、σ̂_t 下行时,加杠杆把 σ̂_t·L_t 拉回到 σ*;当市场动荡、σ̂_t 上行时,减杠杆把 σ̂_t·L_t 拉回到 σ。组合的实现波动率被强制压在 σ 附近,不随市场 regime 大幅漂移。
下面这张图把一年内的 σ̂(t) 和 L(t) 画在同一时间轴上。
图里值得停的细节有三个。其一,σ̂(t)(橙线)不是平滑变化,市场冲击日附近会突然飙到 28%,这正是 vol-targeting 要处理的场景。其二,L(t)(蓝线)和 σ̂(t) 几乎完美反相位:σ̂ 高时 L 低,σ̂ 低时 L 高。其三,红色虚线标出的 L_max = 3.0 是杠杆上限,平静期 L 顶到上限就不再上调——这条线避免了「波动率压缩期」的过度杠杆,也是 vol-targeting 工程实现里最容易被忘掉的细节。
三点二、波动率估计
公式简单,估计 σ̂_t 才是工程难点。常见的三种估计器:
第一种是滚动方差:取最近 N 天对数收益,计算样本标准差再年化。N = 20 得到月度波动率,N = 60 得到季度波动率。优点是简单、易解释;缺点是对历史窗口内的所有数据等权处理,对最近的市场变化反应慢。
第二种是 EWMA(exponentially weighted moving average,指数加权移动平均):用衰减因子 λ ∈ (0, 1) 给近期数据更高权重,递推关系是
σ̂_t² = λ · σ̂_{t−1}² + (1 − λ) · r_t²
RiskMetrics 文档建议日频用 λ = 0.94。EWMA 对 regime 切换的反应快,是机构 vol-targeting 的默认选择。
第三种是 GARCH(1,1):
σ̂_t² = ω + α · r_{t−1}² + β · σ̂_{t−1}²
α + β 接近 1 时表现最好,但参数估计本身需要数百个样本,对短窗口策略不实用。GARCH 在跨日策略和宏观对冲基金里更常见。
工程上一个常被忽视的细节:vol-targeting 不能用「未来波动率预测」,只能用「历史波动率估计」。把任何用到当日或未来收益的数据塞进 σ̂_t,会引入前视偏差(lookahead bias),回测结果会显著偏好。每一笔 σ̂_t 必须严格使用截止 t-1 的数据。
三点三、目标波动率的常见数值
实务里 σ* 不是随便定的,要看资金性质和监管要求。几个常见的锚点:
| 资金类型 | σ* 参考值 | 工程含义 |
|---|---|---|
| 公募权益基金 | 12% – 18% | 接近大盘指数波动 |
| 多策略对冲基金 | 6% – 10% | 标准 60/40 组合等价 |
| 趋势跟踪 CTA | 12% – 20% | 高波动追求峰值收益 |
| 风险平价基金 | 8% – 10% | 多资产长期复利目标 |
| 量化股票中性 | 4% – 8% | 低 beta、追求稳定 |
| 高频做市 | 1% – 3% | 日内回归、短持有期 |
这张表只是参考,重点在「定 σ* 不是定一个数字,是定一个产品定位」。客户买的是 8% 波动产品,跑成 15% 是事故;监管要求杠杆不超过 3 倍,σ* 设到 30% 是违规。σ* 的选择是产品、监管、风控三方协商的结果,不是研究员一个人的事。
三点四、Vol-Target 的稳态性质
把 vol-targeting 当成一个动态系统看:组合实现波动率 σ_real(t) = L(t) · σ̂(t)。如果 σ̂_t 是无偏估计且变化连续,那么 σ_real(t) ≈ σ*,组合波动率被锚定在目标值。这种「波动率稳态」给了 vol-targeting 三个工程上的好处:
第一,夏普率被自动放大。在波动率聚集(volatility clustering)市场里,大跌往往跟随高波动,连续大跌的概率比正态假设下高得多。Vol-targeting 在大跌前已经因高 σ̂ 降了杠杆,回避了大跌的尾部,长期看夏普率比固定杠杆策略高 0.2 到 0.3。
第二,最大回撤被压缩。Moreira 和 Muir 在 Volatility-Managed Portfolios 里实证发现,vol-targeting 把美股市场组合的最大回撤从 80% 降到 50%。背后机制是「高波动 → 低杠杆 → 小回撤」。
第三,风险预算可比性。不同时段、不同 regime 下的策略,因为波动率被锚定在 σ,可以直接按 σ 切风险预算,不用再做尺度归一化。这一点对多策略基金的 risk budgeting 极其方便。
但这三个好处是有前提的:σ̂_t 必须是「波动率」的有效估计、波动率确实是聚集的、目标 σ* 不能高到撞上杠杆上限。这三个前提里任何一个不成立,vol-targeting 的优势就消失。
三点五、Vol-Target 的失效场景
Vol-Target 不是万能药。下面几个场景里它会失效甚至有害:
第一,纯均值回归短周期。在 1 小时以下的均值回归策略里,波动率聚集弱、信号本身高度依赖瞬时方差,vol-targeting 反而会把握得最有把握的机会过滤掉。
第二,杠杆上限触顶时。低波动期 σ̂ 极小,理论 L 很大,但被 L_max 截断,组合会无法达到 σ*——这种「天花板效应」让 vol-target 退化成固定杠杆,丧失自适应性。
第三,急剧 regime 切换初期。从平静市切到危机市的第一两天,σ̂_t 还没反应过来,仍是低估值,对应的 L_t 仍然很高,正好踩在大跌当口。这个滞后是 vol-targeting 最被诟病的弱点。
第四,协方差结构漂移。组合包含多资产时,σ̂_t 是 √(wᵀΣw),但 Σ 的相关性结构本身在变。vol-target 只调整尺度,不调整方向,相关性飙升时(比如 2020 年 3 月)所有资产同跌,单纯降杠杆压不住组合波动。
工程对策:上面四个场景每一个都要单独配一道闸门。第一种用「策略级 vol-target 关闭开关」;第二种用「上限触顶报警」;第三种用「短窗口 σ̂_short 与长窗口 σ̂_long 取大」(即「保守估计」);第四种用「组合相关性监控 + 整体降仓断路器」。
三点六、Python:Vol-Target 滑动杠杆
下面这段代码实现一个 EWMA 估计器加 vol-target 的完整流水线。
import numpy as np
import pandas as pd
def ewma_vol(returns: pd.Series, lambda_: float = 0.94,
min_periods: int = 20, annualize: int = 252) -> pd.Series:
"""RiskMetrics 风格 EWMA 波动率估计,输出年化日频波动率。"""
sq = returns.fillna(0.0) ** 2
var = sq.ewm(alpha=1 - lambda_, adjust=False, min_periods=min_periods).mean()
return np.sqrt(var * annualize)
def vol_target_leverage(realized_vol: pd.Series, target: float = 0.10,
leverage_cap: float = 3.0,
floor: float = 0.0) -> pd.Series:
"""按 L = sigma_star / sigma_hat 计算目标杠杆并裁剪。"""
L = target / realized_vol.replace(0, np.nan)
return L.clip(lower=floor, upper=leverage_cap).fillna(floor)
def conservative_vol(returns: pd.Series, short_lambda: float = 0.94,
long_lambda: float = 0.97) -> pd.Series:
"""同时跑短/长窗口 EWMA 取大,避免 regime 切换初期低估。"""
s = ewma_vol(returns, short_lambda)
l = ewma_vol(returns, long_lambda)
return np.maximum(s, l)
def apply_vol_target(returns: pd.Series, target: float = 0.10,
leverage_cap: float = 3.0) -> pd.DataFrame:
"""完整流水线:返回每日 sigma_hat、L、加杠杆后的等价收益。"""
sigma_hat = conservative_vol(returns).shift(1)
L = vol_target_leverage(sigma_hat, target=target, leverage_cap=leverage_cap)
levered = L * returns
return pd.DataFrame({
"ret": returns,
"sigma_hat": sigma_hat,
"L": L,
"levered_ret": levered,
})
if __name__ == "__main__":
rng = np.random.default_rng(0)
T = 1000
sigma_path = 0.01 + 0.005 * np.sin(np.arange(T) / 60)
eps = rng.standard_normal(T) * sigma_path
rets = pd.Series(eps, name="ret")
out = apply_vol_target(rets, target=0.10, leverage_cap=3.0)
print(out.tail())
print("Realized vol after vol-target (annualized):",
out["levered_ret"].std() * np.sqrt(252))这段代码里的关键是 apply_vol_target 函数中的
.shift(1):当日的杠杆只能用截止昨日的数据估的
σ̂,这条 shift 是 vol-targeting
工程实现里最容易写错的一行。少一个
shift,回测就引入前视偏差,所有指标都会偏好。
四、风险预算
风险预算(risk budgeting)的思路是把「总风险」当成一个有限资源,按因子、品种、时段切成份额分给不同的子策略或子组合。它和 vol-targeting 的关系是:vol-target 决定总尺度,风险预算决定总尺度内的切分。
四点一、按因子切预算
把组合分解到 K 个因子上:r_p = β₁·f₁ + β₂·f₂ + … + β_K·f_K + ε。每个因子的边际风险贡献(marginal contribution to risk,MCR)和总风险贡献(total contribution to risk,TCR)分别为
MCR_k = ∂σ_p / ∂β_k
TCR_k = β_k · MCR_k
σ_p = Σ TCR_k + ε_risk
如果定义因子 k 的风险预算 b_k 满足 ∑ b_k = 1,那么风险预算约束写成
TCR_k / σ_p ≤ b_k ∀ k
工程含义:单个因子吃掉的总风险份额不能超过预算。比如「价值因子的风险贡献不超过 30%」「动量因子的风险贡献不超过 25%」「特异风险(idiosyncratic risk)不超过 20%」。这种切分让组合即使在某个因子方向上有强信号,也不会把所有风险都压在那一个方向上,避免「单因子崩溃 → 组合崩溃」的尾部风险。
四点二、按品种与时段切预算
按品种切是更直观的版本:
Σ_{i ∈ asset_class_a} TCR_i ≤ b_a · σ_p
实务里典型的切法:股票、债券、商品、外汇、加密各占 20%;或者股票内部按行业切(科技、金融、消费、医疗、能源各 20%)。这种切分的目的不是数学最优,而是杜绝隐性集中。一个看起来分散在 100 只股票上的组合,可能 70% 的风险都集中在科技板块。按行业切预算就是把这种隐性集中明面化。
按时段切预算是更高阶的玩法:把交易日切成 N 个时间桶(亚洲早盘、欧洲午盘、美东开盘、收盘前),每个时段的风险贡献单独限额。这种做法在跨时区的全球宏观对冲基金里常见,目的是避免「某个时段的流动性事件吃掉全天的风险预算」。
四点三、CVaR 预算
CVaR(conditional value at risk,条件风险价值)预算把「风险」从波动率换成尾部期望损失。CVaR 在置信水平 α 下定义为
CVaR_α(L) = E[L | L ≥ VaR_α(L)]
其中 L 是损失。CVaR 比 VaR 更稳健(VaR 不次可加,CVaR 次可加),更适合作风险预算。CVaR 预算的形式和方差预算一样:每个子组合贡献的 CVaR 份额不超过预算。
CVaR 的工程难点在估计:需要足够的尾部样本。日频策略要至少 5 年数据才能估出 99% CVaR;分钟频策略可以用更短窗口但需要重采样。Rockafellar 和 Uryasev 在 Optimization of Conditional Value-at-Risk 里给出了一个非常实用的凸优化形式:
CVaR_α(L) = min_t { t + (1/(1−α)) · E[max(L − t, 0)] }
这个形式让 CVaR 优化变成线性规划(把期望换成历史样本平均),在 cvxpy 里几行就能写出来。
四点四、Python:风险预算分解
下面这段代码实现一个完整的因子风险贡献分解器和预算检查器。
import numpy as np
import pandas as pd
def factor_risk_decomposition(weights: np.ndarray,
factor_loadings: np.ndarray,
factor_cov: np.ndarray,
specific_var: np.ndarray) -> dict:
"""
weights: 资产权重 N
factor_loadings: N x K 因子暴露矩阵 B
factor_cov: K x K 因子协方差
specific_var: N 维特异方差
返回每个因子的 TCR 与特异风险的 TCR。
"""
B = factor_loadings
F = factor_cov
D = np.diag(specific_var)
cov = B @ F @ B.T + D
sigma_p = float(np.sqrt(weights @ cov @ weights))
factor_betas = B.T @ weights
factor_var = factor_betas @ F @ factor_betas
specific_var_p = float(weights @ D @ weights)
tcr_factors = (factor_betas * (F @ factor_betas)) / sigma_p
tcr_specific = specific_var_p / sigma_p
return {
"sigma_p": sigma_p,
"tcr_factors": tcr_factors,
"tcr_specific": tcr_specific,
"share_factors": tcr_factors / sigma_p,
"share_specific": tcr_specific / sigma_p,
}
def check_risk_budget(decomp: dict, budget: dict) -> pd.DataFrame:
"""对照预算检查每个因子的份额是否超限。"""
rows = []
for i, share in enumerate(decomp["share_factors"]):
key = f"factor_{i}"
b = budget.get(key, 1.0)
rows.append({"key": key, "share": share, "budget": b, "ok": share <= b})
rows.append({
"key": "specific",
"share": decomp["share_specific"],
"budget": budget.get("specific", 1.0),
"ok": decomp["share_specific"] <= budget.get("specific", 1.0),
})
return pd.DataFrame(rows)
def historical_cvar(returns: np.ndarray, alpha: float = 0.95) -> float:
"""历史模拟法 CVaR(损失为正)。"""
losses = -np.asarray(returns)
var = np.quantile(losses, alpha)
tail = losses[losses >= var]
return float(tail.mean()) if len(tail) else float(var)
def cvar_budget_check(weights: np.ndarray, asset_returns: np.ndarray,
alpha: float = 0.95, total_cvar_budget: float = 0.05,
per_asset_budget: float = 0.02) -> dict:
"""对组合 CVaR 与每只资产 CVaR 贡献做预算检查。"""
pnl_total = asset_returns @ weights
cvar_total = historical_cvar(pnl_total, alpha)
contributions = []
for i in range(len(weights)):
single = asset_returns[:, i] * weights[i]
contributions.append(historical_cvar(single, alpha))
contributions = np.array(contributions)
return {
"cvar_total": cvar_total,
"contributions": contributions,
"total_ok": cvar_total <= total_cvar_budget,
"per_asset_ok": (contributions <= per_asset_budget).all(),
}
if __name__ == "__main__":
rng = np.random.default_rng(7)
N, K, T = 8, 3, 1000
B = rng.standard_normal((N, K)) * 0.5
F = np.diag([0.04, 0.02, 0.015])
spec = np.full(N, 0.03)
w = np.full(N, 1.0 / N)
decomp = factor_risk_decomposition(w, B, F, spec)
print("sigma_p =", round(decomp["sigma_p"], 4))
print("share factors =", np.round(decomp["share_factors"], 3))
print("share specific =", round(decomp["share_specific"], 3))
budget = {"factor_0": 0.4, "factor_1": 0.3, "factor_2": 0.2, "specific": 0.3}
print(check_risk_budget(decomp, budget))
factor_returns = rng.standard_normal((T, K)) * np.sqrt(np.diag(F))
spec_returns = rng.standard_normal((T, N)) * np.sqrt(spec)
asset_returns = factor_returns @ B.T + spec_returns
cvar = cvar_budget_check(w, asset_returns,
alpha=0.95, total_cvar_budget=0.05,
per_asset_budget=0.015)
print("CVaR total =", round(cvar["cvar_total"], 4),
"ok =", cvar["total_ok"])代码里 factor_risk_decomposition 用 TCR
切分总风险,check_risk_budget
是把切分结果对照预算做硬上限检查,cvar_budget_check
是同样的逻辑换成 CVaR
度量。三个函数串起来就是一个完整的风险预算检查器,可以在每次再平衡前跑一遍:超预算的因子就把对应权重压回来,超
CVaR 的资产就降仓。
四点五、风险预算的工程边界
风险预算听上去优雅,工程上最容易出问题的是「预算定多少」。常见的两种偏差:
第一种是预算太松。每个因子都给 40%,总和远超 100%,最终预算约束在很多场景下根本不起作用。这种情况下风险预算只是个摆设。
第二种是预算太紧。每个因子都给 5%,总和加起来 30%,剩下 70% 的风险预算无处安放,组合优化器找不到可行解,要么报错要么强制松弛约束。
正确的做法:预算总和略小于 100%,留 10% 至 20% 缓冲;每个因子的预算根据策略历史 TCR 中位数定,而不是拍脑袋。这一点和资本市场的 stress test 思路一致——预算是基于历史观测的合理切分,不是基于「希望的切分」。
五、回撤管理
回撤(drawdown)是头寸管理的最后一道防线。所有 Kelly、vol-target、风险预算的结果都是「预期意义下的最优」,但市场真的把头寸打到回撤极限时,必须有一套独立于预期最优的硬规则把仓位降下去。
五点一、最大回撤约束
最大回撤(maximum drawdown,MDD)的定义:
MDD_t = max_{s ≤ t} (NAV_s − NAV_t) / max_{s ≤ t} NAV_s
它是从历史峰值到当前的最大相对损失。工程上 MDD 是给客户和监管看的核心指标,任何一只私募基金的 PPM(私募发行备忘录)里都会写 MDD 的硬上限,比如「全周期 MDD 超过 20% 触发清盘评估」「单年 MDD 超过 15% 暂停申购」。
MDD 约束分硬约束和软约束。硬约束是「触发即强制行动」:达到 MDD = X% 时全仓平仓、暂停交易、走清盘流程。软约束是「触发后逐步降仓」:达到 MDD = X% 时按比例降杠杆、达到 MDD = 2X% 时降到 50%、达到 MDD = 3X% 时全平。
五点二、动态降仓的几种规则
最常见的动态降仓规则有三种。
第一种是线性降仓:
L_t = L_target · max(0, 1 − DD_t / DD_max)
DD_max 是预设的最大回撤阈值(比如 15%)。当 DD_t = 0 时 L_t = L_target,DD_t 达到 DD_max 时 L_t = 0,中间线性插值。这种规则简单、可解释,是 CTA 行业里的默认做法。
第二种是阶梯降仓:
DD_t < 5% → L_t = L_target
5% ≤ DD_t < 10% → L_t = 0.7 · L_target
10% ≤ DD_t < 15% → L_t = 0.4 · L_target
DD_t ≥ 15% → L_t = 0
阶梯比线性更稳健,避免在 DD 微小变化时频繁调仓产生交易成本。阶梯的数量和阈值要根据策略波动率和换手率定。
第三种是 CPPI(constant proportion portfolio insurance,固定比例组合保险):
cushion_t = NAV_t − floor
risk_exposure_t = m · cushion_t
floor 是事先设定的「保本下限」(比如初始资本的 90%),m 是杠杆倍数(通常 3 到 5)。当 NAV 越接近 floor,cushion 越小,风险敞口自动收缩。CPPI 在保本类产品里是标准做法,但它对路径依赖极强——一次大跳空可能让 cushion 直接跌破零,触发「gap risk」。
五点三、CPPI 的工程实现
import numpy as np
import pandas as pd
def cppi_path(returns: pd.Series, initial_nav: float = 1.0,
floor_pct: float = 0.85, multiplier: float = 4.0,
risk_free: float = 0.0, leverage_cap: float = 3.0) -> pd.DataFrame:
"""
在给定风险资产收益序列下回放 CPPI 净值路径。
"""
nav = [initial_nav]
floor = initial_nav * floor_pct
risky_share = []
for r in returns:
cur = nav[-1]
cushion = max(cur - floor, 0.0)
m = min(multiplier * cushion / cur, leverage_cap) if cur > 0 else 0.0
risky_share.append(m)
risky_ret = m * r
safe_ret = (1 - m) * risk_free
nav.append(cur * (1 + risky_ret + safe_ret))
return pd.DataFrame({
"nav": nav[1:],
"risky_share": risky_share,
"ret": returns.values,
}, index=returns.index)
def drawdown_curve(nav: pd.Series) -> pd.Series:
"""计算 nav 的滚动最大回撤序列。"""
peak = nav.cummax()
return (nav - peak) / peak
def linear_dd_throttle(returns: pd.Series, dd_max: float = 0.15,
leverage: float = 1.0) -> pd.DataFrame:
"""按当前回撤线性降低杠杆。"""
nav = (1 + returns).cumprod()
dd = drawdown_curve(nav)
L = leverage * (1 + dd / dd_max).clip(lower=0.0, upper=1.0)
levered_ret = L.shift(1).fillna(leverage) * returns
return pd.DataFrame({"ret": returns, "dd": dd, "L": L,
"levered_ret": levered_ret})
if __name__ == "__main__":
rng = np.random.default_rng(11)
rets = pd.Series(rng.standard_normal(252) * 0.012, name="r")
out = cppi_path(rets)
print("CPPI final NAV:", round(out["nav"].iloc[-1], 4))
print("Min risky share:", round(out["risky_share"].min(), 3))
throttle = linear_dd_throttle(rets, dd_max=0.10, leverage=1.0)
print("Max DD before throttle:",
round(drawdown_curve((1 + rets).cumprod()).min(), 4))
print("Max DD after throttle:",
round(drawdown_curve((1 + throttle["levered_ret"]).cumprod()).min(), 4))这段代码里 cppi_path 是 CPPI
的标准实现,linear_dd_throttle
是线性降仓器。两者都用 NAV 的 cummax 计算回撤,再据此算
L。linear_dd_throttle 中
L.shift(1)
这一行同样是为了避免前视偏差——当日的杠杆只能根据昨日回撤决定,不能根据当日回撤决定。
五点四、回撤管理的工程争议
回撤管理在量化圈里争议最大的点是:降仓后什么时候加回去。降的逻辑相对清楚(触发阈值即降),加的逻辑却没有共识。常见的几种做法:
第一种是 NAV 创新高再加回。最保守,但实务里几乎不可行:从 −15% 回撤恢复到新高可能要 6 到 12 个月,期间策略以低杠杆运行,错过大量机会。
第二种是 NAV 回到 −5% 时加回。中等保守,回撤恢复一半就开始加杠杆。
第三种是 按回撤恢复路径线性加回:DD = −15% 时 L = 0,DD = 0 时 L = L_target,中间线性。这和降仓规则对称,工程上最自然。
第四种是 直接基于信号强度加回:回撤后看信号 IC(information coefficient,信息系数),IC 恢复到历史中位数以上才加杠杆。这种做法把「加回时机」和「策略本身的健康度」绑定,是机构里更稳健的做法。
工程上没有标准答案,但降的速度永远要快于加的速度这条原则是稳定的。降仓是一锤子买卖,错了大不了少赚;加仓如果踩在二次回撤上,会把损失放大成不可逆的尾部事件。
六、资金管理与杠杆约束
到这里,所有「软目标」都讲完了。下一层是「硬约束」:监管杠杆、保证金、自有资金 vs 客户资金的差异。
六点一、保证金机制
保证金(margin)分两类:初始保证金(initial margin)和维持保证金(maintenance margin)。前者是开仓时必须存入的最低资金比例,后者是头寸维持期间账户净值不能跌破的最低比例。两者一起决定了账户的最大可用杠杆。
以美股 Reg T 规则为例:
- 现金账户:杠杆 1 倍。
- 保证金账户:日内杠杆 4 倍(pattern day trader),隔夜杠杆 2 倍。
- Portfolio Margin:根据组合整体风险计算保证金,杠杆可达 6 至 7 倍(高净值客户)。
期货账户:
- 期货合约的初始保证金一般是合约面值的 5% 至 15%,对应杠杆 6.67 至 20 倍。
- 维持保证金通常比初始保证金低 10% 至 20%,跌破触发 margin call。
加密资产:
- 现货账户:杠杆 1 倍(除非借贷)。
- 永续合约:交易所定杠杆,常见 1 至 100 倍,过度杠杆与高费率挂钩。
工程上不能假设「监管允许多少就能用多少」。Reg T 允许 4 倍日内,但券商内部风控可能只批 2.5 倍;交易所允许永续 100 倍,但实际运行 5 倍以上的策略半年内爆仓概率极高。真实可用杠杆 = min(监管杠杆, 券商杠杆, 内部风险杠杆)。
六点二、自有资金 vs 客户资金
自有资金(prop trading)和客户资金(asset management)的头寸管理逻辑差别非常大。
自有资金的特点:
- 不存在客户赎回压力,资金可以承受较深回撤;
- 风险预算由内部决定,不需要披露;
- 杠杆上限由公司风控政策决定,常见 5 至 10 倍;
- 几何增长率(Kelly)几乎是核心目标,因为这是公司利润的直接来源。
客户资金的特点:
- 存在赎回压力,回撤直接影响 AUM(assets under management,管理规模);
- 风险预算需要在 PPM、产品说明书里披露,事后追责严格;
- 杠杆上限由 PPM 写死,不可以越过;
- 算术增长率(即名义收益)权重往往超过几何增长率,因为「年度收益排名」决定 AUM 增量。
这两种差别直接体现在头寸管理参数上:自有资金的策略可以跑 ½ 到 1 倍 Kelly,客户资金的策略一般不超过 ¼ Kelly。自有资金 σ* 可以设到 15% 至 20%,客户资金 σ* 一般在 8% 至 12%。
不理解这个差别的研究员在两种环境之间切换时,往往做出错误参数选择:把自有盘的杠杆套到客户盘上,结果一次正常波动就吓飞客户;把客户盘的保守参数套到自有盘上,结果几年下来跑输公司基线收益。
六点三、监管约束
监管对杠杆和头寸有两类硬约束:
第一类是直接杠杆约束。UCITS(欧盟受规制基金)规定衍生品总敞口不超过 NAV 的 100%(用 commitment approach)或 200%(用 VaR approach)。CFTC(美国期货监管委员会)对 CPO(commodity pool operator,商品池经营者)的杠杆有 4.7:1 的约束(CFTC Rule 4.7 之外)。中国证监会对公募基金衍生品总名义价值不得超过基金净值 100%(股票型)或 50%(债券型)。
第二类是头寸限制。SEC 13F 大宗持仓披露门槛 1 亿美元,超过必须季度披露;CFTC 的大额头寸报告(Large Trader Report)门槛随合约不同;港交所对单一股票的大宗持仓 5% 必须披露。这些规则不是单纯的合规义务,它们直接决定了策略能跑的最大资金规模——超过披露门槛后,策略动作完全公开,alpha 几乎一定被市场吃掉。
六点四、保证金动态管理
保证金不是开仓那一瞬间的事,是每天甚至每分钟都在变。三个工程要点:
第一,保证金充足率监控。账户保证金比例 = 当前权益 / 维持保证金。一旦低于某个阈值(比如 1.2),就要预警;低于 1.0,券商会强平。工程上要在每分钟 mark-to-market(按市场价计算盯市权益)后立刻计算这个比例,触发预警就主动降仓,不要等券商强平。
第二,保证金浮动条款。期货合约在高波动期,交易所会临时上调保证金(比如 2020 年 4 月原油负价后 CME 多次上调原油保证金)。工程上要把保证金参数当作时变量,订阅交易所的保证金调整公告,自动重算可用杠杆。
第三,多账户保证金调度。一个机构往往有多个交易账户、多个 prime broker,保证金分布在不同账户。工程上要做实时调度:A 账户保证金紧张时,自动从 B 账户调资金过来,避免任何一个账户单点爆仓。这种调度系统通常和 OMS 绑死,实现复杂度极高,但对大资金不可省略。
六点五、给工程师的几条务实建议
把这一节收尾,给一份资金与杠杆管理的务实清单:
- 真实可用杠杆永远小于名义可用杠杆:监管允许、券商允许、内部风险,三者取最小。
- 保证金参数当时变量处理:订阅交易所公告,不要硬编码。
- 保证金充足率每分钟监控:低于阈值主动降仓,不要等强平。
- 自有盘和客户盘参数独立:σ*、Kelly 比例、回撤阈值都要分开标定。
- 监管披露门槛影响策略容量:策略容量上限要写在策略元信息里。
- 杠杆变更走审批流:研究员或 PM 不能擅自调整杠杆参数。
七、心理偏差与执行
讲到这里都还是数学和工程。但真正让头寸管理失效的,往往是人。研究员、PM、风控、运营,每个角色都有典型的心理偏差,每一种偏差都可能让纸面上完美的头寸管理在现实里失效。
七点一、强制止损与心理偏差
最常见的偏差是「损失厌恶下的止损推迟」。回撤到达 −10% 时,PM 会本能地想「再等一天看看」「下周可能反弹」,把硬止损推迟成软止损。这种推迟的代价:原本 −10% 的回撤变成 −18%,原本可控的风险变成清盘事件。
工程对策:止损必须由系统执行,而不是由人决定。把止损规则写进自动化脚本,触发即下单,不留人工决策空间。这种做法在 CTA 行业被叫做「systematic stops」,是把人从最危险的决策点上撤下来的必要措施。具体实现:
def hard_stop_check(nav: float, peak: float, threshold: float = 0.15) -> bool:
"""硬止损检查:当前回撤超过阈值,返回 True 触发清仓。"""
if peak <= 0:
return False
dd = (peak - nav) / peak
return dd >= threshold这段代码看起来像是不需要写的废话,但工程上必须有这么一个独立函数:止损检查不能埋在策略主循环里,要单独跑、单独报警、单独执行。把它和策略主逻辑解耦,是「让脚本替人决策」的工程基础。
七点二、纪律:参数变更走流程
第二种偏差是「参数微调的滑坡」。市场出现一次异常波动,PM 觉得「这次特殊」,把杠杆上限临时上调 20%;下次又异常,再上调 10%;半年下来,杠杆上限已经从原来的 3 倍变成 4.5 倍,整个风险曲线悄悄漂移。
工程对策:所有参数变更走变更流程。任何对 σ*、L_max、DD_max、Kelly fraction 的变更都要经过 commit、review、approve、deploy 四步,记录在变更日志里。这样的流程在熊市后回查时极其重要——可以精确知道「在什么时间因为什么原因把哪个参数调到多少」,避免参数蠕变。
七点三、回测信任阈值
第三种偏差是「回测过度信任」。一个回测夏普 2.5 的策略,PM 立刻按全 Kelly 上线;上线后实际夏普跌到 0.8,亏了三个月才停手。回测信任阈值的工程做法是:
- 回测夏普的 50% 是上线初期的工作假设。回测 2.5,实盘按 1.25 跑参数。
- 样本外验证至少 6 个月。前 6 个月用 ¼ 至 ½ 的目标杠杆运行,跟踪 IC、夏普、回撤;指标稳定后再加杠杆。
- 滚动回测一致性检查。把回测分成 3 至 5 段,每段单独算夏普、回撤;任何一段表现明显偏差,都要分析原因,不能简单平均。
把回测当成「给策略上线发的入场券」,而不是「策略表现的承诺」。这一字之差,是好多策略上线后失败的根源。
七点四、决策延迟与黑箱化
第四种偏差是「决策延迟」。市场出现极端事件时,PM 倾向于「等等再看」「需要更多信息」「先开个会讨论」。这种延迟在分钟级、秒级事件里足以让账户穿仓。
工程对策:关键决策黑箱化。把降仓、止损、断路器写进脚本,不依赖人的判断;把人的角色定位为「监控脚本是否正常运行」,而不是「在脚本之外做决策」。这听起来反直觉——交易员不就是要做决策吗?但事实是:当人在面对实时市场冲击时,决策质量普遍下降;让脚本接管这一时刻的决策,是机构级量化的标准做法。
七点五、心理偏差检查表
把这一节收尾,给一份针对常见心理偏差的工程检查表:
- 止损必须由系统执行:PM 不能手动越过止损规则。
- 参数变更走流程:commit、review、approve、deploy 四步缺一不可。
- 回测夏普打 50% 折扣:上线初期按打折后的杠杆运行。
- 样本外至少 6 个月:通不过样本外考验的策略不上正杠杆。
- 极端事件脚本接管:人监控脚本,不在脚本之外做决策。
- 每月复盘参数蠕变:检查所有头寸管理参数是否被悄悄调整过。
这六条都不是数学问题,都是工程纪律问题。但纪律决定了头寸管理在长周期里是否真的能稳定运行。
八、工程实现
最后一节回到具体的工程实现。前面七节给了所有概念和公式,这一节回答「怎么把它们写成一个可上线的系统」。
八点一、实时仓位调整管道
把所有头寸管理逻辑串成一条管道:
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
@dataclass
class PositionContext:
nav: float
peak_nav: float
weights: np.ndarray
cov: np.ndarray
sigma_target: float = 0.10
leverage_cap: float = 3.0
dd_max: float = 0.15
kelly_fraction: float = 0.5
factor_loadings: np.ndarray | None = None
factor_cov: np.ndarray | None = None
specific_var: np.ndarray | None = None
risk_budget: dict = field(default_factory=dict)
def step1_vol_target(ctx: PositionContext) -> float:
"""目标杠杆 = sigma_star / sigma_hat,并裁剪到 leverage_cap。"""
sigma_p = float(np.sqrt(ctx.weights @ ctx.cov @ ctx.weights))
if sigma_p <= 0:
return 0.0
L = ctx.sigma_target / sigma_p
return float(min(L, ctx.leverage_cap))
def step2_kelly_clip(L: float, ctx: PositionContext) -> float:
"""根据组合 Kelly 上限再次裁剪。"""
full_kelly = float(np.linalg.solve(
ctx.cov + 1e-6 * np.eye(len(ctx.cov)),
ctx.weights * 0 + ctx.weights.sum() / len(ctx.weights)
).sum())
cap = abs(full_kelly) * ctx.kelly_fraction
return float(min(L, max(cap, 0.1)))
def step3_dd_throttle(L: float, ctx: PositionContext) -> float:
"""根据当前回撤线性降低杠杆。"""
if ctx.peak_nav <= 0:
return L
dd = (ctx.peak_nav - ctx.nav) / ctx.peak_nav
factor = max(0.0, 1.0 - dd / ctx.dd_max)
return float(L * factor)
def step4_risk_budget(L: float, ctx: PositionContext) -> tuple[float, np.ndarray]:
"""因子风险预算检查与按比例缩放。"""
w = L * ctx.weights
if ctx.factor_loadings is None:
return L, w
B, F, D = ctx.factor_loadings, ctx.factor_cov, np.diag(ctx.specific_var)
cov = B @ F @ B.T + D
sigma_p = float(np.sqrt(w @ cov @ w))
if sigma_p <= 0:
return L, w
factor_betas = B.T @ w
tcr = (factor_betas * (F @ factor_betas)) / sigma_p
shares = tcr / sigma_p
over = []
for i, share in enumerate(shares):
b = ctx.risk_budget.get(f"factor_{i}", 1.0)
if share > b:
over.append(b / share)
if over:
scale = min(over)
L *= scale
w = L * ctx.weights
return L, w
def step5_margin_check(weights_dollar: np.ndarray,
margin_available: float,
margin_ratio: float = 0.25) -> np.ndarray:
"""保证金检查:所需保证金不能超过可用保证金。"""
required = np.abs(weights_dollar).sum() * margin_ratio
if required <= margin_available:
return weights_dollar
scale = margin_available / required
return weights_dollar * scale
def position_sizing_pipeline(ctx: PositionContext,
margin_available: float,
margin_ratio: float = 0.25) -> dict:
L = step1_vol_target(ctx)
L = step2_kelly_clip(L, ctx)
L = step3_dd_throttle(L, ctx)
L, w = step4_risk_budget(L, ctx)
dollar_pos = w * ctx.nav
dollar_pos = step5_margin_check(dollar_pos, margin_available, margin_ratio)
return {"L": L, "dollar_pos": dollar_pos}这条管道把前面七节的所有概念串成一个可调用的函数。step1
到 step5 严格按「软目标 →
硬约束」的顺序执行,每一步只做一件事,每一步的输入输出明确。这种分层是工程实现的核心:任何一步出问题,都可以单独定位、单独修复,不需要重写整条管道。
八点二、再平衡频率
再平衡频率的选择是工程上一个独立的优化问题。每天再平衡 vs 每周再平衡 vs 触发式再平衡,三种各有特点。
每天再平衡:实现简单、跟踪信号最紧、最大限度兑现 alpha;缺点是换手率高、交易成本占比大、对滑点敏感。
每周再平衡:成本低、换手稳定;缺点是信号衰减期内的 alpha 兑现率低,错过日内变动。
触发式再平衡:只在头寸偏离目标超过阈值(比如 5%)时再平衡;优点是成本与跟踪误差自适应,缺点是实现复杂、回测验证难、对参数敏感。
工程上的折中是:每天计算目标头寸,每天对比实际头寸,只在偏差超过阈值时下单。这种做法既保留了高频信号兑现,又控制了实际换手。阈值的选择和策略波动率绑定:高波动策略阈值 5% 至 8%,低波动策略阈值 2% 至 3%。
def rebalance_orders(target_dollar: np.ndarray,
current_dollar: np.ndarray,
dollar_threshold: float = 0.0,
pct_threshold: float = 0.03) -> np.ndarray:
"""
生成再平衡订单:只下偏差大于阈值的部分。
"""
diff = target_dollar - current_dollar
abs_diff = np.abs(diff)
rel_diff = abs_diff / (np.abs(current_dollar) + 1e-9)
mask = (abs_diff >= dollar_threshold) & (rel_diff >= pct_threshold)
orders = np.where(mask, diff, 0.0)
return orders这段代码同时检查绝对偏差和相对偏差。绝对偏差阈值用来过滤「金额太小不值得下单」的 case;相对偏差阈值用来过滤「占比太小忽略不计」的 case。两个阈值同时满足才下单。
八点三、成本权衡
每次再平衡都要付交易成本。把成本写进头寸管理的目标函数里,是工程上区分「合格策略」和「优秀策略」的分水岭。
成本主要分三类:
第一类是显式成本:佣金、印花税、监管费。可以精确计算,直接进 PnL。
第二类是滑点:执行价与决策价的差异。日频策略平均滑点 1 至 5 bp,分钟频策略 5 至 15 bp,秒频策略 15 至 50 bp。滑点和资金量成正比,大资金滑点高得多。
第三类是冲击成本:自己的下单对市场价格的影响。Almgren-Chriss 模型给出永久冲击与瞬时冲击的分解,详见后续 执行成本。
成本权衡的工程做法是「带成本的目标函数」:
maximize α^T w − λ_risk · w^T Σ w − λ_cost · |w − w_prev|^T c
其中 c 是每个标的的单位成本估计(包括佣金、滑点、冲击)。这个目标函数把交易成本作为线性惩罚项加进来,凸优化求解器(cvxpy、quadprog)都能直接处理。λ_cost 的标定要根据策略持有期和换手特征来定,常见数值在 0.5 到 2.0 之间。
八点四、实时监控与报警
最后一步是监控。所有头寸管理逻辑跑出来的结果,都要被实时监控;任何异常都要立刻报警。最少要监控的指标:
| 指标 | 报警阈值 | 处置动作 |
|---|---|---|
| 实时 σ_real / σ* | > 1.5 持续 5 日 | 自动按比例降杠杆 |
| 当前回撤 | > DD_max | 触发硬止损 |
| 保证金充足率 | < 1.2 | 主动降仓 |
| 杠杆 L | > L_max | 拒绝下单 |
| 因子 TCR 份额 | > 风险预算 | 缩放权重 |
| 日均换手 | > 历史 2 倍 | 检查信号异常 |
| 单只标的占比 | > 单只上限 | 缩到上限 |
| 总名义敞口 | > 监管杠杆 | 拒绝下单 |
每条都要在监控系统里做单独的 metric 和单独的 alert,不能合并成「一个综合指标」。理由很简单:综合指标在某一个维度异常时不会突变,而单独指标会立刻触发。早一秒触发,可能就是一次回撤事件和一次清盘事件的差别。
八点五、给工程师的最终 checklist
把这一节作为全文收尾,给一份可以贴在墙上的头寸管理上线 checklist:
- 头寸管理独立成层:组合优化器只输出 w,杠杆与断路器在独立模块里。
- 管道严格分层:vol-target → kelly clip → dd throttle → risk budget → margin check,五步不可调换。
- σ̂ 估计加 shift(1):当日杠杆只能用截止昨日的数据估,杜绝前视偏差。
- Kelly 实际跑 ½ 或 ¼:不要按全 Kelly 上线。
- σ* 由产品定位决定:不是研究员自由选择。
- DD_max 写进硬止损脚本:触发即清仓,不留人工决策空间。
- 保证金参数订阅交易所公告:不要硬编码。
- 风险预算预留 10% 至 20% 缓冲:避免预算太紧无可行解。
- 再平衡按阈值触发:高波动策略 5%-8%,低波动 2%-3%。
- 成本写进目标函数:λ_cost 单独标定,每季度复核。
- 参数变更走流程:commit → review → approve → deploy。
- 每个指标单独监控、单独报警:不要做综合指标。
- 样本外至少 6 个月才加杠杆:通不过样本外考验的策略不上正杠杆。
- 每月复盘参数蠕变:检查所有头寸管理参数是否被悄悄调整。
- 极端事件脚本接管:人监控脚本,不在脚本之外决策。
这十五条不是公理,是从机构量化的实际事故里压榨出来的最低线。每一条不通过,都不要上线;任何一条事后被发现绕过了,都要做事后复盘。
八点六、一句话总结
如果只能记住一句话:头寸管理的工程难度不在公式,而在让公式输出的「软目标」和现实的「硬约束」之间留出明确的边界、明确的分层、明确的报警。Kelly、Vol-Targeting、风险预算、回撤管理这些工具,每一个都对应一类风险来源;把它们叠在一起,构成一条「软到硬」的链条。链条任意一个环节缺失,前面的所有工作都会在某个尾部事件里被一次性抹掉。
九、参考文献
论文与书
- Kelly J L. A New Interpretation of Information Rate. Bell System Technical Journal, 1956, 35(4): 917-926.
- Thorp E O. The Kelly Criterion in Blackjack, Sports Betting, and the Stock Market. Handbook of Asset and Liability Management, 2006.
- MacLean L C, Thorp E O, Ziemba W T (eds.). The Kelly Capital Growth Investment Criterion. World Scientific, 2010.
- MacLean L C, Ziemba W T, Blazenko G. Growth versus Security in Dynamic Investment Analysis. Management Science, 1992, 38(11): 1562-1585.
- Markowitz H. Portfolio Selection. Journal of Finance, 1952, 7(1): 77-91.
- Moreira A, Muir T. Volatility-Managed Portfolios. Journal of Finance, 2017, 72(4): 1611-1644.
- Harvey C R, Hoyle E, Korgaonkar R, Rattray S, Sargaison M, van Hemert O. The Impact of Volatility Targeting. Journal of Portfolio Management, 2018, 45(1): 14-33.
- Maillard S, Roncalli T, Teïletche J. The Properties of Equally Weighted Risk Contribution Portfolios. Journal of Portfolio Management, 2010, 36(4): 60-70.
- Roncalli T. Introduction to Risk Parity and Budgeting. Chapman & Hall / CRC, 2013.
- Rockafellar R T, Uryasev S. Optimization of Conditional Value-at-Risk. Journal of Risk, 2000, 2: 21-41.
- Acerbi C, Tasche D. On the Coherence of Expected Shortfall. Journal of Banking & Finance, 2002, 26(7): 1487-1503.
- Black F, Perold A F. Theory of Constant Proportion Portfolio Insurance. Journal of Economic Dynamics and Control, 1992, 16(3-4): 403-426.
- Almgren R, Chriss N. Optimal Execution of Portfolio Transactions. Journal of Risk, 2001, 3(2): 5-39.
- Grinold R C, Kahn R N. Active Portfolio Management. 2nd ed. McGraw-Hill, 2000.
- Boyd S, Vandenberghe L. Convex Optimization. Cambridge University Press, 2004.
- Engle R F. Autoregressive Conditional Heteroskedasticity with Estimates of the Variance of United Kingdom Inflation. Econometrica, 1982, 50(4): 987-1007.
- Bollerslev T. Generalized Autoregressive Conditional Heteroskedasticity. Journal of Econometrics, 1986, 31(3): 307-327.
- RiskMetrics Group. RiskMetrics Technical Document. 4th ed. J.P. Morgan/Reuters, 1996.
- Kahneman D, Tversky A. Prospect Theory: An Analysis of Decision under Risk. Econometrica, 1979, 47(2): 263-291.
- de Prado M L. Advances in Financial Machine Learning. Wiley, 2018.
监管与文档
- Federal Reserve Board. Regulation T: Credit by Brokers and Dealers. 12 CFR Part 220.
- SEC. Pattern Day Trader Rule. FINRA Rule 4210.
- CFTC. Part 4 — Commodity Pool Operators and Commodity Trading Advisors.
- ESMA. UCITS Directive 2009/65/EC and Risk Management Process Guidelines.
- CME Group. Margin Methodology — SPAN and SPAN 2. CME Group documentation.
软件
- numpy 项目文档. https://numpy.org/doc/
- pandas 项目文档. https://pandas.pydata.org/docs/
- scipy 项目文档. https://docs.scipy.org/doc/scipy/
- cvxpy 项目文档. https://www.cvxpy.org/
- arch 项目(GARCH 估计)文档. https://arch.readthedocs.io/
导航:上一篇 【量化交易】风险模型:Barra、因子风险归因、压力测试 | 下一篇 【量化交易】执行成本:冲击、滑点、TCA
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【量化交易】绩效指标:Sharpe、Sortino、最大回撤、信息比率
策略好不好,不能只看一条净值曲线。本文系统梳理绩效指标的工程口径:年化收益与几何均值、Sharpe / Sortino / Calmar / Omega、最大回撤与水下曲线(underwater curve)、信息比率与跟踪误差、稳健 Sharpe(Newey-West、bootstrap、Deflated SR、PSR)、绩效归因与 GIPS 报表口径。给出一份可直接运行的 Python 工具箱。
【量化交易】量化交易全景:从信号到订单的工程链路
量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。
【量化交易】市场结构:交易所、做市商、暗池、ECN
系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。
【量化交易】市场微结构:订单簿、价差、流动性、冲击
系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。