问题描述
解决方案
这是一个基于期货期限结构(Term Structure)的量化交易策略设计。
1. 策略原理:期限结构(Contango 与 Backwardation)
期货期限结构是指同一品种、不同到期月份合约价格之间的关系。
- Contango(正向市场/升水): 远月价格 > 近月价格。
- 含义:通常代表持有成本(仓储、资金成本)较高,或者市场预期未来价格上涨。
- 策略逻辑:做空近月合约(或主力合约)。因为随着时间推移,期货价格会向现货价格收敛,多头需支付“展期成本”,空头则获得“展期收益”。
- Backwardation(反向市场/贴水): 远月价格 < 近月价格。
- 含义:通常代表现货供应紧张(便利收益高),市场愿意支付溢价购买即期商品。
- 策略逻辑:做多近月合约(或主力合约)。多头获得“展期收益”。
核心逻辑:
- 获取某品种(如螺纹钢 RB)的所有活跃合约。
- 根据成交量或持仓量找出主力合约(流动性最好)和次主力合约(通常是远月)。
- 计算价差:
价差 = 主力合约价格 - 次主力合约价格。 - 交易信号:
- 若
主力 > 远月(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. 代码关键点解析
-
合约生成 (
get_contract_list):- PTrade API 中没有直接返回“某品种当前所有上市合约”的函数。因此,我们通过当前日期动态生成未来12个月的合约代码(例如
RB2305,RB2310)。这是量化回测中处理期货合约的常用技巧。
- PTrade API 中没有直接返回“某品种当前所有上市合约”的函数。因此,我们通过当前日期动态生成未来12个月的合约代码(例如
-
主力识别 (
get_dominant_contracts):- 策略通过
get_price获取合约的成交量(Volume)。 - 将合约按成交量排序,第一名为“主力合约”,第二名为“次主力合约”(或远月合约)。
- 注意:在实盘中,也可以使用
get_snapshot获取持仓量(Open Interest/Amount)来判断主力,但在回测模式下get_snapshot可能无法获取历史切片数据,因此使用get_price更稳健。
- 策略通过
-
信号生成:
- Backwardation (主力 > 远月): 市场缺货或现货强势,做多主力合约,以此捕获展期收益(Roll Yield)。
- Contango (主力 < 远月): 市场库存高或远期看涨成本高,做空主力合约。
-
移仓换月:
- 代码中包含了一个检查机制:
if g.current_contract != main_code。 - 一旦成交量最大的合约发生变化(例如从 2305 切换到 2310),策略会自动平掉旧合约的仓位,并在新合约上开仓。这是期货策略必须处理的逻辑。
- 代码中包含了一个检查机制:
-
下单接口:
- 使用了
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) 和保证金比例。期货交易对滑点和手续费非常敏感,尤其是频繁换月的策略。