问题描述
解决方案
这是一个基于 QMT 平台的期货主力合约自动移仓换月策略。
策略逻辑说明
- 主力合约识别:使用
ContextInfo.get_main_contract()实时获取指定品种(如沪深300股指期货 IF)的当前主力合约代码。 - 持仓监控:通过
get_trade_detail_data()获取当前账户的持仓情况。 - 移仓条件:
- 条件 A(主力切换):当持仓的合约代码与当前系统判定的主力合约代码不一致时,触发移仓。
- 条件 B(临近到期):作为双重保险,检查持仓合约的到期日。如果距离到期日小于设定阈值(如 2 天),强制平仓并开仓到新的主力合约(防止进入交割月风险)。
- 执行动作:
- 平掉旧合约的持仓(保持原有方向)。
- 在新的主力合约上开出等量的仓位(保持原有方向)。
策略代码
# -*- coding: gbk -*-
import time
import datetime
def init(ContextInfo):
# ================= 策略参数设置 =================
# 设置资金账号 (请修改为您自己的实盘或模拟账号)
ContextInfo.accID = '6000000000'
# 设置账号类型:'FUTURE' 为期货
ContextInfo.accType = 'FUTURE'
# 设置要跟踪的品种代码 (格式:品种.市场,例如 IF.IF 代表中金所IF品种)
ContextInfo.target_product = 'IF.IF'
# 临近到期强制移仓阈值 (天)
# 如果持仓合约距离到期日小于此天数,强制移仓
ContextInfo.expire_threshold = 2
# 绑定账号,用于接收交易回报
ContextInfo.set_account(ContextInfo.accID)
print("策略初始化完成,监控品种: {}, 账号: {}".format(ContextInfo.target_product, ContextInfo.accID))
def handlebar(ContextInfo):
# 获取当前 K 线的时间,用于判断是否在交易时间
# 如果是回测模式,barpos 代表回测进度;如果是实盘,代表最新行情
if not ContextInfo.is_last_bar():
return
# 1. 获取当前市场上的主力合约代码
current_main_contract = ContextInfo.get_main_contract(ContextInfo.target_product)
if not current_main_contract:
print("未获取到主力合约信息")
return
# 2. 获取当前账户的持仓信息
positions = get_trade_detail_data(ContextInfo.accID, ContextInfo.accType, 'POSITION')
# 3. 遍历持仓,检查是否需要移仓
for pos in positions:
# 过滤出属于目标品种的持仓 (例如只处理 IF 开头的合约)
# pos.m_strInstrumentID 是具体的合约代码,如 IF2312.IF
# ContextInfo.target_product 是 IF.IF
product_code = ContextInfo.target_product.split('.')[0] # 获取 'IF'
if not pos.m_strInstrumentID.startswith(product_code):
continue
# 获取持仓合约的到期日 (int格式,如 20231215)
expire_date_int = ContextInfo.get_contract_expire_date(pos.m_strInstrumentID)
# 获取当前日期 (int格式)
current_timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
current_date_str = timetag_to_datetime(current_timetag, '%Y%m%d')
current_date_int = int(current_date_str)
# 计算距离到期还有几天 (简单估算)
days_to_expire = days_between(current_date_int, expire_date_int)
# === 移仓逻辑判断 ===
need_rollover = False
reason = ""
# 情况1: 持仓合约已经不是主力合约了
if pos.m_strInstrumentID != current_main_contract:
need_rollover = True
reason = "主力合约已切换 (旧: {}, 新: {})".format(pos.m_strInstrumentID, current_main_contract)
# 情况2: 持仓合约临近到期 (双重保险)
elif days_to_expire <= ContextInfo.expire_threshold:
need_rollover = True
reason = "合约临近到期 (剩余 {} 天)".format(days_to_expire)
# === 执行移仓 ===
if need_rollover and pos.m_nVolume > 0:
print("触发移仓: {}".format(reason))
perform_rollover(ContextInfo, pos, current_main_contract)
def perform_rollover(ContextInfo, pos, new_contract):
"""
执行移仓操作:平旧仓,开新仓
"""
# 获取持仓方向和数量
# m_nDirection: 48(多头), 49(空头) -> QMT常量
# 为了方便,我们使用 passorder 的 opType 逻辑
# 多头持仓(48) -> 需要卖平(opType=6 平多优先平今) -> 新开多(opType=0)
# 空头持仓(49) -> 需要买平(opType=8 平空优先平今) -> 新开空(opType=3)
vol = pos.m_nVolume
old_contract = pos.m_strInstrumentID
# 48 代表多头持仓 (Buy)
if pos.m_nDirection == 48:
print("平多头旧合约: {}, 数量: {}".format(old_contract, vol))
# 6: 平多,优先平今
passorder(6, 1101, ContextInfo.accID, old_contract, 5, -1, vol, ContextInfo)
print("开多头新合约: {}, 数量: {}".format(new_contract, vol))
# 0: 开多
passorder(0, 1101, ContextInfo.accID, new_contract, 5, -1, vol, ContextInfo)
# 49 代表空头持仓 (Sell)
elif pos.m_nDirection == 49:
print("平空头旧合约: {}, 数量: {}".format(old_contract, vol))
# 8: 平空,优先平今
passorder(8, 1101, ContextInfo.accID, old_contract, 5, -1, vol, ContextInfo)
print("开空头新合约: {}, 数量: {}".format(new_contract, vol))
# 3: 开空
passorder(3, 1101, ContextInfo.accID, new_contract, 5, -1, vol, ContextInfo)
def days_between(date1_int, date2_int):
"""
计算两个日期整数之间的天数差
"""
try:
d1 = datetime.datetime.strptime(str(date1_int), '%Y%m%d')
d2 = datetime.datetime.strptime(str(date2_int), '%Y%m%d')
return (d2 - d1).days
except:
return 999 # 如果日期格式解析错误,返回一个大数值防止误触发
def timetag_to_datetime(timetag, format_str):
"""
将时间戳转换为指定格式的字符串
"""
import time
return time.strftime(format_str, time.localtime(timetag / 1000))
代码关键点解析
get_main_contract: 这是 QMT 提供的核心函数,它会根据交易所或数据商的规则(通常是持仓量或成交量最大)返回当前的主力合约代码(例如从IF2311.IF变为IF2312.IF)。passorder参数:1101: 代表单股/单合约、单账号、普通模式、按股/手下单。5: 代表使用最新价下单(prType)。opType: 使用了6(平多) 和8(平空) 以及0(开多) 和3(开空),这是期货交易的标准操作码。
is_last_bar(): 确保策略只在最新的行情切片上运行,避免在历史 K 线上重复触发交易信号(除非你在做回测)。days_between: 辅助函数,用于计算当前日期距离合约交割日还有多少天。
如何使用
- 修改账号:将
ContextInfo.accID修改为您实际的资金账号。 - 选择品种:修改
ContextInfo.target_product为您想跟踪的品种(如rb.SF螺纹钢,m.DF豆粕等)。 - 运行模式:
- 在 QMT 界面中,将此策略加载到模型交易或实盘交易中。
- 设置运行周期(建议设置为 1分钟 或 5分钟,甚至 Tick 级,以便及时捕捉主力切换)。
- 点击运行。
Q&A
Q: 这个策略会自动开仓吗?
A: 不会。这个策略是一个移仓(Rollover)策略。它假设你手里已经持有某个合约的仓位。如果你的账户是空仓,它什么都不会做。你需要配合其他策略开仓,或者手动开仓后挂载此策略进行维护。
Q: 为什么使用“最新价”下单?
A: 代码中 passorder 的第 5 个参数设为 5 (最新价)。在实盘中,为了保证成交,通常建议使用 对手价 (14) 或 超价 (指定价模式下给予一定滑点)。如果需要更激进的成交,可以将参数 5 改为 14 (对手价)。
Q: 什么是 ContextInfo.expire_threshold?
A: 这是一个安全阈值。有时候主力合约切换得比较晚,但旧合约马上就要交割了(个人投资者通常不能进入交割月)。设置这个参数(比如 2 天),意味着如果距离交割日只剩 2 天了,不管主力合约有没有切过来,都强制平掉旧合约并开到新主力上,防止违规持仓。