欢迎加入专注于财经数据与量化投研的【数据科学实战】知识星球!在这里,您将获取持续更新的《财经数据宝典》和《量化投研宝典》,这两部宝典相辅相成,为您在量化投研道路上提供明确指引。 《量化投研宝典》精选了业内持续维护且实用性强的开源工具(Backtrader、Qlib、VeighNa等),配合详细教程与代码示例,帮助您快速构建量化策略;《财经数据宝典》则汇集了多年财经数据维护经验,全面介绍从 AKShare、Tushare 到 Wind、iFind 等国内外数据源,并附有丰富的使用技巧。 无论您是量化投资新手还是经验丰富的研究者,星球社区都能帮您少走弯路,事半功倍,共同探索数据驱动的投资世界!
引言
在瞬息万变的金融市场中,理解和把握波动率的变化是许多交易策略的核心。你是否想过,短期波动率和长期波动率之间的关系能否帮助我们预测市场走向?本文将深入探讨一个有趣的概念——波动率比率回归策略,并使用 Python 的 Backtrader 框架进行完整的回测分析。
波动率回归的核心理念
波动率是衡量价格波动幅度的指标,它并非恒定不变。市场往往呈现这样的规律:高波动期之后往往是低波动期,反之亦然。这种波动率向均值回归的特性,正是我们策略要捕捉的机会。
什么是波动率比率?
波动率比率 = 短期波动率 ÷ 长期波动率
- 当波动率比率较低时:表明短期市场相对平静,可能预示着即将到来的波动增加和价格上涨
- 当波动率比率较高时:意味着短期市场异常动荡,可能预示着市场即将平静下来或出现回调
策略增强:趋势过滤器
单纯依靠波动率回归交易存在风险,尤其是在强势趋势市场中。为此,我们引入简单移动平均线(SMA)作为趋势过滤器:
-
做多条件:波动率比率低 + 价格高于长期 SMA(确认上升趋势)
- 做空条件:波动率比率高 + 价格低于长期 SMA(确认下降趋势)
这种组合策略旨在捕捉与市场主趋势一致的回归机会。
风险管理:ATR 追踪止损
有效的风险管理是交易成功的关键。我们采用基于平均真实波幅(ATR)的追踪止损:
- 在波动市场:ATR 较大,止损距离更宽,给交易更多呼吸空间
- 在平静市场:ATR 较小,止损距离更紧,更好地保护利润
使用 Backtrader 实现策略
首先,我们定义自定义的波动率比率指标:
import backtrader as bt
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
class VolatilityRatioIndicator(bt.Indicator):
"""计算波动率比率(短期波动率 / 长期波动率)"""
lines = ('vol_ratio', 'trend_sma')
params = (
(
'short_vol_window', 7), # 短期波动率窗口
('long_vol_window', 30), # 长期波动率窗口
('trend_sma_window', 50), # 趋势 SMA 窗口
)
def __init__(self):
# 计算日收益率
daily_returns = (self.data.close - self.data.close(-1)) / self.data.close(-1)
# 使用收益率的标准差计算滚动波动率
short_vol = bt.indicators.StdDev(daily_returns, period=self.params.short_vol_window)
long_vol = bt.indicators.StdDev(daily_returns, period=self.params.long_vol_window)
# 波动率比率:短期波动率相对于长期波动率
self.lines.vol_ratio = short_vol / long_vol
# 趋势过滤器:收盘价的简单移动平均
self.lines.trend_sma = bt.indicators.SMA(self.data.close, period=self.params.trend_sma_window)
接下来,构建完整的交易策略:
class VolatilityRatioStrategy(bt.Strategy):
"""
波动率比率回归策略:
- 当波动率比率 < 下限阈值且价格 > 趋势时做多
- 当波动率比率 > 上限阈值且价格 < 趋势时做空
- 使用 ATR 追踪止损退出
"""
params = (
# 波动率参数
('short_vol_window', 7),
('long_vol_window', 30),
('upper_threshold', 1.2), # 高波动率阈值,预期向下回归
('lower_threshold', 0.8), # 低波动率阈值,预期向上回归
('trend_sma_window', 50),
# 风险管理
('atr_period', 14),
('atr_multiplier', 1.0), # 止损距离,以 ATR 的倍数表示
('position_size', 0.95), # 每笔交易使用的资金比例
# 输出
('printlog', True),
)
def __init__(self):
# 初始化自定义波动率比率指标
self.vol_ratio = VolatilityRatioIndicator(
short_vol_window=self.params.short_vol_window,
long_vol_window=self.params.long_vol_window,
trend_sma_window=self.params.trend_sma_window
)
# 初始化 ATR 用于追踪止损
self.atr = bt.indicators.ATR(period=self.params.atr_period)
# 跟踪订单和交易细节
self.entry_price = None
self.trailing_stop = None
self.order = None
# 性能跟踪
self.trade_count = 0
self.winning_trades = 0
def next(self):
"""每个新 K 线执行的主要策略逻辑"""
# 如果数据不足或有待处理订单,则跳过
if (self.order or
len(self.data) < max(self.params.long_vol_window, self.params.trend_sma_window) or
np.isnan(self.vol_ratio.vol_ratio[0]) or
np.isnan(self.atr[0])):
return
current_price = self.data.close[0]
vol_ratio = self.vol_ratio.vol_ratio[0]
trend_sma = self.vol_ratio.trend_sma[0]
# 如果持有仓位,更新追踪止损
if self.position:
self._update_trailing_stop()
if self._check_stop_exit():
return
# 根据波动率比率和趋势过滤器生成信号
signal = 0
# 做多信号:低波动率比率且价格高于趋势 SMA
if vol_ratio < self.params.lower_threshold and current_price > trend_sma:
signal = 1
# 做空信号:高波动率比率且价格低于趋势 SMA
elif vol_ratio > self.params.upper_threshold and current_price < trend_sma:
signal = -1
# 如果没有仓位,执行信号
if signal == 1andnot self.position:
self._enter_long()
elif signal == -1andnot self.position:
self._enter_short()
def _enter_long(self):
"""执行买入订单并设置初始追踪止损"""
size = int(self.broker.getcash() * self.params.position_size / self.data.close[0])
if size > 0:
self.order = self.buy(size=size)
self.entry_price = self.data.close[0]
# 多头头寸的初始追踪止损:入场价 - (ATR * 倍数)
self.trailing_stop = self.entry_price - (self.params.atr_multiplier * self.atr[0])
self.log(f'做多信号:波动率比率 {self.vol_ratio.vol_ratio[0]:.3f},止损位 ${self.trailing_stop:.2f}')
def
_update_trailing_stop(self):
"""调整追踪止损,多头向上,空头向下"""
if self.trailing_stop isNone:
return
current_price = self.data.close[0]
atr_value = self.atr[0]
if self.position.size > 0: # 多头头寸
new_stop = current_price - (self.params.atr_multiplier * atr_value)
if new_stop > self.trailing_stop: # 只向上移动止损
self.trailing_stop = new_stop
else: # 空头头寸
new_stop = current_price + (self.params.atr_multiplier * atr_value)
if new_stop < self.trailing_stop: # 只向下移动止损
self.trailing_stop = new_stop
运行回测
设置回测函数,执行策略测试:
def run_backtest(
# 数据参数
ticker="BTC-USD",
start_date="2021-01-01",
end_date="2024-12-31",
initial_cash=100000,
commission=0.001,
# 策略参数
short_vol_window=7,
long_vol_window=30,
upper_threshold=1.2,
lower_threshold=0.8,
trend_sma_window=50,
atr_period=14,
atr_multiplier=1.0,
position_size=0.95,
# 输出参数
printlog=True,
show_plot=True
):
"""运行波动率比率策略回测,支持可配置参数"""
print(f"波动率比率策略回测")
print(f"=" * 50)
print(f"资产:{ticker}")
print(f"期间:{start_date} 至 {end_date}")
print(f"初始资金:${initial_cash:,}")
print(f"手续费:{commission*100:.2f}%")
# 下载数据
print(
"正在下载数据...")
df = yf.download(ticker, start=start_date, end=end_date, auto_adjust=False, progress=False)
if df.empty:
print(f"指定期间内没有 {ticker} 的数据")
returnNone, None
# 确保标准 OHLCV 列名
df = df[['Open', 'High', 'Low', 'Close', 'Volume']].copy()
print(f"下载了 {len(df)} 个 K 线数据")
# 设置 Cerebro(Backtrader 的"大脑")
cerebro = bt.Cerebro()
# 添加数据
data = bt.feeds.PandasData(dataname=df)
cerebro.adddata(data)
# 添加策略
cerebro.addstrategy(
VolatilityRatioStrategy,
short_vol_window=short_vol_window,
long_vol_window=long_vol_window,
upper_threshold=upper_threshold,
lower_threshold=lower_threshold,
trend_sma_window=trend_sma_window,
atr_period=atr_period,
atr_multiplier=atr_multiplier,
position_size=position_size,
printlog=printlog
)
# 设置经纪商参数
cerebro.broker.setcash(initial_cash)
cerebro.broker.setcommission(commission=commission)
# 设置仓位大小
cerebro.addsizer(bt.sizers.PercentSizer, percents=position_size*100)
# 添加分析器
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
# 运行回测
print("\n正在运行回测...")
results = cerebro.run()
strategy = results[0]
# 打印综合结果
final_value = cerebro.broker.getvalue()
total_return = (final_value / initial_cash - 1) * 100
print(f"\n性能摘要:")
print(f"最终资产价值:${final_value:,.2f}")
print(f"总收益率:{total_return:.2f}%")
# 买入持有对比
buy_hold_return = ((df['Close'].iloc[-1] / df['Close'].iloc[0]) - 1) * 100
print(f"买入持有收益率({ticker}):{buy_hold_return:.2f}%")
print(f"超额收益(相对买入持有):{total_return - buy_hold_return:.2f}%")
# 绘制结果
if show_plot:
print("\n正在生成图表...")
cerebro.plot(style='candlestick', volume=False, figsize=(16, 10))
return cerebro, results
# 执行回测
if __name__ == "__main__":
cerebro, results = run_backtest(
ticker="BTC-USD", # 测试资产
start_date="2023-01-01", # 回测开始日期
end_date="2024-06-01", # 回测结束日期
upper_threshold=1.5, # 做空信号的高阈值
lower_threshold=0.5, # 做多信号的低阈值
atr_multiplier=2.0, # 更宽的止损(2 倍 ATR)
short_vol_window=10, # 短期波动率窗口
long_vol_window=40, # 长期波动率窗口
trend_sma_window=100, # 长期趋势过滤器
printlog=True, # 打印交易日志
show_plot=True # 显示图表
)
策略的局限性与改进方向
虽然这个策略在实现上很稳健,但仍有多个方面值得进一步探索:
1. 参数优化
当前参数是任意选择的。需要进行广泛的优化(如使用 Backtrader 的 optstrategy 或网格搜索)来寻找在不同资产和市场条件下表现更好的参数组合。但要注意避免过度优化。
2. 资产类别敏感性
波动率回归在某些资产类别(如商品、货币)或更倾向于均值回归的指数上可能表现更好,而在成长型股票或加密货币等可能经历长期强势趋势的资产上表现较差。
3. 市场状态适应
策略在不同市场状态(趋势、区间震荡、高波动、低波动)下的表现可能差异显著。更高级的策略通常包含适应这些状态的逻辑。
4. 退出条件优化
除了 ATR 追踪止损,考虑其他退出条件(如固定盈利目标、基于时间的退出或波动率比率信号反转)可能会改善性能。
5. 仓位管理
当前使用固定百分比的仓位管理。可以探索更复杂的方法,如固定分数仓位管理(每笔交易风险固定百分比的资金)或凯利公式。
总结
波动率比率回归策略为我们提供了一个利用市场波动率动态进行交易的有趣视角。通过 Backtrader 框架的实现,我们构建了一个清晰、模块化且功能完整的回测系统。
虽然在 BTC-USD 上的初始回测结果与简单的买入持有相比并不突出,但这只是一个数据切片和一组参数的结果。这种探索的真正价值在于:
- 学习回测工具:熟练使用 Backtrader 等强大框架
- 形成假设:利用初始结果来优化策略、探索不同参数,甚至转向全新的想法
从基本想法到盈利稳健的交易系统是一个迭代过程,需要持续的测试、优化和对市场行为的深入理解。这个策略为量化交易的进一步研究和开发提供了一个优秀的起点。
参考文章
加入专注于财经数据与量化投研的知识星球【数据科学实战】,获取完整研究解析、详细回测框架代码实现和完整策略逻辑实操指南。财经数据与量化投研知识社区
核心权益如下:
- 赠送《财经数据宝典》完整文档,汇集多年财经数据维护经验
- 赠送《量化投研宝典》完整文档,汇集多年量化投研领域经验
- 赠送《PyBroker-入门及实战》视频课程,手把手学习量化策略开发
星球已有丰富内容积累,包括量化投研论文、财经高频数据、 PyBroker 视频教程、定期直播、数据分享和答疑解难。适合对量化投研和财经数据分析有兴趣的学习者及从业者。欢迎加入我们!