🚀 AI 一键生成 ptrade 策略代码

如何在 PTrade 中利用期货的 Contango 和 Backwardation 结构进行交易?

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

问题描述

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

解决方案

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

1. 策略原理

期货期限结构反映了同一品种不同交割月份合约的价格差异。

  • Backwardation(现货溢价/贴水/反向市场):近月价格 > 远月价格。
    • 逻辑:通常意味着现货短缺或持有成本低。随着交割日临近,远月价格往往会向近月/现货价格靠拢(上涨)。
    • 操作做多(获取正的展期收益 Roll Yield)。
  • Contango(期货溢价/升水/正向市场):近月价格 < 远月价格。
    • 逻辑:通常意味着供应充足或仓储成本高。随着交割日临近,远月价格往往会下跌以收敛至现货价格。
    • 操作做空(获取负的展期收益,即做空获利)。

核心指标:展期收益率 (Roll Yield)
$$ \text{Roll Yield} = \frac{\ln(\text{近月价格}) - \ln(\text{远月价格})}{\text{远月到期时间} - \text{近月到期时间}} $$
或者简化版:
$$ \text{Roll Yield} = \frac{\text{近月价格} - \text{远月价格}}{\text{近月价格}} $$

2. 策略逻辑

  1. 标的池:选取流动性较好的商品期货品种(如螺纹钢、铁矿石、铜、PTA等)。
  2. 合约选择:对于每个品种,选取主力合约(近月)次主力合约(远月)
  3. 信号计算:计算每个品种的展期收益率。
  4. 交易规则
    • 做多:展期收益率最高的前N个品种(Backwardation最强)。
    • 做空:展期收益率最低的前N个品种(Contango最强)。
    • 平仓:如果持仓品种不再属于前N名,则平仓。

3. PTrade 策略代码实现

以下代码实现了一个多品种期限结构策略。

注意:由于PTrade API中获取“某品种所有合约列表”的功能较为复杂,本示例采用手动指定/定期更新主力与次主力合约的方式,这是实盘中最稳健的做法。

import numpy as np
import pandas as pd

def initialize(context):
    """
    策略初始化函数
    """
    # 1. 设定基准和手续费
    set_benchmark('000300.SS')
    # 设置期货保证金比例(示例:10%)
    set_margin_rate('RB', 0.10) 
    set_margin_rate('I', 0.10)
    set_margin_rate('CU', 0.10)
    set_margin_rate('TA', 0.10)
    
    # 2. 定义交易品种的合约对 {品种: [近月主力, 远月次主力]}
    # 注意:在实盘中,这些代码需要根据主力换月情况定期更新(例如每月更新一次)
    # 这里仅为示例代码,请替换为当前实际交易的合约代码
    g.future_pairs = {
        'RB': ['RB2310.XSGE', 'RB2401.XSGE'], # 螺纹钢
        'I':  ['I2309.XDCE',  'I2401.XDCE'],  # 铁矿石
        'CU': ['CU2308.XSGE', 'CU2309.XSGE'], # 沪铜
        'TA': ['TA309.XZCE',  'TA401.XZCE']   # PTA
    }
    
    # 提取所有涉及的合约加入股票池,以便获取行情
    all_contracts = []
    for pair in g.future_pairs.values():
        all_contracts.extend(pair)
    set_universe(all_contracts)
    
    # 3. 策略参数
    g.long_count = 1   # 做多排名前1的品种
    g.short_count = 1  # 做空排名倒数第1的品种
    g.trade_lot = 1    # 每次交易手数(简化演示,实盘应根据资金计算)
    
    # 设置定时任务,每天开盘后运行一次(例如 10:00)
    run_daily(context, trade_logic, time='10:00')

def get_roll_yield(context):
    """
    计算展期收益率
    返回 DataFrame: index=品种, columns=['roll_yield', 'near_contract']
    """
    yields = {}
    
    # 获取所有合约的快照数据
    all_contracts = []
    for pair in g.future_pairs.values():
        all_contracts.extend(pair)
    
    snapshots = get_snapshot(all_contracts)
    
    for variety, pair in g.future_pairs.items():
        near_c = pair[0]
        far_c = pair[1]
        
        # 检查数据有效性
        if near_c not in snapshots or far_c not in snapshots:
            continue
            
        p_near = snapshots[near_c]['last_px']
        p_far = snapshots[far_c]['last_px']
        
        # 排除停牌或无价格的情况
        if p_near <= 0 or p_far <= 0:
            continue
            
        # 计算简化版展期收益率: (近月 - 远月) / 近月
        # 正值越大,Backwardation越强(适合做多)
        # 负值越小,Contango越强(适合做空)
        r_yield = (p_near - p_far) / p_near
        
        yields[variety] = {
            'roll_yield': r_yield,
            'near_contract': near_c # 我们交易近月主力合约
        }
        
        # 记录日志
        log.info("品种: %s, 近月: %s (%.2f), 远月: %s (%.2f), Yield: %.4f" % (
            variety, near_c, p_near, far_c, p_far, r_yield
        ))
        
    return pd.DataFrame(yields).T

