QMT 下单错误回调完全指南:orderError_callback、废单原因与委托状态怎么读

这是一份写给 QMT(迅投极速策略交易系统)实盘用户 的下单错误排查指南。
全文分四块:先分清”出错”有四种形态(很多人把四件事混成一件事)、再讲回调机制与关键字段(为什么下单函数没有返回值、错误从哪里冒出来)、然后给局部代码片段(怎么接错误回调、怎么识别废单、怎么建委托台账)、最后给错误对照表与免责声明。
片段基于 QMT 内置 Python 3.6,面向实盘策略。


一、这份指南给你什么

先按你的情况挑路径:

你是谁 建议路径
策略能跑但偶尔没下出去单 从 §2 读起,对号入座
想知道错误信息从哪来 直奔 §3 机制 + §4 回调字段
已经在接回调但读不懂字段 §4 状态表 + §5 片段 3
见过”废单”两个字但不知道去哪找原因 §4 的 m_strCancelInfo + §6 对照表

要解决的是实盘里最常见的一段空白:

策略写了、信号也出来了,但单子就是没进去,或者进去了变成了废单——我从哪里知道原因?


二、先分清:下单”出错”其实是四种不同的形态

这是全文最关键的一张表。不同的错误形态,由不同的机制暴露,你不能指望一个回调把所有问题都兜住:

形态 现象 什么时候发生 谁会发现它
A. 策略层根本没发单 有信号但没有任何委托,也没有任何报错 模拟模式运行(只显示信号);编辑器界面运行(不产生真实委托);quickTrade=0 且主图是日线及以上周期;非交易时段 没有任何回调——只能靠你自己核对业务逻辑
B. 参数/前置校验失败 客户端消息区出现报错,委托从未产生 参数组合不合法(如 prType=11 指定价但价格非法)、行情级别不足导致对手价取不到 orderError_callback(ContextInfo, orderArgs, errMsg)
C. 报出后被拒或成为废单 委托出现在委托列表里,但状态是废单 价格超出涨跌幅/价格笼子、数量超可用、证券状态异常等 order_callback 里读 m_nOrderStatus(57=废单)+ m_strCancelInfo(废单原因)
D. 报出去了但没成交 状态长期停在”已报”、”部成”,或被撤 挂单价格偏离、流动性不足、你自己撤单 order_callback / deal_callback,属于正常业务不是错误

一句话记忆:A 靠自查、B 靠 orderError_callback、C 靠委托状态 + 废单原因、D 靠委托/成交回调的字段变化。

很多人第一次写错误处理,只在 orderError_callback 里打了日志,结果真出问题时一行都没打印——因为那次错误属于 C 类,压根不走这个回调。


三、机制:为什么”下单函数”没有返回值可判断

理解这一点,错误处理的设计就顺了。QMT 的交易接口是异步的:

  1. 调用即返回。 passorder 发出委托后立刻返回,不等待回报,也不阻塞 Python 线程。所以没有”成功/失败”的返回值给你 if——判断必须靠回调。
  2. 回调读的是客户端本地缓存。 get_trade_detail_data 与四种交易回调(委托/成交/持仓/资金),都是从客户端本地缓存读取或触发,不是每次去问柜台。缓存由后台定期接收柜台推送刷新:有交易主推的柜台约 50ms 一次,没有的约 1~6 秒一次。
    • 由此推出一条非常实用的结论:卖出委托后立刻查询,很可能查不到这笔委托,可用资金也不会立刻变多。这不是 bug。
  3. 所有策略共用一个线程。 任意一个策略阻塞线程(sleep、死循环、加锁等待)会拖住所有策略。所以答案是明确的:不要在下单后写 sleep 等待结果,用回调或定时核对代替。
  4. 回调只在实盘模式生效,且需要先在 init 里调用 ContextInfo.set_account(account)。模拟模式、编辑器界面都不产生真实委托,也就没有这些回调可看。

顺带说一个容易误判的工具:get_last_order_id 能取”最新委托或成交的委托号”,但知识库明确提示——下单后需要一段不确定的时间才能查到本次委托号,而且如果这次委托废单,你查到的会是上一次成功下单的委托号。所以它不能用来判断”本次下单是否成功”,只能作为辅助。


四、三个主推回调与关键字段

4.1 谁负责什么

