news 2026/11/3 12:08:36

Level-2全推行情Python API:盘口策略与涨停板监控实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Level-2全推行情Python API:盘口策略与涨停板监控实战

简介:同花顺Level-2全推行情Python接口手册面向证券高频交易与量化策略开发者,服务于涨停板、盘口异动和低延迟行情等实战场景。内容围绕行情服务的C/S架构,讲清沪深Level-1、Level-2行情的接入方式,并覆盖五档与十档快照、逐笔成交、逐笔委托、委托队列、极速盘口等数据的订阅与回调处理;同时梳理多连接模式、上证逐笔委托、级联与异步订阅、离线调试、通道号和委托序号、快照字段扩充及数据精度调整等版本演进。压缩包为1个PDF,约850KB,目录清晰便于按版本与接口模块查阅,并给出运行环境、安装验证、初始化与回调函数定义等落地细节。已有2846人学习,适合需要理解接口字段、排查订阅问题并搭建行情接入流程的量化开发者参考,也可作为排错与实战思路。

1. 盘口上封单还压着八十万手,你的委托却排在几万手之后

这不是行情慢,是五档行情本身看不到队列。做涨停板、封单衰减、撤单率这类信号,光靠十个价格档位的汇总数字是不够的,你得有 Level-2 全推行情:逐笔成交、逐笔委托、委托队列,加上带委托笔数的十档快照。这份《Level-2 全推行情 Python API 操作手册》解决的就是「怎么用 Python 把这些数据接进策略进程」这一段路——回调驱动、全推推送、不轮询,覆盖沪深两市的指数快照、证券快照、逐笔成交、逐笔委托、委托队列和极速盘口。

它适合两类人:一类是 Python 已经写熟、准备把策略从日线级推到盘口级的量化开发者;另一类是要给自建回测环境灌真实高频行情的人。读完至少能判断清楚:这台机器的带宽、账号权限和代码结构,撑不撑得起一条盘口级的数据链路。

2. TDS 架构与三种运行形态:解码、订阅、分发分别在哪一层

行情 SDK 最怕被当成黑匣子用——装上、跑通、然后某天回调不来了,完全不知道断在哪。TDS 这套东西的结构其实很直白,先把它拆成三段看清楚,后面调参和排查才有落点。

2.1 三段式链路:从行情源到你的回调函数

TDS 是典型的 C/S 结构。服务端从上游行情接入网关拿到实时数据,中间做解码和订阅管理,最后以 API 形式对客户端分发。落到代码上,你看到的是三个动作:set_env注册回调、login建立会话、sub或add_sub声明你要哪些代码的哪类数据。之后数据不是你去拉,而是 SDK 在内部线程里推过来。

这条链路里,SDK 本身做了两件容易被忽略的事。一是多连接:V2.0.0 之后从单连接改成了多连接模式,不同市场注册不同的域名和端口,沪市和深市的数据走各自的通道,一个通道抖动不会把另一个拖死。二是高可用:断线重连、重连后重发订阅、级联转发,都在 SDK 内部处理掉,暴露给 Python 层的只有回调。

回调函数的签名是固定的三个参数,这一点在写第一行代码前就要记住:

def fnc_data(data_list: list, count: int, stats: dict): """ SDK 行情回调,六类数据共用这一个签名 @param data_list: 行情数据列表,一次可能推多条 @param count: data_list 的条数,别拿 len(data_list) 硬算,以它为准 @param stats: 链路时间戳字典,三个 key: produce_time 服务端收到行情的 unix 微秒时间戳 send_time 服务端发出的时间戳 recv_time SDK 收到的时间戳 """ pass

stats这个参数很多人直接丢掉,其实它是排查延迟的唯一抓手。用recv_time - produce_time就能算出这一段链路的端到端耗时,单位是微秒。做盘口策略时,如果这个差值长期超过几十毫秒,先别怀疑策略,先看网络和机器负载。

多语言支持是这套 SDK 的另一个设计点:同一套 C/C++ 内核,外面套 Python、Java 的绑定,所以字段名、返回码、订阅语义在三种语言里是一致的。你在 Python 文档里看到的-1018 版本过低,换成 Java 也是这个码。

2.2 Python 包的安装与模块布局

Python 版本是以 tar.gz 分发的,不是从包索引拉。安装有两条路,效果一样:

# 方式一:直接 pip 安装本地包 pip install ./L2-tdsapi-python-X.X.X-XXXXXXXX.tar.gz # 方式二:解包后走 setup.py(Linux 下需要 root) python setup.py install # 验证:不报异常即安装成功 python -c "import datafeed; print('ok')"

