🚀 AI 一键生成 qmt 策略代码

如何构建一个股票-期货对冲策略,在 QMT 中实现 Beta 套期保值?

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

问题描述

我想写一个对冲策略,用股指期货来对冲我的股票持仓风险,应该怎么做?

解决方案

这是一个非常经典的量化交易应用场景。在 QMT 中实现股指期货对冲策略,核心逻辑是持有股票多头(Long)的同时,建立股指期货空头(Short)头寸,以抵消市场系统性风险(Beta 风险)。

以下是实现该策略的详细步骤、逻辑解析以及完整的 Python 代码示例。

策略逻辑解析

  1. 计算股票持仓市值:实时获取账户中所有股票的总市值。
  2. 选择对冲标的:根据股票池的属性选择对应的股指期货:
    • 大盘股(如上证50成分):选择 IH (上证50股指期货)
    • 蓝筹股(如沪深300成分):选择 IF (沪深300股指期货)
    • 中小盘(如中证500成分):选择 IC (中证500股指期货)
    • 小盘股(如中证1000成分):选择 IM (中证1000股指期货)
  3. 计算需对冲的手数
    $$ \text{目标空单手数} = \text{round}\left( \frac{\text{股票总市值} \times \text{对冲比例}}{\text{期货价格} \times \text{合约乘数}} \right) $$
  4. 动态调整仓位
    • 如果当前空单 < 目标手数:卖出开仓(加空)。
    • 如果当前空单 > 目标手数:买入平仓(减空)。

QMT 策略代码实现

以下代码展示了一个完整的对冲策略。为了演示方便,策略会在初始化时买入一篮子股票,随后在每个行情切片(handlebar)动态调整期货空单数量以保持对冲。

# -*- coding: gbk -*-
import math

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置账号信息 (请替换为您真实的资金账号)
    # 假设股票和期货在同一个资金账户下,如果是分开的,请分别设置
    ContextInfo.account_id = 'YOUR_ACCOUNT_ID' 
    ContextInfo.account_type = 'STOCK' # 这里主要用于股票下单,期货下单函数会单独指定
    ContextInfo.set_account(ContextInfo.account_id)
    
    # 2. 策略参数设置
    ContextInfo.hedge_ratio = 1.0      # 对冲比例,1.0 表示 100% 等市值对冲
    ContextInfo.future_product = 'IF'  # 对冲标的品种:IF(沪深300), IC(中证500), IH(上证50)
    ContextInfo.future_exchange = 'IF' # 交易所代码: IF(中金所)
    
    # 3. 定义股票池 (示例:买入几只沪深300成分股)
    ContextInfo.stock_list = ['600000.SH', '600036.SH', '600519.SH']
    
    # 4. 设定定时器或变量,控制股票买入只执行一次 (仅作演示用)
    ContextInfo.stocks_bought = False

    print("对冲策略初始化完成")