def trade_logic(context):
    """
    核心交易逻辑
    """
    # 1. 获取因子数据
    df_yield = get_roll_yield(context)
    
    if df_yield.empty:
        return

    # 2. 排序
    df_yield = df_yield.sort_values(by='roll_yield', ascending=False)
    
    # 3. 筛选目标持仓
    # 做多列表:收益率最高的 g.long_count 个
    target_long = df_yield.head(g.long_count).index.tolist()
    # 做空列表:收益率最低的 g.short_count 个
    target_short = df_yield.tail(g.short_count).index.tolist()
    
    # 获取具体的合约代码
    target_long_contracts = [df_yield.loc[v, 'near_contract'] for v in target_long]
    target_short_contracts = [df_yield.loc[v, 'near_contract'] for v in target_short]
    
    log.info("目标做多品种: %s" % target_long)
    log.info("目标做空品种: %s" % target_short)
    
    # 4. 交易执行:先平仓非目标,再开仓目标
    
    # --- 平仓逻辑 ---
    # 遍历当前所有持仓
    positions = context.portfolio.positions
    for contract in list(positions.keys()):
        pos = positions[contract]
        
        # 如果有多单
        if pos.long_amount > 0:
            # 如果该合约不在目标做多列表中,平多
            if contract not in target_long_contracts:
                log.info("平多仓: %s" % contract)
                sell_close(contract, pos.long_amount)
        
        # 如果有空单
        if pos.short_amount > 0:
            # 如果该合约不在目标做空列表中,平空
            if contract not in target_short_contracts:
                log.info("平空仓: %s" % contract)
                buy_close(contract, pos.short_amount)
    
    # --- 开仓逻辑 ---
    # 执行做多
    for contract in target_long_contracts:
        pos = get_position(contract)
        # 如果当前没有多单,则开多
        if pos.long_amount == 0:
            log.info("开多仓: %s" % contract)
            buy_open(contract, g.trade_lot)
            
    # 执行做空
    for contract in target_short_contracts:
        pos = get_position(contract)
        # 如果当前没有空单,则开空
        if pos.short_amount == 0:
            log.info("开空仓: %s" % contract)
            sell_open(contract, g.trade_lot)

def handle_data(context, data):
    """
    盘中tick级别处理(本策略为日级别,此处留空或用于风控)
    """
    pass

4. 代码关键点解析

  1. 合约对管理 (g.future_pairs)
    • 期限结构策略最关键的是数据的准确性。由于主力合约会随时间切换(如从2305切换到2310),在实盘中,建议通过外部文件读取或在 before_trading_start 中动态计算主力合约代码(如果具备相应算法),本例为了代码清晰,采用了字典配置方式。
  2. 展期收益率计算 (get_roll_yield)
    • 使用了 (近月 - 远月) / 近月 的公式。
    • 如果结果为,说明近月贵,是 Backwardation 结构,做多近月合约可以享受价格向现货回归(如果不跌)或换月时的贴水收益。
    • 如果结果为,说明远月贵,是 Contango 结构,做空可以赚取时间价值。
  3. 交易执行 (buy_open/sell_close 等)
    • 期货交易与股票不同,需要明确开平仓方向。
    • 策略采用“先平非目标仓位,再开目标仓位”的逻辑,避免资金占用过高。
  4. 数据获取 (get_snapshot)
    • 使用快照数据获取最新价格,这在盘中运行(run_daily)时比 get_history 更具时效性。

5. 风险提示与改进方向

  1. 移仓换月:本策略主要交易近月主力合约。当主力合约临近交割时,必须强制平仓并切换到下一个主力合约。需要在代码中增加“距离交割日天数”的判断逻辑。
  2. 流动性风险:某些品种的远月合约流动性极差,计算出的价差可能失真,或者无法成交。建议在计算 Yield 前过滤掉成交量过低的合约。
  3. 资金管理:示例中固定了 g.trade_lot = 1。实际应用中应根据账户总权益和各品种的保证金占用、波动率(ATR)来动态计算开仓手数。

Q&A

Q: 如何自动获取主力合约而不是手动输入?
A: PTrade API 中没有直接返回“当前主力合约”的函数。通常的做法是:

  1. 获取某品种的所有合约列表(利用 get_instruments 结合规则生成代码,或查询外部数据源)。
  2. 遍历获取这些合约的 open_interest(持仓量)。
  3. 持仓量最大的即为主力合约,次大的通常为次主力。
    这需要编写较长的辅助函数,上述代码为了保持简洁采用了配置法。

Q: 为什么使用 run_daily 而不是 handle_data
A: 期限结构是基于日线级别的逻辑,不需要每分钟或每笔Tick都计算。使用 run_daily 定时在上午10点或下午收盘前运行一次,既节省计算资源,又能过滤掉开盘瞬间的噪音。