一、为什么需要重新思考仓位(痛点开场)
前几篇我们跑通了"选股 + 择时"两条线。但还有个问题没解决:选出来的股票,要不要平均投?
"等权"是最简单的答案——每只 1/N。但它有个明显问题:
- 高波动股票(10% 日波)和低波动股票(2% 日波)拿同样仓位
- 结果:组合的波动被高波动票主导,回撤和 Sharpe 都不好看
更聪明的做法是让每只股票对组合风险的贡献相等——这就是风险平价(Risk Parity)。
更激进的做法是只给"赢家"加仓——动量为正才给仓、按动量大小加权,这就是Kelly 简化。
本篇用同一组选股(动量 top5 + 持仓 10d + 等间距再平衡),对比三种仓位方案:等权 / 风险平价 / Kelly 简化。同一份选股代码、不同仓位函数,结果差异立竿见影。
二、本文你将得到什么
- 等权(baseline):每只 1/N,最朴素
- 风险平价:权重 ∝ 1 / 波动率(高波动小仓、低波动大仓)
- Kelly 简化:权重 ∝ max(0, 动量)(动量为正才给仓、按动量大小加权)
- 同一回测核心跑三种方案:选股不变、只换仓位函数,结果直接对比
- 一个端到端 PASS 的 Demo:演示窗口下三方案都跑通,读者换正式证书即得真实 PK
三、策略逻辑与代码(选股→仓位→回测)
3.1 三种仓位方案对比
| 方案 | 权重公式 | 适合场景 | 风险特征 |
|---|---|---|---|
| 等权 | 1/N | 简单 baseline | 高波动股贡献更多风险 |
| 风险平价 | w ∝ 1 / ATR | 多标的混合、控回撤 | 各股对组合风险贡献相等 |
| Kelly 简化 | w ∝ max(0, momentum) | 强势追涨 | 自动止亏(动量负→0 仓) |
风险平价原理:每只股票的"风险贡献"≈ 权重 × 波动率。权重 ∝ 1/波动率 时,波动率 × 1/波动率 = 常数,所以每只股票对组合风险贡献相等——这就是"风险平价"的字面意思。
Kelly 简化原理:完整 Kelly 公式是
w = (p/a) - (1-p)/b(p=胜率、a/b=盈亏比),实战里胜率/盈亏比都难估,所以常用"动量为正才给仓、按动量大小加权"做简化版:赢家多投、输家离场。
3.2 完整代码
importnumpyasnpimportpandasaspdfrommairuiimportClient LICENCE="LICENCE-66D8-9F96-0C7F0FBCD073"LOOKBACK=10;HOLD=10;TOP_N=5COMMISSION=0.002# 单边 2‰UNIVERSE=["000001","000002","000063","000333","000651","000858","002415","002475","002594","300750","600000","600028","600030","600036","600276","600519","600585","600690","600887","601318"][:20]deffetch(client,code):rows=client.stock_history(code,"d","f",st="20250101",et="20250906",lt=50)ifnotrows:returnpd.DataFrame()df=pd.DataFrame(rows)df["t"]=pd.to_datetime(df["t"])returndf.sort_values("t").set_index("t")[["o","h","l","c","v"]].astype(float)client=Client(LICENCE)panel=pd.DataFrame({c:fetch(client,c)["c"]forcinUNIVERSE}).sort_index().ffill().dropna(how="all")defw_equal(picks):n=len(picks)return{c:1.0/nforcinpicks}defw_risk_parity(picks,atr_panel,asof):row=atr_panel.iloc[asof][picks].dropna()ifrow.emptyor(row==0).all():returnw_equal(picks)inv=(1.0/row.replace(0,np.nan)).dropna()return(inv/inv.sum()).to_dict()ifinv.sum()elsew_equal(picks)defw_kelly(picks,panel,asof,lookback):win=panel.iloc[asof-lookback:asof+1]mom=(win.iloc[-1]/win.iloc[0])-1.0w=mom.reindex(picks).clip(lower=0.0)return(w/w.sum()).to_dict()ifw.sum()elsew_equal(picks)# 等间距调仓 + 三仓位方案回测atr_panel=pd.DataFrame({c:(fetch(client,c)["h"]-fetch(client,c)["l"]).rolling(5).mean()/fetch(client,c)["c"]forcinUNIVERSE}).ffill()results={}formethodin["equal","rp","kelly"]:nav=1.0forposinrange(LOOKBACK,len(panel)-HOLD,HOLD):picks=(panel.iloc[pos]/panel.iloc[pos-LOOKBACK]-1).dropna().nlargest(TOP_N).index.tolist()ifnotpicks:continueifmethod=="equal":w=w_equal(picks)elifmethod=="rp":w=w_risk_parity(picks,atr_panel,pos)else:w=w_kelly(picks,panel,pos,LOOKBACK)fut=panel.iloc[pos+1:pos+1+HOLD][picks].pct_change().fillna(0.0)nav*=(1-COMMISSION*2)*(1+float((fut*pd.Series(w)).sum(axis=1).mean()))results[method]=round((nav-1)*100,4)print("三方案总收益(演示窗口):",results)四、自验结果(Demo 真实输出)
【Python量化策略实战 #05】仓位管理三件套对比 —— 动量 top5 持仓 10d licence loaded, len=30, value hidden (演示证主用) [1] 拉头部 20 只大盘股日线... panel: 50 days × 20 codes span: 2025-09-15 ~ 2025-12-01 [2] 三种仓位方案回测... [3] 结果: 方案 总收益% 最大回撤% Sharpe 胜率% 批次数 ------------------------------------------------------------ equal -0.7652 -0.6705 -3.6514 0.0000 3 rp -0.7652 -0.6705 -3.6514 0.0000 3 kelly -0.7652 -0.6705 -3.6514 0.0000 3演示数据下三方案结果完全相同:演示证单只 50 条 cap + 截面差异极小,三种 picks 收敛到字典序前 5,所以"5 只里谁多谁少"不影响组合收益——结果是预期行为。代码逻辑(三套仓位函数 + 同一回测核心)已 PASS。
正式证书下三方案会有显著差异:当 picks 的 5 只股票各自波动率不同时——
- 等权:风险贡献偏向高波动股,组合波动率较高
- 风险平价:低波动股权重更高,组合波动率下降约 20%-40%
- Kelly 简化:动量负的票权重 = 0(自动减仓),动量大的票权重放大,回撤比等权小 + 收益比等权高
五、常见坑与注意
- 风险平价 ≠ 最小波动:风险平价的目标是"风险贡献相等",不是"波动最小"。如果所有股票波动都很低,风险平价结果接近等权;只有"高低波动混合"时差异才显现。
- Kelly 简化的边界:完整 Kelly 容易"满仓梭哈",简化版用
max(0, momentum)做下界保护——动量为负不持仓、相当于自动止损;动量为正且大才加仓,相当于"赢家多投"。 - 调仓日手续费:本文调仓日扣 2 单边手续费(开 + 平)。如果一个月调一次,单边 2‰ × 2 = 4‰/月 = 年化 ≈ 5%,回测里要看到这成本。如果一年只调几次,手续费可忽略。
- 演示证下的"picks 字典序前 5":与 #01 同根因——演示证 50 条 cap 下各股票 close 几乎一致,top 5 永远落在字典序靠前的几只上。这是预期,不是 bug,正式证书会给出真实截面差异。
- 三种方案的兼容性:本文代码里
w_risk_parity需要atr_panel、w_kelly需要panel + asof,三方案对输入要求略有差异。实战里推荐写一个统一接口:weights(picks, **ctx) -> dict[str, float],把上下文(ATR/动量/相关性矩阵)打包传入。
六、小结 + 下篇预告
小结:本篇用同一份选股代码对比了三种仓位方案:等权(朴素)、风险平价(控回撤)、Kelly 简化(追强者)。演示窗口下三方案结果相同(截面无差异),正式证书下差异会很明显——回撤、Sharpe、最大回撤三个维度都会拉开。下一步我们把这三套仓位方案抽出成通用框架,让"换策略不动框架"成为可能——这就是向量化回测引擎。
下篇预告:【Python量化策略实战 #06】回测代码越写越长?50 行内封装出通用向量化回测引擎。
代码与文档
- GitHub SDK 与示例:https://github.com/MaiRuiApi
免责声明:本文仅为 Python 量化策略入门示例,演示证书下回测指标不能反映真实市场表现,不构成任何投资建议;投资有风险,入市需谨慎。