← 返回首页

QMT 内置 Python 实战规范(下):品种判定、行情字段与自证方法 🔍

上一篇讲完了环境、生命周期和下单。这篇讲三个同样高频、但更容易被文档误导的部分:品种判定、行情取数、交易对象字段。

最后一节讲一个我认为最有用的方法:怎么让客户端自己回答「这个语法到底行不行」。

配图

一、品种判定:优先用 C.is_stock,别手写代码前缀表

早期判品种基本靠手写前缀(110 是沪市转债、123 是深市转债……),又长又容易漏。QMT 其实给了现成的判定函数,实测准确:

PYTHON
C.is_stock('600901.SH')     # True   —— 沪深A股(主板 / 创业板 / 科创板)
C.is_stock('510300.SH')     # False  —— ETF 不是股票
C.is_stock('000001.SH')     # False  —— 指数不是股票
C.is_fund('510300.SH')      # True   —— ETF

注意只传 1 个参数,而且是完整代码。 写成 C.is_stock('SH', '600901') 会报 takes 2 positional arguments but 3 were given。

有两个函数名很像,但不能盲用:

  • C.is_suspended_stock('000001.SH') 返回 True —— 它按「当前是否可交易」判定,非股票代码一律返回 True,不能当个股停牌查询。
  • C.get_stock_type(code) 在客户端里恒返回 0,不可用。

一个会静默返回空的板块名陷阱

get_stock_list_in_sector(name) 是取板块成分股的主力接口。但名字不认识时它不报错,只返回空列表 —— 这是最容易误判的地方:

CODE
+------------------+---------+------------------------------------------+
| 板块名           | 数量    | 说明                                     |
+------------------+---------+------------------------------------------+
| 沪深A股          | 5226    | 可用                                     |
+------------------+---------+------------------------------------------+
| 沪深京A股        | 5574    | 可用(含北交所)                         |
+------------------+---------+------------------------------------------+
| 沪深ETF          | 1727    | 可用                                     |
+------------------+---------+------------------------------------------+
| 沪深转债         | 318     | 可用 —— 判可转债就用这个                 |
+------------------+---------+------------------------------------------+
| 沪深300          | 300     | 可用                                     |
+------------------+---------+------------------------------------------+
| 中证500          | 500     | 可用                                     |
+------------------+---------+------------------------------------------+
| 沪深可转债       | 0       | X 最顺口的名字反而返回空                 |
+------------------+---------+------------------------------------------+
| 可转债 / 转债    | 0       | X                                        |
+------------------+---------+------------------------------------------+
| ETF              | 0       | X 必须写「沪深ETF」                      |
+------------------+---------+------------------------------------------+

判可转债时直觉会写 '沪深可转债',返回 0,然后你会以为「今天没有可转债」。正确名字是 '沪深转债'。

is_typed_stock 的类型码:手册有表,但不全生效

is_typed_stock(type_num, market, code) 返回 1 / 0。手册里有完整分类表,实测只有一部分在本机生效:

CODE
+------------------+--------------------------------------------+---------+
| 类型码           | 实测                                       | 可用    |
+------------------+--------------------------------------------+---------+
| 100001           | 沪深A股,不命中 ETF / 债 / 回购 / 指数     | OK      |
+------------------+--------------------------------------------+---------+
| 100013           | ETF(沪、深)                              | OK      |
+------------------+--------------------------------------------+---------+
| 100029           | 主板,不命中创业板 / 科创板                | OK      |
+------------------+--------------------------------------------+---------+
| 100005           | 指数                                       | OK      |
+------------------+--------------------------------------------+---------+
| 100016           | 债券(含可转债,不区分)                   | OK      |
+------------------+--------------------------------------------+---------+
| 100020 / 100021  | 债券回购 / 质押式回购,判逆回购用          | OK      |
+------------------+--------------------------------------------+---------+
| 135              | 沪市可转债(真实样本命中)                 | OK      |
+------------------+--------------------------------------------+---------+
| 100011           | 深市可转债(真实样本命中)                 | OK      |
+------------------+--------------------------------------------+---------+
| 100004           | 会把 159xxx ETF 一起判成封基               | X 陷阱  |
+------------------+--------------------------------------------+---------+
| 100019           | 手册写「深市可转债」,实测不生效           | X       |
+------------------+--------------------------------------------+---------+
| 100040~100047    | 各期限逆回购,实测不生效                   | X       |
+------------------+--------------------------------------------+---------+

两个结论值得单独说。

第一,深市可转债是 100011,不是手册写的 100019。 我用板块名单里的真实转债复核过:110075.SH 命中 135,127025.SZ、127027.SZ 命中 100011,而 100019 对真代码、假代码都不命中。第一轮验证时我拿的是一个已摘牌的 110055.SH,导致得出「135 未实现」的错误结论 —— 样本一定要取自板块名单,不要凭记忆写代码。

