Python 盘口数据校验:bids/asks、timestamp、spread 和信号边界
摘要
你付费开了 Level 2,看到买一突然挂出一笔大单,卖一变薄,bids/asks 看起来很有方向感。你的第一反应可能是:盘口已经给信号了。但过一会儿,大单撤了,价格没按你想的走。问题不在数据本身——Level 2 没有骗你。问题在于,你把“能看到盘口”误解成了“盘口已经能给交易信号”。这篇文章把盘口数据拆成三层——展示、状态观察、信号候选——讲清楚为什么大多数人在第一层以为自己看到了第三层,以及真正能做信号的盘口数据需要跨过哪几道验证门槛。
盘口深度能展示买卖盘状态,但不能直接推出价格方向。
1. 为什么付费 Level 2 反而可能让你更自信地误判
Level 2 给你打开了一扇窗,让你看到比免费行情更多的买卖挂单。但这扇窗带来的“信息增量”,也很容易变成“自信增量”——你能看到更多东西了,于是你觉得自己能判断更多东西了。这里有三种最常见的误判。
挂单不是成交。买一挂了 1000 手,一分钟后消失了。被吃掉了还是撤掉了?大多数行情数据只告诉你当前挂单状态,不告诉你最终去向。你看到的是“有人在这一刻愿意在这个价格买”,不是“有人已经在这个价格买了”。bids/asks 展示的是意愿,不是最终行为。挂单可以撤,可以改,可以只是试探。把意愿当成行为,是盘口误判的第一大来源。
大单不等于方向。买一突然出现一笔大单。你的直觉是:有大资金看好,价格要涨。但大单只能说明某一刻盘口上有量,不等于未来价格会怎么走。大单可能来自指数基金被动调仓、算法交易拆单执行、做市商双边报价、或者多个散户订单的偶然聚合。你看到的不是“主力在行动”,只是“盘口上有一笔大单”。从大单到价格方向之间,隔着撤单、成交、对手盘、市场环境等一系列变量。把相关当成因果,是盘口误判的第二大来源。
多档深度不等于看穿市场。Level 2 让你看到买一到买十,比免费行情的买一卖一多了一个数量级的信息。但这离“看穿市场”还很远。可见挂单之外,还有隐藏订单、其他交易所的报价、以及没有挂出来而是在等待合适时机才出手的资金。看到更多层,并不等于理解了市场机制、时序和样本稳定性。数据维度的增加,如果没配上验证框架的升级,结果不是判断更准,而是更自信地误判。
同一档位 size 变化,只说明盘口状态变了,不能单独判断成交、撤单或改价。
2. 盘口展示、状态观察、信号候选,是三层不同的东西
从“看见盘口”到“研究信号”,中间必须经过字段、时间和样本验证。
把盘口数据放进决策流程之前,先搞清楚它在你手里到底属于哪一层。
第一层:盘口展示。这是最基础的层次。你能看到买一卖一、bids/asks、多档深度、价格和数量。屏幕上的数字在跳动,颜色在变化,信息在流动。展示层只回答一个问题:“现在屏幕上有什么。”它不回答“这能不能用于判断”。但很多人直接把展示当判断——看到买一量大,就觉得支撑强;看到卖一量薄,就觉得要突破。这是把所有判断都建立在静态截图上的行为。
第二层:状态观察。当你不再盯着单笔大单找方向,而是系统性地用盘口数据观察市场状态时,你进入了第二层。这一层可以观察四样东西:bid/ask spread 在扩大还是缩小;买一侧和卖一侧的深度是否均衡,有没有突变;某一侧突然变薄或消失——这是异常信号,不是交易机会;价格跳空、挂单突变、刷新中断。状态观察层只做诊断,不做预测。它能告诉你市场现在是活跃还是冷清、稳定还是波动、正常还是异常。
第三层:信号候选。当你把观察内容量化为可回测的规则,并且通过了字段、时间、机制和样本的验证之后,盘口数据才进入“信号候选”的范畴。注意,是“候选”,不是“有效信号”。这一层有三道门槛:字段关——bids/asks 的价格和数量语义是否明确?层级定义是否清晰?历史样本是否完整可核对?时间关——时间戳是行情发生时间还是接收时间?刷新是全量快照还是增量更新?两次更新之间的间隔是否稳定?机制关——你的判断逻辑在不同市场机制下是否一致?A股T+1和涨跌停、美股多交易所和做市商、港股竞价时段——同一个盘口现象在这些不同制度下含义完全不同。
绝大多数人困在第二层。不是因为不够聪明,是因为从“观察”到“候选”之间,隔的不是一个指标,而是一整套验证流程。
3. 盘口字段表与 Python 校验代码
在把盘口数据从“观察”升级为“信号候选”之前,先逐字段核对以下结构。以下字段以 TickDB 盘口接口返回结构为示例,实际以当前接口文档和实测为准。
核心字段表:
| 字段 | 含义 | 核对点 |
|---|---|---|
bids | 买盘数组,每项为[price, quantity] | 价格降序排列,元素为字符串 |
asks | 卖盘数组,每项为[price, quantity] | 价格升序排列,元素为字符串 |
price | 挂单价格 | 非空字符串,可解析为有限 Decimal |
quantity | 挂单数量 | 非空字符串,可解析为有限 Decimal |
timestamp | 盘口快照时间 | 整数且非 bool,单位以接口文档为准 |
spread | 买卖价差 | 本地计算best_ask - best_bid |
checked_at | 本地检查时间 | ISO 8601 格式 |
raw_snapshot | 原始响应 JSON | 完整保存 |
Python 校验代码:
fromdecimalimportDecimal,InvalidOperationdefvalidate_order_book(data:dict)->dict:"""校验盘口数据:bids/asks 排序、spread 计算、timestamp 单位检查。 以当前接口返回结构为准,本示例假设 bids/asks 为 [[price, qty], ...] 格式。 """result={"ok":True,"issues":[],"spread":None,"raw_snapshot":None}bids=data.get("bids",[])asks=data.get("asks",[])ifnotisinstance(bids,list)ornotisinstance(asks,list):return{"ok":False,"reason":"bids/asks 不是数组"}iflen(bids)==0orlen(asks)==0:result["issues"].append("bids 或 asks 为空")# 校验 bids 价格降序bid_prices=[]fori,iteminenumerate(bids):ifnotisinstance(item,list)orlen(item)<2:return{"ok":False,"reason":f"bids[{i}] 结构无效"}try:p=Decimal(str(item[0]))q=Decimal(str(item[1]))ifnotp.is_finite()ornotq.is_finite():result["issues"].append(f"bids[{i}] 非有限数:{item}")bid_prices.append(p)except(InvalidOperation,ValueError):return{"ok":False,"reason":f"bids[{i}] 无法解析:{item}"}foriinrange(len(bid_prices)-1):ifbid_prices[i]<bid_prices[i+1]:result["issues"].append(f"bids 价格未降序: bids[{i}]={bid_prices[i]}, bids[{i+1}]={bid_prices[i+1]}")# 校验 asks 价格升序ask_prices=[]fori,iteminenumerate(asks):ifnotisinstance(item,list)orlen(item)<2:return{"ok":False,"reason":f"asks[{i}] 结构无效"}try:p=Decimal(str(item[0]))q=Decimal(str(item[1]))ifnotp.is_finite()ornotq.is_finite():result["issues"].append(f"asks[{i}] 非有限数:{item}")ask_prices.append(p)except(InvalidOperation,ValueError):return{"ok":False,"reason":f"asks[{i}] 无法解析:{item}"}foriinrange(len(ask_prices)-1):ifask_prices[i]>ask_prices[i+1]:result["issues"].append(f"asks 价格未升序: asks[{i}]={ask_prices[i]}, asks[{i+1}]={ask_prices[i+1]}")# 本地计算 spreadifbid_pricesandask_prices:best_bid=bid_prices[0]best_ask=ask_prices[0]result["spread"]=str(best_ask-best_bid)# timestamp 单位检查ts=data.get("timestamp")ifisinstance(ts,bool)ornotisinstance(ts,int):result["issues"].append(f"timestamp 类型异常:{type(ts).__name__}")# 保存原始快照result["raw_snapshot"]=dataifresult["issues"]:result["ok"]=Falseresult["reason"]="; ".join(result["issues"])returnresult实测截图说明:通过 TickDB get_order_book 查询 600519.SH 盘口字段,展示 bids/asks/timestamp/spread 与本地校验结果;仅用于字段结构验证,不构成交易信号或投资建议。
4. 盘口数据常见错误表
| 错误行为 | 正确边界 | 为什么容易犯 |
|---|---|---|
| 把挂单当成交 | bids/asks 只展示挂单意愿,不是成交记录 | 看到大单量,直觉以为已经成交 |
| 把大单当方向 | 大单可能来自做市商、被动调仓、拆单等 | 大单量在视觉上冲击力强,容易过度解读 |
| 把快照当完整订单流 | 两次快照之间发生的变化你不知道 | 屏幕刷新频率让你以为看到的是连续变化 |
| 把 bids/asks 当 NBBO | 单一交易所的盘口不等于全市场最优报价 | 美股多交易所场景下尤其常见 |
| 拿 A 股盘口逻辑套美股 | 不同市场的交易机制、涨跌停规则、做市商制度完全不同 | 盘口“看起来相似”,但底层机制差异巨大 |
5. 盘口能做什么,不能做什么
| 能做 | 不能直接做 |
|---|---|
| 观察 bid/ask spread | 预测涨跌 |
| 判断流动性状态 | 证明策略有效 |
| 发现报价缺失或异常 | 给买卖建议 |
| 记录盘口变化样本 | 重建完整订单流 |
| 作为信号研究的候选输入 | 承诺高频做市或套利能力 |
| 验证字段结构和数据质量 | 证明某个数据源永远更准 |
盘口是一扇窗,不是一张地图。窗让你看到更多,但地图告诉你方向。把窗当成地图,你看到的东西越多,越容易走错路。
6. TickDB 在这里的合理位置
已经在看盘口、bids/asks、深度数据,但想把观察过程结构化记录下来——TickDB 可以作为候选结构化行情入口,帮助你把盘口和深度相关字段、时间信息、样本记录和异常排查放进同一套验证流程里。
怎么验证:用自己的 symbol 和市场场景,按五个问题逐项核对——层级、字段语义、时间戳、刷新语义、跨市场适用性。以当前接口返回为准做字段验证,保存请求参数、原始返回和检查时间。先证明数据能被解释,再讨论它是否能进入信号研究。
不适合什么:不证明盘口信号一定有效;不做荐股;不承诺收益;不替代策略回测、样本检验和风控系统。不提供完整 Level 2、NBBO、暗池、全量订单簿或高频能力。
盘口适合做状态观察和样本记录,不适合直接包装成买卖建议或策略有效性证明。
7. 发布前检查清单
| 序号 | 检查项 | 通过标准 |
|---|---|---|
| 1 | bids 价格降序 | 无逆序项 |
| 2 | asks 价格升序 | 无逆序项 |
| 3 | price/quantity 为字符串 | 可解析为有限 Decimal |
| 4 | spread 本地计算 | 与 best_ask - best_bid 一致 |
| 5 | timestamp 为整数且非 bool | 单位以接口文档为准 |
| 6 | 区分挂单与成交 | bids/asks 不作为成交信号使用 |
| 7 | 区分快照与连续流 | 不把单次快照当完整订单流 |
| 8 | raw_snapshot 已保存 | 每次请求保留原始 JSON |
| 9 | 无投资建议 | 文档不输出买卖信号 |
你现在看盘口时,是把它当展示、当状态观察,还是已经当成信号了?你有没有遇到过买一大单看起来很强,结果很快撤掉或完全不按预期走的情况?欢迎在评论区聊聊你的经历和判断。