装完之后的模块结构很简单,datafeed包里最核心的是tds_api,TDS类就定义在这里:

from datafeed.tds_api import TDS # 创建实例,这一步不建立任何网络连接 tds = TDS()

有几个路径规则需要提前知道,它们决定了你后面调试时去哪找日志、去哪放配置:

  • 日志默认写在datafeed包的安装目录下,通过set_env(m_logpath=...)可以改;
  • 级联模式的tool.ini在 Python 版本里要求和程序文件同级(C 版本是跟动态库同级,Java 版本是跟 jar 同级,别混);
  • 离线调试用的 CSV 数据放在你自己指定的任意目录,通过start_offline_debug(path=...)传进去。

硬件和系统门槛不高但别忽略:64 位双核以上 CPU、8G 以上内存、下行 100Mb 以上带宽,系统支持 Windows 7 及以上和主流 Linux 发行版,Python 需要 3.5 的 64 位版本以上。带宽这一项在订阅全市场逐笔时是硬约束——十档快照加两路逐笔全推,峰值流量不小,百兆是底线而不是推荐值。

2.3 级联模式:一台机器收数据,多台机器用数据

级联解决的问题很具体:你手上有五个策略进程,每个都要行情,但行情账号的连接数是有限的,而且五个进程各自直连服务端,带宽和连接都会被放大五倍。级联模式让主 SDK 只连一次服务端,其余从 SDK 连主 SDK,在局域网内分发。

切换靠一个叫tool.ini的本地配置文件,Python 版本放在程序文件同级目录。主节点和从节点的差别只有两个字段:

# 主 SDK 的 tool.ini master=true socket_port=9999 # 从 SDK 的 tool.ini(同一台机器上端口必须错开) master=false socket_port=9998

master决定主从属性,默认是true;socket_port是监听端口。这里有个血泪经验:端口号后面必须接换行符,文件末尾缺了这一行,配置参数可能读不出来,表现是 SDK 静默按默认值跑,你怎么改都不生效。

配置好之后,从 SDK 侧在登录前调一次register_addr:

from datafeed.tds_api import TDS tds = TDS() def callback_stock(data, count, stats): print("receive count:", count) tds.set_env(fnctickstock=callback_stock) # 从 SDK 专用:注册主 SDK 的 IP 和监听端口 # market 固定填 'master',不是 USHA/USZA 这类市场码 tds.register_addr('master', '192.168.1.10', 9999) # 从 SDK 的账号密码必须与主 SDK 一致 tds.login('test1', '123456')

调用顺序是死的,错了就是连不上:主 SDK 先login成功,从 SDK 再register_addr,然后从 SDK 才login。register_addr必须在登录前调用,登录后再注册是无效的。另外从 SDK 侧的账号密码要和主 SDK 完全一致,不是随便填一个能过就行。

2.4 离线调试:用 CSV 造一段行情做回放

盘后想验证策略逻辑,总不能等到第二天开盘。start_offline_debug就是干这个的:读本地 CSV 文件,按你设定的节奏往回调里推,完全离线,不连线上服务端,login直接返回成功。

from datafeed.tds_api import TDS tds = TDS() def callback_stock(data, count, stats): print("receive count:", count, "first:", data[0]) tds.set_env(fnctickstock=callback_stock) # 注意:必须在实例创建之后、登录之前调用;已登录状态下调用不生效 tds.start_offline_debug( path='D:\\datafeed', # 数据目录 subtype=['stock', 'index', 'order', 'queue', 'trans', 'superstock'], push_space=3000, # 两次推送间隔,单位毫秒 push_count=5000, # 单次推送条数 playback_times=1 # 文件遍历次数 ) # 离线状态下 login 不连服务端,直接返回成功 tds.login('test1', '123456')

参数里最容易踩的是subtype:它填的是文件名前缀,不是完整文件名,也不要把codelist塞进去——代码表是 SDK 自己按需读的。文件命名必须严格是stock.csv、index.csv、order.csv、queue.csv、trans.csv、superstock.csv、codelist.ini这几者之一,SDK 靠文件名判断数据类型,命名不规范直接跳过不读,而且不报错。

回放行为和实时行情对齐得比较细:调用订阅接口一律返回成功;调用快照查询时,SDK 只读文件前 3000 条(指数是前 300 条)并剔除重复代码,生成一份可查询的数据集;回放时按subtype的顺序挨个文件读,每次读push_count条,间隔push_space毫秒,读完一个文件再读下一个,所有文件跑完playback_times遍后结束。如果某次读出来的数据里没有你订阅的代码,那一次就不触发回调——这点和实盘一致,别以为是 SDK 卡了。

