🚀 AI 一键生成 qmt 策略代码

可转债双低策略(价格+溢价率)Python 源码解析

qmt | 发布时间: 2025/12/21
以下内容由 EasyQuant 生成。

问题描述

如何编写可转债双低轮动策略?

解决方案

这是一个关于如何在 QMT (Quantitative Trading Platform) 中编写 可转债双低轮动策略 的详细指南。

策略逻辑概述

“双低” 是可转债投资中最经典的策略之一,核心逻辑是寻找 价格低转股溢价率低 的转债。

  1. 双低公式:通常定义为 双低值 = 转债价格 + 转股溢价率 * 100
  2. 选债逻辑:在全市场可转债中,剔除停牌、临期(即将到期)、强赎的转债,按照“双低值”从小到大排序,选取排名前 N 只(例如前 10 只)。
  3. 轮动逻辑
    • 定期调仓:每天或每周检查一次。
    • 卖出:如果持仓的转债跌出排名前 N(或前 M,设置缓冲区)的范围,则卖出。
    • 买入:买入新进入排名前 N 的转债。
    • 权重:通常采用等权买入。

关键难点:数据获取

在 QMT 原生环境中,直接获取历史的“转股溢价率”可能不如股票行情那么直接。通常有两种方法:

  1. 计算法:获取转债价格、正股价格、转股价,自行计算(需要维护历史转股价数据,较复杂)。
  2. 引用扩展数据(推荐):将计算好的“转股溢价率”作为扩展数据导入 QMT,或者如果你的券商数据源(如 Wind/Choice)已集成在 QMT 中,可以直接调用相关字段。

本代码示例将演示标准的轮动框架,并假设你已经可以通过 ext_data (扩展数据) 或自定义函数获取到溢价率数据。

QMT 策略代码实现

# -*- coding: gbk -*-
import pandas as pd
import numpy as np

'''
策略名称:可转债双低轮动策略
策略逻辑:
1. 选取全市场可转债。
2. 计算双低值 = 价格 + 溢价率 * 100。
3. 每日轮动,持有双低值最小的前N只。
4. 资金等权分配。
'''

def init(ContextInfo):
    # --- 策略参数设置 ---
    ContextInfo.holding_num = 10      # 持仓数量
    ContextInfo.adjust_period = 1     # 调仓频率(天)
    ContextInfo.buy_threshold = 130   # 价格安全线(可选):价格高于此值不买
    
    # --- 账户设置 ---
    # 设置资金账号,实盘中请替换为真实账号
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID' 
    ContextInfo.account_type = 'STOCK'
    ContextInfo.set_account(ContextInfo.account_id)
    
    # --- 费率设置 (回测用) ---
    # 佣金万分之三,印花税0 (转债无印花税)
    ContextInfo.set_commission(0, [0, 0, 0.0003, 0.0003, 0, 5])
    ContextInfo.set_slippage(1, 0.02) # 设置滑点 0.02元

    # --- 股票池设置 ---
    # 获取沪深转债成分股
    # 注意:板块名称可能因券商版本略有不同,通常为 '上证转债', '深证转债'
    sh_bonds = ContextInfo.get_stock_list_in_sector('上证转债')
    sz_bonds = ContextInfo.get_stock_list_in_sector('深证转债')
    ContextInfo.universe = list(set(sh_bonds + sz_bonds))
    
    print(f"策略初始化完成,转债池数量: {len(ContextInfo.universe)}")