回调 触发时机 主要用途
orderError_callback(ContextInfo, orderArgs, errMsg) 账号下单异常时 抓参数级错误:把 errMsg 和 orderArgs 一起落盘
order_callback(ContextInfo, orderInfo) 账号委托状态变化时 抓废单/拒绝:读 m_nOrderStatus 与 m_strCancelInfo
deal_callback(ContextInfo, dealInfo) 账号成交时 确认成交价量,更新台账
position_callback / account_callback / task_callback 持仓 / 资金账号 / 任务状态变化时 对账、资金占用确认、任务型下单的状态跟踪

4.2 orderError_callback 的两个参数

orderArgs(下单参数对象 PassorderArguments)——就是把你那次 passorder 的参数原样还给你:

字段 类型 含义
opType int 操作类型(股票买入 23 / 卖出 24)
orderType int 下单方式(如 1101 按数量)
accountID string 资金账号
orderCode string 交易代码(注意格式为 SZ000001 这类”市场+代码”)
prType int 报价类型(11 指定价 / 5 最新价 / 14 对手价 等)
modelPrice float 下单价格
modelVolume int 下单量(手数或股数)
strategyName string 策略名_&&&_投资备注

errMsg 是字符串形式的错误描述。典型实测输出(知识库示例,已脱敏):

1
[函数交易] 函数: passorder, 证券 [SZ000001] 指定价 无效, 无法下单!

看这句话就能定位:prType=11 走指定价,但价格非法。errMsg + orderArgs 组合起来,绝大多数参数级错误一眼可判。

4.3 order_callback 里必须认识的字段

字段 含义 用法
m_nOrderStatus 委托状态(见下表) 57 就是废单,重点盯它
m_strCancelInfo 废单原因 状态为废单时来这里读原因
m_nOrderSubmitStatus 报单状态 52 = 报单已经被拒绝
m_nErrorID / m_strErrorMsg 状态 ID / 状态信息 兜底参考
m_nVolumeTotalOriginal / m_nVolumeTraded / m_nVolumeTotal 原始委托量 / 已成交量 / 剩余量 判断部成
m_strOrderRef / m_strOrderSysID 内部委托号 / 合同编号(委托号) 与台账、券商流水对账
m_strRemark 投资备注 与下单时 userOrderId 一致,是你独有的幂等键
m_strInsertDate / m_strInsertTime 委托日期 / 时间 排查时序问题
m_strExchangeName / m_strInstrumentName 交易所 / 证券名称 日志可读性

委托状态(EEntrustStatus)速查:

值 含义 值 含义
49 待报 54 已撤
50 已报(已报出到柜台,待成交) 55 部成(已有部分成交)
51 已报待撤 56 已成
52 部成待撤 57 废单(不符合报单条件被打回,原因看废单原因字段)
53 部撤

报单状态(EEntrustSubmitStatus)速查:48 已提交 / 49 撤单已提交 / 50 修改已提交 / 51 已经接受 / 52 报单已经被拒绝 / 53 撤单被拒绝 / 54 改单被拒绝。


五、局部代码片段:怎么接、怎么判、怎么兜

文件编码提醒:QMT 内置 Python 要求源文件 GBK 编码,首行 # coding:gbk。

片段 1:回调生效的两个前提

1
2
3
4
5
6
7
8
9
10
11
# coding:gbk
account = "test" # 策略交易界面运行时会自动替换为配置账号

def init(ContextInfo):
# 交易回调只在实盘模式生效,且必须先设置账号
ContextInfo.set_account(account)

def after_init(ContextInfo):
# 注意:编辑器界面运行时,下单函数不会产生真实委托,
# 因此这三个回调都不会被触发 —— 别在这里怀疑代码写错了
pass

片段 2:把 orderError_callback 变成可排查的日志

要点:不要 print 整个对象(里面含账号、账号 key 等敏感字段),只取你需要的字段,并对账号做掩码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def _mask(s):
"""账号类字段掩码:只留后 4 位"""
s = str(s)
return "****" + s[-4:] if len(s) > 4 else "****"

def orderError_callback(ContextInfo, orderArgs, errMsg):
# errMsg 是人话,orderArgs 是现场参数 —— 两者一起记,才定位得动
print("[下单异常] %s | 账号=%s 代码=%s opType=%s prType=%s 价=%s 量=%s 策略=%s"
% (errMsg,
_mask(orderArgs.accountID),
orderArgs.orderCode,
orderArgs.opType,
orderArgs.prType,
orderArgs.modelPrice,
orderArgs.modelVolume,
orderArgs.strategyName))

