4、计算历史波动率:使用对数收益率计算不同期限的历史波动率

好,咱们接着往下走。上一节我们把收益率算出来了,现在要干正事了——算历史波动率。

我个人习惯把历史波动率叫做“已经发生的波动”。它不像隐含波动率那样带着市场情绪,它很纯粹,就是过去一段时间的真实波动。说白了,它就是价格波动的标准差,只不过我们用的是对数收益率。

为什么非得用对数收益率?

你可能会问:直接用价格涨跌幅不行吗?嗯,这里要注意。对数收益率有个天然优势——它满足时间可加性。什么意思?就是5天的对数收益率加起来,刚好等于这5天整体的对数收益率。普通收益率做不到这一点。

我在项目中遇到过一件事:有次做跨品种波动率对比,一个用普通收益率,一个用对数收益率,结果两个品种的波动率锥形状完全对不上。排查了半天,才发现是收益率口径不统一。从那以后,我所有波动率计算一律用对数收益率。

核心公式:

对数收益率 rt = ln(Pt / Pt-1)

历史波动率 σ = 标准差(r1, r2, ..., rn) × √(年化因子)

不同期限,不同视角

我们为什么要算5天、20天、60天、120天这么多期限?

你想想看,5天的波动率反映的是短期情绪,可能被某个突发事件带偏。120天的波动率就平滑多了,更能体现趋势。做波动率套利时,我们需要同时看多个期限,才能判断当前波动率在历史中的位置。

我一般这样用:

  • 5日波动率:用来捕捉短期事件冲击,比如财报发布、政策突变
  • 20日波动率:约等于一个月的交易窗口,适合做月度的波动率交易
  • 60日波动率:一个季度的视角,用来判断中期趋势
  • 120日波动率:半年维度,看的是大周期

代码实现:一步到位

下面这段代码,我用了很多年,基本没改过。你直接拿去用就行。

import numpy as np
import pandas as pd

def calc_historical_volatility(price_series, periods=[5, 20, 60, 120]):
    """
    计算不同期限的历史波动率
    :param price_series: 价格序列,pandas Series
    :param periods: 期限列表,单位:交易日
    :return: DataFrame,每列是一个期限的波动率序列
    """
    # 1. 计算对数收益率
    log_returns = np.log(price_series / price_series.shift(1))
    
    # 2. 年化因子:假设一年252个交易日
    annual_factor = np.sqrt(252)
    
    # 3. 滚动计算标准差并年化
    hv_dict = {}
    for period in periods:
        # 滚动标准差,乘以年化因子
        hv = log_returns.rolling(window=period).std() * annual_factor
        hv_dict[f'HV_{period}d'] = hv
    
    return pd.DataFrame(hv_dict)

# 示例用法
# 假设 df 是包含收盘价的DataFrame,列名为 'close'
# hv_df = calc_historical_volatility(df['close'])
# print(hv_df.tail())

避坑指南:

我曾经犯过一个低级错误——直接用价格的标准差代替收益率的标准差。结果算出来的波动率数值完全不对,还纳闷为什么跟期权定价模型对不上。记住:波动率是收益率的波动,不是价格的波动。

年化因子:为什么是√252?

这个问题经常有人问。简单说,我们算出来的日波动率是“每天”的,但期权定价模型用的是年化波动率。所以需要把日波动率放大到年。

为什么是252?因为A股一年大概有252个交易日。美股也是这个数。如果你做的是加密货币,那得用365,因为币圈全年无休。

数学上,如果日收益率是独立同分布的,那么年化波动率 = 日波动率 × √(交易日数)。这就是平方根法则。

注意:平方根法则假设收益率是独立同分布的。实际市场中,收益率存在自相关和异方差性。所以年化后的波动率只是一个近似值,别太较真。

可视化:波动率走势对比

光看数字不够直观。我习惯把不同期限的波动率画在一起,一眼就能看出短期波动和长期波动的关系。

import matplotlib.pyplot as plt

# 假设 hv_df 是上面函数返回的DataFrame
hv_df.plot(figsize=(12, 6), title='不同期限历史波动率对比')
plt.ylabel('年化波动率')
plt.xlabel('日期')
plt.grid(True, alpha=0.3)
plt.show()

画出来之后,你会发现一个规律:短期波动率(5日)上蹿下跳,长期波动率(120日)相对平滑。当短期波动率跑到长期波动率上面时,往往意味着市场出现了异常波动——这时候就是波动率套利的机会窗口。

核心逻辑图

下面这张图,把整个计算流程串起来了。你看一遍就能记住。

历史波动率计算流程 价格序列 P₁, P₂, ..., Pₙ 对数收益率 rₜ = ln(Pₜ/Pₜ₋₁) 滚动标准差 (窗口=5,20,60,120) 年化 × √252 输入: 日频收盘价序列 核心计算: 对数收益率 → 标准差 输出: HV_5d, HV_20d, HV_60d, HV_120d 用途: 构建波动率锥

几个容易踩的坑

最后,分享几个我踩过的坑,你注意避开:

  1. 数据缺失问题:停牌日、节假日会导致收益率计算出现NaN。我一般用前向填充,或者直接剔除缺失值。
  2. 窗口大小选择:5日窗口太短,容易受单日极端值影响。120日窗口太长,对近期变化反应迟钝。没有完美窗口,多试几个。
  3. 年化因子别搞错:A股用252,港股用247(因为港股节假日少),美股用252。做跨市场对比时,统一用252就行。
  4. 收益率序列的平稳性:如果价格序列存在明显的趋势或季节性,对数收益率可能不平稳。这时候算出来的波动率会有偏差。

好了,历史波动率的计算就这些。代码拿去跑一遍,看看你的品种波动率长什么样。下一节我们拿这些波动率去构建波动率锥——那才是真正有意思的部分。