def handlebar(ContextInfo):
    # 跳过历史K线,只在最后根K线或回测的每一天执行
    # 如果是实盘,通常建议使用定时器 run_time 触发,这里演示标准 handlebar 逻辑
    if not ContextInfo.is_last_bar() and ContextInfo.do_back_test:
        # 回测模式下,每根K线都运行
        pass
    elif not ContextInfo.is_last_bar():
        # 实盘模式下,历史K线不运行
        return

    # 控制调仓频率
    bar_index = ContextInfo.barpos
    if bar_index % ContextInfo.adjust_period != 0:
        return

    print(f"--- 开始执行调仓逻辑,当前Bar索引: {bar_index} ---")
    
    # 1. 获取当前时间
    current_date = ContextInfo.get_bar_timetag(bar_index)
    # 转换为 YYYYMMDD 格式字符串,用于获取数据
    date_str = timetag_to_datetime(current_date, '%Y%m%d')

    # 2. 获取行情数据 (收盘价)
    # 注意:get_market_data_ex 效率更高
    market_data = ContextInfo.get_market_data_ex(
        ['close', 'vol'], 
        ContextInfo.universe, 
        period='1d', 
        count=1,
        subscribe=True
    )
    
    # 3. 构建数据表
    data_list = []
    for code in ContextInfo.universe:
        if code in market_data:
            df_code = market_data[code]
            if not df_code.empty:
                close_price = df_code.iloc[-1]['close']
                volume = df_code.iloc[-1]['vol']
                
                # 过滤停牌 (成交量为0通常意味着停牌)
                if volume == 0:
                    continue
                
                # 过滤价格过高的 (安全边际)
                if close_price > ContextInfo.buy_threshold:
                    continue
                
                # --- 获取转股溢价率 (关键步骤) ---
                # 方法A: 如果你有扩展数据 (ext_data),推荐使用此方法
                # premium_rate = ContextInfo.get_ext_data('转股溢价率', code, 0, ContextInfo)
                
                # 方法B: 模拟计算 (此处为了代码可运行,使用模拟数据或简化逻辑)
                # 在实际生产中,你需要自行计算:(转债价 - 转股价值)/转股价值
                # 这里我们假设 premium_rate 为 0 (仅作演示,实盘必须替换!)
                premium_rate = 0.20 # 假设 20%
                
                # 计算双低值
                # 双低 = 价格 + 溢价率 * 100
                double_low = close_price + premium_rate * 100
                
                data_list.append({
                    'code': code,
                    'close': close_price,
                    'double_low': double_low
                })
    
    if not data_list:
        print("无符合条件的转债")
        return

    df = pd.DataFrame(data_list)
    
    # 4. 排序并筛选
    # 按双低值从小到大排序
    df = df.sort_values(by='double_low', ascending=True)
    
    # 取前 N 只
    target_list = df.head(ContextInfo.holding_num)['code'].tolist()
    print(f"今日目标持仓: {target_list}")

    # 5. 交易执行 (轮动)
    rebalance_portfolio(ContextInfo, target_list)

def rebalance_portfolio(ContextInfo, target_list):
    '''
    调仓函数:卖出不在目标池的,买入在目标池的
    '''
    # 获取当前持仓
    positions = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'POSITION')
    current_holdings = [obj.m_strInstrumentID + '.' + obj.m_strExchangeID for obj in positions if obj.m_nVolume > 0]
    
    # 1. 卖出逻辑
    for code in current_holdings:
        if code not in target_list:
            # 卖出所有持仓
            passorder(24, 1101, ContextInfo.account_id, code, 14, -1, 1, ContextInfo)
            print(f"卖出: {code}")
    
    # 2. 买入逻辑
    # 计算单只转债目标市值 (总资产 / 计划持仓数)
    # 注意:这里简单使用总资产计算,实盘需考虑可用资金
    account_info = get_trade_detail_data(ContextInfo.account_id, ContextInfo.account_type, 'ACCOUNT')
    if not account_info:
        return
        
    total_asset = account_info[0].m_dBalance
    target_value_per_bond = total_asset / ContextInfo.holding_num
    
    for code in target_list:
        if code not in current_holdings:
            # 使用 order_target_value 更加方便,直接调整到目标市值
            # 注意:QMT 的 order_target_value 对股票/转债通常有效,
            # 如果无效,需手动计算 volume = target_value / price
            
            # 获取最新价用于计算手数
            last_price = ContextInfo.get_market_data_ex(['close'], [code], period='1d', count=1)[code].iloc[-1]['close']
            
            # 计算买入数量 (向下取整到10张,转债1手=10张)
            # 注意:QMT下单通常以“股”为单位,转债1张面值100,但在交易接口中通常1手=10张。
            # 这里的 volume 单位取决于券商接口,通常填 10 代表 10张(1手) 或者 100 代表 100元面值
            # 建议使用 order_value 接口让系统自动计算
            
            order_target_value(code, target_value_per_bond, ContextInfo, ContextInfo.account_id)
            print(f"买入/调仓: {code}, 目标市值: {target_value_per_bond}")

