问题描述
解决方案
这是一个关于可转债折溢价套利策略的详细设计与实现方案。
在量化交易中,可转债(Convertible Bond)的套利主要分为两种逻辑:
- 折价套利(无风险/低风险套利):当转债价格 < 转股价值时,买入转债 -> 转股 -> 卖出股票。
- 双低/折溢价轮动(统计套利):买入“价格低 + 溢价率低”的转债,博取估值修复和正股上涨的收益。这是目前资金容量较大且在QMT上最容易实现的策略。
由于QMT回测引擎默认不支持在策略代码中直接模拟“T+1转股”的动作(需要复杂的持仓调整),下文将为你提供一个基于“双低”(价格+溢价率)因子的轮动策略代码。这是可转债最主流的量化策略。
策略逻辑:双低轮动策略
- 核心公式:
- 转股价值 = (100 / 转股价) × 正股价格
- 转股溢价率 = (转债价格 - 转股价值) / 转股价值 × 100%
- 双低值 = 转债价格 + 转股溢价率 × 100
- 选债逻辑:
- 选取市场上双低值最小的前N只转债。
- 过滤掉剩余规模过小(防止流动性风险)或价格过高(防止强赎风险)的标的。
- 交易逻辑:
- 每日或每周进行一次持仓检查。
- 卖出不再属于“双低”排名的持仓,买入新的双低标的。
QMT Python 策略代码
注意:计算溢价率需要正股价格和转股价。
- 正股价格可以通过API获取。
- 转股价的历史数据在标准行情API中通常不直接提供(需要购买额外的F10数据或财务数据包)。
- 为了保证代码可直接运行,本示例采用**“低价策略”**(价格是双低因子的主要权重)作为演示,并保留了双低计算的框架供你填入转股价数据。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 1. 设置资金账号 (请替换为你的实际账号)
ContextInfo.accID = '6000000000'
ContextInfo.set_account(ContextInfo.accID)
# 2. 设置回测参数
ContextInfo.start = '20220101'
ContextInfo.end = '20230101'
ContextInfo.benchmark = '000300.SH' # 参考基准
ContextInfo.capital = 1000000 # 初始资金
# 3. 设定费率 (转债通常无印花税,手续费较低,这里设为万一)
ContextInfo.set_commission(0, [0, 0, 0.0001, 0.0001, 0, 0])
ContextInfo.set_slippage(1, 0.02) # 设置滑点 0.02元
# 4. 策略参数
ContextInfo.hold_num = 10 # 持仓数量
ContextInfo.adjust_period = 1 # 调仓周期(天)
ContextInfo.days_counter = 0 # 计数器
# 5. 设定股票池 (这里为了演示,手动列举了一些转债,实盘可订阅板块)
# 实际使用建议使用 ContextInfo.get_stock_list_in_sector('沪深转债')
# 注意:需要确保本地有这些转债的历史数据
ContextInfo.cb_list = [
'110059.SH', '113050.SH', '128062.SZ', '127061.SZ', '110088.SH',
'123128.SZ', '113642.SH', '128111.SZ', '110052.SH', '123031.SZ',
'127027.SZ', '113632.SH', '118000.SH', '123106.SZ', '113537.SH'
]
ContextInfo.set_universe(ContextInfo.cb_list)
def handlebar(ContextInfo):
"""
K线周期运行函数 (日线回测)
"""
# 跳过非交易时间或未完成的K线
if not ContextInfo.is_last_bar():
return
# 1. 调仓周期判断
index = ContextInfo.barpos
realtime = ContextInfo.get_bar_timetag(index)
ContextInfo.days_counter += 1
if ContextInfo.days_counter % ContextInfo.adjust_period != 0:
return
print(f'>> 开始调仓检测: {timetag_to_datetime(realtime, "%Y-%m-%d")}')
# 2. 获取行情数据
# 获取收盘价
market_data = ContextInfo.get_market_data_ex(
['close', 'vol'],
ContextInfo.cb_list,
period='1d',
count=1,
subscribe=True
)
if not market_data:
return
# 3. 计算因子 (双低 = 价格 + 溢价率*100)
# 由于API直接获取历史转股价较难,这里演示【低价策略】(双低策略的简化版)
# 如果你有转股价数据,可解开下方注释进行完整双低计算
factor_data = []
for code, df in market_data.items():
if df.empty:
continue
close_price = df['close'].iloc[-1]
volume = df['vol'].iloc[-1]
# 过滤停牌或无成交的标的
if volume <= 0 or np.isnan(close_price):
continue
# --- 完整双低策略逻辑扩展区域 (需要外部数据源) ---
# underlying_code = get_underlying_stock(code) # 需自定义映射函数
# stock_price = get_stock_price(underlying_code)
# convert_price = get_convert_price(code) # 难点:需获取历史转股价
# convert_value = (100.0 / convert_price) * stock_price
# premium_rate = (close_price - convert_value) / convert_value * 100
# double_low = close_price + premium_rate
# -------------------------------------------
# 【演示逻辑】:使用“价格”作为排序因子 (低价转债通常溢价率也较低,具有债底保护)
# 过滤掉价格过高(>130)的转债,防止追高
if close_price < 130:
factor_data.append({
'code': code,
'factor': close_price, # 此处用价格代替双低值
'price': close_price
})
# 4. 排序选债
# 按因子从小到大排序 (低价优先)
df_factor = pd.DataFrame(factor_data)
if df_factor.empty:
return
df_factor = df_factor.sort_values(by='factor', ascending=True)
# 取前N只
target_list = df_factor['code'].head(ContextInfo.hold_num).tolist()
print(f"目标持仓: {target_list}")
# 5. 交易执行
# 获取当前持仓
positions = get_trade_detail_data(ContextInfo.accID, 'stock', 'position')
current_holdings = [obj.m_strInstrumentID + '.' + obj.m_strExchangeID for obj in positions]
# 卖出不在目标列表中的
for code in current_holdings:
if code not in target_list:
# 检查是否是转债持仓(防止误卖其他股票)
if code in ContextInfo.cb_list:
passorder(24, 1101, ContextInfo.accID, code, 14, -1, 0, ContextInfo)
print(f"卖出: {code}")
# 买入目标列表中的
# 简单均仓模型:总资金 / 目标数量
# 注意:实盘中需考虑可用资金
target_value = ContextInfo.capital / ContextInfo.hold_num
for code in target_list:
# 使用 order_target_value 自动调整到目标市值
# 注意:转债一手是10张,约1000元,QMT会自动向下取整
order_target_value(code, target_value, ContextInfo, ContextInfo.accID)
print(f"调整持仓: {code} 至市值 {target_value}")
# 辅助函数:时间戳转换
def timetag_to_datetime(timetag, format_str):
import time
return time.strftime(format_str, time.localtime(timetag / 1000))
代码关键点解析
-
数据获取 (
get_market_data_ex):- 使用了
get_market_data_ex批量获取转债的收盘价。这是QMT中最高效的数据获取方式。 - 注意:你需要确保在QMT客户端的“数据管理”中下载了转债的历史日线数据,否则回测会没有数据。
- 使用了
-
因子计算 (核心逻辑):
- 代码中演示的是低价策略(
factor = close_price)。这是因为在纯API环境下,获取历史动态的“转股价”比较困难。 - 如何升级为真正的双低策略?
- 你需要准备一份包含历史转股价的CSV文件,或者通过
ContextInfo.get_financial_data尝试获取(但转股价变动通常属于公告事件,不在标准财报字段中)。 - 如果是在实盘运行(非回测),你可以通过
get_full_tick获取当天的实时转股溢价率(部分券商行情源支持),或者手动维护一个转股价字典。
- 你需要准备一份包含历史转股价的CSV文件,或者通过
- 代码中演示的是低价策略(
-
交易执行 (
order_target_value):- 使用了
order_target_value函数。这个函数非常方便,它会自动计算当前持仓和目标市值的差额,自动下单买入或卖出,使该标的持仓市值达到指定金额。
- 使用了
-
标的池 (
set_universe):- 代码中硬编码了一个
cb_list。在实际使用中,建议使用ContextInfo.get_stock_list_in_sector('沪深转债')来获取全市场转债,并配合get_instrument_detail过滤掉已退市的品种。
- 代码中硬编码了一个
如何进行折价套利(进阶)
如果你想做真正的折价套利(买入转债->转股),逻辑如下,但QMT回测不支持,仅限实盘参考:
- 监控:实时计算
溢价率。 - 触发:当
溢价率 < -1.5%(覆盖交易成本) 时。 - 操作:
buy_open买入转债。- 调用券商特定的转股接口(QMT标准API无转股函数,通常需要通过
passorder发送特定的指令代码,如上海市场通常是卖出操作,代码为转股代码)。 - 风险对冲:由于转股得到的股票T+1才能卖,你需要在期货市场(如IC/IF)做空,或者融券卖出正股(如果能融到券)来锁定T+0的利润。
常见问题 Q&A
Q: 为什么代码里没有计算转股溢价率?
A: QMT的标准API主要提供行情和基础财务数据。历史上的“转股价”是随分红、下修等事件变动的,标准API很难直接获取准确的历史转股价序列。对于回测,建议先下载好包含溢价率的外部数据(如CSV),在 init 中读取到内存中使用。
Q: 如何在实盘中获取实时溢价率?
A: 在实盘模式下,你可以通过订阅Level-2数据或计算实时数据:
转股价值 = (100 / 当前转股价) * 正股现价。
你需要维护一个当前转股价的字典(可以每天早上从外部数据源更新一次)。
Q: 回测没有成交?
A: 1. 检查是否下载了“沪深转债”的日线数据。2. 检查 ContextInfo.cb_list 中的代码格式是否正确(如 110059.SH)。3. 检查初始资金是否足够买入一手转债(约1000元)。