片段 3:在 order_callback 里识别废单与拒绝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
STATUS_JUNK    = 57   # 废单
SUBMIT_REJECT = 52 # 报单已经被拒绝

def order_callback(ContextInfo, orderInfo):
st = orderInfo.m_nOrderStatus

if st == STATUS_JUNK:
# 废单原因就在 m_strCancelInfo 里,这是最该打的一行日志
print("[废单] %s %s 原因=%s 备注=%s 委托号=%s"
% (orderInfo.m_strInstrumentName,
orderInfo.m_strInstrumentID,
orderInfo.m_strCancelInfo,
orderInfo.m_strRemark,
orderInfo.m_strOrderSysID))
elif orderInfo.m_nOrderSubmitStatus == SUBMIT_REJECT:
print("[报单被拒] %s %s 状态信息=%s"
% (orderInfo.m_strInstrumentID, orderInfo.m_nErrorID, orderInfo.m_strErrorMsg))
else:
# 正常状态:待报/已报/部成/已成…按需更新你自己的委托台账
pass

片段 4:台账思路——用”投资备注”当幂等键

错误处理真正的价值不在打印日志,而在防止错误导致的下一次错误。知识库给出的标准做法,思路是三步:

  1. 下单时给每一笔委托一个唯一的投资备注(passorder 的 userOrderId 参数),记为台账的 key。
  2. 台账里记状态:下单后默认置为**”待报”**。
  3. 收到 order_callback / deal_callback 后更新状态;若某品种存在”待报”状态的委托,暂停该品种的后续报单,防止超单。