# 辅助函数:时间戳转字符串
def timetag_to_datetime(timetag, format_str):
    import time
    return time.strftime(format_str, time.localtime(timetag / 1000))

代码关键点解析

  1. 股票池获取 (get_stock_list_in_sector)

    • 使用 '上证转债''深证转债' 板块获取全市场转债代码。这是最方便的方法,无需手动维护列表。
  2. 双低值计算 (double_low)

    • 代码中使用了 double_low = close_price + premium_rate * 100
    • 重要提示:代码中的 premium_rate 被赋值为固定值仅作演示。在实际使用中,你需要:
      • 方案一:如果你的 QMT 终端接入了 Wind 或 Choice 数据,可能在 get_market_data 的字段中直接支持 'premium_rate'
      • 方案二(最常用):使用 Python 的 ext_data 接口。你需要先编写一个脚本每天计算溢价率并存入 QMT 的扩展数据中,然后在策略里通过 ContextInfo.get_ext_data() 读取。
  3. 交易执行 (rebalance_portfolio)

    • 使用了 order_target_value。这是一个高级交易函数,它会自动计算当前持仓和目标市值的差额,进行买入或卖出,非常适合轮动策略。
    • 注意:转债交易通常是 T+0,但在轮动策略中我们通常按日级别处理。
  4. 过滤逻辑

    • 代码中简单过滤了停牌(成交量为0)和价格过高(大于130)的转债。
    • 实盘中建议增加强赎过滤。由于强赎公告是非结构化数据,通常需要维护一个“强赎黑名单”列表,在代码中 if code in blacklist: continue

如何完善“转股溢价率”数据?

如果你没有现成的数据源,可以在 handlebar 中尝试自行计算(不推荐,因为需要历史转股价),或者使用以下逻辑结构读取外部数据:

# 假设你已经将溢价率数据导入到了名为 'premium' 的扩展数据中
import numpy as np

# 在循环中:
premium_rate = ContextInfo.get_ext_data('premium', code, 0, ContextInfo)

# 检查数据有效性
if np.isnan(premium_rate):
    continue # 如果没有数据则跳过

double_low = close_price + premium_rate # 注意单位,有的数据源是百分比(20),有的是小数(0.2)

总结

这个策略模板提供了一个完整的 QMT 轮动框架。要将其用于实盘,你只需要解决转股溢价率的数据源问题,并根据你的风险偏好调整持仓数量 (holding_num) 和调仓频率 (adjust_period)。

SEO Q&A

Q: QMT中如何获取可转债的转股溢价率?
A: QMT 标准行情接口通常不直接包含转股溢价率。用户通常需要通过“扩展数据”功能导入,或者如果购买了 Level-2 或专业数据服务(如 Wind/Choice 插件),可以通过特定的字段代码获取。

Q: 可转债双低策略适合什么市场环境?
A: 双低策略在震荡市和牛市初期表现较好,因为它兼具债底保护(低价格)和正股上涨的弹性(低溢价)。在极度熊市中,需注意低价转债违约风险;在疯牛市中,双低转债可能跑输高价妖债。

Q: QMT回测可转债策略需要注意什么?
A: 1. 费率设置:转债通常无印花税,佣金较低。2. 数据除权:转债价格通常不复权,但要注意强赎退市的数据处理。3. 强赎风险:回测很难完全模拟强赎公告发布后的暴跌,需在策略逻辑中加入价格止盈(如价格>130或140强制卖出)来模拟规避强赎风险。