波动率目标化与基于GARCH预测的交易
本系列的前三篇文章教会了你如何预测波动率。我们在第一部分中构建了单变量GARCH(1,1)模型,在第二部分中通过GJR和Student-t新息加入了杠杆效应和厚尾特性,并在第三部分中用DCC-GARCH对整个协方差矩阵随时间的演变进行了建模。每一部分的结尾,我们都打印出一个数字:明天的预期波动率。然后,说实话,我们就止步于此——仿佛产出预测本身就是目的。
但事实并非如此。波动率预测不是盈亏。从来没有人因为QLIKE得分低而获得报酬。预测只有在恰好改变了你原本会做出的不同决策的那一刻才变得有价值——买多少、何时止损、分配多少资本。如果你的预测没有改变仓位,它的统计准确性就只是一种私人爱好。
这最后一部分要做的就是闭合这个循环。我们把第一到三部分的预测投入到最纯粹的决策场景中使用:波动率目标化(volatility targeting)——调整仓位大小,使投资组合的已实现波动率达到一个恒定目标。然后我们要做这个博客比任何单一模型都更看重的事情:诚实地评估。我们将GARCH与朴素但强劲的基准(滚动已实现波动率、EWMA)进行比较,使用对我们永远无法观测到真实波动率这一事实具有稳健性的损失函数,运行一个带成本且无前视偏差的walk-forward回测,并坦率地说明波动率目标化能带来什么、不能带来什么。剧透一下:它在改善风险调整后收益和抑制回撤方面的可靠性,远高于它在制造阿尔法方面的能力。
为什么波动率目标化是正确的检验方式
使用波动率预测还有更花哨的方式——期权定价、VaR限额、动态对冲——但波动率目标化是最能以最少的其他因素污染来分离出预测价值的一种方式。这个想法归结为一个方程。
持有一个具有信号所隐含方向(暂且只考虑"做多")的风险资产。不采用固定仓位,而是让敞口与预测波动率成反比缩放:
其中是你对下一期波动率的预测(仅使用截至时刻的数据得出),是你希望策略运行的年化波动率——比如15%或20%。当模型预测市场平静时,你会加杠杆至(或超过)1.0;当它预测风暴来临时,你会缩减仓位。缩放后仓位的已实现波动率,在一阶近似下为
只要成立。所以整个操作的质量都取决于一件事:你的预测与下一期实际波动率有多接近。更好的预测会产生更平坦的已实现波动率曲线,并且——正如我们将看到的——带来更好的Sharpe比率。这就是为什么这是正确的检验方式。预测不是装饰品;它是分母本身。
为什么这能提升Sharpe比率并控制回撤
两个经验事实在这里起作用。
波动率比收益率更具可预测性得多。 在日频层面,明天BTC收益率的方向接近于抛硬币;但幅度并非如此。波动率具有聚集性——大波动之后往往跟着大波动——这正是GARCH存在的全部原因(第一部分推导了编码这一特性的方差AR(1)结构)。对一日前波动率预测得到0.4-0.6的是家常便饭;同样的数字用于收益率预测,那将是文艺复兴级别的信号。波动率目标化利用了可预测的量,而对不可预测的量保持中立。
Sharpe比率并非随时间恒定;它在波动率飙升时会下降。 加密货币中的高波动率状态——去杠杆连环爆仓、交易所故障、一切都跳空30%的日子——往往具有更差的单位风险收益,而非更好。通过在预测波动率高时机械地削减敞口,你就低配了那些对回撤贡献最大、对复合收益贡献最小的时段。Moreira and Muir(2017)在股票领域证明了波动率管理型投资组合——正是这种缩放——能提升Sharpe比率,并相对于未管理的因子产生正的阿尔法。这个机制并非魔法;它只是拒绝在可预见的动荡时段里持有固定美元仓位。
回撤方面的好处更为直接。最大回撤主要由仓位收益分布的尾部决定。由于的波动率被固定在接近的水平,固定名义本金策略在波动率爆发期间遭受的肥厚左尾被压缩了:你一开始的仓位就已经很小了。波动率目标化并不能预测崩盘,但它在市场动荡时系统性地保持低敞口,而动荡恰恰是崩盘发生的时候。
与Kelly和分数式头寸规模的关系
波动率目标化是Kelly准则的近亲。对于一个具有期望超额收益和方差的单一资产,增长最优(全额Kelly)的比例为
如果你假设Sharpe比率大致恒定——这是一个很强的假设,但也是"市场对风险给出稳定定价"这一说法隐含的假设——那么,,这恰好就是的波动率目标化。换句话说,在期望收益随波动率成比例变化的假设下,波动率目标化就是Kelly头寸规模化。 我们在Kelly准则与策略头寸规模一文中详细讨论了全额和分数式Kelly,以及为什么没有理智的人会采用全额Kelly交易。那里得出的实用经验在这里同样适用:使用理论仓位的一部分,因为分母(你的波动率预测)尤其是分子(期望收益)中的估计误差,会使全额头寸规模变得危险地激进。
还有两点值得牢记。首先,波动率目标化是基于收益的对称离散度来设定头寸规模的,但加密货币的收益并不对称——一旦涉及杠杆和强平,下跌20%的成本并非上涨20%的镜像。我们在损益不对称性一文中直接处理了这种不对称性,而GJR/EGARCH预测(第二部分)已经通过对负面冲击反应更强烈,将部分不对称性融入了之中。其次,波动率预测是一个点估计;更完整的风险视角会为其附加一个区间。交易中的保形预测展示了如何将模型输出转化为可用于头寸规模设定的、不依赖分布假设的区间,这与本文诚实评估的主题自然契合。
竞争对手:GARCH必须击败的对象
这里正是把真正的评估与演示区分开来的严谨之处。在给GARCH加冕之前,你必须给它提供廉价、显而易见、却出人意料地难以击败的对手。如果你精心设计的GJR-t模型无法胜过五行代码的EWMA,那你就学到了宝贵的一课,并为自己省下了大量的生产复杂度。
我们基准比较了四种下一期波动率预测方法。
(a)滚动已实现波动率(滚动标准差)
最朴素的预测:明天的波动率等于过去个日收益率的样本标准差。
它只有一个超参数(窗口,通常为20-60天),没有模型。它的缺陷在于窗口内的每个观测值权重相同,然后在退出窗口时会突然跌落悬崖——这就是"重影"或"回声"效应,一个崩盘日会使预测值膨胀恰好天,然后一夜之间消失,无论市场是否真的已经平静下来。
(b)EWMA / RiskMetrics()
指数加权移动平均通过对较早的平方收益率赋予几何衰减权重,解决了回声问题:
这是RiskMetrics(J.P. Morgan, 1996)估计量。对于日频数据采用标准的,有效记忆长度约为天,但衰减是平滑的——没有悬崖式跌落。请注意EWMA的本质:它是一个**且的整合GARCH(1,1)**,即没有均值回归、没有长期方差的GARCH。如果你接受,它就零自由参数,是本文中最难以击败的单一基准。相当一部分"GARCH击败X"的论文,在样本外都悄悄未能击败EWMA。
import numpy as np
import pandas as pd
def ewma_vol(returns: pd.Series, lam: float = 0.94, sigma0: float | None = None) -> pd.Series:
"""RiskMetrics EWMA conditional volatility (returns in decimal, e.g. 0.03).
sigma2_t = lam * sigma2_{t-1} + (1 - lam) * r_t^2
Returns a series aligned to `returns` where value at t uses info up to t.
"""
r2 = np.square(returns.values)
var = np.empty_like(r2)
var[0] = sigma0**2 if sigma0 is not None else r2[0]
for t in range(1, len(r2)):
var[t] = lam * var[t - 1] + (1.0 - lam) * r2[t - 1] # note: r_{t-1}, no look-ahead
return pd.Series(np.sqrt(var), index=returns.index, name="ewma_vol")
有一个容易让人栽跟头的细微之处:针对期的预测(可用于设定持有至期的仓位)必须由之前观测到的收益率构建而成。在上面的递归中,var[t]使用的是r2[t-1],因此该序列是真正意义上的一步前预测。把这个索引对齐正确,是回测与幻想之间的分界线——关于这一点,在walk-forward部分会有更多讨论。
(c)GARCH(1,1)和GJR-t(第一、二部分)
我们的主角登场。标准GARCH(1,1):
其长期方差为,一步预测直接从递归式中得出(第一部分)。GJR-GARCH扩展加入了杠杆项,使负面冲击比正面冲击更能提升方差:
再配合Student-t新息来处理加密货币的厚尾特性,这就是第二部分中的GJR-t。GARCH能够胜过EWMA的原因在于均值回归:受到冲击后,GARCH会以由决定的速率将预测值拉回,而EWMA(作为整合模型)永不回归。当波动率飙升然后恢复正常——这是常见情形——GARCH的预测能更快、更准确地衰减回落。当波动率真正具有持续性时,两者几乎无法区分。
(d)基于已实现方差的HAR-RV(如果你有日内数据)
如果你有日内K线数据——而在24/7运转的加密货币市场中你几乎总是有——你可以构建一个比日收益率平方噪声小得多的波动率代理指标:已实现方差(realized variance),即当日日内收益率平方之和。
Corsi(2009)的异质自回归模型(Heterogeneous Autoregressive model)利用过去的日、周、月平均值来预测明天的已实现方差——这是一种粗糙但出奇有效的方法,用三个回归变量捕捉长记忆持续性:
其中和是过去5天和22天日的滚动平均值。这是一个普通的OLS回归,利用了质量更高的日内代理指标,并且往往是四种方法中日波动率预测最好的一种——通常正是因为是比更干净的目标而击败了GARCH。
import numpy as np
import pandas as pd
def realized_variance(intraday_returns: pd.Series, day_index) -> pd.Series:
"""Daily realized variance = sum of squared intraday (log) returns per day.
`intraday_returns` indexed by timestamp; `day_index` maps to calendar day.
"""
return intraday_returns.pow(2).groupby(day_index).sum()
def har_features(rv: pd.Series) -> pd.DataFrame:
"""Build HAR regressors from a daily realized-variance series."""
df = pd.DataFrame({"rv": rv})
df["rv_d"] = df["rv"].shift(1) # yesterday
df["rv_w"] = df["rv"].shift(1).rolling(5).mean() # trailing week
df["rv_m"] = df["rv"].shift(1).rolling(22).mean() # trailing month
df["target"] = df["rv"] # predict today's RV
return df.dropna()
def fit_har(rv: pd.Series):
"""Fit HAR-RV by OLS. Returns (coef_dict, predict_fn)."""
df = har_features(rv)
X = np.column_stack([np.ones(len(df)), df["rv_d"], df["rv_w"], df["rv_m"]])
y = df["target"].values
beta, *_ = np.linalg.lstsq(X, y, rcond=None)
names = ["const", "rv_d", "rv_w", "rv_m"]
def predict(rv_d, rv_w, rv_m):
return float(beta @ np.array([1.0, rv_d, rv_w, rv_m]))
return dict(zip(names, beta)), predict
关于log-HAR的一点说明:由于是右偏且严格为正的,许多从业者会用log HAR特征对进行回归,这能改善拟合并保证预测为正。当你对结果取指数还原时,应该加上一个半方差的Jensen修正项,,否则你会系统性地低估预测值。
有了这四种预测方法——RV、EWMA、GARCH/GJR-t、HAR——问题就变成了:当我们永远无法看到它们都在试图预测的那个真实量时,我们如何判断哪一个最好?
诚实地评估波动率预测
这是本文的核心内容,所以在这里请放慢脚步。
你想把预测与真实条件方差进行比较。但是一个潜在(latent)量——它是数据生成过程的一个参数,永远无法直接观测到。你能得到的只是每天一个已实现的收益率。所以每一次波动率评估实际上都是把你的预测与真值的一个含噪声代理指标进行比较。两种标准代理指标:
- 平方收益率。在零均值模型下对是无偏的(),但噪声极大:单个日收益率只是对标准差的单观测估计。代理指标在真实波动率很高时也可能为0(平静的一天),或者在幸运的尾部日出现巨大数值。
- 来自日内数据的已实现方差。噪声要小得多——日内采样平均掉了单个收益率的特异性噪声——这正是HAR-RV有效的原因,也是为什么如果你有日内数据的话,应该用作为代理指标。
几乎每个人都会中招的细微之处在于:由于代理指标是有噪声的,损失函数的选择并非无关紧要。 用错误的损失函数对两个预测进行排名,噪声代理指标可能会颠倒排名,告诉你更差的预测反而更好。Patton(2011)精确地推导出哪些损失函数在以下意义上是"稳健的":按对含噪声代理指标的期望损失对预测进行排名,得到的排名与你在真实(不可观测的)方差上得到的排名相同。只有一个特定的函数族符合条件。实践中有两个成员至关重要。
MSE与QLIKE
方差层面的均方误差:
其中是预测值,由或代理。MSE在Patton意义上是稳健的(其排名对代理指标是一致的),但它是对称且依赖尺度的:它对相同绝对幅度的过度预测和不足预测的惩罚是一样的,并且它对高波动率时期误差的权重要远大于平静时期的误差。一个在平静的95%的日子里表现完美、但在三个危机日的方差预测上出错的模型,在MSE下会显得很糟糕,即便它在危机中的表现恰恰是你真正想要的。
QLIKE(准似然)损失是主力工具:
它是方差上高斯似然所隐含的损失,同样在Patton意义上是稳健的,并且具有两个使其成为波动率评估首选的性质。首先,它是在正确方向上不对称的:它对方差的低估惩罚要重于高估。对风险管理者或波动率目标化者而言,这正是正确的不对称性——低估波动率意味着你在关键时刻承担了过大的仓位,这是代价高昂的错误。其次,它(大致上)是尺度不变的:因为它依赖于比率,无论是发生在平静日还是危机日,10%的预测误差所付出的代价大致相同,所以评估不会像MSE那样被少数几个高方差观测值所劫持。这种对代理指标异方差性的稳健性,正是当整个重点在于波动率剧烈变化时所需要的。
注意,当且仅当时取等号。数值越低越好,与MSE相同。
import numpy as np
def qlike(proxy: np.ndarray, forecast_var: np.ndarray) -> np.ndarray:
"""Per-observation QLIKE loss. `proxy` is r^2 or RV (a variance proxy),
`forecast_var` is h = sigma_hat^2. Both strictly positive, same units.
"""
ratio = proxy / forecast_var
return ratio - np.log(ratio) - 1.0
def mse_var(proxy: np.ndarray, forecast_var: np.ndarray) -> np.ndarray:
"""Per-observation MSE on the variance scale."""
return np.square(proxy - forecast_var)
有两点操作上的提醒。让代理指标和预测值保持相同单位(都是日方差,或都是年化的),否则比率就毫无意义。并且永远不要让forecast_var触及零——把它裁剪到一个较小的下限,因为会毒害整个平均值。
Mincer-Zarnowitz回归
单一的损失数值告诉你哪个预测更好;它不会告诉你一个预测为何出错。Mincer-Zarnowitz(1969)回归可以做到这一点。将代理指标对预测值做回归:
在一个最优的无偏预测下,且:平均而言,已实现方差等于预测值。偏离这个结果可以诊断出病理:
- 且:这是预测波动过度的经典特征——它反应过度,预测出了不会完全实现的极端值。这在由原始平方收益率驱动的模型中非常常见。
- :预测反应不足,随真实方差变化的幅度太小。
- 回归较低:即使平均看起来还不错,预测在逐日跟踪方差方面表现较差。由于代理指标噪声很大,不要因为对的MZ 通常只有0.05-0.20而感到惊讶;对而言这个值会高得多。对的无论预测多好,都被严格限制在远低于1的水平,纯粹是因为代理指标的噪声所致。
对的联合检验给出了一个正式的校准检验。实践中,用MZ作为诊断工具来理解一个预测,用QLIKE来排名预测。
Diebold-Mariano检验:差异是真实的吗?
假设GARCH的平均QLIKE是0.183,EWMA的是0.191。GARCH"赢了"。但0.008是真实的优势还是抽样噪声?Diebold-Mariano(1995)检验正是回答这个问题。定义每期损失差
针对两个预测和(此处为QLIKE)。原假设是预测准确性相等,。检验统计量是均值差经其长期(HAC)标准误标准化后的结果,因为是序列相关的:
其中是Newey-West型长期方差估计。超过的DM统计量在5%水平上拒绝准确性相等的原假设。至关重要的是,DM是关于预测而非嵌套模型的检验,它处理了损失序列中的序列依赖性,而对进行朴素的检验会忽略这一点。
import numpy as np
from scipy import stats
def diebold_mariano(loss_a: np.ndarray, loss_b: np.ndarray, h: int = 1):
"""Diebold-Mariano test of equal predictive accuracy.
loss_a, loss_b: per-period losses (e.g. QLIKE) for forecasts A and B.
h: forecast horizon; Newey-West lag = h - 1 (>=0), plus a small-sample buffer.
Returns (DM_stat, p_value). Negative DM => A has lower loss (A better).
"""
d = np.asarray(loss_a) - np.asarray(loss_b)
T = len(d)
d_bar = d.mean()
lag = max(h - 1, 0)
gamma0 = np.mean((d - d_bar) ** 2)
lrv = gamma0
for k in range(1, lag + 1):
w = 1.0 - k / (lag + 1)
cov = np.mean((d[k:] - d_bar) * (d[:-k] - d_bar))
lrv += 2.0 * w * cov
dm = d_bar / np.sqrt(lrv / T)
adj = np.sqrt((T + 1 - 2 * h + h * (h - 1) / T) / T)
dm *= adj
p = 2.0 * (1.0 - stats.t.cdf(abs(dm), df=T - 1))
return float(dm), float(p)
那条注释里的示例结果是诚实且常见的结果,也正是这一节存在的全部原因:GARCH经常会给出略低于EWMA的平均损失,而这种优势也同样经常无法通过DM显著性检验。如果你只报告平均QLIKE,你会说服自己相信一些本会被DM检验否决的优势。请报告DM统计量。这与我们在没有稳健优势的诚实评估一文中对策略收益所采用的严谨态度是一样的——一个战胜基准的点估计,在你排除它是噪声之前,都算不上是真正的优势。
回测:Walk-Forward波动率目标化策略
现在我们把两个部分——预测器和头寸规模规则——组合成一个策略,并用唯一有意义的方式来评估它:walk-forward、样本外、含成本。
该策略刻意保持简单,因为简单性正是能让我们把结果归因于波动率预测本身、而非某个巧妙信号的原因。纯多头、波动率目标化的BTC。 每天预测次日波动率,将仓位设为,持有隔夜,然后重复。多头/空仓变体在趋势过滤器为负时将仓位关闭;小型投资组合变体则使用第三部分中的DCC协方差矩阵而非单一资产的方差来设定仓位规模。我们将完整描述纯多头案例,并说明其扩展方式。
Walk-forward机制与无前视偏差契约
这个回测中最重要的单一性质是:用于设定第天仓位的每一个量,都必须仅使用第天收盘时可获得的数据计算得出。 GARCH参数在截至的滚动窗口上重新估计;预测是该拟合结果给出的一步前预测;仓位由该预测设定;所获得的收益是,其中是模型从未见过的下一天的收益率。在全样本上重新拟合GARCH然后"预测"过去,是人们不慎捏造出漂亮回测结果的最常见方式。我们在walk-forward优化一文中处理了这个前视陷阱及一般方法论。
import numpy as np
import pandas as pd
from arch import arch_model
def walk_forward_voltarget(
returns: pd.Series, # daily log returns in decimal (e.g. 0.021)
proxy_var: pd.Series, # variance proxy aligned to returns (RV or r^2)
target_vol_annual: float = 0.20,
window: int = 750, # rolling estimation window (days)
refit_every: int = 5, # refit GARCH weekly, forecast daily (cost saver)
w_max: float = 3.0, # leverage cap
cost_bps: float = 5.0, # per-unit-turnover cost in basis points
ann: int = 365, # crypto trades 365 days/yr
):
"""Long-only vol-targeted BTC, GARCH(1,1)-t forecasts, strictly walk-forward.
Returns a DataFrame with position, forecast vol, net returns, and turnover.
"""
idx = returns.index
target_daily = target_vol_annual / np.sqrt(ann)
fcast_vol = pd.Series(index=idx, dtype=float, name="fcast_vol")
last_res = None
for i in range(window, len(returns) - 1):
if (i - window) % refit_every == 0 or last_res is None:
train = returns.iloc[i - window:i + 1] * 100.0 # scale for the optimizer
am = arch_model(train, mean="Constant", vol="GARCH",
p=1, o=1, q=1, dist="t") # GJR-t, Part 2
last_res = am.fit(disp="off")
f = last_res.forecast(horizon=1, reindex=False)
var_next = f.variance.values[-1, 0] / (100.0 ** 2) # unscale
fcast_vol.iloc[i + 1] = np.sqrt(var_next)
raw_w = (target_daily / fcast_vol).clip(upper=w_max)
position = raw_w.shift(0) # w_{i+1} known at close i
gross_ret = position * returns # earned on day i+1
turnover = position.diff().abs().fillna(0.0)
cost = turnover * (cost_bps / 1e4)
net_ret = (gross_ret - cost).dropna()
out = pd.DataFrame({
"position": position,
"fcast_vol": fcast_vol,
"gross_ret": gross_ret,
"turnover": turnover,
"net_ret": net_ret,
}).dropna()
return out
def performance_stats(net_ret: pd.Series, ann: int = 365) -> dict:
"""Sharpe, realized vol, max drawdown, CAGR, annual turnover."""
mu = net_ret.mean() * ann
sigma = net_ret.std() * np.sqrt(ann)
sharpe = mu / sigma if sigma > 0 else np.nan
equity = (1.0 + net_ret).cumprod()
peak = equity.cummax()
max_dd = (equity / peak - 1.0).min()
cagr = equity.iloc[-1] ** (ann / len(net_ret)) - 1.0
return {
"sharpe": round(sharpe, 2),
"realized_vol": round(sigma, 3),
"max_drawdown": round(max_dd, 3),
"cagr": round(cagr, 3),
}
有几点实现细节,其重要性远超其表面看起来的样子:
- 为优化器进行缩放。 当收益率以百分比表示时,
arch拟合在数值上会更顺畅,因此有了* 100的缩放以及在还原方差时相应的/ 100**2。忘记还原缩放,你的目标波动率就会偏差10000倍。 - 重新拟合的节奏。 每天都重新估计GARCH参数既昂贵又几乎没有任何好处——参数在周与周之间是稳定的。每周重新拟合(
refit_every=5)同时每天预测(即使不重新拟合,递归式也会用新的收益率更新)是标准的折中方案。这与联合风险的copula模型一文中copula流程的缓存建议异曲同工。 - 上限不是装饰性的。 当预测波动率在死寂平静的市场状态下崩溃时,可能会暴涨到5倍、10倍杠杆。不设上限的波动率目标化会在波动率状态即将转变、预测即将最为错误的那一刻,欣然给你带来灾难性的杠杆。设置上限(此处为3倍),并认识到这个上限恰恰会在事后看来最平静、也最危险的时期被触及。
- 成本随换手率而变,而波动率目标化正是一台换手率制造机。 预测中的每一次波动都会重新调整仓位规模。在一个波动率反复无常的低波动率资产上,你可能每天都在churning整个仓位。
cost_bps项不是一个可以四舍五入忽略的细节;对于高换手率的波动率目标化策略而言,它可能吃掉毛Sharpe改善中相当大的一部分。
输出结果大致是什么样的(示意性)
在多年期的BTC日频数据上运行此策略,比较四种预测方法作为仓位规模分母时,往往会产生具有以下形态的表格。下面的数字是示意性的——是精心挑选出来展示典型模式的,而非真实回测的输出——但排序和量级具有代表性,反映了从业者所报告的情况。
| 头寸规模预测方法 | Sharpe | 已实现波动率 | 目标波动率 | 最大回撤 | 年化换手率 |
|---|---|---|---|---|---|
| 固定名义本金(无目标化) | 0.71 | 0.68 | — | -0.78 | 0.1x |
| 滚动RV(60天) | 0.94 | 0.24 | 0.20 | -0.41 | 12x |
| EWMA() | 1.02 | 0.21 | 0.20 | -0.35 | 19x |
| GARCH(1,1)-t | 1.05 | 0.21 | 0.20 | -0.33 | 22x |
| HAR-RV(日内代理指标) | 1.09 | 0.20 | 0.20 | -0.31 | 20x |
两个值得构建的变体
纯多头案例分离出了预测本身的作用,但有两种扩展方式常见到值得明确展示。
带趋势关口的多头/空仓。 波动率目标化设定了仓位规模,但不给出任何方向性观点——它始终是多头。一个廉价而诚实的改进是:当一个缓慢的趋势过滤器转为负值时,关闭仓位,这样你只在上升趋势中持有波动率目标化的多头仓位,其余时间保持空仓。这使得仓位规模逻辑保持不变,只是在其上叠加了一个粗略的状态过滤器;它并不假装能择时入场,只是避免在明显的下降趋势中持有仓位。
def apply_trend_gate(position: pd.Series, price: pd.Series, fast: int = 20, slow: int = 100):
"""Zero out the (already vol-targeted) position when the fast SMA is below
the slow SMA. Both SMAs use only past prices, so no look-ahead is introduced.
"""
fast_ma = price.rolling(fast).mean().shift(1) # shift: known at prior close
slow_ma = price.rolling(slow).mean().shift(1)
gate = (fast_ma > slow_ma).astype(float) # 1.0 in uptrend, 0.0 otherwise
return position * gate.reindex(position.index).fillna(0.0)
趋势关口从下行方向削减了换手率(你不再在熊市中反复调整一个正在萎缩的仓位),但也带来了自身的状态风险——它在震荡横盘市场中会被反复打脸,在转折点上会滞后。它是否有帮助是一个必须用与仓位规模规则本身同样严谨的walk-forward、DM检验来回答的经验问题;趋势过滤器正是那种样本内看起来很棒、样本外却蒸发殆尽的附加组件的典型例子。
基于DCC协方差的投资组合波动率目标化。 对于一个包含多个资产的账簿,标量预测变成了投资组合波动率,其中是来自第三部分DCC-GARCH的随时间变化的协方差矩阵。你选择基础权重(等权重、市值加权,或均值方差倾斜),计算在下投资组合的预测波动率,然后缩放整个权重向量以达到投资组合目标。
def portfolio_voltarget_weights(base_w, cov_forecast, target_vol_daily, w_gross_max=3.0):
"""Scale a base weight vector so forecast portfolio vol hits the target.
base_w: (n,) base allocation (need not sum to 1).
cov_forecast: (n, n) one-step-ahead covariance from DCC-GARCH (daily units).
"""
base_w = np.asarray(base_w, dtype=float)
port_var = float(base_w @ cov_forecast @ base_w)
port_vol = np.sqrt(max(port_var, 1e-12))
scale = min(target_vol_daily / port_vol, w_gross_max / np.abs(base_w).sum())
return scale * base_w
这自然地与投资组合构建文献联系了起来:基础权重可以来自Markowitz均值方差,或者基于风险的方法如HRP/CVaR,而波动率目标化随后作为一个叠加层,将总体风险缩放到一个恒定水平。DCC矩阵之所以重要,是因为相关性在崩盘中会飙升(第三部分)——一个在平静市场中看起来多元化的投资组合,可能恰恰在关键时刻,其预测波动率会远高于静态协方差所暗示的水平,而这一叠加层会相应地削减总敞口。
你应该始终绘制的诊断图
永远不要只相信汇总表格。对于任何波动率目标化策略,在相信任何Sharpe数字之前,绘制以下三样东西并用肉眼检查。第一,策略的已实现滚动波动率相对于目标线——它应该紧贴目标线;系统性地高于目标线,意味着你的预测偏低(这是代价高昂的方向)。第二,仓位/杠杆序列——留意上限被触及的情况,以及在回撤发生前杠杆的飙升,这是预测被状态转变打了个措手不及的特征。第三,预测与代理指标的散点图(即Mincer-Zarnowitz图)——一个斜率远离1的散点云会告诉你,预测的尺度存在QLIKE平均值可能掩盖的偏差。这三张图能捕捉到比任何单一统计量都多的漏洞和自我欺骗。
用你应该阅读每一份回测表格的方式来阅读这张表:看什么是稳健的、什么是边际的。稳健的事实一目了然。每一个波动率目标化变体在Sharpe上都碾压了固定名义本金策略,在回撤和已实现波动率的稳定性上更是如此——固定名义本金策略以68%的年化波动率和78%的回撤运行,这简直无法投资。而且每一种目标化方法交付的已实现波动率都接近20%的目标,这正是该机制有效运作的全部承诺所在。边际的事实是预测方法之间的差异:HAR略胜GARCH,GARCH略胜EWMA,EWMA略胜滚动RV,但差距很小——只有十分之一个Sharpe点——而且如果用Diebold-Mariano检验预测、或用自助法(bootstrap)检验收益,这些差距往往无法通过显著性检验。这个在精细预测方法和朴素预测方法之间存在的、微小、脆弱、依赖于市场状态的差距,正是整个系列的诚实结论。
诚实面对这能给你带来什么
这个博客有一整个关于"回测但不自欺"的合集,所以让我们把它应用到我们自己的结果上,而不是悄悄地祈祷你不会这么做。
波动率目标化能改善风险调整后收益和回撤。它不会凭空制造阿尔法。 再看一眼那张表。目标化带来的Sharpe改善是真实的,也是值得拥有的——但把它分解开来看,其中大部分来自于不在高波动率状态下持有恒定仓位,这机械性地避开了最糟糕的回撤,并稳定了复利路径。这个策略仍然是做多BTC;它没有市场未曾给出的任何观点。如果BTC在你的样本期内Sharpe为负,波动率目标化会给你一个没那么差的负Sharpe,而不是正的。它重塑了收益分布——更薄的尾部、更稳定的波动率、更好的几何复利——但原始的方向性优势就是底层多头本身的优势。不要让一条漂亮的净值曲线欺骗你,让你以为发现了阿尔法,而实际上你发现的是风险管理。Moreira-Muir在股票因子中通过波动率管理发现了真实的阿尔法,但那个结果是关于该因子随时间变化的风险收益权衡的,它不会自动转移到不同样本期内的单一加密货币资产上。
GARCH相对于EWMA的预测质量优势通常很小,且依赖于市场状态。 这是第一到三部分不太舒服的回报。你构建了越来越精细的模型——杠杆项、Student-t尾部、动态相关性——而每一个模型相对于朴素EWMA、对波动率目标化盈亏的边际贡献,往往都在噪声范围之内。GARCH的优势(冲击后的均值回归)主要体现在特定的市场状态中:急剧飙升然后恢复正常的情形。在磨人的趋势或持续性高波动率状态中,它与EWMA几乎没有差别。这并不意味着GARCH毫无用处——均值回归结构、可解释的参数、模拟未来路径并据预测为期权定价的能力,这些都是EWMA所不具备的价值——但如果你唯一的用途是设定头寸规模,在为复杂性付出代价之前先运行DM检验,并且要知道状态检测正在从另一个角度告诉你同样的事情:哪个模型胜出取决于市场状态。
回测Sharpe是实盘Sharpe的上界,而波动率目标化会扩大这个差距。 由于该策略是高换手率、杠杆缩放的,它对朴素回测所忽略的摩擦因素异常敏感:你的成交价比你用来设定仓位规模的收盘价要差,杠杆永续仓位的资金费率是持续累积的,杠杆上限还会与保证金和强平机制产生交互,而简单的w * return忽略了这一切。这些因素每一个都会使实盘表现劣于回测。我们在回测-实盘一致性一文中系统地处理了这个差距;对于波动率目标化而言,具体的应对方法是使用保守的成本估计、现实(较低)的杠杆上限,并且在计算出信号的下一根K线的开盘时执行,而非收盘时。
关于波动率风险溢价的一点补充。 以上所有内容都在预测已实现波动率。还有一个平行的、可交易的对象:隐含波动率,它体现在期权定价中,平均而言会高于随后的已实现波动率——这就是波动率风险溢价,是承担波动率飙升风险的补偿。这个差距本身就是一种收益来源(卖出方差可以获取它,买入方差可以对冲尾部风险),而这是一个与波动率目标化截然不同的博弈:它是对波动率价格的押注,而非对波动率预测的运用。我们在这里不深入探讨,但相关机制的起点是Black-Scholes期权定价一文中的定价模型,而一个好的已实现波动率预测(第一、二部分)正是你判断隐含波动率是贵是便宜所需要的输入。将你的GARCH预测与期权市场的隐含波动率进行比较,是本系列所构建一切内容中较为诚实的用途之一。
实践中的考量
一些能把一个可用的波动率目标化策略与一个脆弱的策略区分开来的杂项要点。
- 在正确的期限上进行估计。 如果你为持有一天的仓位设定规模,就预测一日波动率。如果你按周再平衡,就预测(并以此为目标)周波动率,或者把日GARCH预测在该期限上聚合起来——多步GARCH预测会向回归,而朴素的""缩放方式忽略了这一点。第一部分涵盖了多步GARCH预测。
- 24/7市场中的年化处理。 加密货币一年365天交易,没有周末和假期,所以要用对日波动率进行年化,而不是股票市场的。搞错这一点会在你毫无察觉的情况下,让你的目标偏差约20%。
- 分母可以是一个协方差矩阵。 对于多资产账簿,用来自第三部分DCC-GARCH的投资组合波动率替代标量的,并缩放整个权重向量以达到投资组合目标波动率。这将波动率目标化与均值方差头寸规模化(加密货币的Markowitz方法)以及基于风险的配置(HRP和CVaR流程)联系了起来——波动率目标化是缩放至投资组合风险预算的单一资产特例。
- 波动率目标化以一种微妙的方式具有顺周期性。 当所有人都运行同样的规则时,一次波动率飙升会迫使同步去杠杆,这会压低价格,从而推高已实现波动率,进而迫使更多的去杠杆。这种反馈循环(在2018年的"volmageddon"以及各种加密货币去杠杆连环爆仓事件中都有充分记录)意味着,当许多参与者都使用这个规则时,它的效果反而会变差。这不是放弃它的理由,但确实是限制杠杆、且不要假设你在波动率飙升期间的成交会与平静市场中的成交相似的理由。
- 对预测值设下限并裁剪。 接近零的预测波动率会产生无限的杠杆。始终将限定在一个合理的最小值,并给仓位设上限,并记录每种限制被触及的频率——如果上限大多数时候都被触及,说明你的目标对该资产而言过于激进。
总结
- 波动率预测在改变决策之前没有任何价值。波动率目标化——将敞口设定为(设上限)——是检验预测价值最纯粹的方式,因为预测质量直接映射为更平坦的已实现波动率曲线和更高的Sharpe比率。
- 波动率目标化提升了风险调整后收益,尤其是控制了回撤,因为波动率是可预测的(它具有聚集性),而方向不是;也因为Sharpe比率在该规则会自动降低配置的高波动率状态下会下降。它是在期望收益随波动率成比例变化这一假设下的Kelly头寸规模化。
- 诚实地将GARCH与强劲的基准进行比较:滚动已实现波动率、EWMA/RiskMetrics(,一个零自由参数的IGARCH),以及基于日内已实现方差代理指标的HAR-RV。EWMA和HAR都很难被击败。
- 你无法观测到真实波动率,所以要用对代理指标噪声稳健的损失函数来对照代理指标(或更好的)进行评估。相对于MSE,优先选择QLIKE:它对低估预测惩罚更重(代价更高的错误),而且尺度不变,因此不会被少数几个高波动率的日子所劫持。用Mincer-Zarnowitz来诊断偏差,用Diebold-Mariano检验来判断某个预测的优势是真实的还是噪声。
- 在一个walk-forward、考虑成本的回测中,波动率目标化在Sharpe和回撤方面可靠地击败了固定名义本金策略,而四种预测方法聚集得很接近——GARCH相对EWMA的优势很小、依赖于市场状态,且往往在统计上不显著。请报告DM检验结果,而不仅仅是平均损失。
- 要诚实:波动率目标化是风险管理,不是阿尔法。它重塑了你原本已有的任何方向性押注的收益分布;它不会凭空创造优势。而且它是高换手率、杠杆缩放的,所以实盘结果比通常情况更滞后于回测。
参考文献:
- Patton, A. J. (2011). Volatility forecast comparison using imperfect volatility proxies. Journal of Econometrics, 160(1), 246-256. DOI
- Corsi, F. (2009). A simple approximate long-memory model of realized volatility. Journal of Financial Econometrics, 7(2), 174-196. DOI
- Diebold, F. X., & Mariano, R. S. (1995). Comparing predictive accuracy. Journal of Business & Economic Statistics, 13(3), 253-263. DOI
- Mincer, J., & Zarnowitz, V. (1969). The evaluation of economic forecasts. In Economic Forecasts and Expectations, NBER.
- Moreira, A., & Muir, T. (2017). Volatility-managed portfolios. Journal of Finance, 72(4), 1611-1644. DOI
- J.P. Morgan / Reuters (1996). RiskMetrics Technical Document, 4th ed.
- Bollerslev, T. (1986). Generalized autoregressive conditional heteroskedasticity. Journal of Econometrics, 31(3), 307-327. DOI
- Glosten, L. R., Jagannathan, R., & Runkle, D. E. (1993). On the relation between the expected value and the volatility of the nominal excess return on stocks. Journal of Finance, 48(5), 1779-1801. DOI
- Harvey, D., Leybourne, S., & Newbold, P. (1997). Testing the equality of prediction mean squared errors. International Journal of Forecasting, 13(2), 281-291. DOI
MarketMaker.cc Team
量化研究与策略