问题描述
解决方案
这是一个基于 QMT 平台的股票债券风险平价(Risk Parity)策略实现。
策略逻辑说明
- 核心思想:风险平价策略的核心不是让资产金额相等,而是让资产对组合的风险贡献相等。通常波动率高的资产(如股票)配置权重低,波动率低的资产(如债券)配置权重高。
- 标的选择:
- 股票端:使用
510300.SH(沪深300ETF) 代表权益类资产。 - 债券端:使用
511010.SH(国债ETF) 代表债券类资产。
- 股票端:使用
- 计算方法:
- 获取过去 N 天(如 60 天)的历史收盘价。
- 计算日收益率序列的标准差(即波动率)。
- 权重计算公式:$w_i = \frac{1/\sigma_i}{\sum (1/\sigma_j)}$。
- 调仓机制:按月调仓(每月第一个交易日进行再平衡)。
QMT 策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
import datetime
def init(ContextInfo):
"""
初始化函数,设定策略参数和标的
"""
# 1. 设定标的:股票ETF(沪深300) 和 债券ETF(国债ETF)
# 注意:实盘请确保这两个标的在您的交易权限内,且有足够的流动性
ContextInfo.stock_code = '510300.SH'
ContextInfo.bond_code = '511010.SH'
ContextInfo.assets = [ContextInfo.stock_code, ContextInfo.bond_code]
# 2. 设定资金账号 (请修改为您自己的资金账号)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID'
ContextInfo.set_account(ContextInfo.account_id)
# 3. 策略参数
ContextInfo.lookback_window = 60 # 回看窗口,用于计算波动率 (天)
ContextInfo.rebalance_period = 'monthly' # 调仓周期
# 4. 记录上一次调仓的月份,用于判断是否跨月
ContextInfo.last_month = -1
print("风险平价策略初始化完成")
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前K线的时间
bar_index = ContextInfo.barpos
current_timetag = ContextInfo.get_bar_timetag(bar_index)
# 将时间戳转换为 datetime 对象以便提取月份
current_date_str = timetag_to_datetime(current_timetag, '%Y%m%d')
current_date = datetime.datetime.strptime(current_date_str, '%Y%m%d')
current_month = current_date.month
# 判断是否需要调仓:
# 如果是回测的第一根K线,或者当前月份与上一次记录的月份不同,则触发调仓
is_rebalance_time = False
if ContextInfo.last_month == -1:
is_rebalance_time = True
elif current_month != ContextInfo.last_month:
is_rebalance_time = True
# 更新月份记录
ContextInfo.last_month = current_month
# 执行调仓逻辑
if is_rebalance_time:
print(f"日期: {current_date_str}, 触发调仓...")
rebalance(ContextInfo)
def rebalance(ContextInfo):
"""
计算风险平价权重并执行调仓
"""
# 1. 获取历史数据
# 使用 get_market_data_ex 获取多只股票的历史数据
# count 设置为 lookback_window + 1 是为了计算收益率时不会少数据
market_data = ContextInfo.get_market_data_ex(
['close'],
ContextInfo.assets,
period='1d',
count=ContextInfo.lookback_window + 1,
dividend_type='front', # 前复权
subscribe=True
)
volatilities = {}
# 2. 计算波动率
for code in ContextInfo.assets:
df = market_data.get(code)
# 数据校验
if df is None or df.empty or len(df) < 20:
print(f"警告: {code} 数据不足,跳过本次调仓")
return
# 计算日收益率
returns = df['close'].pct_change().dropna()
# 计算标准差 (波动率)
vol = returns.std()
# 防止波动率为0导致除零错误
if vol == 0:
vol = 0.0001
volatilities[code] = vol
print(f"标的: {code}, 历史波动率: {vol:.6f}")
# 3. 计算风险平价权重
# 倒数波动率
inv_vols = {k: 1.0 / v for k, v in volatilities.items()}
sum_inv_vol = sum(inv_vols.values())
# 归一化权重
target_weights = {k: v / sum_inv_vol for k, v in inv_vols.items()}
print("目标权重计算结果:", target_weights)
# 4. 执行交易
for code, weight in target_weights.items():
# 使用 order_target_percent 自动调整仓位到目标比例
# 注意:实盘中 order_target_percent 需要配合正确的账号类型和资产查询才能精确运作
# 回测模式下该函数非常准确
order_target_percent(code, weight, ContextInfo, ContextInfo.account_id)
print(f"下单: 调整 {code} 仓位至 {weight*100:.2f}%")
代码关键点解析
-
# -*- coding: gbk -*-:- QMT 的 Python 编辑器默认编码通常是 GBK,加上这行代码可以防止中文注释乱码或运行报错。
-
get_market_data_ex:- 这是 QMT 获取历史行情推荐使用的接口。
dividend_type='front':计算策略收益率和波动率时,必须使用前复权数据,否则分红派息造成的缺口会被误判为大幅波动。
-
波动率倒数加权:
- 代码中
inv_vols = {k: 1.0 / v ...}实现了风险平价的核心逻辑。如果股票波动率是债券的 3 倍,那么债券的配置权重将大约是股票的 3 倍,从而使得两者对组合的风险贡献接近。
- 代码中
-
order_target_percent:- 这是一个非常方便的交易函数,它会自动计算当前持仓与目标持仓的差额,并进行买入或卖出操作。例如,如果目标权重是 20%,当前持仓是 0%,它会买入 20%;如果当前是 30%,它会卖出 10%。
使用建议
-
回测设置:
- 在 QMT 界面点击“模型回测”。
- 主图指标:随便选一个,例如
510300.SH。 - 周期:选择 日线 (1d)。
- 初始资金:建议设置 100万 以上,因为债券ETF价格通常较高或波动小,资金太少可能无法精确分配仓位。
-
实盘注意:
- 请务必将
ContextInfo.account_id修改为您真实的资金账号。 - 实盘运行时,建议将
handlebar中的打印语句保留,以便在日志中监控调仓行为。 - ETF 交易通常有最小单位(100股/份),
order_target_percent会自动向下取整到整手。
- 请务必将
Q&A
Q: 为什么选择 ETF 而不是具体的股票和国债?
A: ETF(交易型开放式指数基金)具有流动性好、费率低、自动分散个股风险的特点,非常适合作为资产配置策略的底层工具。
Q: 如何调整调仓频率?
A: 代码中通过 current_month != ContextInfo.last_month 实现了月度调仓。如果需要周度调仓,可以利用 ContextInfo.get_bar_timetag 获取时间后,计算周数变化。
Q: 波动率计算窗口(Lookback Window)设为多少合适?
A: 代码中默认为 60 天(约一个季度)。窗口越短,对近期市场变化越敏感,但换手率会变高;窗口越长,权重变化越平滑。通常 60-120 天是比较常用的参数。