骨架如下(片段,非完整实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
ORDER_LEDGER = {}     # { 投资备注: 状态 }  —— 用普通全局变量保存,不要塞进 ContextInfo

def on_order_sent(remark):
ORDER_LEDGER[remark] = "待报" # 下单即置待报,相当于占用一个"名额"

def _pending_of(code_remark_prefix):
# 是否还有未回报的委托(按你自己的备注命名规则判断)
return any(k.startswith(code_remark_prefix) and v == "待报"
for k, v in ORDER_LEDGER.items())

# 报单前先自检:该品种还有待报委托就跳过,避免重复下单
# if _pending_of("000001.SZ"):
# return

配合片段 3,废单或被拒时把台账里对应备注置为 "失败",才算把闭环补上。

片段 5:回调没来怎么办

交易接口是异步的,回报可能延迟(本地缓存 50ms16 秒刷新一次),所以需要兜底核对而不是干等:

1
2
3
4
5
6
7
8
9
# 思路:定时(例如每分钟)用 get_trade_detail_data 拉当日委托做核对
# 函数签名:get_trade_detail_data(accountID, strAccountType, strDatatype, strategyName)
# 账号类型 'STOCK'(股票)/ 'CREDIT'(信用)/ 'FUTURE'(期货)…
# 数据类型 'ORDER'(委托)/ 'DEAL'(成交)/ 'POSITION'(持仓)/ 'ACCOUNT'(账号)
# 传入 strategyName 后,只返回你自己这个策略的委托,避免全账号噪音
orders = get_trade_detail_data(account, "STOCK", "ORDER", strategyName)
for o in orders:
# 用 o.m_strRemark 与你的台账 key 对齐,补偿丢失/延迟的回报
pass

⚠️ 别用 get_last_order_id 判断本次下单是否成功——废单时它返回的是上一次成功下单的委托号,会把失败误判成成功。


六、常见错误 → 原因对照表

现象 / 提示 最可能原因 处理方向
指定价 无效, 无法下单! prType=11(指定价)但价格非法/未填 补合法价格,或改用 prType=5/14
对手价无效,无法下单! 行情源的全推行情级别不足,取不到对手价 按客户端提示调整行情级别设置
有信号、无委托、无报错 模拟模式运行;编辑器界面运行;quickTrade=0 且主图是日线及以上周期 切实盘模式;核对 quickTrade 取值与运行位置
有实盘信号但找不到委托 参数被本地校验拦下 看客户端消息区报错,按 orderError_callback 的 errMsg 修改参数
委托状态为 57 废单 不符合报单条件(价格笼子、数量、证券状态等) 读 m_strCancelInfo 定位具体原因
卖出后立刻查不到委托、可用资金没变多 本地缓存尚未刷新(1~6 秒) 不要在策略里 sleep 等,用回调/定时核对
同一状态收到多次推送 状态变化即可触发推送,日志里常见重复行 业务处理做幂等(以委托号+状态去重)

quickTrade 取值场景速记(来自知识库口径):

场景 取值
handlebar 逐 K 线,K 线结束生效 0(默认)
handlebar 盘中触发立刻下单 1(有信号闪烁风险,需自己处理)
定时器 / init / after_init / 行情与交易回调内下单 2(否则容易漏单)

⚠️ quickTrade=2 在历史 Bar 上也会触发下单,回测/重放/重连场景极易产生意外委托,通常不建议用它;仅在明确需要”回调内立即下单”等场景使用,并须先模拟验证。

⚠️ 实盘风险提示:上述片段涉及真实下单逻辑。请先在 QMT「模拟信号模式」或「模拟柜台」中验证回调与台账逻辑无误后,再切换至实盘账号。股票交易受 2% 价格笼子限制,委托数量超过可用数量会产生废单。


七、免责声明(请务必完整阅读)

阅读本文,即视为你已完整阅读、理解并同意以下全部条款。

1. 性质声明:本文及文中示例代码片段仅供编程学习与技术研究使用,不构成任何投资建议、荐股意见或收益承诺。 证券市场有风险,任何交易决策及其后果均由使用者独立作出并独立承担。

2. 模拟测试要求(重要): 在将本文思路或代码片段用于实盘环境之前,你必须在 QMT「模拟信号模式」或「模拟柜台」中进行充分测试,验证回调触发、委托台账、异常处理均符合预期。本文作者/发布者无法也不承诺对你的特定环境进行测试。未经充分模拟验证直接接入实盘所导致的一切后果,由使用者自行承担全部责任。

3. 字段与口径说明: 文中回调字段、枚举值、错误提示均依据 QMT 官方文档知识库整理;不同券商版本、不同行情权限下的表现可能存在差异,最终以你本地客户端实际输出和券商柜台结果为准。委托状态、废单原因的解释权在交易所与券商,本文不构成对任何具体委托结果的承诺。

4. 数据与隐私提示: 交易回调对象包含资金账号、账号 key、委托号、成交号等敏感信息。请勿将回调原始对象直接 print、写入日志、截图或提交到工单;如需排查,请先对敏感字段做掩码处理。

5. 无担保声明: 本文内容与示例代码按”现状”提供,不附带任何明示或默示的担保。作者/发布者不保证示例代码无缺陷、不中断、不出错,也不保证读者自行实现的版本能达到预期效果。

6. 责任限制: 在任何情况下,作者/发布者均不对因使用或无法使用本文内容、示例代码而产生的任何直接、间接、附带、特殊、惩罚性损失(包括但不限于交易亏损、机会损失、数据丢失、停机损失)承担责任。即使作者/发布者已被告知此类损失的可能性。

7. 内容定性声明确认: 本文及其中的示例代码属于计算机编程技术内容,不构成证券投资咨询或投资顾问服务。作者不提供代客理财、代办交易、跟单操作、个股推荐或买卖时机建议等任何服务。

8. 版权声明: 本文版权归作者所有。本文免费分享,欢迎个人学习使用,也欢迎注明出处后自由转发;但未经书面授权,不得用于商业培训、付费社群、二次售卖,或以原创名义转载。

9. 合规使用: 使用者承诺遵守国家法律法规及所在券商的相关规定,不得将本文内容用于任何违法违规用途。


八、写在最后

交易错误处理的分水岭,不是”我会不会写回调”,而是你知不知道错误会从哪几个地方冒出来:

  • 没发出去的单,回调永远不会告诉你(A 类);
  • 参数错了,去找 orderError_callback 的 errMsg(B 类);
  • 报出去被打回,去找委托状态的 57 和 m_strCancelInfo(C 类);
  • 报出去没成交,那是业务,靠委托/成交回调更新台账(D 类)。

把这两类回调接上、给每笔委托一个唯一备注、再补一次定时核对——你的策略就从”下单靠运气”变成”下单有账可查”。

⚠️ 最后再强调一次:任何代码在实盘使用前,请务必在模拟模式下充分测试。 市场有风险,交易需谨慎。