问题描述
解决方案
这是一个基于 QMT 平台的期货跨期套利策略示例。
策略逻辑分析
跨期套利(Calendar Spread Arbitrage)的核心在于利用同一品种、不同到期月份合约之间的价差波动获利。
本策略的逻辑如下:
- 定义价差:
价差 = 近月合约价格 - 远月合约价格。 - 建仓(价差扩大):当
价差 > 开仓阈值时,认为近期合约过贵或远期合约过便宜。此时卖出近月合约(做空),买入远月合约(做多),即“卖出套利”。 - 平仓(价差缩小):当
价差 < 平仓阈值时,认为价差回归正常。此时买入平仓近月合约,卖出平仓远月合约。
策略代码实现
# -*- coding: gbk -*-
import pandas as pd
def init(ContextInfo):
# ================= 策略参数设置 =================
# 资金账号 (请修改为您的实际期货账号)
ContextInfo.accID = 'YOUR_ACCOUNT_ID'
# 套利合约对 (示例:沪深300股指期货)
# 合约A:近月合约 (通常价格较高或波动较敏感)
ContextInfo.contract_A = 'IF2406.IF'
# 合约B:远月合约
ContextInfo.contract_B = 'IF2409.IF'
# 交易手数
ContextInfo.lots = 1
# 价差阈值设置 (Spread = Price_A - Price_B)
# 当价差大于 30 点时,认为价差过大,进行卖出套利(空A多B)
ContextInfo.spread_open_threshold = 30
# 当价差回归到 10 点时,获利平仓
ContextInfo.spread_close_threshold = 10
# ================= 初始化设置 =================
print("策略初始化启动...")
# 绑定账号
ContextInfo.set_account(ContextInfo.accID)
# 设置股票池/合约池,确保能获取到行情
ContextInfo.set_universe([ContextInfo.contract_A, ContextInfo.contract_B])
# 记录当前持仓状态:0-空仓, 1-持有套利仓位
ContextInfo.holding_state = 0
def handlebar(ContextInfo):
# 仅在最后一根K线(实时行情)运行,避免回测时重复发单或历史K线重复计算
if not ContextInfo.is_last_bar():
return
# 获取最新行情数据 (Tick级或分时)
# 使用 get_market_data_ex 获取最新的一条数据
market_data = ContextInfo.get_market_data_ex(
['close'],
[ContextInfo.contract_A, ContextInfo.contract_B],
period='tick',
count=1,
subscribe=True
)
# 数据有效性检查
if ContextInfo.contract_A not in market_data or ContextInfo.contract_B not in market_data:
return
# 提取最新价格
try:
price_A = market_data[ContextInfo.contract_A].iloc[-1]['close']
price_B = market_data[ContextInfo.contract_B].iloc[-1]['close']
except:
# 防止数据未准备好报错
return
# 计算当前价差
current_spread = price_A - price_B
# 在界面上画出价差曲线 (可选)
ContextInfo.paint('Spread', current_spread, -1, 0)
# 获取当前账户持仓信息,更新 holding_state
# 注意:实盘中建议通过 get_trade_detail_data 实时核对持仓,这里简化处理
check_position(ContextInfo)
# ================= 交易逻辑 =================
# 1. 开仓逻辑:价差扩大 -> 卖出套利 (空A 多B)
if ContextInfo.holding_state == 0:
if current_spread > ContextInfo.spread_open_threshold:
print(f"触发开仓信号: 当前价差 {current_spread} > 阈值 {ContextInfo.spread_open_threshold}")
# 卖出开仓 A (近月)
sell_open(ContextInfo.contract_A, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
# 买入开仓 B (远月)
buy_open(ContextInfo.contract_B, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
ContextInfo.holding_state = 1
print(f"已执行: 卖开 {ContextInfo.contract_A}, 买开 {ContextInfo.contract_B}")
# 2. 平仓逻辑:价差缩小 -> 平仓获利 (买平A 卖平B)
elif ContextInfo.holding_state == 1:
if current_spread < ContextInfo.spread_close_threshold:
print(f"触发平仓信号: 当前价差 {current_spread} < 阈值 {ContextInfo.spread_close_threshold}")
# 买入平仓 A (近月) - 优先平今
buy_close_tdayfirst(ContextInfo.contract_A, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
# 卖出平仓 B (远月) - 优先平今
sell_close_tdayfirst(ContextInfo.contract_B, ContextInfo.lots, "LATEST", 0, ContextInfo, ContextInfo.accID)
ContextInfo.holding_state = 0
print(f"已执行: 买平 {ContextInfo.contract_A}, 卖平 {ContextInfo.contract_B}")
def check_position(ContextInfo):
"""
辅助函数:检查账户实际持仓,防止逻辑状态与实际账户不符
"""
# 获取持仓列表
positions = get_trade_detail_data(ContextInfo.accID, 'future', 'position')
pos_A = 0 # A合约净持仓 (空单为负)
pos_B = 0 # B合约净持仓
for pos in positions:
if pos.m_strInstrumentID + "." + pos.m_strExchangeID == ContextInfo.contract_A:
# 期货持仓方向:48为多,49为空
if pos.m_nDirection == 48:
pos_A += pos.m_nVolume
elif pos.m_nDirection == 49:
pos_A -= pos.m_nVolume
if pos.m_strInstrumentID + "." + pos.m_strExchangeID == ContextInfo.contract_B:
if pos.m_nDirection == 48:
pos_B += pos.m_nVolume
elif pos.m_nDirection == 49:
pos_B -= pos.m_nVolume
# 简单的状态判断:如果持有A的空单且持有B的多单,认为在套利持仓中
if pos_A < 0 and pos_B > 0:
ContextInfo.holding_state = 1
else:
ContextInfo.holding_state = 0
代码关键点说明
-
get_market_data_ex:- 这是 QMT 获取行情的推荐接口。
period='tick'表示获取最新的分笔数据,对于套利策略,价格敏感度高,通常使用 Tick 数据或 1分钟数据。subscribe=True确保在实盘运行时订阅实时推送。
-
价差计算:
- 代码中定义
current_spread = price_A - price_B。 - 如果
price_A(近月) 通常高于price_B(远月)(即 Backwardation 结构),价差为正。 - 如果市场结构相反(Contango),价差可能为负,您需要根据实际品种调整阈值(例如设置为 -10 和 -30)。
- 代码中定义
-
交易函数:
sell_open(卖开) 和buy_open(买开) 用于建立套利头寸。buy_close_tdayfirst和sell_close_tdayfirst用于平仓。这里使用了“平今优先”,这对于日内交易或特定交易所(如上期所)非常重要,可以节省手续费或符合交易所规则。
-
持仓检查 (
check_position):- 在量化交易中,仅仅依赖变量
ContextInfo.holding_state是不安全的(例如程序重启后变量会重置)。 - 代码增加了一个辅助函数,通过
get_trade_detail_data读取账户真实的持仓情况来更新策略状态。
- 在量化交易中,仅仅依赖变量
实盘注意事项
- 滑点风险:代码中使用
"LATEST"(最新价) 下单。在流动性不足时,可能导致单边成交(即A成交了,B没成交),从而暴露在单边风险中。实盘中通常需要更复杂的算法(如发单后检查成交,未成交则追单)。 - 合约代码:请务必将
IF2406.IF等代码修改为当前市场上实际存在的合约代码。 - 账号ID:必须在
init中填入真实的资金账号。 - 运行模式:此策略需要在“行情主图”下运行,或者在策略交易界面设置好运行周期(建议设置为 Tick 或 1分钟)。
Q&A
Q: 如何将策略改为“买入套利”(价差缩小时建仓,扩大时平仓)?
A: 只需要反转逻辑:
- 当
current_spread < 低阈值时:buy_open(A)(买入近月) 和sell_open(B)(卖出远月)。 - 当
current_spread > 高阈值时:sell_close(A)和buy_close(B)。
Q: 为什么使用 buy_close_tdayfirst 而不是 buy_close?
A: 在中国期货市场(特别是上期所),平今仓(平掉今日开的仓位)和平昨仓(平掉昨日以前的仓位)的手续费和指令是不同的。tdayfirst 会自动优先平掉今日的仓位,这在策略逻辑中通常更符合预期,也能避免因交易所规则导致的废单。
Q: 如何在回测中查看效果?
A: 在 QMT 策略编辑器中,点击“回测”。注意回测时需要补充对应的历史 Tick 或 分钟数据。回测模式下,ContextInfo.accID 可以是虚拟账号,但在 init 中仍需调用 set_account。