第二,它不校验标的真实存在,但也不是纯代码段匹配。 用一个不存在的代码 159999.SZ 去问,命中的类型码和真实的 159915.SZ 完全一样,说明未登记代码走代码段兜底。但真实存在的 110075.SH 命中 135、已摘牌的 110055.SH 不命中,又说明已登记代码会查类型表。

所以:不要用它判断「这个代码存不存在」,也不要拿「未命中」去反推真实标的的类型。

判品种的优先级:C.is_stock / C.is_fund → 板块名单 → is_typed_stock。

顺带两个能省不少事的函数:

PYTHON
is_sector_stock('沪深300', 'SH', '600000')          # -> 1   浦发银行在沪深300内
is_sector_stock('沪深300', 'SH', '600901')          # -> 0
get_industry_name_of_stock('SW', '600901.SH')       # -> 'SW1非银金融'(申万)
get_industry_name_of_stock('CSRC', '600901.SH')     # -> 'CSRC1金融业'(证监会)

二、交易日判断:一个能把日志刷爆的坑

策略的第一件事通常是判今天是不是交易日。写法就一行:

PYTHON
today = datetime.datetime.now().strftime('%Y%m%d')
is_trading = bool(C.get_trading_dates('000001.SH', today, today, 1, '1d'))

但它有两个坑,我都踩过。

坑一:返回 [] 不代表接口坏了。 实测同一组调用:

CODE
C.get_trading_dates('000001.SH', '20261003', '20261003', -1, '1d')  -> []
C.get_trading_dates('000001.SH', '20261003', '20261003',  1, '1d')  -> []
C.get_trading_dates('000001.SH', '20261003', '20261003', 30, '1d')  -> []

20261003 是个周六。start == end == 非交易日 → 请求区间为空 → 无论 count 传什么都返回 []。 空返回跟 count 完全无关。

我一开始把这个归因成「文档说 count 必须大于 0,实盘代码却传 -1,所以有问题」,结论是错的。实盘里那个「传 1 判 bool」的写法本来就是对的 —— 交易日返回 [today] → True,非交易日返回 [] → False。

坑二:count=-1 配空区间会拉出 30 多年的数据。

CODE
C.get_trading_dates('000001.SH', '', '', -1, '1d')
# -> 1990-12-19 至今的全部交易日,一万多条

这是真事儿:我在探针脚本里写了这么一行,结果整份日志被刷爆,后面几个探测段全部丢失,白跑一次。

规则:count=-1 只在有明确 start / end 时用;交易日判断固定写 (今天, 今天, 1, '1d')。

count 的完整语义(用非空区间实测):

CODE
+----------+----------------------------------------------+
| count    | 返回                                         |
+----------+----------------------------------------------+
| -1       | 区间内全部交易日                             |
+----------+----------------------------------------------+
| 1        | 区间内最后一个交易日                         |
+----------+----------------------------------------------+
| 5        | 区间内最后 5 个交易日                        |
+----------+----------------------------------------------+
| 30       | 区间内全部(被区间长度截断)                 |
+----------+----------------------------------------------+

另外 start_date / end_date 是 'YYYYMMDD' 或 'YYYYMMDDHHMMSS' 字符串,不传时用 '',不是 None。

三、行情:三套数据源分开用

CODE
+--------------+------------------------------+----------------------------+
| 数据源       | 接口                         | 何时用                     |
+--------------+------------------------------+----------------------------+
| 本地数据     | get_market_data_ex()         | 回测、历史统计             |
+--------------+------------------------------+----------------------------+
| 全推数据     | get_full_tick(codes)         | 盘中最新快照,数量无上限   |
+--------------+------------------------------+----------------------------+
| 订阅数据     | subscribe_quote(...)         | 需要 tick 事件驱动         |
+--------------+------------------------------+----------------------------+

(全部写 C. 前缀)

三个必须知道的限制:

  1. init 里 get_market_data 系列只能读到本地数据,取不到实时行情。要取实时得放 after_init 或定时器。
  2. 全推行情默认可能只给一档盘口。 要用对手价 14 或读五档,必须先把客户端的全推行情级别改成五档,否则会报「对手价无效,无法下单」。这是「策略不报单」的高频原因。
  3. 超过订阅上限时不会报错,客户端会用前值填充、返回重复数据。 这是「数据看起来对但全是重复值」的根因。

四、交易对象字段:一个会让止盈止损彻底失效的哨兵值

这部分最值得说的是一个实测挖出来的坑。

1.7976931348623157e+308 是「柜台没给这个字段」

实测持仓对象里读到:

CODE
m_dAvgOpenPrice     = 1.7976931348623157e+308
m_dStaticHoldMargin = 1.7976931348623157e+308

1.7976931348623157e+308 是 IEEE 754 双精度最大值(DBL_MAX),在这里的含义是「这个字段无效」,不是数据。

如果拿 m_dAvgOpenPrice 当持仓成本去算止盈止损,价格会变成天文数字,现价 < 成本 × 0.9 恒真、> 恒假 —— 止损会立刻误触发,或者永远不触发。

持仓成本要用 m_dOpenPrice。 顺手加个通用防御:

PYTHON
def valid(x):
    return isinstance(x, float) and abs(x) < 1e300

m_nDirection 恒为 48,在哪个对象上都是

判买卖方向是 QMT 最经典的字段级坑:m_nDirection 对股票恒为 48,真正的买卖在 m_nOffsetFlag(48 = 买、49 = 卖)。

实测更狠一点:这个 48 不只在委托对象上,账户对象和持仓对象上读到的也是 48。 所以这个字段在股票语境下毫无鉴别力,任何 if x.m_nDirection == 48 都是恒真。

PYTHON
# 正确
is_buy = (int(getattr(o, 'm_nOffsetFlag', 0)) == 48)

其它实测确认的字段行为

CODE
+----------------------------+------------------------------------------+
| 项                         | 实测结论                                 |
+----------------------------+------------------------------------------+
| m_nCanUseVolume            | 正常填充,卖出封顶可以放心用             |
+----------------------------+------------------------------------------+
| get_last_order_id          | 返回字符串 '-1'(不是 int),判空写 == ' |
|                            | -1'                                      |
+----------------------------+------------------------------------------+
| 持仓 m_nVolume 单位        | 是「股」:1300 × 6.94 = 9022.0 = 市值    |
+----------------------------+------------------------------------------+
| 交易函数关键字参数         | 不支持,一律位置传参                     |
+----------------------------+------------------------------------------+
| 对象类型名                 | 账户 CAccountDetail,持仓 CPositionDetai |
|                            | l                                        |
+----------------------------+------------------------------------------+
| m_dFloatProfit 与          | 两个不同口径,同一次实测里 117.00 vs 843 |
|                            | .18                                      |
+----------------------------+------------------------------------------+
| m_dPositionProfit          |                                          |
+----------------------------+------------------------------------------+

「持仓量单位是股」这条是用市值反推的:1300 × 6.94 = 9022.0,与 m_dMarketValue 完全相等,三个样本全中。注意这只证明持仓量;行情 tick 里的 bidVol 单位是另一个问题,不能混为一谈。

五、最有用的方法:让客户端自己回答语法问题

前面所有结论,没有一条是「我觉得」。全部来自一个笨办法:写探针脚本,扔进客户端跑,看它自己怎么说。

流程只有四步:

  1. 不确定的语法不猜。 先查文档,查不到就登记下来。
  2. 写一个只读的探针脚本,只做查询、不产生任何委托。
  3. 交给客户端跑,把日志拿回来。
  4. 结论落进规范文档,状态从「待验证」改成「已证实」。
几个技巧值得抄:
  • 用真实标的交叉验证。 判类型码时不要只试自己记得的代码,要从 get_stock_list_in_sector('沪深转债') 里取板块名单里的真代码。我第一次就是拿了一个已摘牌的代码,得出完全相反的结论。
  • 用不存在的代码做对照实验。 传一个未发行的代码进去,如果它和真代码命中完全相同,说明规则是按代码段兜底,而不是查标的。
  • 看 C++ 报错反推签名。 参数传错时,报错信息会直接列出全部重载签名 —— 这是最权威的签名来源,比任何文档都准。
  • 把「读」和「写」分开。 只读探针随便跑;要测下单路径,就用 prType=11 + price=0 这种文档明确记为「必然废单」的组合,或者干脆传一个不存在的代码,做到零真实委托。
  • 日志里放一行 ASCII 哨兵字符串。 如果这行显示正常,说明编码链路没问题,整份日志都可信。
这套做法的价值不在于这次验证了什么,而在于它把「我记得」「应该是」换成了「客户端说是」。换券商、换客户端版本的时候,重跑一遍就行。

小结

三句话收尾:

  • 判品种优先 C.is_stock / C.is_fund;板块名要精确(沪深转债,不是 沪深可转债)。
  • 行情三套源分开用:全推只管最新值,本地数据要和历史拼,订阅超限会静默填重复值。
  • 字段上最大的雷是 DBL_MAX 哨兵值,最一贯的错误是拿 m_nDirection 判方向。
QMT 的文档不会告诉你这些,但客户端会。学会问它。

💬 评论 (0)

今日还可评论 3 条

还没有评论,来抢沙发吧。