级联场景下有个限制:只有主节点能开离线调试,从节点调这个函数无效,但主节点开起来之后,从节点可以正常拿到回放数据。做多进程联调时这个特性挺省事,一套 CSV 喂给整条链路。

3. 主链路代码拆解:set_env、login、查询、订阅四步走

装好包、想清楚用哪种形态,接下来就是一条固定顺序的调用链。这四步里任何一步的参数设错,后面都不会报错,只会表现为「没数据」——所以每一步都值得单独说清楚。

3.1 set_env:回调注册和环境参数一次性挂上去

set_env是整条链路的地基,规则只有一条但必须遵守:初始化 TDS 之后、注册和登录之前,必须先调它。一般场景下只传回调函数,其余参数留默认值即可。

from datafeed.tds_api import TDS tds = TDS() def fnc_tick_stock(data_list, count, stats): """证券快照回调""" pass def fnc_transaction(data_list, count, stats): """逐笔成交回调""" pass def fnc_orderdata(data_list, count, stats): """逐笔委托回调""" pass def fnc_orderqueue(data_list, count, stats): """委托队列回调""" pass def fnc_notify(msg_notify): """断线通知回调,只收一个参数 msg_notify = {"code": int, "msg": str} code: 1 网络断开 / 2 网卡异常 / 3 重复登录被踢下线 """ print("disconnect:", msg_notify["msg"]) def fuc_subscribe(data_type, op_type, market, code_list, statuscode, callback_type): """异步订阅结果回调,六个参数 callback_type: 1 异步订阅触发 / 2 重连或重新登录后自动重发订阅 """ if statuscode != 0: print("sub failed:", data_type, market, statuscode) ok = tds.set_env( fnctickstock=fnc_tick_stock, fnctransaction=fnc_transaction, fncorderdata=fnc_orderdata, fncorderqueue=fnc_orderqueue, fncnotify=fnc_notify, fucsubscribe=fuc_subscribe, # 下面这些不传就用默认值,按需开 m_heartbeat_interval=5, # 心跳间隔,秒,<1 用系统默认 m_reconn_count=10, # 断线重试次数,<1 表示不重连 m_reconn_interval=3, # 重连间隔,秒,<1 立刻重连 m_timeout=5000, # 同步请求超时,毫秒,默认 5000 m_connectmarket="USH;USZ", # 连接市场,可只填 "USH" 或 "USZ" m_connectsubscribe=1, # 1 = 重连成功后自动重发订阅 m_loglevel="warn", # 日志级别 m_logpath="./logs" # 日志路径,默认是包安装目录 )

参数里最值得动的是三个。m_reconn_count设成 10、m_reconn_interval设成 3,意思是断线后每 3 秒重试一次、最多 10 次,10 次都失败就触发fncnotify告诉你 SDK 已停止工作,此时你得自己重新login加重新订阅。m_timeout只影响同步请求(get_tick_stock这类),默认 5000 毫秒,全市场查询时建议往上调,不然很容易拿到空列表。m_connectmarket是省流量的好开关——只做沪市就填"USH",深市的连接根本不会建。

回调函数本身的写法有硬约束:不要在回调里做耗时长的操作。回调跑在 SDK 的接收线程上,你在里面写数据库、发 HTTP、甚至只是print一大堆内容,都会拖慢接收,轻则延迟飙高,重则堵包丢数据。正确做法是往队列里扔一下就走,消费逻辑放到自己的线程里,这一点在第 6 章会展开。

3.2 login:返回码表比 try/except 更可靠

login返回的是 int,不是抛异常。0 表示成功,负数是失败。这套返回码覆盖了认证、网络、订阅校验各个层面,比笼统的异常信息有用得多。

ret = tds.login('test1', '123456') if ret != 0: # 关键错误码单独处理,其余归入通用失败 if ret == -1018: # 版本过低:SDK 版本与账号权限不匹配,必须升级 SDK raise SystemExit("SDK 版本过低,请更新到最新版本") elif ret == -1005: # 用户名或密码错误,重试没意义 raise SystemExit("账号或密码错误") elif ret in (-1004, -1006, -1014): # IP 受限 / 错误 IP / 级联主 IP 异常,属于网络与环境问题 raise SystemExit("网络或 IP 受限,检查出口 IP 与主节点") elif ret == -1016: # 重复登录:同一账号在别处已登录,会导致先前会话被踢 print("重复登录,旧会话可能已被顶下线") else: raise SystemExit(f"登录失败,返回码 {ret}")

完整返回码里几个需要记住的:-1001一般错误、-1002超时无响应、-1003IP 禁止访问、-1004用户名密码错误、-1005无法连接服务器、-1006错误 IP、-1007错误市场、-1008错误代码、-1009无效 handle、-1010无效数据类型、-1011非法订阅操作、-1012认证中心登录认证失败、-1013服务端登录验证失败、-1014级联模式主 IP 连接异常、-1015级联模式要求同一局域网、-1016重复登录、-1017通行证校验失败次数过多、-1018版本过低。

-1015这一条踩坑率很高:级联模式要求主从 SDK 在同一局域网内,跨网段或者过了 NAT,register_addr就会返回这个码。别想着用端口映射绕过,这个限制是设计出来的。-1018是 V2.1.3 新增的,SDK 版本和账号权限对不上时出现,遇到它不要改代码,去换包。

3.3 同步查询:先把代码表和快照拿全

查询接口是同步的,调一次拿一次结果,不走回调。四个接口覆盖了代码表、指数快照、证券快照和极速盘口:

# 1. 拿代码表:market 传市场码,返回字符串列表,失败返回 [] codes = tds.get_code_list('USHA') print("沪市A股代码数:", len(codes)) # 2. 指数快照:code_list 传 [] 表示全市场 index_snap = tds.get_tick_index('USHI', []) # 只查特定指数 index_snap = tds.get_tick_index('USHI', ['000001']) # 3. 证券快照:十档 + 委托笔数,盘口策略的主力数据 stock_snap = tds.get_tick_stock('USHA', ['600000']) # 4. 极速盘口:需要额外的产品权限,没权限返回空 super_snap = tds.get_super_stock('USHA', ['600000'])

市场码必须填准,这是「查不到数据」最常见的原因。沪深两市拆得比想象中细:

市场码对应市场市场码对应市场
USHI沪市指数USZI深市指数
USHA沪市 A 股USZA深市 A 股
USHB沪市 B 股USZB深市 B 股
USHD沪市债券USZD深市债券
USHJ沪市基金USZJ深市基金
USHP沪市退市整理USZP深市退市整理
USHT沪市风险警示板——

USHT是独立市场码,容易被漏掉。纯 A 股策略如果只订阅USHA,ST 和风险警示板的票是拿不到的,而这类票恰恰是涨停板策略的高频目标。查某只票之前,先确认它在哪个市场码下面。

get_code_info在文档里标了暂未实现,别在它上面花时间。

3.4 订阅:sub、add_sub、del_sub 和异步订阅的分工

订阅接口有四个同步的加一个异步的,语义差别很大,尤其是sub:

接口行为适用场景
sub(data_type, market, code_list)取消该市场该类型的所有订阅后,重新按 code_list 订阅整体切换股票池
unsub(data_type, market)取消该市场该类型的全部订阅收盘或切换策略
add_sub(data_type, market, code_list)在现有订阅基础上追加代码盘中动态加票
del_sub(data_type, market, code_list)从现有订阅中移除指定代码盘中剔除
async_sub(data_type, op_type, market, code_list)异步提交,结果走fucsubscribe回调订阅量大、不想阻塞主线程

data_type支持六种:index指数快照、stock证券快照、queue委托队列、order逐笔委托、trans逐笔成交、superstock极速盘口。

盘中动态加票千万别用sub。假设你已经订阅了 3000 只票的逐笔成交,现在想再加一只,用sub('trans', 'USHA', ['600001'])的结果是:先把你原来 3000 只全退了,再只订这一只。行情会突然少一大片,而且不报错——这个坑我见过不止一次。

正确写法是用add_sub:

# 全市场订阅:code_list 传 [] 表示该市场全部代码 tds.sub('stock', 'USHA', []) # 沪市 A 股全市场快照 tds.sub('trans', 'USZA', []) # 深市 A 股全市场逐笔成交 # 盘中追加,不影响已有订阅 tds.add_sub('order', 'USHA', ['600000', '600001']) # 盘中剔除 tds.del_sub('order', 'USHA', ['600001']) # 异步订阅:适合订阅请求本身很重的场景,结果从 fucsubscribe 回来 # op_type: 1 全新订阅 / 2 取消订阅 / 3 增加订阅代码 / 4 取消订阅代码 tds.async_sub('queue', 1, 'USHA', [])

异步订阅的价值在于不阻塞。同步订阅在订阅量极大时会卡住调用线程几百毫秒甚至更久,如果你的策略主循环和订阅在同一线程,这段时间是失明的。async_sub提交后立刻返回,真实结果从fucsubscribe里拿,statuscode为 0 才算成功。注意async_sub是新版本才有的接口,老包上会直接属性报错。

4. 数据结构与字段:快照、逐笔、委托队列怎么读

数据到手之后,真正决定策略质量的是字段。Level-1 和 Level-2 的差别不只是「五档 vs 十档」,还有委托笔数、撤单量、内外盘、盘后成交这些能直接算成因子的东西。

4.1 五档与十档快照的字段差异

两套快照共用一批基础字段:market、code、codename、status、date、time、pre_price、open_price、high_price、low_price、new_price、volume、turnover、trade_num、uplimit_price、downlimit_price、inner_volume、outer_volume、aftermarket_volume、aftermarket_volmount。

差别在盘口和委托维度。Level-1 是五档,字段名是bidorder_price/bidorder_volume/askorder_price/askorder_volume,列表长度 5。Level-2 是十档,除了这四个列表变成 10,还多出四组:

字段类型含义
bid_numorderslist买一到买十每个档位的委托笔数
ask_numorderslist卖一到卖十每个档位的委托笔数
totalbid_volumeint总委买量
totalask_volumeint总委卖量
avgbid_pricefloat加权平均委买价
avgask_pricefloat加权平均委卖价

委托笔数这个字段是封单分析的起点:同样是 80 万手封单,由 200 笔构成和由 8000 笔构成,性质完全不同。再加上沪市独有的撤单字段withdraw_buynum、withdraw_buyvolume、withdraw_buyturnover、withdraw_sellnum、withdraw_sellvol、withdraw_sellturnover,以及totalbid_num、totalask_num、bidorder_num、askorder_num四个总量字段,封单质量和撤单率就能实时算出来。

def calc_seal_strength(snap: dict) -> dict: """ 从十档快照里算封单强度指标 snap 来自 fnctickstock / get_tick_stock 的单条数据 """ bids = snap.get('bidorder_volume', []) bid_orders = snap.get('bid_numorders', []) if not bids or not bid_orders: return {} # 买一档单笔均量:值越小说明封单越分散,越可能是散户堆的 avg_per_order = bids[0] / bid_orders[0] if bid_orders[0] else 0 # 十档买单集中度:买一占十档总量的比例 total = sum(bids) or 1 concentration = bids[0] / total # 沪市才有撤单量;深市取 0 withdraw_buy = snap.get('withdraw_buyvolume', 0) volume = snap.get('volume', 0) or 1 withdraw_ratio = withdraw_buy / volume return { 'code': snap.get('codename'), 'avg_per_order': avg_per_order, 'concentration': concentration, 'withdraw_ratio': withdraw_ratio, }

这段代码只做了三件事:算买一档的单笔均量、算买一集中度、算撤单比。三个值都从快照单条数据里就能得到,不需要跨包状态。注意withdraw_buyvolume是沪市字段,深市数据里没有,.get带默认值是必须的。另外 V2.0.6 把withdraw_buyturnover和withdraw_sellturnover从 int 改成了 float,如果你的下游表结构还是整型,落库时会截断小数——这种字段类型变更在版本升级说明里只有一行,但引发的线上问题不小。

4.2 逐笔成交、逐笔委托与委托队列

逐笔数据是 Level-2 区别于五档的核心。三条流各有分工:trans是已经成交的每一笔,order是每一笔新进来的委托,queue是买一/卖一档位上的排队明细。

逐笔这一路在版本演进里加了不少字段,做逐笔还原必须用上:channel_no(通道号,V2.0.7 加)、业务序号(V2.0.1 加)、order_no(委托序号,V2.1.9 加)、deal_mode(成交方式,V2.1.4 加)。业务序号和委托序号是做逐笔委托到逐笔成交配对的关键——没有它们,你没法把一笔成交准确追溯到是哪一笔委托产生的。

def on_transaction(data_list, count, stats): """逐笔成交回调:只做解析和入队,不做任何 IO""" for item in data_list: record = { 'code': item.get('code'), 'market': item.get('market'), 'time': item.get('time'), # HHMMSSsss 'price': item.get('new_price'), 'volume': item.get('volume'), 'turnover': item.get('turnover'), 'channel_no': item.get('channel_no'), # 通道号,多通道去重用 'deal_mode': item.get('deal_mode'), # 1 匹配 2 协商 3 点击 4 询价 5 竞买 } raw_queue.put(record) # 只入队,消费在另一线程

这段代码的重点不在字段,而在最后一行。逐笔成交在全市场订阅下每秒可能是几万条,回调里做任何字符串拼接、数据库写入都会拖垮接收线程。raw_queue是一个有界队列,满了就丢弃并计数——丢数据比堵死好,至少你知道丢了多少。

委托队列(queue)是另一个维度:它给的是买一档或卖一档上的具体排队构成,是判断「我前面还有多少人」的直接依据。打板策略里,同一个价位上封单从 200 笔变成 20 笔,往往比封单量本身更早给出信号。

极速盘口(superstock)需要单独的产品权限,没开通的话订阅返回失败、查询返回空。它比普通快照更快,V2.0.7 之后加了最新价字段。如果你的策略对盘口刷新频率敏感,这是可选的一层加速,但别把它当默认能力规划——权限是额外的。

4.3 status、deal_mode、mode_state 三个状态字段

这三个字段是策略的开关,判断错了会在不该下单的时候下单。

status在沪深两市的编码规则不同。沪市是 4 位字符串:第 1 位表示时段(S 启动、C 开盘集合竞价、T 连续交易、E 闭市、P 停牌、M 可恢复熔断、N 不可恢复熔断、U 收盘集合竞价),第 2 位是能否正常交易(0/1),第 3 位是是否上市(0/1),第 4 位是当前时段能否接受订单申报(0/1),无意义的位填空格。深市是 2 位:第 1 位是时段(S 启动、O 开盘集合竞价、T 连续竞价、B 休市、C 收盘集合竞价、E 已闭市、H 临时停牌、A 盘后交易、V 波动性中断),第 2 位是状态(0 正常、1 全天停牌)。

def is_continuous_trading(snap: dict) -> bool: """判断当前是否处于可下单的连续交易时段""" status = snap.get('status', '') market = snap.get('market', '') if market.startswith('USH'): # 沪市:第 1 位 T 连续交易,第 4 位 1 表示当前时段可接受订单申报 return len(status) >= 4 and status[0] == 'T' and status[3] == '1' else: # 深市:第 1 位 T 连续竞价,第 2 位 0 表示正常状态 return len(status) >= 2 and status[0] == 'T' and status[1] == '0'

深市的deal_mode和mode_state是 2022 年 5 月 16 日之后的产物。在此之前深市只发竞价交易数据;之后拆成匹配成交、协商成交、点击成交、询价成交、竞买成交五种,快照里的原有字段变成五种方式的汇总,另外单独给matching_volume、matching_turnover、matching_price、deal_mode、mode_state五个字段。deal_mode取值为 1 到 5 对应五种方式,快照里的值表示最近一笔成交的方式;mode_state是拼接串,格式像1T2T3T4T5T,两位一组,前一位是交易方式编号,后一位是该方式的状态码。

这套变化对回测的影响是实打实的:如果你拿 2022 年 5 月之前的深市数据算「成交量」,和之后的数据不是同一个口径,混在一起做因子会失真。落库时把deal_mode一起存下来,后面按方式过滤才有余地。

5. 避坑与常见问题排查:断线、堵包、级联、字段类型

这套 SDK 的坑有个共同特征:大部分错误不抛异常。返回空列表、回调不来、订阅静默失败,都得靠你自己判断。下面五条是我在实际项目里反复遇到的。

5.1 现象:跑了一晚上,第二天发现中间少了两个小时的数据

原因通常是两类。一类是回调里做了耗时操作导致堵包——SDK 的接收线程被你的数据库写入或日志打印卡住,内部缓冲区满了就丢数据,而且丢得悄无声息。另一类是断线后没有重连成功,fncnotify报了code=1但没人处理。

解决分两步。第一,回调只入队,消费另起线程:

import queue import threading raw_queue = queue.Queue(maxsize=200000) # 有界队列,满了丢弃而不是无限涨 drop_count = 0 def on_tick(data_list, count, stats): global drop_count try: raw_queue.put_nowait((data_list, stats)) except queue.Full: drop_count += 1 # 记下来,监控里要能看到 def consumer(): while True: data_list, stats = raw_queue.get() # 这里才做落库、计算、告警 batch_insert(data_list) threading.Thread(target=consumer, daemon=True).start()

第二,fncnotify里必须实现重新登录和重新订阅。SDK 在多次重连失败后会停止工作,此时它不会自己恢复:

def on_notify(msg): if msg["code"] in (1, 2): # code 1 网络断开 / 2 网卡异常,两种情况都走同一套恢复流程 ret = tds.login(USER, PWD) if ret == 0: resubscribe_all(tds) # 重新拉起全部订阅 elif msg["code"] == 3: # 重复登录被踢,这台机器不要再自动重连,会互相踢 raise SystemExit("账号在别处登录,本进程退出")

code=3单独处理很关键。如果是同一账号在另一台机器上登录把你踢了,你还自动重连,会变成两台机器互相踢,谁都收不到数据。

顺带说一句,m_connectsubscribe=1能让 SDK 在重连成功后自动重发订阅,但这个自动重发的结果从fucsubscribe回来(callback_type=2),你没注册这个回调的话,根本不知道重发成功了没。

5.2 现象:订阅全市场之后,CPU 直接打满,延迟飙到几百毫秒

原因几乎都是回调里做了 Python 层的重计算。全市场逐笔成交在活跃时段每秒几万条,每条都做一次dict组装加上字符串格式化,GIL 直接顶住。

解决办法有三层。一是回调里只入队,这点上面说过。二是减少订阅范围,只订你真正要的代码,get_code_list拿全表之后自己筛一遍再sub。三是把重计算挪到批量消费端,一次处理 5000 条比 5000 次处理 1 条快一个量级。还有个小技巧:m_connectmarket只填你需要的市场,能砍掉将近一半的流量。

5.3 现象:级联模式从 SDK 一直连不上主 SDK,register_addr返回负数

按出现频率排,三个原因。第一,tool.ini最后一行没换行,配置根本没被读到,SDK 还在默认的非级联模式下跑。第二,主从 SDK 放在同一台机器上,socket_port配成了同一个端口,监听冲突。第三,调用顺序错——先login再register_addr,或者主 SDK 还没登录成功从 SDK 就开始连。

排查顺序建议这么走:先cat -A tool.ini看行尾有没有$(Linux 下确认换行符),再确认两个端口不重复,最后核对代码里的调用顺序是不是set_env→register_addr→login。如果返回-1015,那是跨网段了,级联模式要求主从在同一局域网,这个改不了。

5.4 现象:升级 SDK 之后,落库报类型错误或者金额小数全变 0

字段类型在版本之间变过。V2.0.6 把withdraw_buyturnover和withdraw_sellturnover从 int 改成 float;V2.1.3 把消息通知回调的入参从 int 改成了 dict;V2.1.6 给订阅回调加了参数。这几处都是破坏性变更。

解决办法是升级前先做字段对齐:把新版本的字段清单拉出来,和自己下游的表结构、序列化方案逐项比一遍,重点看数值类型和回调签名。建表时金额类字段统一用DECIMAL或至少DOUBLE,别用整数。回调签名变更更麻烦——参数个数对不上不会报错,只会让你的处理逻辑读错位。升级后在离线调试模式下跑一遍全链路,比在实盘发现快得多。

5.5 现象:登录返回-1018,或者盘中突然收到权限到期提醒

-1018是 V2.1.3 之后新增的返回码,含义是版本过低,通常出现在 SDK 包版本落后于账号权限要求的场景。它不是配置问题,改代码没用,直接换最新的包。

权限到期是另一回事。V2.0.5 之后 SDK 会在快照数据里附带权限到期提醒和升级提醒,这些信息不会让接口报错,只是悄悄混在数据里。如果你的进程长期无人值守,到期那天所有订阅突然返回空,排查起来很费时间。稳妥做法是把这类提醒当成一个独立监控项,收到就告警。

6. 二次开发:把回调数据接进落库管道和涨停板监控

跑通 demo 只是第一步,真要用起来,得解决三件事:数据怎么存、订阅怎么自愈、策略怎么验证。

先说服役端结构。我的习惯是三层:回调层只做put_nowait,中间层按 5000 条或 200 毫秒两个条件哪个先到就批量提交,存储层用列式文件或时序库按天分区。批量提交这一步效果很明显,单条 insert 和 5000 条批量 insert 在总吞吐上差一个数量级,而行情数据天然就是流的形态,攒批没有任何副作用。队列一定要有界,满了丢弃并计数,drop_count挂到监控上——盘口策略丢一秒数据和丢一小时数据,结论完全不同。

第二件事是订阅自愈,这块比想象中重要。完整的自愈链路应该是:fncnotify收到断线 → 判断code是否为 3(被踢则直接退出,不要重连)→ 重新login→ 重新拉起全部订阅 → 从fucsubscribe的callback_type=2确认重发结果。同时把当前订阅状态维护成一份内存里的字典,sub/add_sub/del_sub每次调用都同步更新,自愈时直接按字典重放,别靠人工记。我一般还会加一个兜底定时器:每 30 秒对比一次「期望订阅」和「实际回调里出现过的代码」,连续 N 个周期没数据的代码重新add_sub一次。

第三件事是验证,这块最容易被跳过。start_offline_debug的价值就在这里:把一段真实的涨停过程录成 CSV,改完代码先用回放跑一遍,看封单强度、撤单率、委托队列变化的时间点对不对,再去碰实盘。回放参数我习惯设成push_space=1000、push_count=2000,接近真实推送节奏;想压测消费端吞吐就把push_space调到 0,看队列会不会打满、drop_count涨不涨。

至于涨停板监控本身,最小实现其实不复杂:维护一个「当前涨停代码」的集合,每次快照回调检查new_price == uplimit_price,进入集合时记录首次封单量和封单笔数,之后每个快照更新封单衰减率、撤单比、买一集中度。三个指标同时异常(封单量掉、撤单比升、集中度降)时给出信号。这套逻辑全部基于第 4.1 节那几个字段,不需要额外数据源。

有个细节值得一提:涨停价的字段是uplimit_price,但不要用它做判断的唯一依据,沪市和深市在停牌、风险警示板的涨跌幅限制不同,status里的可交易标志和涨跌停价要一起看。我吃过一次亏,某只 ST 票的涨跌幅限制是 5%,代码里按 10% 算的上限去比对,全天都没触发。

最后一句话是我自己的教训:从那以后,我每次升级 SDK 版本,都强制走一遍「字段清单 diff → 离线回放 → 小范围订阅实盘观察一天」这三步,一步不省。这套流程比起在盘中发现数据不对再回头查,省下的时间不止十倍。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 12:08:01

IPv4特殊地址判断:私有、环回、实验、TEST-NET、本地链路全解析

判断一个IPv4地址属于哪一类特殊用途地址&#xff0c;这件事看起来像是网络基础课里的送分题&#xff0c;但我在实际排查问题的过程中发现&#xff0c;真正能把私有地址、环回地址、实验地址、TEST-NET地址、本地链路地址这几类一次性说清楚、并且写出正确判断逻辑的人并不多。…

作者头像 李华
网站建设 2026/11/3 12:08:00

五种AI编程范式:Vibe、Plan、Glue、Spec、Smell的选型与实战指南

1. 五种AI编程范式的底层逻辑与选型思路1.1 为什么需要区分不同的AI编程范式很多人用AI写代码&#xff0c;习惯打开对话框直接甩一句“帮我写个XX功能”&#xff0c;然后拿到代码复制粘贴。这种方式在简单场景下确实能用&#xff0c;但一旦项目复杂度上来&#xff0c;问题就暴露…

作者头像 李华
网站建设 2026/11/3 12:07:29

哈希表核心原理与高级实现技术详解

1. 为什么需要深入理解哈希表&#xff1f;第一次翻开《Handbook of Data Structures and Applications》的哈希表章节时&#xff0c;我被其中精妙的设计思想所震撼。哈希表作为计算机科学中最基础也最重要的数据结构之一&#xff0c;其应用场景远超大多数人的想象。从数据库索引…

作者头像 李华
网站建设 2026/11/3 12:07:25

FileZilla SFTP文件传输完整指南:从下载安装到故障排查

SFTP 传文件这件事&#xff0c;说简单也简单&#xff0c;说折腾也真能折腾一下午。我见过太多人卡在同一个地方&#xff1a;服务器地址、端口、协议类型全都填对了&#xff0c;就是连不上&#xff0c;最后发现是协议选错或者密钥格式不对。FileZilla 算是这个领域里最老牌、最通…

作者头像 李华
网站建设 2026/11/3 12:05:28

MetaHuman资产购买与跨平台应用实战指南

1. MetaHuman资产购买前的准备功课 第一次接触MetaHuman Creator时&#xff0c;我被官方商城的数字人类资产惊艳到了——那些皮肤纹理、发丝细节和微表情简直以假乱真。但真正开始使用这些付费资产时&#xff0c;才发现从购买到最终在项目中应用&#xff0c;整个过程暗藏不少玄…

作者头像 李华
网站建设 2026/11/3 12:05:28

流放之路伤害结算与配装机制解析:加法池、乘法槽与判定顺序

简介&#xff1a;《流放之路从基础到深入全面讲解》是一份面向POE玩家&#xff08;含新手与进阶&#xff09;的游戏机制规则整理文档&#xff0c;用于集中解答游戏中反复出现的机制疑问&#xff0c;可当作随身手册随时查阅&#xff0c;也可作为深入研究的参考资料。资源包内仅有…

作者头像 李华