🚀 AI 一键生成 ptrade 策略代码

PTrade 期货进阶:如何基于 Contango 和 Backwardation 结构制定交易策略?

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

问题描述

如何编写一个基于期货期限结构(Contango/Backwardation)的交易策略?

解决方案

这是一个基于期货期限结构(Term Structure)的量化交易策略设计。

1. 策略原理:期限结构(Contango 与 Backwardation)

期货期限结构是指同一品种、不同到期月份合约价格之间的关系。

  • Contango(正向市场/升水): 远月价格 > 近月价格。
    • 含义:通常代表持有成本(仓储、资金成本)较高,或者市场预期未来价格上涨。
    • 策略逻辑:做空近月合约(或主力合约)。因为随着时间推移,期货价格会向现货价格收敛,多头需支付“展期成本”,空头则获得“展期收益”。
  • Backwardation(反向市场/贴水): 远月价格 < 近月价格。
    • 含义:通常代表现货供应紧张(便利收益高),市场愿意支付溢价购买即期商品。
    • 策略逻辑:做多近月合约(或主力合约)。多头获得“展期收益”。

核心逻辑:

  1. 获取某品种(如螺纹钢 RB)的所有活跃合约。
  2. 根据成交量或持仓量找出主力合约(流动性最好)和次主力合约(通常是远月)。
  3. 计算价差:价差 = 主力合约价格 - 次主力合约价格
  4. 交易信号
    • 主力 > 远月 (Backwardation):做多主力合约。
    • 主力 < 远月 (Contango):做空主力合约。

2. 策略代码实现 (PTrade)

以下代码实现了一个基于期限结构的自动轮动策略。为了保证回测和实盘的兼容性,我们使用成交量(Volume)来动态识别主力合约。

import numpy as np
import pandas as pd
import datetime

def initialize(context):
    """
    策略初始化函数
    """
    # 1. 设定要交易的品种(以螺纹钢为例)
    g.product_code = 'RB' 
    g.exchange_suffix = '.XSGE' # 上期所后缀
    
    # 2. 设定资金管理参数
    g.leverage = 2.0 # 杠杆倍数,用于计算下单数量
    
    # 3. 设定全局变量用于记录当前持仓合约
    g.current_contract = None
    
    # 4. 设定交易费率(可选,根据实际情况调整)
    set_commission(commission_ratio=0.0001, min_commission=5.0, type='future')
    
    # 5. 设定定时运行:每天收盘前10分钟运行策略
    run_daily(context, trade_logic, time='14:50')

def get_contract_list(context, product, suffix):
    """
    辅助函数:生成当前时间点可能存在的合约代码列表
    PTrade没有直接获取某品种所有合约的API,需要根据日期推算
    """
    contract_list = []
    current_date = context.blotter.current_dt
    year = current_date.year
    month = current_date.month
    
    # 生成从当前月开始往后推12个月的合约代码
    # 格式通常为:RB2305.XSGE (年份后两位 + 月份两位)
    for i in range(12):
        y = year
        m = month + i
        if m > 12:
            y += 1
            m -= 12
        
        # 构建合约代码,注意年份取后两位
        y_str = str(y)[-2:]
        m_str = "{:02d}".format(m)
        code = "{}{}{}{}".format(product, y_str, m_str, suffix)
        contract_list.append(code)
        
    return contract_list

def get_dominant_contracts(contract_list):
    """
    辅助函数:根据成交量找出主力合约和次主力合约
    """
    # 获取这些合约的行情快照或最新行情
    # 注意:回测模式下 get_snapshot 可能受限,这里使用 get_price 获取当日数据
    df_list = []
    
    for code in contract_list:
        # 获取过去1天的日线数据,包含成交量
        data = get_price(code, count=1, frequency='1d', fields=['close', 'volume'])
        if data is not None and not data.empty and data['volume'].iloc[-1] > 0:
            last_close = data['close'].iloc[-1]
            volume = data['volume'].iloc[-1]
            df_list.append({'code': code, 'close': last_close, 'volume': volume})
    
    if not df_list:
        return None, None
        
    # 转换为DataFrame并按成交量降序排列
    df = pd.DataFrame(df_list)
    df = df.sort_values(by='volume', ascending=False)
    
    # 取出成交量最大的作为主力,第二大的作为次主力(用于比较价格)
    if len(df) >= 2:
        main_contract = df.iloc[0]
        next_contract = df.iloc[1]
        return main_contract, next_contract
    elif len(df) == 1:
        return df.iloc[0], None
    else:
        return None, None

