这份中金公司的金融工程研究报告《量化择时系列(1):金融工程视角下的技术择时艺术》的核心内容总结如下:
核心观点:
报告旨在探讨如何从金融工程的角度,系统性地开发和评估技术择时指标,以克服传统技术分析机械化应用效果不佳的问题。报告提出了一个包含择时逻辑构思、代理指标构造、指标拆解改进、效果评估与外推的完整开发范式,并通过构建一个名为 **QRS (阻力支撑相对强弱) **的技术择时指标作为实例,证明了该范式的有效性。
主要内容与发现:
-
问题提出与挑战:
- 传统技术指标(如均线、MACD)直接应用于A股量化择时效果不佳。
- 主要原因:1)许多指标依赖特定的市场环境(趋势/震荡),而量化模型难以准确预测和切换市场状态;2)指标参数基于历史优化,但市场结构和模式会变化,导致参数失效(“技术模式动量”衰减)。
-
金融工程开发范式:
- 寻找适合量化的择时逻辑: 优先寻找不依赖特定市场行情(牛/熊/震荡)的普适性逻辑。报告以“阻力与支撑相对强度”为例,认为当支撑强度大于阻力强度时,市场倾向上涨,反之亦然。
- 构造代理指标: 将择时逻辑通过量价数据转化为可计算的指标。QRS指标初步通过计算近期最高价序列相对于最低价序列的线性回归斜率
β
来代理阻力支撑相对强度。 - 指标拆解与改进: 对初步指标进行深入分析和优化。
- 对
β
值进行标准化处理 (zscore(β)
)。 - 引入回归的决定系数
R²
作为“惩罚项”,以降低线性关系不显著时的噪音影响,即指标 = zscore(β) * R²
。 - 进一步发现,直接使用
R²
会因其小于1而导致惩罚力度随幂次增加而减弱,因此对其进行滚动均值归一化处理,以保证不同惩罚力度下的量级可比性,最终优化后的形式(隐式为zscore(β) * (R² / RollingMean(R²))
)效果更佳。 - 测试发现,保留最高价与最低价的相关系数
corr(high, low)
在信号项中是必要的,删除后会削弱指标效果。
- 对
- 估计外推有效性: 评估指标在未来应用中的真实能力和适用范围,避免过拟合。
- 提出动态参数模型:通过优化“择时能力系数”(指标值与未来预期收益的相关性),而非依赖净值回测,来动态选择模型参数,减少路径依赖和过拟合。
- 使用动态参数模型在沪深300上进行测试,结果显示QRS指标仍具备显著择时能力,虽然略低于固定参数回测,但提供了更稳健的效果预期(年化收益约15%,夏普比率约1.0)。
- 将动态参数模型应用于上证50、沪深300、中证500、上证综指等多个主流宽基指数,均表现出明显的择时效果,验证了QRS底层逻辑的普适性,但效果在不同指数间存在差异(沪深300上最优,中证500相对稍弱)。
-
QRS 指标核心表现:
- 基于优化后的QRS指标构建的择时策略(固定参数),在沪深300指数上的回测(2007/8 - 2020/12)表现优异:
- 年化收益率:16.1% (同期指数0.7%)
- 夏普比率:0.96 (同期指数0.16)
- 最大回撤:46.2% (同期指数72.3%)
- 开仓胜率:61.1%
- 平均盈亏比:3.2
- 基于优化后的QRS指标构建的择时策略(固定参数),在沪深300指数上的回测(2007/8 - 2020/12)表现优异:
结论:
报告成功构建了一个金融工程视角下的技术择时指标开发框架,并通过QRS指标实例证明了其有效性。QRS指标基于阻力支撑相对强度逻辑,经过细致的构建、拆解与改进,并在多指数、动态参数模型下验证了其择时效果的稳健性与普适性,为量化择时研究提供了一个有价值的范式和工具。
Okay, 让我们基于 Backtrader 框架来实现中金报告中描述的 QRS(阻力支撑相对强弱)择时策略。
请注意:
- 数据准备: 你需要准备符合 Backtrader 格式的 OCHLV 数据(开盘价、收盘价、最高价、最低价、成交量)。通常是一个 CSV 文件,包含
datetime
,open
,high
,low
,close
,volume
列。确保datetime
列被正确解析为日期时间对象。 - 参数选择: 报告中提到了多组参数和改进过程。这里我们实现最终改进后的版本逻辑,但初始参数仍采用报告中较为明确的
N=18
,M=600
,S=0.7
作为示例。实际应用中可能需要根据动态参数模型或重新优化来确定最佳参数。 - 依赖库: 需要安装
backtrader
和pandas
(pip install backtrader pandas matplotlib
)。 - 简化: 实现核心逻辑,省略了报告中动态参数选择的复杂部分,采用固定的阈值
S
。
import backtrader as bt
import backtrader.indicators as btind
import pandas as pd
import datetime
import math
# 1. 定义 QRS 指标 (基于报告最终改进逻辑)
class QRSIndicator(bt.Indicator):
"""
QRS 指标:衡量阻力与支撑的相对强弱。
核心思想:通过线性回归分析近期最高价(high)与最低价(low)的关系。
指标计算:QRS = zscore(β) * (R² / RollingMean(R²))
其中:
β: 最高价对最低价的回归系数 (斜率)
R²: 回归的决定系数 (拟合优度)
zscore(β): β 在过去 M 期的 z-score 值
RollingMean(R²): R² 在过去 M 期的滚动均值 (用于归一化惩罚项)
"""
lines = ('qrs', 'beta', 'r_squared', 'zscore_beta', 'rolling_r2_mean')
params = (
('n', 18), # 回归期 N
('m', 600), # z-score 和滚动 R² 均值的计算期 M
('eps', 1e-10) # 防止除零错误的小值
)
def __init__(self):
# 计算最近 N 期的 OLS 回归
# 使用 OLS_TransformationN 更方便,直接得到 beta (slope) 和 r_squared
# y = high, x = low
ols = btind.OLS_TransformationN(
self.data.high, self.data.low, period=self.p.n
)
self.l.beta = ols.slope # 获取 beta (β)
self.l.r_squared = ols.rsq # 获取 R²
# 计算 β 的 M 期滚动均值和标准差
mean_beta_m = bt.ind.SimpleMovingAverage(self.l.beta, period=self.p.m)
std_beta_m = bt.ind.StandardDeviation(self.l.beta, period=self.p.m)
# 计算 β 的 z-score
# 添加 eps 防止除以零
self.l.zscore_beta = (self.l.beta - mean_beta_m) / (std_beta_m + self.p.eps)
# 计算 R² 的 M 期滚动均值 (用于归一化惩罚项)
# 添加 eps 防止除以零
self.l.rolling_r2_mean = bt.ind.SimpleMovingAverage(self.l.r_squared, period=self.p.m)
# 计算最终的 QRS 指标
# QRS = zscore(β) * (R² / RollingMean(R²))
self.l.qrs = self.l.zscore_beta * (self.l.r_squared / (self.l.rolling_r2_mean + self.p.eps))
super(QRSIndicator, self).__init__() # 调用父类的构造函数
# 2. 定义 QRS 择时策略
class QRSStrategy(bt.Strategy):
"""
QRS 择时策略:
- 当 QRS 指标上穿开仓阈值 S 时,买入。
- 当 QRS 指标下穿平仓阈值 -S 时,卖出(平仓)。
- 其余时间保持仓位不变。
"""
params = (
('n', 18), # QRS 指标参数 N
('m', 600), # QRS 指标参数 M
('signal_threshold', 0.7), # 开/平仓阈值 S
('printlog', True), # 是否打印日志
)
def log(self, txt, dt=None, doprint=False):
""" 策略日志记录函数 """
if self.params.printlog or doprint:
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()} - {txt}')
def __init__(self):
""" 初始化策略 """
self.data_close = self.datas[0].close # 收盘价
self.data_high = self.datas[0].high # 最高价
self.data_low = self.datas[0].low # 最低价
# 初始化 QRS 指标
self.qrs_indicator = QRSIndicator(
self.datas[0], n=self.p.n, m=self.p.m
)
# 使用 bt.ind.CrossOver 检测穿越信号
# 买入信号:QRS 上穿 S
self.buy_signal = bt.ind.CrossOver(self.qrs_indicator.qrs, self.p.signal_threshold)
# 卖出信号:QRS 下穿 -S
self.sell_signal = bt.ind.CrossOver(self.qrs_indicator.qrs, -self.p.signal_threshold)
# 订单状态跟踪
self.order = None
self.buyprice = None
self.buycomm = None
def notify_order(self, order):
""" 订单状态通知 """
if order.status in [order.Submitted, order.Accepted]:
# 订单已提交/接受 - 无需操作
return
# 检查订单是否完成
if order.status in [order.Completed]:
if order.isbuy():
self.log(
f'买入成交, 价格: {order.executed.price:.2f}, '
f'成本: {order.executed.value:.2f}, '
f'手续费: {order.executed.comm:.2f}'
)
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
elif order.issell():
self.log(
f'卖出成交, 价格: {order.executed.price:.2f}, '
f'成本: {order.executed.value:.2f}, '
f'手续费: {order.executed.comm:.2f}'
)
self.bar_executed = len(self) # 记录成交发生的 bar
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('订单 Canceled/Margin/Rejected')
# 重置订单状态
self.order = None
def notify_trade(self, trade):
""" 交易结果通知 """
if not trade.isclosed:
return
self.log(f'交易利润, 毛利润: {trade.pnl:.2f}, 净利润: {trade.pnlcomm:.2f}')
def next(self):
""" 策略核心逻辑 - 每个 K 线 bar 调用一次 """
# 记录当前收盘价
# self.log(f'Close: {self.data_close[0]:.2f}, QRS: {self.qrs_indicator.qrs[0]:.4f}')
# 如果有订单正在处理,则不执行新操作
if self.order:
return
# 检查是否持有仓位
if not self.position:
# 没有仓位,检查买入信号
# if self.qrs_indicator.qrs[0] > self.p.signal_threshold: # 使用原始条件判断
if self.buy_signal[0] > 0: # 使用 CrossOver 判断上穿 (更精确)
self.log(f'发出买入信号, QRS: {self.qrs_indicator.qrs[0]:.4f} > {self.p.signal_threshold}')
# 发出买入订单 (这里使用市价单,也可以改成限价单等)
self.order = self.buy()
else:
# 持有仓位,检查卖出信号
# if self.qrs_indicator.qrs[0] < -self.p.signal_threshold: # 使用原始条件判断
if self.sell_signal[0] < 0: # 使用 CrossOver 判断下穿 (更精确)
self.log(f'发出卖出信号, QRS: {self.qrs_indicator.qrs[0]:.4f} < {-self.p.signal_threshold}')
# 发出卖出订单 (平仓)
self.order = self.close() # 使用 close() 更方便平仓
def stop(self):
""" 策略结束时调用 """
self.log(f'(N={self.p.n}, M={self.p.m}, S={self.p.signal_threshold}) --- 策略结束 ---', doprint=True)
self.log(f'期末总价值: {self.broker.getvalue():.2f}', doprint=True)
# 3. 主程序:运行回测
if __name__ == '__main__':
# 创建 Cerebro 引擎
cerebro = bt.Cerebro()
# 添加策略
cerebro.addstrategy(QRSStrategy, n=18, m=600, signal_threshold=0.7, printlog=False) # 根据需要调整参数和日志
# --- 数据加载 ---
# 假设你的数据文件名为 'your_data.csv'
# 格式: datetime,open,high,low,close,volume (确保 datetime 是第一列且能被 pandas 解析)
try:
dataframe = pd.read_csv('your_data.csv', index_col=0, parse_dates=True)
# !!! 重要:确保数据时间升序排列 !!!
dataframe.sort_index(inplace=True)
# !!! 选择回测时间段 (可选) !!!
# dataframe = dataframe.loc['2010-01-01':'2020-12-31']
# !!! 数据清洗 (可选) !!!
# 检查 NaN 值等
# dataframe.dropna(inplace=True) # 简单处理,可能会丢失数据
# 确保 Backtrader 需要的列存在,即使值是虚拟的
if 'openinterest' not in dataframe.columns:
dataframe['openinterest'] = 0
data_feed = bt.feeds.PandasData(
dataname=dataframe,
datetime=None, # 使用索引作为时间
open='open',
high='high',
low='low',
close='close',
volume='volume',
openinterest='openinterest',
# fromdate=datetime.datetime(2007, 8, 1), # 可选:设置回测开始时间
# todate=datetime.datetime(2020, 12, 31) # 可选:设置回测结束时间
)
cerebro.adddata(data_feed)
except FileNotFoundError:
print("错误:请确保 'your_data.csv' 文件在当前目录下,或提供正确的文件路径。")
print("数据文件应至少包含以下列:datetime, open, high, low, close, volume")
exit()
except Exception as e:
print(f"加载数据时出错: {e}")
exit()
# --- 数据加载结束 ---
# 设置初始资金
start_cash = 1000000.0
cerebro.broker.setcash(start_cash)
# 设置交易手续费 (例如:万分之三)
cerebro.broker.setcommission(commission=0.0003)
# 设置仓位大小 (例如:每次交易投入总资产的 95%)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
# 添加分析器
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe_ratio', timeframe=bt.TimeFrame.Days, compression=1, riskfreerate=0.0) # 年化无风险利率设为0
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns', timeframe=bt.TimeFrame.Days, compression=1) # 按年计算收益
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade_analyzer') # 交易分析
# 打印初始信息
print(f'回测标的: {dataframe.index.name if dataframe.index.name else "Data"}')
print(f'回测期间: {dataframe.index.min()} to {dataframe.index.max()}')
print(f'初始资金: {start_cash:.2f}')
print(f'使用参数: N={QRSStrategy.params.n}, M={QRSStrategy.params.m}, S={QRSStrategy.params.signal_threshold}')
# 运行回测
results = cerebro.run()
strat = results[0] # 获取策略实例
# 打印回测结果
print('\n--- 回测结果分析 ---')
print(f'期末总价值: {cerebro.broker.getvalue():.2f}')
print(f'总收益率: {(cerebro.broker.getvalue() / start_cash - 1) * 100:.2f}%')
# 获取分析器结果
try:
sharpe = strat.analyzers.sharpe_ratio.get_analysis()
# Backtrader 的 SharpeRatio 默认可能不是年化的,取决于数据频率和 timeframe/compression 设置
# 如果数据是日线,上面设置了TimeFrame.Days,需要手动年化 Sharpe
# (假设一年 252 交易日)
print(f"夏普比率 (Sharpe Ratio): {sharpe['sharperatio'] * math.sqrt(252) if sharpe.get('sharperatio') is not None else 'N/A'}")
except KeyError:
print("夏普比率: N/A (可能是数据量不足或未运行足够长时间)")
except Exception as e:
print(f"计算夏普比率时出错: {e}")
try:
returns_analysis = strat.analyzers.returns.get_analysis()
print(f"年化收益率 (Annual Return): {returns_analysis['rnorm100']:.2f}%")
except KeyError:
print("年化收益率: N/A")
except Exception as e:
print(f"计算年化收益率时出错: {e}")
try:
drawdown_analysis = strat.analyzers.drawdown.get_analysis()
print(f"最大回撤 (Max Drawdown): {drawdown_analysis.max.drawdown:.2f}%")
print(f"最大回撤期间长度: {drawdown_analysis.max.len}")
except KeyError:
print("最大回撤: N/A")
except Exception as e:
print(f"计算最大回撤时出错: {e}")
try:
trade_analysis = strat.analyzers.trade_analyzer.get_analysis()
if trade_analysis.total.total > 0:
print(f"总交易次数: {trade_analysis.total.total}")
print(f"盈利交易次数: {trade_analysis.won.total}")
print(f"亏损交易次数: {trade_analysis.lost.total}")
print(f"胜率 (Win Rate): {trade_analysis.won.total / trade_analysis.total.total * 100:.2f}%")
if trade_analysis.lost.total > 0 and trade_analysis.won.pnl.average != 0:
print(f"平均盈亏比 (Avg Win/Loss): {abs(trade_analysis.won.pnl.average / trade_analysis.lost.pnl.average):.2f}")
else:
print("平均盈亏比 (Avg Win/Loss): N/A (无亏损交易或盈利为0)")
else:
print("总交易次数: 0")
except KeyError:
print("交易分析: N/A")
except Exception as e:
print(f"计算交易分析时出错: {e}")
# 绘制结果图表
print("\n正在生成图表...")
try:
# cerebro.plot() # 可能会因为环境问题报错,尤其是非 GUI 环境
cerebro.plot(style='candlestick', barup='red', bardown='green') # 使用更传统的颜色
print("图表生成完毕 (如果窗口未弹出,请检查您的 matplotlib 后端设置)")
except Exception as e:
print(f"绘制图表时出错: {e}")
print("如果您在服务器或无 GUI 环境中运行,绘图可能失败。")
如何使用:
- 保存代码: 将上面的代码保存为 Python 文件(例如
qrs_strategy.py
)。 - 准备数据: 获取你想要回测的标的(例如沪深300指数)的日线数据,保存为
your_data.csv
文件,并放在与 Python 文件相同的目录下。确保 CSV 文件包含datetime
,open
,high
,low
,close
,volume
列,并且datetime
是索引列或第一列,且能被 pandas 识别。 - 运行: 在终端中执行
python qrs_strategy.py
。 - 查看结果: 程序会输出回测的关键指标(期末价值、总收益率、夏普比率、年化收益率、最大回撤、交易统计等),并尝试绘制包含 K 线、QRS 指标和交易标记的图表。
重要提示与可扩展性:
- 参数优化: 报告中的参数是基于特定时期和标的优化的,实际应用需要根据你的目标和数据进行调整或优化。可以使用 Backtrader 的
addoptimizer
功能进行参数寻优。 - 数据质量: 回测结果高度依赖于数据的质量和准确性。
- 动态阈值: 报告中提到了动态参数模型,包括动态选择阈值
S
(例如通过“对折函数”)。这部分实现较为复杂,可以作为进阶改进方向。 - 风险管理: 可以加入止损、止盈等风险管理逻辑。
- 不同市场: QRS 指标声称具有普适性,但应用于不同市场(如期货、外汇)或不同频率数据时,可能需要调整参数或逻辑。
- matplotlib 后端: 如果在服务器或无图形界面的环境中运行,
cerebro.plot()
可能会失败。你可能需要设置 matplotlib 的后端(如Agg
)或取消绘图。
这个实现为你提供了一个基于 Backtrader 的 QRS 策略框架,你可以根据自己的需求进行修改和扩展。