简介:《同花顺level-2全推行情Python API操作手册》面向从事高频交易与量化策略开发的Python工程师,尤其适合涨停板打板、盘口异动监控等场景的中高级读者,帮助其掌握沪深Level-2实时行情的接入与数据解析。资源包仅含1个PDF文档,约850KB,篇幅紧凑却覆盖完整,可随查随用。手册以TDS系统的C/S架构为主线,先交代运行环境与安装步骤,再详解常规模式的初始化流程:创建TDS实例、定义fnc_data等回调函数,通过set_env注册证券快照、指数快照、逐笔成交、逐笔委托、委托队列与极速盘口六类数据接口,并提示断线通知后需重新登录订阅。文档另附V2.0.0至V2.1.9的完整修订表,逐条记录多连接模式改造、级联性能优化、字段类型由int改为float、通道号与委托序号新增、异步订阅接口async_sub等变更,便于排查字段对不上的问题。已有2846人学习下载,适合边写代码边对照字段说明的实战型读者。
1. 盯涨停板的人都懂:五档快照永远慢半拍
盘口封单刚推到屏幕上,等你确认完再点下单,价格已经封死了。做涨停板策略的开发者多半都遇到过这种画面,问题往往不在网络,而在只看五档快照这种决断方式本身。这份 Level-2 全推行情 Python API 操作手册要解决的,是把逐笔委托、委托队列、极速盘口这些明细数据直接接进自己的量化程序里,让证券交易信号建立在逐笔级明细而非几百毫秒前的快照上。它是行情数据管道,不是策略库也不是回测框架:C/S 架构,SDK 直连行情服务端,回调推数据、同步接口拉快照,支持 C/C++、Python、Java 多语言和 Windows、Linux 双平台。适合两类人,一类是做打板和盘中监控、对明细字段和时延敏感的策略开发者,另一类是想先把数据接稳、再慢慢打磨信号的工程同学。跑通门槛不高,但有那么几个配置点,不看手册准踩坑。
2. 架构与环境:从行情源到 Python 回调的三段链路
2.1 三段链路与两种取数方式
整套系统是典型 C/S 结构,服务端从行情源接入实时数据,中间做解码、订阅管理和分发,最后以 API 形式对外提供完整、低延迟的非展示性行情。链路可以拆成三段:行情源接入层负责对接原始数据流,数据处理层负责解码和按订阅关系过滤,API 层负责把数据推给上层应用。高可用保障在这三层分别做,任何一层抖动都可能导致数据延迟。
对写 Python 的人来说,实际接触到的只有第三段。取数有两条路:一条是回调推送,注册回调函数之后行情自己流进来,逐笔成交、逐笔委托、委托队列这类高频数据只能走这条;另一条是同步查询,调用一次拿一次结果,适合启动时拉代码表、盘中偶尔校准个别标的状态。两条路共用同一个 TDS 实例,但要先 set_env 再登录,顺序错了后面全是空转。
SDK 分 Level-1 和 Level-2 两个版本。Level-1 只给五档快照和基础成交,Level-2 给十档快照、逐笔成交、逐笔委托、委托队列,极速盘口还要单独开产品权限。做打板的基本绕不开 Level-2,因为封单排队位置和撤单动作,五档快照里根本看不出来。
还有一个容易被忽略的形态是级联模式,主 SDK 连行情服务端、从 SDK 连主 SDK,在局域网内做数据分发,适合一台机器上跑多个策略实例却不想每个实例都去抢服务端连接的场景。这个模式要改配置文件,后面单独说。
版本演进里有几处改动直接影响代码写法,值得先记住:早期版本单连接,从某个版本起改成多连接,需要针对不同市场注册域名和端口,Linux 版本后来把默认配置写进去了,不用再手动填;再往后加了级联、加了离线调试、加了异步订阅接口async_sub,还把消息通知回调的参数从 int 改成了 dict。手上代码如果是照老版本手册写的,升级后大概率在这一两处翻车。
2.2 安装与环境基线:8G 内存不是写着好看的
硬件和系统基线在手册里写得很直接,照着对一遍能省掉很多“为什么收不到数据”的排查时间。
| 项目 | 要求 |
|---|---|
| CPU | 64 位双核及以上 |
| 内存 | 8G 及以上 |
| 带宽 | 下行 100Mbps 及以上 |
| 操作系统 | Windows 7 及以上,CentOS 7 等常用 Linux 发行版 |
| Python | 3.5(64 位)及以上 |
带宽这一条最容易被低估。全市场订阅十档快照加逐笔,数据量是按每秒几万条算的,下行只有几十兆的时候,表现是回调延迟越来越大、最后开始丢包,而不是直接报错,排查起来很费时间。内存 8G 同理,回调里如果顺手把数据往列表里堆,跑一上午就得上 swap。
安装走离线包,两种方式都行:
# 方式一:直接 pip 安装本地 tar.gz pip install ./L2-tdsapi-python-X.X.X-XXXXXXXX.tar.gz # 方式二:解包后走 setup.py tar -zxvf L2-tdsapi-python-X.X.X-XXXXXXXX.tar.gz cd L2-tdsapi-python-X.X.X-XXXXXXXX python setup.py installLinux 下要用 root 权限装,否则装到用户目录里,换一个账号跑脚本就 import 不到。装完验证很简单,进 Python 环境导入包:
import datafeed # 不抛异常就说明装上了日志路径默认落在包安装目录,这个默认值在容器里往往指向一个只读或者会被清掉的位置,我在 set_env 里一律显式指定m_logpath,图个安心。跑起来之前先把日志级别调到 warn,调试阶段再降到 debug,不然全市场推数据的时候日志文件涨得比数据还快。
2.3 市场代码表:USH 系和 USZ 系的对应关系
订阅和查询都要传市场代码,传错的表现是返回空列表或者直接返回错误码,不报异常,很容易误判成“今天这只票没数据”。两个市场的代码是分开编的:
| 市场代码 | 含义 | 市场代码 | 含义 |
|---|---|---|---|
| USHI | 沪市指数 | USZI | 深市指数 |
| USHA | 沪市 A 股 | USZA | 深市 A 股 |
| USHB | 沪市 B 股 | USZB | 深市 B 股 |
| USHD | 沪市债券 | USZD | 深市债券 |
| USHJ | 沪市基金 | USZJ | 深市基金 |
| USHP | 沪市退市整理 | USZP | 深市退市整理 |
| USHT | 沪市风险警示板 | — | — |
有个细节手册专门点了出来:沪市 A 股的范围里还包括 USHT 这个风险警示板市场,只订 USHA 是订不到 ST 类标的的。做涨停板策略时这一点尤其要注意,ST 股的涨停幅度和普通票不一样,如果你的策略脚本把 ST 混在里面算涨跌幅,回测结果和实盘会对不上。
环境参数里还有个m_connectmarket,默认值是"USH;USZ",两个市场都连。只做单市场的话可以填"USZ"或"USH",少连一半市场,资源占用和心跳负担都会降下来。这个参数是后来版本才加的,老代码里没有,升级后可以顺手补上。
3. 常规模式四步走:set_env、login、查询、订阅
3.1 实例化与 set_env:回调注册是唯一必须先做的事
整个流程的第一颗扣子是 set_env。创建实例本身没什么内容,一行就够,但 set_env 必须在所有注册、登录之前调用,否则后续动作全部无效,而且不一定报错,只是什么都不发生。
from datafeed.tds_api import TDS import queue tds = TDS() raw_queue = queue.Queue(maxsize=200000) def on_stock(data_list: list, count: int, stats: dict): """证券快照回调:只做入队,不做任何计算""" for item in data_list: raw_queue.put(item) # 满了会阻塞,这里故意让它阻塞形成背压 def on_notify(msg_notify: dict): """断线通知:1 网络断开 2 网卡异常 3 重复登录被踢""" print("notify:", msg_notify.get("code"), msg_notify.get("msg")) def on_subscribe(data_type, op_type, market, code_list, statuscode, callback_type): """订阅结果回调:callback_type=1 主动异步订阅,2 重连后自动重发""" print(data_type, op_type, market, statuscode, callback_type) ok = tds.set_env( fnctickstock=on_stock, # 证券快照回调,签名必须是 (data, count, stats) fncnotify=on_notify, # 断线通知回调,签名必须是 (msg_notify) fucsubscribe=on_subscribe, # 异步订阅回调 m_heartbeat_interval=10, # 心跳间隔,秒,小于 1 用系统默认 m_reconn_count=5, # 断线重试次数,小于 1 不重连 m_reconn_interval=3, # 重连间隔,秒 m_timeout=5000, # 同步请求超时,毫秒 m_connectmarket="USH;USZ", # 只做单市场可以改成 "USHA" 之外的写法 m_connectsubscribe=1, # 重连成功后自动重发订阅,强烈建议开 m_logpath="/data/tdslog", m_loglevel="warn", )回调一共六个数据类:fnctickindex指数快照、fnctickstock证券快照、fncorderqueue委托队列、fncorderdata逐笔委托、fnctransaction逐笔成交、fncsuperstock极速盘口,再加fncnotify断线通知和fucsubscribe异步订阅回调。只注册自己用得到的,注册了不订阅也不会有数据,但多注册一个空函数除了占点内存没别的坏处。
参数里最值钱的是m_connectsubscribe。老版本的痛点是断线重连成功后,订阅关系全丢,数据静悄悄地停掉,程序还活着,你以为还在收行情。设成 1 之后重连会自动重发订阅请求,并通过订阅回调通知你,callback_type 传 2 就是这条路径。
注意:手册里 fucsubscribe 的签名前后出现过四参数和六参数两种说法,以 set_env 里实际注册后触发的六参数版本为准,写四参数的话回调里拿不到 statuscode,订阅失败完全无感。
3.2 登录与返回码:从 -1005 到 -1018
登录本身是同步函数,返回 0 成功,小于 0 失败。失败码表手册列了十几个,实际排障最常撞见的是这几个:
| 返回码 | 含义 | 常见诱因 |
|---|---|---|
| 0 | 成功 | — |
| -1004 | 用户名密码错误 | 账号串了环境,测试号连生产 |
| -1005 | 无法连接至服务器 | 网络不通、端口被挡、级联时连错 IP |
| -1004 之外 | 一般错误 | 参数格式问题 |
| -1014 | 级联模式下主 IP 连接异常 | 从 SDK 先于主 SDK 登录 |
| -1016 | 重复登录 | 同一账号在别处已登录并被踢掉 |
| -1017 | 通行证校验失败次数太多 | 密码错了太多次,账号被临时锁 |
| -1018 | 版本太低 | 服务端要求升级 SDK |
ret = tds.login("your_account", "your_password") if ret != 0: raise RuntimeError(f"login failed, code={ret}")-1018 这条要单独记住。它不是密码错也不是网络问题,而是本地 SDK 版本落后于服务端要求,症状是换谁登录都失败,只有升级包才能解决。手上项目如果是打包好的可执行程序,遇到这个码就得回去重新出包,临时改代码绕不过去。
登录之后如果连接断了,重连失败达到阈值,SDK 会停止工作并回调通知函数。手册里的处理建议很明确:收到通知要重新调用 login 和订阅。所以业务侧一定要有个状态标志位,收到 code=1 或 2 的通知就置成“不可用”,别让策略在断线期间继续按老数据做决策。
3.3 同步查询:代码表、快照、极速盘口
查询接口是同步的,调用即返回,适合做初始化,不适合放进高频循环。
# 拉某个市场的全部代码 codes = tds.get_code_list("USHA") # 拉指定代码的证券快照,code_list 传 [] 表示全市场 snap = tds.get_tick_stock("USHA", ["600000"]) # 拉指数快照 idx = tds.get_tick_index("USHI", []) # 拉极速盘口,需要单独的产品权限,没权限返回空 super_snap = tds.get_super_stock("USZA", ["000001"])四个查询函数的签名完全一致,都是(market, code_list)。几个使用细节:code_list里的代码必须属于传入的市场,拿 USHA 去查深市代码返回空;[]表示全市场,但全市场查询返回的是列表,几万条数据一次性构造出来,内存和耗时都不小,只建议在盘前做一次;get_code_info在手册里标注了暂未实现,调用会拿到负数返回,别在业务代码里依赖它。
同步查询和回调推送拿到的数据结构是一致的,都是字典组成的列表,字段含义看第 4 章。所以盘前用查询拉一遍基准数据、盘中用回调增量更新,两条路可以平滑拼起来。
3.4 订阅:sub 和 async_sub 怎么选
订阅接口有五个,同步四个加异步一个,语义别搞混:
# 取消该市场该类型的所有订阅,然后按新列表重新订阅 tds.sub("stock", "USHA", []) # 取消该市场该类型的所有订阅 tds.unsub("stock", "USHA") # 在已有订阅基础上增加代码 tds.add_sub("trans", "USHA", ["600000"]) # 在已有订阅基础上取消指定代码 tds.del_sub("stock", "USHA", ["600000"]) # 异步订阅,结果走 fucsubscribe 回调 tds.async_sub("order", 1, "USZA", [])sub和add_sub的差别是很多人的第一个坑:sub是“先清空再重设”,add_sub才是追加。盘中想给已有订阅加一只票却用了sub,结果全市场订阅被覆盖成一只票,表现是其他票的数据突然全停了。血泪经验是:启动阶段用sub建基线,运行期一律用add_sub和del_sub。
异步订阅多一个op_type参数:1 全新订阅、2 取消订阅、3 增加订阅代码、4 取消订阅代码。它不阻塞当前线程,结果通过订阅回调返回,回调里带 data_type、op_type、market、code_list、statuscode、callback_type 六个参数。statuscode 为 0 才算成功。全市场订阅那种动辄上万代码的场景,用异步接口比同步接口舒服,至少不会把主循环卡住几百毫秒。
数据类型的取值是五个字符串:index、stock、queue、order、trans,极速盘口用superstock。写错字符串不会报错,只是订不上,返回 0 也可能是假成功,建议把类型字符串定义成常量集中管理。
4. 数据字段拆解:快照十档、逐笔和状态机怎么读
4.1 回调三参数与链路时间戳
所有行情回调的签名都是(data_list, count, stats)。data_list是这一批数据的列表,可能一条也可能几百条;count是条数;stats是这条数据在链路上经过各个节点时打的三个时间戳,单位是微秒。
import time def on_stock(data_list, count, stats): now_us = int(time.time() * 1_000_000) produce = stats["produce_time"] # 服务端收到行情的时间 send = stats["send_time"] # 服务端发出行情的时间 recv = stats["recv_time"] # SDK 收到行情的时间 print("服务端处理 us:", send - produce) print("网络传输 us:", recv - send) print("本机排队 us:", now_us - recv)这三个差值分别对应三段耗时:服务端解码和处理、网络传输、回调队列排队。做低频策略时它们意义不大,做打板时它们就是你的数据新鲜度指标。如果本机排队这个值持续往上走,说明回调里干了重活,SDK 的推送线程被拖住了,数据在缓冲区里堆积。这时候改策略逻辑没用,必须把回调改成纯入队。
data_list里每条数据的字段是多层嵌套的,买卖价量是列表,委托笔数也是列表,逐个下标对应买一到买十。别用固定下标硬编码,写成循环,档位数量将来变了也不用改。
4.2 证券快照:十档、委托笔数、撤单和内外盘
Level-2 证券快照的基础字段和 Level-1 大体一致,多出来的是十档深度和一堆统计量。挑实战里真正会用到的说:
| 字段 | 类型 | 说明 |
|---|---|---|
| market / code / codename | str | 市场、代码、名称 |
| status | str | 当前状态,编码规则见 4.4 |
| date / time | int | YYYYMMDD / HHMMSSsss |
| pre_price / open_price / new_price | float | 昨收、开盘、最新 |
| high_price / low_price | float | 最高、最低 |
| volume / turnover / trade_num | int/float/int | 成交量、成交金额、成交笔数 |
| bidorder_price / bidorder_volume | list | 申买价量,买一到买十 |
| bid_numorders | list | 买一到买十各自的委托笔数 |
| askorder_price / askorder_volume | list | 申卖价量 |
| ask_numorders | list | 卖一到卖十各自的委托笔数 |
| totalbid_volume / totalask_volume | int | 总委买量、总委卖量 |
| avgbid_price / avgask_price | float | 加权平均委买价、委卖价 |
| withdraw_buynum / withdraw_buyvolume | int | 买入撤单笔数与数量(沪市) |
| withdraw_buyturnover / withdraw_sellturnover | float | 买/卖撤单金额,早先版本是 int,精度不够才改成 float |
| inner_volume / outer_volume | int | 内盘、外盘成交量 |
| aftermarket_volume / aftermarket_volmount | int | 盘后成交量、盘后成交次数 |
| matching_volume / matching_turnover / matching_price | int/float/float | 匹配成交量、金额、价格(深市) |
| deal_mode | int | 最近一笔成交的成交方式 |
| mode_state | str | 各交易方式的当前状态 |
打板场景里最有信息量的三个字段是bid_numorders、totalbid_volume和withdraw_buynum。封单量大不代表封得住,如果买一价位挂了几万手但委托笔数只有几十笔,说明是大单堆的,撤一笔就塌;反过来笔数几千笔、每笔都不大,才是散户和中小资金一起堆的堆头,相对结实。撤单字段在沪市快照里才有,买入撤单数量和买入撤单笔数比值异常放大,通常意味着封单在松动。
avgbid_price和totalbid_volume配合能算出委托的加权平均成本位置,用来判断买盘是不是集中在某个价位附近。这些都不是现成的信号,只是原料,怎么用取决于你的策略,别指望 SDK 给你算好。
注意:带“沪”“深”标注的字段只有对应市场有效,跨市场统一处理时要么按 market 分支,要么在预处理层把缺失字段填成 None,不要填 0,0 和缺失在后续计算里语义完全不同。
4.3 逐笔成交、逐笔委托和委托队列
这三个是 Level-2 的核心增量数据,打板策略真正依赖的就是它们。
逐笔成交(trans)记录每一笔撮合,关键字段是通道号、业务序号、成交价、成交量、成交方式。逐笔委托(order)记录每一笔新进的委托单,包含通道号、委托序号、价格、数量、买卖方向。两者配合能还原出“谁在什么价位排了队、后来撤了没有”的完整过程。
委托队列(queue)给的是买一或卖一价位上的排队明细,通常是前若干笔。做打板时用来看自己排的位置,判断成交概率;做盘中监控时用来看有没有大单突然撤走。队列数据的更新频率比快照高得多,只订队列不订快照也能跑,但定位需要跟快照配合。
def on_order(data_list, count, stats): for o in data_list: # 逐笔委托:只看买方向的撤单和新挂大单 side = o.get("side") # 买卖方向字段,按实际情况取 price = o.get("price") volume = o.get("volume") channel = o.get("channel_no") # 通道号,多通道场景下用来做去重和排序 seq = o.get("order_seq") # 委托序号,同通道内单调递增 if volume and volume > 5000: print(channel, seq, side, price, volume)通道号和序号是后来版本加的字段,作用是排序和去重。多个通道的数据到达顺序不保证一致,靠时间戳排可能撞上同毫秒,靠通道号加序号才是可靠的全序。如果你的策略对“先挂单后撤单”这种顺序敏感,一定要按(channel_no, order_seq)排,不要按到达顺序。
成交量单位也要注意,股票按股、权证按份、债券按张,别拿股票的股数逻辑直接套债券。
4.4 status 和 mode_state:把状态机读明白
status是个字符串,两个市场规则不一样,长度和每一位的含义都不同。
沪市四位:第一位是时段,S 启动(开市前)、C 开盘集合竞价、T 连续交易、E 闭市、P 停牌、M 可恢复交易的熔断、N 不可恢复交易的熔断、U 收盘集合竞价;第二位是能否正常交易,0 否 1 是;第三位是是否上市,0 未上市 1 已上市;第四位是当前时段是否接受订单申报,0 否 1 是。
深市两位:第一位是时段,S 启动、O 开盘集合竞价、T 连续竞价、B 休市、C 收盘集合竞价、E 已闭市、H 临时停牌、A 盘后交易、V 波动性中断;第二位是停牌标记,0 正常 1 全天停牌。
def parse_status(market: str, status: str) -> dict: if market.startswith("USH"): return { "phase": status[0], # S/C/T/E/P/M/N/U "tradable": status[1:2], # 0/1 "listed": status[2:3], # 0/1 "accept_order": status[3:4], # 0/1 或空格 } return { "phase": status[0:1], # S/O/T/B/C/E/H/A/V "halt": status[1:2], # 0 正常 1 全天停牌 }mode_state是深市后来新增的字段,格式是“交易方式1交易状态1交易方式2交易状态2”这样的连续串,比如1T2T3T4T5T,说明五种交易方式当前都处在连续竞价状态。交易方式编码:1 匹配成交、2 协商成交、3 点击成交、4 询价成交、5 竞买成交。交易状态编码复用深市的时段字母。
def parse_mode_state(s: str) -> dict: # "1T2T3T4T5T" -> {"1": "T", "2": "T", "3": "T", "4": "T", "5": "T"} return {s[i]: s[i + 1] for i in range(0, len(s) - 1, 2)}快照里的deal_mode是最近一笔成交的成交方式,1 到 5 对应上面五种,其他值无意义。如果你的策略只关心竞价撮合,就在特征计算前先按deal_mode == 1过滤一遍,否则大宗和协商成交的数据会污染量价统计,这个坑我在做分钟成交量重构时踩过一次,数据对不上对了两天才找到原因。
5. 级联、离线回放的避坑排查:主从配置、端口、堵包和断线重连
5.1 级联模式:tool.ini 和 register_addr 的先后顺序
级联模式用于一台机器上跑多个 SDK 实例的场景:主 SDK 连行情服务端,负责收数据和分发,从 SDK 连主 SDK,从主 SDK 拿数据。好处是服务端只维持一份连接,坏处是配置写错时表现非常隐蔽。
SDK 默认是非级联模式,要切换得建一个本地配置文件tool.ini,Python 版本放在程序文件同目录。里面两个字段:master标识主从,true 为主、false 为从,默认 true;socket_port是监听端口,主从同机时两个端口必须不一样。
; 主 SDK 的 tool.ini master=true socket_port=9999; 从 SDK 的 tool.ini,放在另一个程序目录下 master=false socket_port=9998从 SDK 还要调一次register_addr注册主 SDK 的地址,这个函数必须在 login 之前调用:
from datafeed.tds_api import TDS tds = TDS() tds.set_env(fnctickstock=on_stock) # market 固定填 master,addr 是主 SDK 所在机器 IP,port 与主 SDK 的 socket_port 一致 ret = tds.register_addr("master", "192.168.1.10", 9999) if ret != 0: raise RuntimeError(f"register_addr failed, code={ret}") # 从 SDK 的账号密码必须和主 SDK 保持一致 tds.login("your_account", "your_password")三个硬性约束:register_addr只给从 SDK 用,主 SDK 不要调;必须等主 SDK 登录成功后再让从 SDK 连;从 SDK 的账号密码要和主 SDK 一致,不一致会认证失败。还有一条容易忽略的,级联要求主从在同一局域网内,跨网段会直接返回 -1015。
5.2 离线回放:csv 命名规范和五个参数
离线调试(后来版本加入)解决的是没有实时行情时怎么开发的问题。它由数据驱动,读本地 csv 文件模拟推送,登录不连线上网关直接返回成功。调用时机有硬要求:实例创建之后、登录之前,已经登录了再调不生效。
文件命名是硬性的,SDK 靠文件名识别数据类型,名字不对就不读:
| 文件名 | 数据类型 |
|---|---|
| stock.csv | 证券快照 |
| index.csv | 指数快照 |
| order.csv | 逐笔委托 |
| queue.csv | 委托队列 |
| trans.csv | 逐笔成交 |
| superstock.csv | 极速盘口 |
| codelist.ini | 代码列表 |
csv 的表头字段名要跟模板一致,列顺序不强制。回放时按 subtype 里给的顺序读文件,每个文件每次读push_count条,间隔push_space毫秒,读完一个文件接着读下一个,全部遍历playback_times遍后结束。
tds = TDS() tds.set_env(fnctickstock=on_stock) # path 数据目录,subtype 要读的文件名(不带 .csv),不需要填 codelist ret = tds.start_offline_debug( path="/data/replay", subtype=["stock", "order", "trans"], push_space=3000, # 推送间隔,毫秒 push_count=5000, # 单次推送条数 playback_times=1, # 文件遍历次数 ) tds.login("any", "any") # 离线模式下登录不连服务端,直接成功查询接口在离线模式下也有对应行为:get_code_list读 codelist.ini 里对应市场的数据,没有就返回空;get_tick_stock读对应文件的前 3000 条(指数前 300 条)并按传入代码筛选。订阅接口一律返回成功,回放的数据包按订阅的代码过滤后才触发回调,所以订阅关系没建对的话,回放跑完一条回调都不会触发,看起来像“离线模式坏了”。
5.3 五条踩坑记录
回调里做计算,行情越推越慢。现象是全市场订阅跑了几分钟后stats里的本机排队时间持续增长,数据像是慢半拍。原因是回调函数运行在 SDK 的推送线程里,里面做了 pandas 聚合、写数据库这类重活,线程被卡住,缓冲区堆满。解决办法是把回调简化成纯入队,业务逻辑放到独立消费者线程里做,队列设上限形成背压,宁可丢数据也别让推送线程阻塞。
tool.ini 改了没生效,还是按非级联跑。现象是从 SDK 起来后连不上主 SDK,或者register_addr返回错误。原因是socket_port后面的换行符被编辑器吃掉了,参数读取失败退回默认值,也可能主从两个配置文件都写了同一个端口导致抢占。解决方式是cat -A tool.ini看行尾有没有换行符,主从端口一个 9999 一个 9998,配置文件放在程序文件同目录而不是当前工作目录。
从 SDK 登录返回 -1005。现象是从 SDK 调 login 直接失败,日志里提示无法连接服务器。原因通常是先 login 后register_addr,或者主 SDK 还没登录成功从 SDK 就开始连。解决是把顺序固定成:主 SDK set_env → login 成功 → 从 SDK set_env → register_addr → login,写成启动脚本里的固定流程,别靠人工记。
离线回放一条回调都不触发。现象是start_offline_debug返回 0,login 也返回 0,但控制台什么都不打。原因有三类:csv 文件名不是规范名;调用了回放但没建订阅,回放只对已订阅代码触发回调;回放目录里 codelist.ini 缺失导致市场代码查不到。解决办法是回放前先sub一次对应类型,确认文件名严格匹配,目录里补上 codelist.ini。
断线重连后收不到数据。现象是网络抖动一下,之后策略还在跑但数据停了,也没有报错。原因是没开m_connectsubscribe,重连成功但订阅关系没恢复;或者开了但订阅回调里没处理callback_type=2的分支,失败了也不知道。解决办法是把m_connectsubscribe设成 1,订阅回调里对 statuscode 非 0 的情况打日志并重试,同时在fncnotify里维护一个可用状态标志,断线期间禁止策略下单。
6. 二次开发:把全推回调接进自己的策略管道
拿到几百个字段的回调之后,第一步不是写策略,是先搭一个能承接住数据速率的管道。我的做法是三层:回调层只入队,消费层做字段归一和特征计算,状态层存最近 N 笔的滚动窗口。中间那层用固定长度 deque 控制内存,别用 list 无限追加。
from collections import deque class SnapshotStore: def __init__(self, maxlen=2000): self.window = {} # code -> deque,控制内存上限 self.maxlen = maxlen def update(self, item: dict): code = item["code"] dq = self.window.get(code) if dq is None: dq = self.window[code] = deque(maxlen=self.maxlen) dq.append({ "ts": item["time"], "last": item["new_price"], "bid1": item["bidorder_price"][0], "bid1_vol": item["bidorder_volume"][0], "bid1_orders": item["bid_numorders"][0], # 买一委托笔数,封单结实度参考 "cancel_buy": item.get("withdraw_buyvolume", 0), "deal_mode": item.get("deal_mode", 1), }) def seal_strength(self, code: str) -> float: """封单结实度:委托笔数越多、单笔越小,越不像能被一笔撤掉的假封单""" dq = self.window.get(code) if not dq: return 0.0 s = dq[-1] if s["bid1_vol"] <= 0: return 0.0 return s["bid1_orders"] / s["bid1_vol"]这段代码在消费线程里跑,回调只负责raw_queue.put。maxlen设成 2000 是按每秒最多几百条、保留几秒窗口估的,全市场订阅要再放大或者改成只缓存关注标的。seal_strength只是个示例特征,真正的信号要结合逐笔委托的撤单节奏一起看,单看快照容易误判。
改造时有三处最值得动手:
| 改造点 | 做法 | 验证方式 |
|---|---|---|
| 回调解耦 | 回调纯入队,消费线程分离 | 观察 stats 里的本机排队时间是否稳定 |
| 字段归一 | 对缺失字段填 None 并在消费层统一处理 | 用离线回放跑一遍,检查有没有异常值 |
| 回放回归 | 固定一批 csv,改完代码重放同一份数据比对输出 | 输出 diff 为 0 才算没破坏逻辑 |
第三点是最值得养成的习惯。策略逻辑每改一次,就把同一份离线数据重放一遍,比对特征输出和交易信号是否与预期一致。没有这个环节,改一行代码只能等实盘验证,一次盘中翻车的代价远大于写一个比对脚本。
从那以后我每次动行情相关代码,都强制走一遍“离线回放 → 输出 diff → 再上模拟盘”的流程,一次不落。数据管道这种东西,出问题的时候不报错,只是安静地给你错的东西,等你发现的时候仓位已经在那儿了。希望这套流程也能帮到你,需要这份手册的话,按文中的安装和配置动手跑一遍,比读十遍都管用。
本文还有配套的精品资源,点击获取