def handlebar(ContextInfo):
    """
    K线/行情驱动函数
    """
    # 获取当前K线索引
    index = ContextInfo.barpos
    # 获取当前时间
    realtime = ContextInfo.get_bar_timetag(index)
    
    # ----------------------------------------------------------------
    # 第一步:构建股票多头持仓 (仅演示,实盘中可能已有持仓)
    # ----------------------------------------------------------------
    if not ContextInfo.stocks_bought:
        print("开始构建股票多头仓位...")
        for stock in ContextInfo.stock_list:
            # 按最新价买入 1000 股
            order_shares(stock, 1000, 'LATEST', 0, ContextInfo, ContextInfo.account_id)
        ContextInfo.stocks_bought = True
        # 下单后直接返回,等待下一根bar或tick数据更新后再计算对冲,确保成交数据同步
        return

    # ----------------------------------------------------------------
    # 第二步:获取主力合约代码
    # ----------------------------------------------------------------
    # 拼接主力合约代码形式,如 'IF.IF'
    generic_code = ContextInfo.future_product + '.' + ContextInfo.future_exchange
    # 获取当前主力合约,如 'IF2306.IF'
    main_contract = ContextInfo.get_main_contract(generic_code)
    
    if not main_contract:
        print(f"未获取到主力合约: {generic_code}")
        return

    # ----------------------------------------------------------------
    # 第三步:计算股票持仓总市值
    # ----------------------------------------------------------------
    total_stock_value = 0.0
    
    # 获取持仓对象列表
    positions = get_trade_detail_data(ContextInfo.account_id, 'STOCK', 'POSITION')
    
    for pos in positions:
        # 过滤掉非股票持仓 (假设只对冲股票)
        # m_nVolume: 持仓数量, m_dLastPrice: 最新价
        # 注意:实盘中建议使用 m_dMarketValue (市值),但需确保数据已更新
        # 这里手动计算更稳健:数量 * 最新价
        
        # 获取股票最新行情
        quote = ContextInfo.get_market_data_ex(['close'], [pos.m_strInstrumentID], period='tick', count=1)
        if pos.m_strInstrumentID in quote and not quote[pos.m_strInstrumentID].empty:
            current_price = quote[pos.m_strInstrumentID].iloc[-1]['close']
            market_value = pos.m_nVolume * current_price
            total_stock_value += market_value
    
    # ----------------------------------------------------------------
    # 第四步:计算期货对冲目标手数
    # ----------------------------------------------------------------
    # 获取期货合约行情
    future_quote = ContextInfo.get_market_data_ex(['close'], [main_contract], period='tick', count=1)
    if main_contract not in future_quote or future_quote[main_contract].empty:
        print(f"未获取到期货行情: {main_contract}")
        return
        
    future_price = future_quote[main_contract].iloc[-1]['close']
    
    # 获取合约乘数 (例如 IF 是 300)
    multiplier = ContextInfo.get_contract_multiplier(main_contract)
    
    # 计算单张合约价值
    contract_value = future_price * multiplier
    
    if contract_value == 0:
        return

    # 计算理论需要持有的空单手数 (四舍五入)
    # 公式:(股票总市值 * 对冲比例) / 单张合约价值
    target_short_lots = round((total_stock_value * ContextInfo.hedge_ratio) / contract_value)
    
    print(f"股票市值: {total_stock_value:.2f}, 期货价格: {future_price}, 目标空单手数: {target_short_lots}")

    # ----------------------------------------------------------------
    # 第五步:获取当前期货持仓并调整
    # ----------------------------------------------------------------
    current_short_lots = 0
    
    # 获取期货持仓
    # 注意:期货账号类型通常是 'FUTURE',如果资金账号是同一个,ID可能相同
    future_positions = get_trade_detail_data(ContextInfo.account_id, 'FUTURE', 'POSITION')
    
    for pos in future_positions:
        # 找到当前主力合约的空单
        # m_nDirection: 48是多(ENTRUST_BUY), 49是空(ENTRUST_SELL)
        if pos.m_strInstrumentID == main_contract and pos.m_nDirection == 49:
            current_short_lots = pos.m_nVolume
            break
            
    # 计算偏差
    diff = target_short_lots - current_short_lots
    
    # ----------------------------------------------------------------
    # 第六步:执行交易
    # ----------------------------------------------------------------
    if diff > 0:
        # 需要增加空单:卖出开仓
        print(f"执行对冲:卖出开仓 {diff} 手 {main_contract}")
        # opType=3 (开空), orderType=1101 (单股/单账号/普通/股手方式)
        # price=-1 (使用prType控制), prType=14 (对手价), 5 (最新价)
        passorder(3, 1101, ContextInfo.account_id, main_contract, 14, -1, diff, ContextInfo)
        
    elif diff < 0:
        # 需要减少空单:买入平仓
        # 注意:期货平仓区分平今(CloseToday)和平昨(CloseYesterday)
        # 这里为了简化,使用智能平仓或优先平今
        qty_to_close = abs(diff)
        print(f"执行对冲:买入平仓 {qty_to_close} 手 {main_contract}")
        
        # opType=6 (平多优先平今 - 错误,这里是平空), 
        # 正确的 opType: 
        # 8: 平空,优先平今
        # 9: 平空,优先平昨
        passorder(8, 1101, ContextInfo.account_id, main_contract, 14, -1, qty_to_close, ContextInfo)

    else:
        print("当前对冲仓位合适,无需调整")

关键点说明

  1. 合约乘数 (get_contract_multiplier)
    • 这是计算对冲数量的关键。IF/IH 的乘数通常是 300,IC/IM 是 200。必须通过 API 动态获取,不要写死,以防规则变动。
  2. 主力合约 (get_main_contract)
    • 股指期货有到期日,代码会变(如 IF2306 -> IF2307)。使用 get_main_contract('IF.IF') 可以自动获取当前流动性最好的合约,避免手动改代码。
  3. 市值计算
    • 代码中通过遍历持仓并乘以最新价来计算市值。这是因为 get_trade_detail_data 返回的 m_dMarketValue 属性在某些情况下更新可能有延迟,手动计算更实时。
  4. 交易函数 (passorder)
    • 期货交易与股票不同,有“开仓”和“平仓”之分。
    • 开空 (Short Open)opType = 3
    • 平空 (Short Close)opType = 8 (平空优先平今) 或 9 (平空优先平昨)。
  5. Beta 调整 (进阶)
    • 上述代码假设 Beta = 1(即股票组合波动与指数完全一致)。
    • 如果您的股票组合波动率远大于指数(例如全是高科技股),您可能需要计算组合的 Beta 值,然后调整公式为:
      目标手数 = (股票市值 * Beta) / 合约价值

常见问题 Q&A

Q: 这个策略可以直接回测吗?
A: 可以。在 QMT 的“模型研究”或“策略回测”界面,设置好回测区间、初始资金和基准,直接运行即可。注意回测时需要下载对应的期货和股票历史数据。

Q: 如何处理合约换月(移仓换月)?
A: 上述代码使用了 get_main_contract。当主力合约发生切换时(例如从 06 合约切到 07 合约),代码逻辑会发现当前持有的旧合约空单不等于新主力合约的目标手数(新合约持仓为0),从而在新合约上开空。
但是,上述简化代码没有处理旧合约的平仓逻辑。在实盘中,完善的逻辑应该是:检测到主力合约变更 -> 平掉旧合约持仓 -> 在新合约开仓。或者简单地让策略在旧合约到期前自动平仓。

Q: 为什么使用 get_market_data_ex 而不是 get_market_data
A: get_market_data_ex 是 QMT 推荐的新版行情接口,效率更高,且返回的数据结构(Pandas DataFrame)更易于处理。

Q: 资金账号怎么填?
A: 如果您的股票和期货在同一个资金账号下(全能账户),填一个即可。如果是分开的,需要在 passorder 中分别传入对应的账号 ID。