固定仓位大小会忽略市场环境。波动率目标化根据当前市场波动程度,自动调整每次下注的规模。
同一支策略,在平静的那几个月无聊到你想关掉它;在市场开始剧烈跳动的那个星期,回撤深得跟回测平均完全对不起来。信号没有变,变的是市场——而你每次下注的大小一直没变。
波动率目标化是让仓位大小跟市场的已实现波动反向联动:波动比目标高就投少一点,比目标低就投多一点,但有上限。
基本的 Type A 策略输出 1.0(做多)或 0.0(空仓)。Runner 在信号为 1.0 时把全部配置资金都投入。这样做有效,但有个隐性问题:不管市场是平静还是剧烈波动,策略下的注都一样大。
在低波动期,1% 的日线波动已经算大。在高波动期,1% 的波动只是噪声。如果策略在两种环境都用同样的仓位大小入场,实际的金额风险就会大幅波动——高波动期的回撤会远大于回测平均所暗示的水平。
Blave Agent 的波动率目标化分两步:
log_return = log(Close / Close.shift(1)) realized_vol = log_return.rolling(lookback).std() * sqrt(periods_per_year)
计算对数收益率的滚动标准差,再乘以 sqrt(periods_per_year) 换算成年化。结果就是年化已实现波动率——和 Sharpe ratio 使用的波动率单位相同。
默认值:lookback=720 根 K 线、periods_per_year=8760。这是为1h 周期策略校准的:720 根 = 30 天 × 24 小时;8760 = 24 × 365。
scale = (target_vol / realized_vol).clip(upper=vol_cap) scaled_signal = signal * scale
信号(1.0 = 全额做多)乘以波动率比值,上限为 vol_cap,得到小数仓位大小。
默认值:target_vol=0.30(年化 30%)、vol_cap=2.0(最大 2 倍杠杆)。
| 已实现波动(年化) | 目标波动 | 原始倍数 | 套用上限后 | 仓位大小 |
|---|---|---|---|---|
| 10% | 30% | 3.0× | 2.0×(上限) | 配置资金的 200% |
| 20% | 30% | 1.5× | 1.5× | 配置资金的 150% |
| 30% | 30% | 1.0× | 1.0× | 100%——基准 |
| 60% | 30% | 0.5× | 0.5× | 配置资金的 50% |
| 90% | 30% | 0.33× | 0.33× | 配置资金的 33% |
已实现波动在 30% 时,策略以全额运行。危机期间波动飙升到 90% 时,策略自动缩减至三分之一仓位。平静期波动降至 10% 时,放大到上限 2 倍。
默认值(lookback=720、periods_per_year=8760)是为1h 策略设定的。其他周期必须重新计算:
| 周期 | periods_per_year | lookback(30 天窗口) |
|---|---|---|
5min | 105,120(12 × 24 × 365) | 8,640 根 |
1h(默认) | 8,760 | 720 根 |
4h | 2,190 | 180 根 |
8h | 1,095 | 90 根 |
1d | 365 | 30 根 |
VOL_TARGETING = True
TARGET_VOL = 0.30 # 年化目标波动 30%
VOL_CAP = 2.0 # 最大 2 倍杠杆
VOL_LOOKBACK = 720 # lookback 窗口长度(根 K 线)
PERIODS_PER_YEAR = 8760 # 1h 周期用
def fetch_data(hdrs):
from lib.data import fetch_kline
from lib.strategy import add_realized_vol
df = fetch_kline(SYMBOL, INTERVAL, START, END, hdrs)
if VOL_TARGETING:
add_realized_vol(df, VOL_LOOKBACK, PERIODS_PER_YEAR)
return df
def compute_signals(df):
from lib.strategy import apply_vol_scaling
signal = pd.Series(np.nan, index=df.index)
# ... 信号逻辑 ...
if VOL_TARGETING:
return apply_vol_scaling(signal, df, TARGET_VOL, VOL_CAP)
return signal
| 参数 | 默认 | 意思 | 什么时候要改 |
|---|---|---|---|
TARGET_VOL | 0.30 | 年化目标波动 30% | 照你能接受的 MDD ÷ 2 |
VOL_CAP | 2.0 | 最大 2 倍杠杆 | 完全不想要杠杆就设 1.0 |
VOL_LOOKBACK | 720 | 波动估计的窗口长度(根 K 线) | 换周期就要重算,见上面那张表 |
PERIODS_PER_YEAR | 8760 | 年化用的每年期数(1h 周期) | 换周期就要重算,见上面那张表 |
注意:add_realized_vol 必须在 fetch_data 里调用(信号计算之前),因为 apply_vol_scaling 会读取 df['realized_vol'],而这个字段是第一个函数就地写入的。
上限的存在是因为在持续低波动的环境下,波动率目标化可能产生极端杠杆。如果已实现波动降到 5%,没有上限的倍数会是 6 倍。单日不利的走势就可能造成毁灭性的回撤,即使信号本身完全正确。
默认 2× 的上限让策略最多只用到配置资金的两倍。如果你完全不想有杠杆,把 vol_cap=1.0——波动率目标化就只会在高波动时缩减仓位,不会在低波动时放大超过全额。
一个实用的经验法则:target_vol ≈ 可接受 MDD ÷ 2。这来自趋势跟踪策略中年化波动率和最大回撤的经验关系。
| target_vol | 隐含典型 MDD | 适合场景 |
|---|---|---|
| 10% | 约 −20% | 保守型,资金保全优先 |
| 20% | 约 −40% | 中等 |
| 30%(默认) | 约 −60% | 积极型——用满风险预算 |
periods_per_year 跟你的周期对得上吗?默认 8760 是 1h 用的。5min 是 105,120、4h 是 2,190、8h 是 1,095、1d 是 365。填错,年化那一步就跟着错,后面每一格都是错的。
lookback 还是 30 天吗?720 根=1h × 30 天。换周期要跟着换:5min 8,640 根、4h 180 根、8h 90 根、1d 30 根。日线只用 30 根偏短,可以考虑 252 根(一年)换一个更稳的估计。
target_vol 跟你能接受的 MDD 对得上吗?经验法则是 target_vol ≈ 可接受 MDD ÷ 2:10% 对约 −20%、20% 对约 −40%、默认的 30% 对约 −60%。(MDD 怎么读,见〈回测是什么?回测报告怎么看〉。)
vol_cap 给你的杠杆是你要的吗?默认 2.0 表示最多用到配置资金的两倍。设 1.0,它就只会在高波动时缩仓位,不会在低波动时放大超过全额。
个别策略的 TARGET_VOL 跟组合 Manager 的 target_vol_pct 一致吗?Manager 决定整体杠杆用的是同一套逻辑(杠杆 = target_vol ÷ 组合已实现波动)。两边不一致,多个策略组起来会重复计算风险。
lookback 根算出来的:窗口越长,估计越稳、反应也越慢;窗口越短,越容易对一次短期飙升过度反应。两边都不会让你提前知道波动要跳。