3. 历史波动率Python实现:数据获取、计算与可视化

好,咱们直接进入实战环节。

历史波动率这东西,说白了就是「过去的价格波动有多剧烈」。你想想看,如果一只股票过去一年上蹿下跳,那它的历史波动率肯定高;如果它像心电图一样平,那波动率就低。这个指标是期权定价、风险管理的基石,也是我们做量化交易绕不开的基本功。

我个人习惯用 yfinance 拿数据,用 numpy 算收益率,再用 matplotlib 画图。这一套组合拳打下来,你就能把历史波动率从原始数据变成可视化的曲线。嗯,咱们一步步来。

3.1 数据获取:用 yfinance 拉取行情

先装库。如果你还没装 yfinance,终端里跑一句:

pip install yfinance numpy pandas matplotlib

然后咱们写代码。我习惯把数据获取封装成一个函数,这样后面改标的、改时间范围都很方便。

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

def get_stock_data(ticker, start='2023-01-01', end='2024-12-31'):
    """
    获取股票历史日线数据
    ticker: 股票代码,比如 'AAPL' 或 '600519.SS'
    start, end: 起止日期
    """
    df = yf.download(ticker, start=start, end=end, progress=False)
    # 我只取收盘价,够用了
    return df['Close']

# 拿苹果的数据试试
close_prices = get_stock_data('AAPL')
print(close_prices.head())
小提示: 如果你在国内,yfinance 有时候会连不上。我遇到过这种情况,解决办法是用代理,或者换 aksharetushare 这些国内数据源。不过为了课程统一,咱们先用 yfinance。

3.2 对数收益率计算:为什么不用简单收益率?

拿到收盘价之后,下一步就是算收益率。这里有个关键选择:用简单收益率还是对数收益率?

我个人强烈推荐对数收益率。原因有三:

  • 时间可加性:对数收益率可以直接相加得到多期总收益率,简单收益率不行。
  • 分布更接近正态:虽然实际数据有厚尾,但比简单收益率好得多。
  • 计算波动率更方便:对数收益率的标准差可以直接年化,简单收益率需要调整。

代码实现很简单:

# 计算对数收益率
log_returns = np.log(close_prices / close_prices.shift(1))

# 去掉第一行的 NaN
log_returns = log_returns.dropna()

print(log_returns.head())
注意: 这里有个坑。我曾经在项目里直接用 pct_change() 算简单收益率,然后拿去算波动率,结果和期权市场报价对不上。后来才发现,对数收益率和简单收益率在小波动时差别不大,但遇到大跳空(比如财报日),差异就非常明显了。所以,做波动率分析,请用对数收益率。

3.3 年化波动率计算:把日波动率放大到年

算完日收益率,我们得到的是「日波动率」。但实际交易中,我们更关心年化波动率。怎么转换?

公式很简单:

σ年化 = σ × √252

为什么是252?因为一年大约有252个交易日。这个数字不是拍脑袋定的,是美国市场实际交易日统计出来的。A股大概243天左右,但为了和国际接轨,很多机构统一用252。

# 计算日波动率(标准差)
daily_vol = log_returns.std()

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

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

跑一下苹果的数据,你大概会得到年化波动率在0.2到0.3之间,也就是20%-30%。嗯,这个范围对于大盘股来说是正常的。

核心要点: 年化波动率 = 日波动率 × √252。这个公式背后假设收益率是独立同分布的。实际中这个假设不完美,但作为一阶近似,够用了。

3.4 滚动窗口可视化:看波动率如何随时间变化

算一个整体的年化波动率太粗糙了。我们更想知道:波动率在过去一年里是怎么变化的?是平稳的,还是忽高忽低?

这时候就需要「滚动窗口」了。说白了,就是固定一个窗口长度(比如60天),每天往前滑动,算这个窗口内的波动率。这样就能得到一条波动率曲线。

# 设置滚动窗口大小(交易日)
window = 60

# 计算滚动年化波动率
rolling_vol = log_returns.rolling(window=window).std() * np.sqrt(252)

# 去掉前 window-1 天的 NaN
rolling_vol = rolling_vol.dropna()

# 可视化
plt.figure(figsize=(12, 6))
plt.plot(rolling_vol.index, rolling_vol, label=f'{window}日滚动年化波动率', color='#FF6B6B')
plt.title('AAPL 历史波动率(滚动窗口)')
plt.xlabel('日期')
plt.ylabel('年化波动率')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

跑完这段代码,你会看到一条上下起伏的曲线。比如2023年7月到10月,苹果的波动率明显上升,那是因为当时市场对iPhone销量有担忧。2024年初又降下来了。

我的经验: 窗口大小的选择很讲究。窗口太小(比如20天),曲线太敏感,噪音大;窗口太大(比如252天),曲线太平滑,反应迟钝。我个人做短线交易时用20-30天,做中线用60-90天。你可以根据自己的策略调整。

3.5 本章知识体系总览

下面这张图,把咱们这一章的核心逻辑串起来了。从数据源到最终的可视化曲线,每一步都有它的意义。

历史波动率计算流程 ① 数据获取 yfinance 拉取收盘价 ② 对数收益率 ln(Pt / Pt-1) ③ 年化波动率 σ日 × √252 ④ 滚动窗口计算 rolling(window).std() ⑤ 可视化输出 matplotlib 绘制波动率曲线 窗口大小选择:短线20-30天,中线60-90天,长线252天

3.6 完整代码整合

最后,我把上面所有代码整合成一个完整的函数。你以后做分析,直接复制这个函数,改一下参数就能用。

def historical_volatility(ticker, start='2023-01-01', end='2024-12-31', window=60):
    """
    计算并可视化历史波动率
    参数:
        ticker: 股票代码
        start, end: 日期范围
        window: 滚动窗口大小(交易日)
    返回:
        rolling_vol: 滚动年化波动率序列
    """
    # 1. 获取数据
    close = yf.download(ticker, start=start, end=end, progress=False)['Close']
    
    # 2. 对数收益率
    log_ret = np.log(close / close.shift(1)).dropna()
    
    # 3. 滚动年化波动率
    rolling_vol = log_ret.rolling(window=window).std() * np.sqrt(252)
    rolling_vol = rolling_vol.dropna()
    
    # 4. 可视化
    plt.figure(figsize=(12, 6))
    plt.plot(rolling_vol.index, rolling_vol, 
             label=f'{window}日滚动年化波动率', color='#FF6B6B', linewidth=2)
    plt.title(f'{ticker} 历史波动率')
    plt.xlabel('日期')
    plt.ylabel('年化波动率')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()
    
    return rolling_vol

# 使用示例
aapl_vol = historical_volatility('AAPL', window=60)
避坑提醒: 我曾经在回测时直接用这个函数算波动率,然后拿去和期权隐含波动率对比,发现总是差一点。后来排查了半天,发现是数据频率问题——我用的是日线,但期权定价用的是交易日历。如果你要做精确对比,记得把日期对齐到交易日。

好了,这一章的内容就到这里。你跟着代码跑一遍,应该能拿到一条漂亮的波动率曲线。下一章咱们会深入探讨如何用这个波动率去做交易策略,敬请期待。


公众号:蓝海数据掘金营,微信deep3321