4、波动率计算:使用Python计算历史波动率、滚动窗口波动率、GARCH模型简介
波动率,说白了就是市场波动的剧烈程度。在碳市场里,这东西比股票市场更刺激——政策一出,价格能瞬间跳几个百分点。我做了这么多年量化,见过太多人只盯着价格涨跌,却忽略了波动率这个核心变量。今天咱们就把它彻底讲透。
4.1 历史波动率:最朴素的度量
历史波动率是最基础的方法。它的逻辑很简单:用过去一段时间的收益率标准差,来估计未来的波动程度。嗯,这里要注意——它假设历史会重演,但碳市场往往不按套路出牌。
计算步骤其实就三步:
- 取对数收益率:
ln(P_t / P_{t-1}) - 计算标准差:对收益率序列求样本标准差
- 年化处理:乘以 sqrt(252)(交易日数量)
我在项目中遇到过一个问题:直接用收盘价算收益率,结果发现隔夜跳空特别大。后来我改用开盘价和收盘价的中间价,效果好了不少。你想想看,碳配额交易有时候一天都没几笔成交,收盘价可能失真。
核心公式:
历史波动率 = σ × √252
其中 σ 是日收益率的标准差,252 是年化交易日数。
import numpy as np
import pandas as pd
def historical_volatility(prices, window=None):
"""
计算历史波动率
prices: 价格序列
window: 如果为None,使用全部数据
"""
log_returns = np.log(prices / prices.shift(1)).dropna()
if window is None:
sigma = log_returns.std()
else:
sigma = log_returns.tail(window).std()
# 年化
hv = sigma * np.sqrt(252)
return hv
# 示例:计算碳配额EUA的历史波动率
eua_prices = pd.Series([...]) # 你的数据
hv = historical_volatility(eua_prices)
print(f"历史波动率: {hv:.2%}")
我的经验:碳市场的数据频率很关键。日度数据够用,但如果你想做高频策略,得用分钟级数据。不过要注意,碳市场的流动性不如股票,分钟级数据里全是噪音。
4.2 滚动窗口波动率:动态捕捉变化
历史波动率有个硬伤:它把所有历史数据一视同仁。但碳市场的波动率是时变的——政策密集期波动大,平淡期波动小。滚动窗口波动率就是为了解决这个问题。
说白了,就是用一个固定大小的窗口,在时间轴上滑动,每滑动一次就计算一次波动率。窗口大小怎么选?我个人习惯用20个交易日(约一个月),既能捕捉短期变化,又不会太敏感。
def rolling_volatility(prices, window=20):
"""
计算滚动窗口波动率
window: 滚动窗口大小(交易日数)
"""
log_returns = np.log(prices / prices.shift(1)).dropna()
rolling_std = log_returns.rolling(window=window).std()
rolling_vol = rolling_std * np.sqrt(252)
return rolling_vol
# 计算并可视化
rolling_vol = rolling_volatility(eua_prices, window=20)
print(f"最新滚动波动率: {rolling_vol.iloc[-1]:.2%}")
避坑指南:我曾经用30天窗口做回测,结果发现波动率曲线太平滑,错过了几次大的波动事件。后来改成20天窗口,效果好了很多。窗口太小也不行——10天窗口噪音太大,容易误判。建议多试几个窗口,找到最适合你策略的那个。
4.3 GARCH模型简介:让波动率自己说话
滚动窗口波动率虽然动态了,但它还是基于历史数据。GARCH模型更聪明——它认为波动率本身也有规律可循,比如大波动之后往往跟着大波动(波动率聚集效应)。
GARCH(1,1)是最常用的形式,公式长这样:
σ²_t = ω + α × ε²_{t-1} + β × σ²_{t-1}
其中:
- ω:长期平均波动率
- α:新信息的影响权重(市场冲击)
- β:旧波动率的衰减速度
- α + β:波动率持续性(越接近1,波动越持久)
你想想看,碳市场的波动率持续性通常很高——因为政策影响是长期的。我见过α+β超过0.98的情况,这意味着一次冲击的影响会持续很久。
from arch import arch_model
def garch_volatility(prices, p=1, q=1):
"""
使用GARCH模型估计波动率
p: ARCH项阶数
q: GARCH项阶数
"""
log_returns = np.log(prices / prices.shift(1)).dropna() * 100
# 拟合GARCH模型
model = arch_model(log_returns, vol='Garch', p=p, q=q)
results = model.fit(disp='off')
# 获取条件波动率
conditional_vol = results.conditional_volatility / 100 # 还原为小数
annualized_vol = conditional_vol * np.sqrt(252)
return annualized_vol, results
# 拟合GARCH(1,1)
garch_vol, garch_results = garch_volatility(eua_prices)
print(f"GARCH参数: ω={garch_results.params['omega']:.6f}, "
f"α={garch_results.params['alpha[1]']:.4f}, "
f"β={garch_results.params['beta[1]']:.4f}")
print(f"最新GARCH波动率: {garch_vol.iloc[-1]:.2%}")
三种方法对比:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 历史波动率 | 简单、直观 | 假设波动率恒定 | 长期趋势判断 |
| 滚动窗口波动率 | 动态、易实现 | 窗口选择主观 | 中短期交易 |
| GARCH模型 | 捕捉波动率聚集 | 参数估计复杂 | 高频/事件驱动策略 |
4.4 实战:碳市场波动率计算全流程
光说不练假把式。咱们用真实数据走一遍完整流程。我记得有一次做碳配额套利策略,就是用这套流程找到了波动率异常点,赚了一波。
# 完整流程示例
import yfinance as yf # 假设数据源
import matplotlib.pyplot as plt
# 1. 获取数据(以EUA期货为例)
# 实际中你需要从ICE或碳交易所获取数据
data = pd.read_csv('eua_futures.csv', index_col=0, parse_dates=True)
prices = data['Close']
# 2. 计算三种波动率
hv = historical_volatility(prices)
rv = rolling_volatility(prices, window=20)
gv, _ = garch_volatility(prices)
# 3. 可视化对比
plt.figure(figsize=(12, 6))
plt.plot(rv.index, rv, label='滚动波动率(20天)', alpha=0.7)
plt.plot(gv.index, gv, label='GARCH波动率', alpha=0.7)
plt.axhline(y=hv, color='r', linestyle='--', label=f'历史波动率({hv:.2%})')
plt.legend()
plt.title('碳市场EUA波动率对比')
plt.show()
# 4. 输出最新值
print(f"历史波动率: {hv:.2%}")
print(f"滚动波动率: {rv.iloc[-1]:.2%}")
print(f"GARCH波动率: {gv.iloc[-1]:.2%}")
我的建议:别只依赖一种方法。我通常把三种波动率都算出来,如果它们方向一致,说明信号可靠;如果分歧很大,就要警惕了——可能是市场结构在变化。
4.5 知识体系总览
下面这张图帮你理清本章的核心逻辑。波动率计算不是终点,而是策略的起点。
三种方法各有千秋。历史波动率适合做基准,滚动窗口波动率适合做信号,GARCH模型适合做预测。我个人习惯把它们结合起来用——就像用三把尺子量同一块布,量出来的结果越一致,越可信。
最后提醒:碳市场的波动率计算有个特殊之处——非交易日处理。碳市场有拍卖日和非拍卖日,波动率模式完全不同。我曾经因为没处理这个,回测结果漂亮得离谱,实盘直接打脸。记得把拍卖日的波动单独建模。