def trade_logic(context):
    """
    核心交易逻辑
    """
    # 1. 获取潜在合约列表
    potential_contracts = get_contract_list(context, g.product_code, g.exchange_suffix)
    
    # 2. 识别主力与次主力
    main_info, next_info = get_dominant_contracts(potential_contracts)
    
    if main_info is None or next_info is None:
        log.info("未找到足够活跃的合约,跳过本次交易")
        return

    main_code = main_info['code']
    main_price = main_info['close']
    next_price = next_info['close']
    
    log.info("主力合约: %s (价格: %.2f), 次主力合约: %s (价格: %.2f)" % (
        main_code, main_price, next_info['code'], next_price))

    # 3. 判断期限结构
    # Backwardation (现货/近月溢价): 主力价格 > 次主力价格 -> 做多
    # Contango (远月溢价): 主力价格 < 次主力价格 -> 做空
    
    signal = 0 # 1为多,-1为空
    if main_price > next_price:
        signal = 1
        log.info("检测到 Backwardation (贴水) 结构,看多主力合约")
    else:
        signal = -1
        log.info("检测到 Contango (升水) 结构,看空主力合约")
        
    # 4. 移仓换月与开平仓逻辑
    
    # 如果主力合约发生了变化(换月),先平掉旧合约
    if g.current_contract is not None and g.current_contract != main_code:
        log.info("主力合约切换,平仓旧合约: %s" % g.current_contract)
        # 平掉旧合约的所有持仓
        position = get_position(g.current_contract)
        if position.amount > 0: # 多头持仓
            order_target(g.current_contract, 0)
        elif position.amount < 0: # 空头持仓
            order_target(g.current_contract, 0)
        g.current_contract = None

    # 计算目标持仓数量
    # 获取当前账户总资产
    total_value = context.portfolio.portfolio_value
    # 简单的资金管理:使用总资金的一定比例开仓
    # 假设合约乘数为10 (螺纹钢),保证金比例假设为10% (粗略估算,实际应通过get_instruments获取)
    contract_multiplier = 10 
    margin_ratio = 0.1
    
    # 可买手数 = (总资金 * 杠杆) / (价格 * 合约乘数)
    target_amount = int((total_value * 0.3) / (main_price * contract_multiplier * margin_ratio))
    
    if target_amount < 1:
        target_amount = 1

    # 执行交易
    # 如果信号为多
    if signal == 1:
        # 目标是持有正向数量
        order_target(main_code, target_amount)
    # 如果信号为空
    elif signal == -1:
        # 目标是持有负向数量
        order_target(main_code, -target_amount)
        
    # 更新当前持有的合约代码
    g.current_contract = main_code

def handle_data(context, data):
    """
    盘中运行函数,本策略主要逻辑在 run_daily 定时任务中,此处留空
    """
    pass

3. 代码关键点解析

  1. 合约生成 (get_contract_list):

    • PTrade API 中没有直接返回“某品种当前所有上市合约”的函数。因此,我们通过当前日期动态生成未来12个月的合约代码(例如 RB2305, RB2310)。这是量化回测中处理期货合约的常用技巧。
  2. 主力识别 (get_dominant_contracts):

    • 策略通过 get_price 获取合约的成交量(Volume)。
    • 将合约按成交量排序,第一名为“主力合约”,第二名为“次主力合约”(或远月合约)。
    • 注意:在实盘中,也可以使用 get_snapshot 获取持仓量(Open Interest/Amount)来判断主力,但在回测模式下 get_snapshot 可能无法获取历史切片数据,因此使用 get_price 更稳健。
  3. 信号生成:

    • Backwardation (主力 > 远月): 市场缺货或现货强势,做多主力合约,以此捕获展期收益(Roll Yield)。
    • Contango (主力 < 远月): 市场库存高或远期看涨成本高,做空主力合约。
  4. 移仓换月:

    • 代码中包含了一个检查机制:if g.current_contract != main_code
    • 一旦成交量最大的合约发生变化(例如从 2305 切换到 2310),策略会自动平掉旧合约的仓位,并在新合约上开仓。这是期货策略必须处理的逻辑。
  5. 下单接口:

    • 使用了 order_target(security, amount)。在 PTrade 中,order_target 会自动计算当前持仓与目标持仓的差额,并执行相应的开仓、平仓、平今或平昨操作,简化了期货交易的复杂性。

4. 常见问题 (Q&A)

Q1: 为什么使用成交量而不是持仓量来判断主力?
A: 在 PTrade 的回测环境中,get_price 接口提供的历史数据中 volume(成交量)是最完整和准确的。虽然持仓量(Open Interest)是定义主力的标准,但在某些数据源中可能缺失。使用成交量通常能达到相同的效果,因为成交量最大的合约通常也是持仓量最大的。

Q2: 这个策略适合哪些品种?
A: 此策略最适合具有明显周期性或库存周期的商品期货,例如:

  • 黑色系:螺纹钢 (RB)、铁矿石 (I)。
  • 化工:甲醇 (MA)、PTA (TA)。
  • 农产品:豆粕 (M)。
  • 不建议用于股指期货,因为股指期货的期限结构主要受分红和利率影响,逻辑不同。

Q3: 如何调整杠杆?
A: 代码中 target_amount 的计算逻辑里,total_value * 0.3 代表使用30%的资金作为保证金。你可以调整这个比例来控制实际杠杆。

Q4: 回测时需要注意什么?
A: 务必设置好手续费 (set_commission) 和保证金比例。期货交易对滑点和手续费非常敏感,尤其是频繁换月的策略。