问题描述
解决方案
在PTrade中实现期货自动换月移仓(Rollover),核心逻辑通常包含以下三个步骤:
- 判断换月时机:通常根据“距离交割日的天数”或“主力合约切换(持仓量/成交量变化)”来判断。最简单且常用的是基于时间的换月(例如:在交割日前N天进行移仓)。
- 获取下月合约:根据当前合约代码解析出下一个主力合约的代码。
- 执行移仓操作:平掉当前合约的持仓,并在新合约上开出同方向、同数量的仓位。
下面是一个完整的PTrade策略示例,实现了基于距离交割日天数的自动换月功能。
策略代码实现
import datetime
def initialize(context):
# 设定初始主力合约(示例为沪深300股指期货)
g.current_contract = 'IF2309.CCFX'
# 设定换月阈值:距离交割日剩余多少天时进行移仓
g.days_before_delivery = 5
# 设置股票池
set_universe([g.current_contract])
# 开启日志打印,方便观察
log.info("策略初始化完成,当前主力合约: %s" % g.current_contract)
def handle_data(context, data):
# 1. 获取当前时间
current_date = context.blotter.current_dt.date()
# 2. 获取当前合约的交割日期
# get_instruments 返回 FutureParams 对象
instr_info = get_instruments(g.current_contract)
if instr_info is None:
log.error("无法获取合约信息: %s" % g.current_contract)
return
# delivery_date 格式通常为 '20230915' 字符串
delivery_date_str = instr_info.delivery_date
delivery_date = datetime.datetime.strptime(delivery_date_str, '%Y%m%d').date()
# 3. 计算距离交割日的天数
days_delta = (delivery_date - current_date).days
# 4. 检查是否满足换月条件
# 如果剩余天数小于等于设定的阈值,且当前合约还在股票池中(说明还没换)
if days_delta <= g.days_before_delivery:
log.info("当前日期: %s, 合约 %s 交割日: %s, 剩余天数: %d, 触发换月逻辑。" % (
current_date, g.current_contract, delivery_date, days_delta))
perform_rollover(context)
# --- 以下为简单的开仓逻辑示例,仅用于演示有持仓的情况 ---
# 如果当前无持仓,且不是换月日,随机开一手多单用于测试
pos = get_position(g.current_contract)
if pos.amount == 0 and days_delta > g.days_before_delivery:
buy_open(g.current_contract, 1)
log.info("测试开仓: 买入 %s" % g.current_contract)
def perform_rollover(context):
"""
执行移仓操作
"""
# 1. 获取下月合约代码
next_contract = get_next_month_contract(g.current_contract)
log.info("准备从 %s 移仓至 %s" % (g.current_contract, next_contract))
# 2. 获取当前合约的持仓信息
position = get_position(g.current_contract)
# 多头持仓量
long_amt = position.long_amount
# 空头持仓量
short_amt = position.short_amount
# 3. 平掉旧合约持仓
if long_amt > 0:
# 平多 (注意:上期所品种可能需要区分平今/平昨,此处使用默认False优先平昨)
sell_close(g.current_contract, long_amt, close_today=False)
log.info("平旧合约多单: %s, 数量: %s" % (g.current_contract, long_amt))
if short_amt > 0:
# 平空
buy_close(g.current_contract, short_amt, close_today=False)
log.info("平旧合约空单: %s, 数量: %s" % (g.current_contract, short_amt))
# 4. 在新合约开出同等仓位
if long_amt > 0:
buy_open(next_contract, long_amt)
log.info("开新合约多单: %s, 数量: %s" % (next_contract, long_amt))
if short_amt > 0:
sell_open(next_contract, short_amt)
log.info("开新合约空单: %s, 数量: %s" % (next_contract, short_amt))
# 5. 更新全局变量和股票池
g.current_contract = next_contract
set_universe([g.current_contract])
log.info("换月完成,当前主力合约更新为: %s" % g.current_contract)
def get_next_month_contract(code):
"""
根据当前合约代码计算下月合约代码
假设代码格式为 'IF2309.CCFX' (品种+年份后2位+月份.后缀)
"""
# 分离代码和后缀,例如 'IF2309' 和 'CCFX'
symbol_part, suffix = code.split('.')
# 提取品种字符(非数字部分)和日期数字
import re
match = re.match(r"([A-Z]+)(\d+)", symbol_part)
if not match:
log.error("合约代码格式解析失败: %s" % code)
return code
product_code = match.group(1) # 例如 IF
date_str = match.group(2) # 例如 2309
year = int(date_str[:2])
month = int(date_str[2:])
# 计算下个月
next_month = month + 1
next_year = year
if next_month > 12:
next_month = 1
next_year = year + 1
# 格式化新的日期字符串,保持2位年份和2位月份
new_date_str = "%02d%02d" % (next_year, next_month)
# 拼接新代码
new_code = product_code + new_date_str + "." + suffix
return new_code
代码逻辑详解
-
get_instruments(contract):- 这是PTrade获取合约基础信息的关键API。
- 我们从中提取
delivery_date(交割日),这是判断是否需要换月的基准。
-
换月判断逻辑:
- 在
handle_data中,每天检查(交割日 - 当前日期)的天数。 - 如果天数小于
g.days_before_delivery(例如5天),则触发移仓。
- 在
-
get_next_month_contract函数:- 这是一个自定义的辅助函数。它利用正则表达式解析当前的合约代码(如
IF2309),提取年份和月份。 - 通过简单的数学运算计算下一个月,并处理跨年逻辑(12月变1月,年份+1)。
- 最后重新拼接成PTrade标准代码格式。
- 这是一个自定义的辅助函数。它利用正则表达式解析当前的合约代码(如
-
perform_rollover函数:- 平旧仓:使用
get_position获取当前合约的多空持仓量,然后调用sell_close(平多)和buy_close(平空)。 - 开新仓:使用
buy_open(开多)和sell_open(开空)在新合约上建立相同的头寸。 - 更新全局变量:非常重要的一步是更新
g.current_contract并调用set_universe,确保后续的交易逻辑(如止盈止损)是针对新合约执行的。
- 平旧仓:使用
注意事项
- 上期所平今仓限制:
- 如果你交易的是上期所(XSGE)的品种(如螺纹钢 RB),在平仓时要注意
close_today参数。PTrade中close_today=True仅平今仓,False优先平昨。通常换月涉及的是老仓,使用False即可。
- 如果你交易的是上期所(XSGE)的品种(如螺纹钢 RB),在平仓时要注意
- 主力合约规则:
- 上述代码假设下个月就是主力合约(适用于股指期货)。
- 对于商品期货(如螺纹钢、铁矿石),主力合约通常是1月、5月、9月。如果是这种情况,你需要修改
get_next_month_contract的逻辑,使其寻找下一个主力月份(例如当前是01,下一个是05,而不是02)。
- 滑点与流动性:
- 换月操作通常涉及两笔交易(一平一开),可能会产生滑点。建议在流动性较好的时间段(如开盘后半小时)执行,避免在集合竞价或尾盘剧烈波动时换月。
Q&A
Q: 如何将换月逻辑改为“基于持仓量”判断?
A: 你可以使用 get_snapshot 或 get_history 获取当前合约和潜在下月合约的 amount (持仓量)。在 handle_data 中对比两者的持仓量,当远月合约持仓量超过近月合约时,触发 perform_rollover。
Q: 为什么代码中不使用 import os?
A: PTrade 策略环境出于安全考虑,禁止使用 os、sys 等系统级库。所有文件读写和路径获取必须使用 PTrade 提供的专用 API(如 get_research_path)。
Q: 移仓时如果资金不足怎么办?
A: 这是一个常见风险。通常先平仓会释放保证金,然后再开新仓是可行的。但如果行情波动剧烈导致可用资金紧缺,或者新合约保证金比例更高,可能会开仓失败。建议在 perform_rollover 中加入 context.portfolio.cash 的检查逻辑。