前两篇我们把波动率算清楚了,这篇回答一个更实际的问题:算出来之后,仓位怎么定?
多数人的仓位管理是拍脑袋:要么满仓,要么"感觉风险大就半仓"。波动率目标法(Volatility Targeting)给出一个机械规则:先定一个你能承受的目标波动率,再让持仓权重 = 目标波动率 / 当前波动率。波动翻倍,仓位自动减半。今天用回测验证它到底有没有用。
本文你将得到什么
- 波动率目标仓位的完整公式与 Python 实现(含无前视处理)。
- 与满仓持有的对照回测:累计收益、年化波动、最大回撤三维对比。
- 仓位序列的正确打开方式:平均仓位、仓位区间、空仓天数。
- 一个必须诚实面对的问题:这套方法什么时候会跑输?
一、方法:三行规则
text
1. 每天收盘后,算过去 20 日年化波动率 vol_t
2. 目标仓位 weight = min(目标波动率 / vol_t, 1.0)
3. 明天按这个权重持仓
三个细节决定回测是否可信:
- 用昨日波动率定今日仓位:今天是收盘后才知道 vol_t 的,所以 weight 必须 shift 一天,否则就是前视偏差(偷看未来)。
- clip 上限 1.0:波动率低时算出来的权重可能超过 100%,除非你想上杠杆,否则截断到满仓。
- 目标值因人而异:20% 是常见起点(约等于宽基指数的长期波动水平),保守投资者可取 10%~15%。
二、这个方法在赌什么
波动率目标法的收益来源是波动率聚集:高波动期往往伴随下跌(风险规避),此时减仓躲过部分回撤;低波动期往往是上涨的舒适区,此时满仓吃满。它不是免费的午餐------如果市场"高波动 + 大涨"(比如逼空行情),它会提前下车。这一点文末实测部分我们细说。
可运行代码
bash
pip install mairui pandas numpy
python
from mairui import Client
import pandas as pd
import numpy as np
LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073" # 演示证书,请换成你自己的正式证书
client = Client(LICENCE)
TRADING_DAYS = 244
TARGET_VOL = 0.20 # 目标年化波动率 20%
VOL_WIN = 20 # 波动率回看窗口
bars = client.stock_history("600519", "d", "n", st="20260701", et="20261012")
df = pd.DataFrame(bars).rename(columns={"t": "date"}).sort_values("date").reset_index(drop=True)
df["date"] = pd.to_datetime(df["date"])
df["log_ret"] = np.log(df["c"] / df["c"].shift(1))
# 波动率目标权重:用截至昨日的 20 日波动率定今日仓位(shift(1) 无前视)
df["vol_20d"] = df["log_ret"].rolling(VOL_WIN).std(ddof=1) * np.sqrt(TRADING_DAYS)
df["weight"] = (TARGET_VOL / df["vol_20d"]).clip(upper=1.0).shift(1)
# 两条净值线
df["ret_bh"] = df["log_ret"].fillna(0.0)
df["ret_vt"] = (df["weight"] * df["log_ret"]).fillna(0.0)
df["nav_bh"] = (1 + df["ret_bh"]).cumprod()
df["nav_vt"] = (1 + df["ret_vt"]).cumprod()
def max_drawdown(nav):
peak = np.maximum.accumulate(nav)
return ((nav / peak) - 1).min()
valid = df.dropna(subset=["weight"])
print(f"回测区间: {valid['date'].iloc[0].date()} ~ {valid['date'].iloc[-1].date()},"
f"共 {len(valid)} 个交易日\n")
for name, ret_col, nav_col in (("满仓持有", "ret_bh", "nav_bh"), ("波动率目标", "ret_vt", "nav_vt")):
ret = df.loc[valid.index, ret_col]
nav = df.loc[valid.index, nav_col]
print(f"{name}: 累计收益 {(nav.iloc[-1]-1)*100:+.2f}% | "
f"年化波动 {ret.std(ddof=1)*np.sqrt(TRADING_DAYS)*100:.2f}% | "
f"最大回撤 {max_drawdown(nav.values)*100:.2f}%")
print(f"\n仓位使用: 平均 {valid['weight'].mean()*100:.1f}% | "
f"区间 [{valid['weight'].min()*100:.1f}%, {valid['weight'].max()*100:.1f}%]")
运行后你会看到类似结果:
text
回测区间: 2025-10-22 ~ 2025-12-01,共 29 个交易日
满仓持有: 累计收益 +16.16% | 年化波动 41.83% | 最大回撤 -13.96%
波动率目标: 累计收益 +20.04% | 年化波动 26.87% | 最大回撤 -7.35%
仓位使用: 平均 57.0% | 区间 [47.5%, 100.0%]
(示例基于 50 个交易日的演示样本、29 个有效回测日测算,样本较短、单标的,结果为教学演示而非普遍规律;演示证书返回就近样例数据,申请正式证书可拉取全市场全量历史做更长样本验证。)
怎么读这组数字
- 风险砍半不止 :年化波动从 41.83% 降到 26.87%,最大回撤从 -13.96% 收窄到 -7.35%------回撤几乎减半。这是波动率目标法最稳定的效果:它首先是个风控工具,其次才谈收益。
- 收益反而更高是"运气好"的部分:本样本后段波动率走高、价格上行,减仓后仍吃到涨幅且躲过了部分深回撤。必须诚实说明:如果样本换成"高波动 + 单边上涨",波动率目标会因为仓位不满而跑输满仓------它买到的是更平稳的净值曲线,不是更高的收益。
- 仓位序列本身就是信号:平均仓位 57%、最低 47.5%------方法在持续提示"当前波动率约 40%,远超 20% 目标,只值得拿一半仓"。这个机械读数替代了"感觉风险大"的模糊判断。
三个容易踩的坑
- 忘了 shift(1):用当天的波动率定当天的仓位 = 前视偏差,回测会虚高。正确的信息集永远是"截至昨日收盘"。
- 波动率窗口与目标值要匹配:20 日窗口 + 20% 目标是中低频配置的组合;如果用 5 日窗口配 20% 目标,仓位会频繁剧烈摆动,交易成本吃掉全部好处。
- 单标的不等于组合:本例对一只股票做波动率目标,实战中通常作用于组合净值层面(对组合的已实现波动定总仓位),个股层面应配合权重分配使用------这正是后续文章的方向。
小结
波动率目标法用三行规则把"风险预算"变成了机械执行:本样本中回撤减半、波动下降 15 个百分点,代价是可能错过高波动期的部分涨幅。它回答了"算波动率有什么用"------这是波动率从指标变成决策的那一步。
下一篇做个反向验证:业界常说"低波动异象"------波动率低的股票长期反而跑赢高波动的。在样本数据里,低波组真的赢了吗?
下篇预告:【Python量化波动率实战 04】低波动异象:低波组真的跑赢高波组吗。
免责声明
本文仅为技术方法演示,不构成任何投资建议。文中示例数据仅用于教学,历史表现不代表未来收益。投资有风险,决策需谨慎。
代码与文档可参考 GitHub:github.com/MaiRuiApi