问题描述
解决方案
在 QMT 量化交易平台中,处理“部成”(部分成交)的情况是实盘策略中非常重要的一环。如果一笔委托只成交了一部分,剩余部分可能会因为价格变动而无法成交,或者一直挂在买一/卖一位置暴露风险。
要让程序知道并处理这种情况,最推荐的方式是使用 交易回报实时主推函数 (order_callback)。
核心机制
-
如何知道(检测):
通过order_callback(ContextInfo, orderInfo)回调函数,QMT 会在委托状态发生变化时自动调用该函数。你需要检查orderInfo对象中的m_nOrderStatus(委托状态)和成交量字段。- 状态码 55 (
ENTRUST_STATUS_PART_SUCC):表示“部成”,即部分成交,剩余部分仍在挂单中。 - 状态码 52 (
ENTRUST_STATUS_PARTSUCC_CANCEL):表示“部成待撤”或“部成已撤”,即部分成交后,剩余部分被撤单了(这是最终状态)。
- 状态码 55 (
-
如何处理(逻辑):
一旦检测到状态为 55(部成),你可以根据策略逻辑选择:- 继续等待:不做操作,等待剩余部分成交。
- 撤单:调用
cancel()函数撤销剩余未成交部分。 - 追单:撤单后,以更激进的价格(如对手价)重新报单剩余数量。
代码实现
以下是一个完整的策略示例。该代码展示了如何通过 order_callback 实时监控委托,一旦发现“部分成交”且剩余部分未成交,程序会打印日志,并演示如何执行撤单操作(防止剩余部分长期挂单)。
# -*- coding: gbk -*-
import time
def init(ContextInfo):
# 1. 必须设置资金账号,否则无法接收回报
# 请将下面的账号替换为您的真实资金账号
ContextInfo.accID = '6000000000'
ContextInfo.accType = 'STOCK' # 账号类型:STOCK股票,FUTURE期货
ContextInfo.set_account(ContextInfo.accID)
print("策略初始化完成,已绑定账号监控交易回报")
def handlebar(ContextInfo):
# 示例:在最后一根K线发单(仅作演示触发,实际逻辑由您的策略决定)
if not ContextInfo.is_last_bar():
return
# 这里可以写您的下单逻辑,例如:
# passorder(23, 1101, ContextInfo.accID, '600000.SH', 5, -1, 100, ContextInfo)
pass
def order_callback(ContextInfo, orderInfo):
"""
委托状态变化回调函数
当委托状态变为“部成”时,触发处理逻辑
"""
# 55 代表 ENTRUST_STATUS_PART_SUCC (部成/部分成交,剩余部分仍在挂单)
if orderInfo.m_nOrderStatus == 55:
print("=" * 30)
print(f"【检测到部分成交】 委托号: {orderInfo.m_strOrderSysID}")
print(f"标的: {orderInfo.m_strInstrumentID}")
# 获取成交量和剩余量
traded_vol = orderInfo.m_nVolumeTraded # 已成交数量
total_vol = orderInfo.m_nVolumeTotalOriginal # 原始委托数量
remain_vol = orderInfo.m_nVolumeTotal # 剩余未成交数量 (股票中通常 = 总量 - 成交)
print(f"原始委托: {total_vol}, 已成交: {traded_vol}, 剩余: {remain_vol}")
# --- 处理逻辑区域 ---
# 策略 1: 激进处理 - 只要出现部成,立即撤销剩余部分(防止价格偏离后一直挂单)
# 注意:实际使用时建议加一些判断,比如判断剩余量是否过小,或者判断时间间隔
print(f"正在对剩余 {remain_vol} 股进行撤单处理...")
# 调用撤单函数
# 参数: orderId, accountId, accountType, ContextInfo
cancel_result = cancel(orderInfo.m_strOrderSysID, ContextInfo.accID, ContextInfo.accType, ContextInfo)
if cancel_result:
print("撤单指令发送成功")
# 如果需要追单,可以在这里记录信息,在下一轮 handlebar 或通过延时任务重新下单
else:
print("撤单指令发送失败 (可能已完全成交或已撤单)")
# 56 代表 ENTRUST_STATUS_SUCCEEDED (全部成交)
elif orderInfo.m_nOrderStatus == 56:
print(f"【全部成交】 委托号: {orderInfo.m_strOrderSysID},标的: {orderInfo.m_strInstrumentID}")
# 52 代表 ENTRUST_STATUS_PARTSUCC_CANCEL (部成已撤)
elif orderInfo.m_nOrderStatus == 52:
print(f"【部成已撤】 委托号: {orderInfo.m_strOrderSysID},剩余部分已撤销,流程结束。")
关键字段说明
在 orderInfo 对象中,处理部分成交时需要关注以下属性:
-
m_nOrderStatus(委托状态):- 55: 部分成交(关键状态,意味着还有剩余部分在撮合队列中)。
- 52: 部成已撤(最终状态,意味着部分成交了,剩下的被撤销了)。
- 56: 全部成交。
-
m_nVolumeTraded: 已经成交的数量。 -
m_nVolumeTotal: 委托剩余量(即挂在交易所还未成交的量)。
进阶处理建议
如果您的策略比较复杂,仅仅撤单可能不够,您可能需要**“撤单后追单”**。由于 order_callback 是异步推送的,直接在回调里发单可能会有逻辑风险(例如死循环:下单->部成->撤单->追单->部成...)。
建议的处理流程是:
- 在
order_callback中发现部成。 - 记录该笔交易的 ID 和未成交数量到全局变量(如
ContextInfo.pending_orders)。 - 发出
cancel()撤单指令。 - 在
order_callback中等待状态变为 52 (部成已撤) 或 54 (已撤),确认撤单成功。 - 在
handlebar或定时器中,检查撤单成功的记录,针对剩余数量,以最新价格(如对手价或涨停价)重新发起一笔新的委托。
Q&A
Q: 为什么我的 order_callback 没有被触发?
A: 请检查 init 函数中是否调用了 ContextInfo.set_account(accountID)。只有绑定了具体的资金账号,QMT 才会向策略推送该账号的交易回报。此外,请确保策略处于“实盘”或“模拟交易”模式,回测模式下回报机制可能有所不同(通常回测假设流动性充足,较少出现部成)。
Q: m_nVolumeTotal 在股票和期货中含义一样吗?
A: 基本一致,但在 QMT 的定义中,m_nVolumeTotal 通常指当前剩余的挂单量。即 m_nVolumeTotal = m_nVolumeTotalOriginal - m_nVolumeTraded - m_dCancelAmount(已撤量)。在处理部成逻辑时,判断 m_nVolumeTotal > 0 是确认是否还有挂单的关键。
Q: 如果我不撤单,剩余部分会怎么样?
A: 如果不撤单,剩余部分会继续挂在交易所的订单簿上,等待市场价格波动触及您的委托价。如果当天收盘仍未成交,交易所会在收盘后自动撤销该委托。