【Python量化波动率实战 01】波动率是怎么算的:年化、滚动与区间估计一次讲清
做量化的人天天把"波动率"挂在嘴边,但真让你写代码算一个,十有八九会卡在三个问题上:日收益率怎么转年化?为什么要用 20 日滚动而不是全样本?除了收盘价标准差,还有别的算法吗?
这篇文章把波动率的计算一次讲透,全部代码可复制运行。
本文你将得到什么
- 标准差法年化波动率的 Python 实现(含 A 股交易日口径说明)。
- 20 日滚动波动率:为什么它比全样本波动率更有用。
- 两个"进阶版"区间估计器:Parkinson 与 Garman-Klass,只用最高价/最低价/开盘价/收盘价就能算。
- 一组三种估计器的实测对比,理解它们为什么量级一致、数值不同。
一、从日收益率到年化波动率
波动率的教科书定义:收益率的标准差。实际计算分三步:
第一步,算对数收益率:
r_t = ln(收盘价_t / 收盘价_{t-1})
用对数收益率而不是简单收益率,是因为对数收益具有可加性,且在计算统计量时性质更好。
第二步,算标准差:
sigma_daily = std(r_t)
第三步,年化。一年约有 244 个交易日(A 股口径;252 是美股惯例,250 是另一种常见取法,本质都只是"一年天数"的约定),日波动率按时间平方根法则放大:
sigma_annual = sigma_daily * sqrt(244)
注意是乘 sqrt(244) 而不是 244——独立随机变量方差可加,标准差只增长平方根倍。这是新手最常见的错误。
二、为什么需要"滚动"波动率
全样本波动率是一个"平均印象",但市场波动是聚集的:平静期和动荡期交替出现。20 日滚动波动率(约一个月的交易日)能告诉你现在市场处于什么状态——这是仓位管理的直接输入,也是后面几篇文章的基础。
三、两个区间估计器:不止用收盘价
收盘价标准差法只用每天一个价格点,浪费了盘中的信息。利用最高价、最低价、开盘价,可以构造更高效的估计器。
Parkinson(1980)——只用日内振幅:
sigma_P = sqrt( mean( ln(高/低)^2 ) / (4 * ln2) * 244 )
Garman-Klass(1980)——同时利用开、收、高、低:
sigma_GK = sqrt( 244 * mean( 0.5*ln(高/低)^2 - (2*ln2 - 1)*ln(收/开)^2 ) )
理论研究表明,这两个估计器的统计效率比收盘价标准差法高数倍(同样数据量下估计误差更小)。代价是它们对隔夜跳空不敏感——跳空大的股票两者会有系统性差异。
可运行代码
pip install mairui pandas numpy
from mairui import Client
import pandas as pd
import numpy as np
LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073" # 演示证书,请换成你自己的正式证书
client = Client(LICENCE)
TRADING_DAYS = 244 # A 股常用年化口径
bars = client.stock_history("600519", "d", "n", st="20260701", et="20261012")
df = pd.DataFrame(bars).rename(columns={"t": "date"}).sort_values("date").reset_index(drop=True)
df["date"] = pd.to_datetime(df["date"])
# 1. 日对数收益率
df["log_ret"] = np.log(df["c"] / df["c"].shift(1))
# 2. 全样本年化波动率(标准差法)
vol_full = df["log_ret"].std(ddof=1) * np.sqrt(TRADING_DAYS)
# 3. 20 日滚动年化波动率
df["vol_20d"] = df["log_ret"].rolling(20).std(ddof=1) * np.sqrt(TRADING_DAYS)
# 4. Parkinson 区间估计器(20 日窗口)
hl = np.log(df["h"] / df["l"])
df["vol_parkinson"] = np.sqrt((hl ** 2).rolling(20).mean() / (4 * np.log(2)) * TRADING_DAYS)
# 5. Garman-Klass 区间估计器(20 日窗口)
co = np.log(df["c"] / df["o"])
gk_term = 0.5 * hl ** 2 - (2 * np.log(2) - 1) * co ** 2
df["vol_gk"] = np.sqrt(gk_term.rolling(20).mean() * TRADING_DAYS)
print(f"全样本年化波动率(标准差法): {vol_full*100:.2f}%")
last = df.iloc[-1]
print("最新一日三种估计器(20 日窗口):")
print(f" 标准差法 : {last['vol_20d']*100:.2f}%")
print(f" Parkinson : {last['vol_parkinson']*100:.2f}%")
print(f" Garman-Klass : {last['vol_gk']*100:.2f}%")
v = df["vol_20d"].dropna()
print(f"20 日滚动波动率区间: 最低 {v.min()*100:.2f}% / 最高 {v.max()*100:.2f}%")
运行后你会看到类似结果:
全样本年化波动率(标准差法): 39.16%
最新一日三种估计器(20 日窗口):
标准差法 : 41.85%
Parkinson : 40.79%
Garman-Klass : 41.53%
20 日滚动波动率区间: 最低 19.74% / 最高 42.06%
(示例基于 50 个交易日的演示样本测算;演示证书返回就近样例数据,申请正式证书可拉取全市场全量历史。)
怎么读这组数字
- 三种估计器量级一致(41.85% / 40.79% / 41.53%),这说明它们都在度量同一个东西;数值上的小幅差异来自各自利用的信息不同——区间估计器用到了盘中高低点,通常估计效率更高。
- 滚动波动率的区间(19.74% ~ 42.06%)比全样本值(39.16%)信息量大得多:市场在样本期内从平静期走到动荡期,波动率翻了一倍多。全样本数字掩盖了这个过程,滚动序列把过程还原了出来。
- 年化 40% 意味着什么:粗略地说,单日波动约 40% / sqrt(244) ≈ 2.6%,一天涨跌两三个百分点属于这个波动率水平下的正常现象。
三个容易踩的坑
- 年化乘错系数:乘 244 而不是 sqrt(244),波动率会凭空放大 15 倍。记住"方差可加、标准差开根号"。
- 窗口太短:10 日窗口的滚动波动率噪声很大,20 日是常见起点,做风险控制建议 20 日以上。
- ddof 别忽略:
std(ddof=1)是样本标准差(除以 n-1),ddof=0是总体标准差。样本小的时候两者差异不可忽略,工程上统一用ddof=1即可。
小结
本文给了波动率计算的完整工具箱:标准差法、滚动窗口、以及两个效率更高的区间估计器。波动率不是常数,19.74% 到 42.06% 的滚动区间就是明证。
那么问题来了:既然波动率会变,用什么方法追踪它最快、最平滑?下一篇介绍业界标准方案——RiskMetrics 的 EWMA 指数加权波动率。
下篇预告:【Python量化波动率实战 02】波动率会传染:EWMA 指数加权给近期行情更大话语权。
免责声明
本文仅为技术方法演示,不构成任何投资建议。文中示例数据仅用于教学,历史表现不代表未来收益。投资有风险,决策需谨慎。
想拉取全市场历史行情做你自己的波动率研究与风险管理?欢迎访问麦蕊智数官网 https://www.mairuiapi.com,查阅 API 文档并免费领取试用证书。
作者:麦蕊智数 MyData
来源:麦蕊智数



