问题描述
解决方案
在PTrade量化交易平台中,计算股票的历史波动率(Historical Volatility, HV)通常遵循以下步骤:
- 获取历史收盘价:使用
get_history获取指定时间窗口的收盘价。 - 计算对数收益率:使用公式 $R_t = \ln(P_t / P_{t-1})$ 计算每日的对数收益率。
- 计算标准差:计算对数收益率序列的标准差。
- 年化处理:将日波动率乘以 $\sqrt{252}$(假设一年有252个交易日)得到年化波动率。
核心逻辑说明
- 复权处理:计算波动率时,必须使用复权价格(通常使用前复权
fq='pre')。如果不复权,分红和拆股导致的价格跳变会被错误地识别为剧烈波动,导致计算结果失真。 - 数据长度:如果要计算N日的历史波动率,通常需要获取 N+1 天的价格数据,因为计算收益率会损失一个数据点。
PTrade 策略代码实现
以下是一个完整的策略示例,计算标的股票过去20日的年化历史波动率。
import numpy as np
def initialize(context):
"""
初始化函数
"""
# 设置要计算的标的,这里以恒生电子为例
g.security = '600570.SS'
set_universe(g.security)
# 设定计算波动率的时间窗口(例如:20日历史波动率)
g.window = 20
def handle_data(context, data):
"""
盘中运行函数,每日运行一次(日线回测模式下)
"""
# 1. 获取历史收盘价
# count = g.window + 1,因为计算收益率需要多一天的数据
# fq='pre' 使用前复权,避免分红拆股造成价格缺口影响波动率计算
history_data = get_history(g.window + 1, '1d', 'close', g.security, fq='pre')
# 检查数据量是否足够
if len(history_data) < g.window + 1:
log.info("历史数据不足,跳过计算")
return
# 提取收盘价数组
# get_history返回的是DataFrame,.values将其转换为numpy数组
closes = history_data['close'].values
# 2. 计算对数收益率
# np.log 计算自然对数
# np.diff 计算相邻元素的差值,即 ln(Pt) - ln(Pt-1) = ln(Pt / Pt-1)
log_returns = np.diff(np.log(closes))
# 3. 计算收益率的标准差 (ddof=1 表示样本标准差)
daily_std = np.std(log_returns, ddof=1)
# 4. 年化处理
# 假设一年有252个交易日
annual_volatility = daily_std * np.sqrt(252)
# 打印结果
# 将小数转换为百分比显示
log.info("标的: %s, %d日年化历史波动率: %.2f%%" % (g.security, g.window, annual_volatility * 100))
代码详解
import numpy as np: 引入NumPy库,这是进行科学计算(如对数、标准差、平方根)的标准库。get_history(..., fq='pre'):count: 设置为g.window + 1,因为np.diff会使数组长度减1。fq='pre': 关键参数。前复权消除了除权除息带来的价格断层,反映真实的投资回报波动。
np.diff(np.log(closes)):- 这是计算连续复利收益率(对数收益率)的高效方法。相比于简单收益率(
(Pt - Pt-1) / Pt-1),对数收益率在统计特性上更符合正态分布假设,更适合波动率计算。
- 这是计算连续复利收益率(对数收益率)的高效方法。相比于简单收益率(
np.std(..., ddof=1):- 计算标准差。
ddof=1表示计算样本标准差(分母为 N-1),这是金融统计中的惯例。
- 计算标准差。
np.sqrt(252):- 波动率通常随时间的平方根扩展。将日波动率转化为年波动率需要乘以根号下的一年交易天数(通常取250或252)。
Q&A
Q1: 为什么要使用对数收益率而不是简单收益率?
A1: 对数收益率具有可加性,且在统计分布上更接近正态分布,这使得其在数学处理和波动率建模(如BS公式)中更加方便和准确。对于短周期(如日频),两者数值差异很小,但在大幅波动时对数收益率更稳健。
Q2: get_history 中的 fq 参数如果不填会有什么后果?
A2: 如果不填 fq(默认为 None,即不复权),当股票发生分红或拆股(例如10送10)时,股价会直接减半。这种非市场因素导致的价格剧烈下跌会被算法误判为巨大的市场波动,从而计算出极其错误的超高波动率。
Q3: 如何计算分钟级别的波动率?
A3: 原理相同,只需将 get_history 的频率改为 '1m' 或 '5m' 等。但在年化时,需要调整系数。例如计算5分钟线波动率,年化系数约为 $\sqrt{252 \times 48}$(假设每天4小时交易时间,即48个5分钟)。
Q4: 为什么计算出来的波动率是 nan?
A4: 这通常是因为获取的历史数据中包含 0 或负数(虽然股价不会为负,但数据异常可能导致),导致 np.log 计算非法;或者数据长度不足(例如新股上市不足20天),导致 log_returns 为空。代码中应加入数据长度检查。