← 返回博客列表

Python量化波动率实战

【Python量化波动率实战 01】波动率是怎么算的:年化、滚动与区间估计一次讲清

2026年10月15日 08:17 · 麦蕊智数 MyData

做量化的人天天把"波动率"挂在嘴边,但真让你写代码算一个,十有八九会卡在三个问题上:日收益率怎么转年化?为什么要用 20 日滚动而不是全样本?除了收盘价标准差,还有别的算法吗?

这篇文章把波动率的计算一次讲透,全部代码可复制运行。


本文你将得到什么

  1. 标准差法年化波动率的 Python 实现(含 A 股交易日口径说明)。
  2. 20 日滚动波动率:为什么它比全样本波动率更有用。
  3. 两个"进阶版"区间估计器:Parkinson 与 Garman-Klass,只用最高价/最低价/开盘价/收盘价就能算。
  4. 一组三种估计器的实测对比,理解它们为什么量级一致、数值不同。

一、从日收益率到年化波动率

波动率的教科书定义:收益率的标准差。实际计算分三步:

第一步,算对数收益率:

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 个交易日的演示样本测算;演示证书返回就近样例数据,申请正式证书可拉取全市场全量历史。)


怎么读这组数字

  1. 三种估计器量级一致(41.85% / 40.79% / 41.53%),这说明它们都在度量同一个东西;数值上的小幅差异来自各自利用的信息不同——区间估计器用到了盘中高低点,通常估计效率更高。
  2. 滚动波动率的区间(19.74% ~ 42.06%)比全样本值(39.16%)信息量大得多:市场在样本期内从平静期走到动荡期,波动率翻了一倍多。全样本数字掩盖了这个过程,滚动序列把过程还原了出来。
  3. 年化 40% 意味着什么:粗略地说,单日波动约 40% / sqrt(244) ≈ 2.6%,一天涨跌两三个百分点属于这个波动率水平下的正常现象。

三个容易踩的坑

  1. 年化乘错系数:乘 244 而不是 sqrt(244),波动率会凭空放大 15 倍。记住"方差可加、标准差开根号"。
  2. 窗口太短:10 日窗口的滚动波动率噪声很大,20 日是常见起点,做风险控制建议 20 日以上。
  3. 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
来源:麦蕊智数

标签: Python量化
QQ 客服 3826425416 咨询时请提供证书号或订单号,便于快速处理
咨询