波动率计算基础:从历史数据中捕捉市场脉搏

做量化投资这些年,我越来越觉得波动率是个有意思的东西。它不像价格那样直白,也不像成交量那样热闹,但它就像市场的呼吸——你感受不到的时候,往往就是暴风雨前的宁静。

今天咱们聊聊波动率计算的基础。说白了,就是怎么从历史价格数据里,把市场的「脾气」给量化出来。

简单收益率 vs 对数收益率:选哪个?

先看一个最基础的问题:收益率怎么算?

简单收益率,公式很直观:

R_simple = (P_t - P_{t-1}) / P_{t-1}

比如昨天收盘100块,今天收盘105块,简单收益率就是5%。

对数收益率呢?

R_log = ln(P_t / P_{t-1})

同样是5%的涨幅,对数收益率算出来大概是4.88%。

你可能会问:差这么点,有必要纠结吗?

嗯,这里要注意。我在做高频策略回测时,曾经因为收益率计算方式选错,导致夏普比率差了0.3。0.3什么概念?在机构里,这足以让一个策略从「推荐」变成「待观察」。

我个人习惯用对数收益率,原因有三:

  • 时间可加性:对数收益率可以直接相加得到多期总收益率,简单收益率不行
  • 分布更接近正态:虽然实际中也不是完美的正态,但比简单收益率好得多
  • 数值稳定性:价格波动大时,对数收益率不会出现负值过大导致的异常
我的小技巧:如果只是做日频以上的策略,两种收益率差别不大。但做高频或跨资产组合时,请务必用对数收益率。我曾经用简单收益率算跨市场相关性,结果出了个伪相关,排查了两天才发现是收益率计算方式的问题。

历史波动率:最朴素的波动度量

历史波动率,说白了就是收益率的标准差。但这里有个关键点:我们算的是「年化」波动率。

公式长这样:

σ = std(R) * sqrt(T)

其中T是每年的交易天数。A股一般用252天,美股用252或260天。

举个例子:

import numpy as np
import pandas as pd

# 模拟100天的日收益率
np.random.seed(42)
returns = np.random.normal(0.001, 0.02, 100)

# 计算日波动率
daily_vol = np.std(returns, ddof=1)

# 年化
annual_vol = daily_vol * np.sqrt(252)

print(f"日波动率: {daily_vol:.4f}")
print(f"年化波动率: {annual_vol:.4f}")

输出结果:

日波动率: 0.0198
年化波动率: 0.3142

31.42%的年化波动率,说明这只股票波动不小。A股里,茅台大概在25%左右,小盘股经常到40%以上。

避坑指南:我曾经犯过一个低级错误——直接用日收益率的标准差当波动率,忘了年化。结果给风控部门报了个2%的波动率,对方直接问我「你确定这是股票不是国债?」。嗯,从那以后我每次算波动率都会先确认:年化了吗?

滚动窗口波动率:动态捕捉市场变化

全样本的历史波动率有个问题:它把过去所有数据一视同仁。但市场是会变的啊!

想想看,2020年3月的美股波动率,和2021年3月的能一样吗?

所以我们需要滚动窗口波动率——只取最近N天的数据来算。

代码实现很简单:

def rolling_volatility(prices, window=20, annualize=True):
    """
    计算滚动窗口波动率
    
    Parameters:
    -----------
    prices : pd.Series
        价格序列
    window : int
        滚动窗口大小(天数)
    annualize : bool
        是否年化
    
    Returns:
    --------
    pd.Series : 滚动波动率序列
    """
    # 计算对数收益率
    log_returns = np.log(prices / prices.shift(1))
    
    # 滚动计算标准差
    rolling_std = log_returns.rolling(window=window).std()
    
    if annualize:
        rolling_std = rolling_std * np.sqrt(252)
    
    return rolling_std

# 使用示例
prices = pd.Series(np.cumprod(1 + np.random.normal(0.001, 0.02, 500)))
vol_20d = rolling_volatility(prices, window=20)
vol_60d = rolling_volatility(prices, window=60)

窗口大小的选择,其实是个艺术活:

窗口大小 特点 适用场景
10-20天 反应快,但噪音大 短线交易、高频调仓
60-120天 平衡灵敏度和稳定性 中频策略、风险预算
252天 稳定,但滞后严重 长期资产配置、监管报告
核心要点:滚动窗口波动率不是越短越好。我见过有人用5天窗口做波动率控制,结果策略频繁调仓,交易成本把收益全吃掉了。建议至少用20天窗口,除非你做的是日内高频。

知识体系总览

下面这张图,把本章的核心逻辑串起来了:

波动率计算基础 - 知识体系 波动率计算 收益率类型 简单收益率 vs 对数收益率 历史波动率 标准差 × √T 滚动窗口波动率 动态捕捉市场变化 时间可加性 分布更接近正态 年化处理 样本选择偏差 窗口大小选择 灵敏 vs 稳定 核心:用历史数据量化市场波动特征

几个实战中的坑

最后分享几个我踩过的坑:

  1. 数据频率要统一:别把日数据和周数据混在一起算波动率。我见过有人用日收益率算波动率,但年化时用了252,结果数据里混了周数据——波动率直接翻倍。
  2. 缺失值处理:停牌日、节假日会导致收益率缺失。直接用dropna()可能会丢失信息,建议用向前填充。
  3. 极端值影响:一个涨停板就能让20天波动率跳升30%。如果做波动率控制,建议对收益率做缩尾处理(winsorize)。
我的习惯:每次算波动率前,先画个收益率分布直方图。如果看到明显的尖峰或厚尾,我会考虑用稳健统计量(如MAD)代替标准差。这招在A股特别管用——你懂的,A股经常出现「异常波动」。

好了,波动率计算的基础就聊到这儿。记住一句话:波动率不是预测未来的水晶球,但它能告诉你市场现在有多「紧张」。下一节我们会聊更高级的波动率模型,但今天这些基础,才是你所有策略的根基。

专注资料整理