历史波动率计算原理:对数收益率计算、标准差计算、年化处理、滚动窗口计算
各位同学,咱们今天来啃一块硬骨头——历史波动率的计算原理。
说实话,我刚入行那会儿,觉得波动率就是个数字,拿Excel一拉就完事了。后来在实盘里吃过亏,才发现这里面门道不少。你想想看,如果连波动率怎么算出来的都不清楚,你怎么敢用它去定价期权?
好,咱们一步步来拆解。
一、对数收益率:为什么不用简单收益率?
先问大家一个问题:你今天赚了10%,明天亏了10%,你的本金变了吗?
算一下:100块 → 110块 → 99块。亏了1块。
这就是简单收益率的陷阱——它不对称。而对数收益率是连续的、可加的,在时间序列分析里更干净。
公式很简单:
r_t = ln(P_t / P_{t-1})
其中P_t是今天的收盘价,P_{t-1}是昨天的。
我个人习惯用对数收益率做波动率计算,原因有三:
- 正态性更好:对数收益率更接近正态分布,方便后续建模
- 时间可加性:多期的对数收益率可以直接相加
- 数值稳定性:避免出现负价格这种荒谬情况
np.log(price / price.shift(1)) 一行搞定。别自己写循环,慢。
二、标准差计算:波动率的原始形态
有了对数收益率序列,下一步就是算标准差。说白了,就是衡量这些收益率到底有多“散”。
样本标准差公式:
σ = sqrt( (1/(n-1)) * Σ(r_t - μ)² )
这里μ是收益率均值,n是样本数量。
嗯,这里要注意:
- 用样本标准差(分母n-1),不是总体标准差(分母n)
- 因为我们是拿样本去估计总体,自由度要减1
我曾经见过一个团队,直接用 np.std() 的默认参数,结果分母是n,算出来的波动率系统性偏低。后来回测怎么都跑不赢,查了两天才发现是这里的问题。
np.std(ddof=0) 是总体标准差,np.std(ddof=1) 才是样本标准差。默认是ddof=0,千万记得改!
三、年化处理:把日波动率“放大”到一年
我们算出来的通常是日波动率。但期权定价、风险度量都需要年化数据。怎么转?
核心逻辑:波动率与时间的平方根成正比。
公式:
σ_年化 = σ_日 × sqrt(252)
为什么是252?因为一年大约有252个交易日。
这里有个坑:
- 美股用252,A股用242-245(扣除节假日)
- 加密货币市场用365(7×24小时交易)
- 千万别搞混,否则波动率直接偏掉
你想想看,如果A股你用了252,年化波动率会偏高约2%。做高频可能不明显,但做中长线策略,这点误差足以影响仓位管理。
四、滚动窗口计算:让波动率“活”起来
静态波动率没意义。市场在变,波动率也在变。我们需要一个滚动窗口,不断更新。
常见的窗口长度:
| 窗口长度 | 适用场景 | 特点 |
|---|---|---|
| 20天(约1个月) | 短期交易、高频策略 | 反应快,但噪音大 |
| 60天(约1季度) | 中期趋势跟踪 | 平衡了灵敏度和稳定性 |
| 252天(1年) | 长期投资、风险预算 | 平滑,但滞后明显 |
我个人习惯用20天和60天双窗口。20天捕捉短期波动,60天看中期趋势。两者交叉验证,比单一窗口靠谱得多。
代码实现也很简单:
# 假设df是包含收盘价的DataFrame
df['log_return'] = np.log(df['close'] / df['close'].shift(1))
df['vol_20d'] = df['log_return'].rolling(20).std() * np.sqrt(252)
df['vol_60d'] = df['log_return'].rolling(60).std() * np.sqrt(252)
这里有个细节:rolling(20).std() 默认用的是ddof=1,也就是样本标准差。Pandas这点做得比NumPy好,默认就是对的。
五、完整流程图:历史波动率计算全流程
下面这张图,把整个计算逻辑串起来了。建议你保存下来,写代码时对照着看。
六、实战中的几个经验
最后,分享几个我在项目中踩过的坑,希望对你有帮助:
- 数据频率要统一:别把日线和小时线混在一起算波动率。我见过有人用日线算波动率,却用小时线做交易,结果波动率完全对不上。
- 异常值处理:遇到停牌、涨跌停、分红除权,收益率会突变。我一般用MAD(中位数绝对偏差)做异常值过滤,比3σ更稳健。
- 不要过度优化窗口:20天就是20天,别为了拟合历史数据去调成19天或21天。过拟合的波动率,在实盘里一文不值。
- 波动率聚集效应:高波动率之后往往跟着高波动率。如果你发现滚动波动率突然飙升,别急着加仓,等它稳定下来再说。
好了,历史波动率的计算原理就讲到这里。记住,波动率不是目的,而是工具。怎么用它去定价、去风控、去交易,才是我们后面要深入的内容。