问题描述
解决方案
这是一个基于**期货期限结构(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. 策略逻辑
- 标的池:选取流动性较好的商品期货品种(如螺纹钢、铁矿石、铜、PTA等)。
- 合约选择:对于每个品种,选取主力合约(近月)和次主力合约(远月)。
- 信号计算:计算每个品种的展期收益率。
- 交易规则:
- 做多:展期收益率最高的前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. 代码关键点解析
- 合约对管理 (
g.future_pairs):- 期限结构策略最关键的是数据的准确性。由于主力合约会随时间切换(如从2305切换到2310),在实盘中,建议通过外部文件读取或在
before_trading_start中动态计算主力合约代码(如果具备相应算法),本例为了代码清晰,采用了字典配置方式。
- 期限结构策略最关键的是数据的准确性。由于主力合约会随时间切换(如从2305切换到2310),在实盘中,建议通过外部文件读取或在
- 展期收益率计算 (
get_roll_yield):- 使用了
(近月 - 远月) / 近月的公式。 - 如果结果为正,说明近月贵,是 Backwardation 结构,做多近月合约可以享受价格向现货回归(如果不跌)或换月时的贴水收益。
- 如果结果为负,说明远月贵,是 Contango 结构,做空可以赚取时间价值。
- 使用了
- 交易执行 (
buy_open/sell_close等):- 期货交易与股票不同,需要明确开平仓方向。
- 策略采用“先平非目标仓位,再开目标仓位”的逻辑,避免资金占用过高。
- 数据获取 (
get_snapshot):- 使用快照数据获取最新价格,这在盘中运行(
run_daily)时比get_history更具时效性。
- 使用快照数据获取最新价格,这在盘中运行(
5. 风险提示与改进方向
- 移仓换月:本策略主要交易近月主力合约。当主力合约临近交割时,必须强制平仓并切换到下一个主力合约。需要在代码中增加“距离交割日天数”的判断逻辑。
- 流动性风险:某些品种的远月合约流动性极差,计算出的价差可能失真,或者无法成交。建议在计算 Yield 前过滤掉成交量过低的合约。
- 资金管理:示例中固定了
g.trade_lot = 1。实际应用中应根据账户总权益和各品种的保证金占用、波动率(ATR)来动态计算开仓手数。
Q&A
Q: 如何自动获取主力合约而不是手动输入?
A: PTrade API 中没有直接返回“当前主力合约”的函数。通常的做法是:
- 获取某品种的所有合约列表(利用
get_instruments结合规则生成代码,或查询外部数据源)。 - 遍历获取这些合约的
open_interest(持仓量)。 - 持仓量最大的即为主力合约,次大的通常为次主力。
这需要编写较长的辅助函数,上述代码为了保持简洁采用了配置法。
Q: 为什么使用 run_daily 而不是 handle_data?
A: 期限结构是基于日线级别的逻辑,不需要每分钟或每笔Tick都计算。使用 run_daily 定时在上午10点或下午收盘前运行一次,既节省计算资源,又能过滤掉开盘瞬间的噪音。