WebSocket 连接成功但行情不更新,应该先查哪几个状态?

WebSocket 连接成功但行情不更新,应该先查哪几个状态?
首条业务消息已收到、后续数据没有进入 stale。本文用 TickDB WebSocket 真实订阅 600519.SH 的 ticker 频道把这四个状态拆开并给出一份可直接运行的 Python 状态校验脚本、真实终端截图和日志字段设计。image.pngWebSocket 行情接入至少要分清 connected、subscribed、first_message_received 和 stale 四个状态。你的 WebSocket 连上了也发了订阅但行情看板一动不动。是网络问题是服务端没推还是你的代码根本没收到大多数开发者会反复检查连接状态但真正的问题往往藏在四个被忽略的状态转换里真的 connected 了吗——通道打开但还没有业务语义。真的 subscribed 了吗——订阅被接受但可能还没收到第一条数据。真的收到了 第一条 ticker 消息 吗——链路跑通的唯一证据。数据是不是已经 stale 了——连接还在但业务消息早就停了。下面按“症状 → 可能原因 → 怎么查 → 日志字段”的顺序把每个状态拆开并配上一段可直接运行的 Python WebSocket 校验脚本。一、connected连接建立了但还没有业务语义症状连接事件触发但没有任何行情数据过来。可能原因只是完成了 WebSocket 握手还没订阅任何频道。connected 只说明客户端和服务端之间的 WebSocket 通道已经建立。在 TickDB 的 WebSocket 实测中连接建立后会收到类似这样的控制消息{“cmd”: “connected”,“code”: 0,“message”: “Connected successfully”,“data”: {“connection_id”: “739c7ebb-97b9-436f-a09a-37f17101f741”,“server_time”: 1783480593712}}怎么查收到 cmdconnected 后立即发送订阅命令。不要在这一步就认为链路正常——通道打开不等于有数据。日志建议记录 connected_at。二、subscribed订阅被接受了但还没有收到业务数据症状订阅返回了 code0但 ticker 消息始终不来。可能原因订阅确认只代表服务端接受了请求不代表该 symbol 正在产生行情比如非交易时段或者推送通道有其他限制。订阅 ticker 频道时命令结构是{“cmd”: “subscribe”,“data”: {“channel”: “ticker”,“symbols”: [“600519.SH”]}}服务端返回订阅成功时会收到类似{“cmd”: “subscribe”,“code”: 0,“message”: “Subscribed successfully”,“data”: {“channel”: “ticker”,“symbols”: [“600519.SH”]}}怎么查等待第一条 cmdticker 消息。设置一个超时比如 30 秒如果超时未收到标记为 stale 或 first_message_timeout。日志建议记录 subscribed_at并启动首条消息超时计时器。image.png实测终端依次记录 connecting - connected、connected - subscribed、subscribed - first_message_received。API Key 已脱敏。三、first_message_received第一条业务消息才证明链路跑通症状订阅成功了但依然没有数据——或者有数据但解析失败。可能原因字段结构不匹配、symbol 不一致、或者收到的根本不是业务消息可能是控制帧。当收到第一条 cmdticker 消息时才说明 WebSocket 从连接、订阅到业务推送的最小链路跑通。本次实测中600519.SH 的 ticker 消息包含这些字段{“cmd”: “ticker”,“data”: {“symbol”: “600519.SH”,“type”: “stock”,“last_price”: “1186.46”,“volume_24h”: “11486”,“high_24h”: “1195”,“low_24h”: “1177”,“price_change_24h”: “-2.34”,“price_change_percent_24h”: “-0.20”,“timestamp”: 1783480596000}}怎么查收到消息后必须逐项校验检查项 为什么要查cmd “ticker” 避免把 connected、subscribe、pong 等控制消息当行情data.symbol 订阅 symbol 避免订阅与返回错配last_price 可用 Decimal 解析 避免浮点精度和脏值timestamp 是整数 避免时间字段无法进入后续 stale 判断保存 raw snapshot 后续排查时能回放现场任何一项不通过都应该把链路标记为异常而不是静默吞掉。日志建议记录 first_message_at、message_count并保存第一帧的原始快照。image.png实测收到 cmdticker 业务消息后至少要核对 symbol、last_price、timestamp并保存 raw snapshot。四、stale连接还在但行情可能已经停了症状看板价格“一动不动”但 WebSocket 没有断心跳也正常。可能原因非交易时段、服务端推送异常、本地网络阻塞但未触发超时。最隐蔽的情况是——程序在运行连接也在但业务消息已经很久没更新了。这时系统不能继续显示“正常”。应该根据交易时段和业务容忍度设定阈值。例如首条消息等待超过 30 秒first_message_timeout交易时段内 120 秒没有新 tickerno_message_in_trading_hours非交易时段或午间休市不能直接判异常需要结合交易日历或市场状态怎么查记录 last_message_at 和 message_count。出现问题时先问两件事最后一条业务消息是什么时候到的在进入 stale 前总共收到了多少条业务消息没有这两个字段排查只能靠猜。日志建议记录 last_message_at、stale_reason。五、状态总览状态 含义 可进入业务使用吗 下一步connected WebSocket 连接建立 否 发送订阅命令subscribed 订阅命令被接受 否 等待首条业务消息first_message_received 收到第一条 ticker 消息 可以进入字段校验 校验字段并写入日志stale 长时间没有新业务消息 否 告警、重连或回补error 连接、订阅或解析失败 否 记录错误并按策略处理六、生产日志字段建议每一次状态变化都应该写日志。最小字段建议如下字段 含义 写入时机symbol 订阅品种 启动订阅时channel 订阅频道 启动订阅时connected_at 连接建立时间 收到 connectedsubscribed_at 订阅确认时间 收到 subscribefirst_message_at 第一条业务消息时间 收到第一条 tickerlast_message_at 最近业务消息时间 每条 ticker 更新message_count 已收到业务消息数量 每条 ticker 更新stale_reason stale 原因 进入 staleraw_snapshot_path 原始消息快照路径 首条消息和异常消息七、可直接运行的 Python WebSocket 状态校验脚本下面这段是可直接运行的 Python 脚本用于验证 TickDB WebSocket ticker 订阅的四状态链路。运行前安装依赖python3 -m pip install websockets certifi设置环境变量export TICKDB_API_KEY“你的 API Key”export TICKDB_SYMBOL“600519.SH”python3 tickdb_ws_state_guard.py代码如下import asyncioimport contextlibimport jsonimport osimport sslimport timefrom dataclasses import asdict, dataclassfrom datetime import datetime, timezonefrom decimal import Decimal, InvalidOperationfrom pathlib import Pathfrom urllib.parse import quoteimport certifiimport websocketsWS_URL “wss://api.tickdb.ai/v1/realtime”CHANNEL os.environ.get(“TICKDB_CHANNEL”, “ticker”)SYMBOL os.environ.get(“TICKDB_SYMBOL”, “600519.SH”)API_KEY os.environ[“TICKDB_API_KEY”]FIRST_MESSAGE_TIMEOUT int(os.environ.get(“FIRST_MESSAGE_TIMEOUT”, “30”))STALE_THRESHOLD int(os.environ.get(“STALE_THRESHOLD”, “120”))RUN_SECONDS int(os.environ.get(“RUN_SECONDS”, “45”))SNAPSHOT_DIR Path(os.environ.get(“SNAPSHOT_DIR”, “./ws_snapshots”))SNAPSHOT_DIR.mkdir(parentsTrue, exist_okTrue)def utc_now():return datetime.now(timezone.utc).isoformat(timespec“milliseconds”).replace(“00:00”, “Z”)dataclassclass StreamLog:symbol: strchannel: strstate: str “connecting”connected_at: str | None Nonesubscribed_at: str | None Nonefirst_message_at: str | None Nonelast_message_at: str | None Nonemessage_count: int 0stale_reason: str | None Noneraw_snapshot_path: str | None Nonedef save_snapshot(payload, symbol: str, label: str) - str:safe_symbol symbol.replace(“.”, ““)path SNAPSHOT_DIR / f”{safe_symbol}{label}_{int(time.time() * 1000)}.json”if isinstance(payload, str):path.write_text(payload, encoding“utf-8”)else:path.write_text(json.dumps(payload, ensure_asciiFalse, indent2), encoding“utf-8”)return str(path)def validate_ticker_message(message: dict, symbol: str) - tuple[bool, str]:if message.get(“cmd”) ! “ticker”:return False, “not_ticker_message”data message.get(data) if not isinstance(data, dict): return False, missing_data_object if data.get(symbol) ! symbol: return False, symbol_mismatch if last_price not in data: return False, missing_last_price try: Decimal(str(data[last_price])) except (InvalidOperation, ValueError): return False, invalid_last_price if not isinstance(data.get(timestamp), int): return False, invalid_timestamp return True, okasync def ping_loop(ws, stop_event: asyncio.Event):while not stop_event.is_set():await ws.send(json.dumps({“cmd”: “ping”}))await asyncio.sleep(1)async def run():log StreamLog(symbolSYMBOL, channelCHANNEL)subscribe {“cmd”: “subscribe”,“data”: {“channel”: CHANNEL,“symbols”: [SYMBOL],},}url f{WS_URL}?api_key{quote(API_KEY)} ssl_context ssl.create_default_context(cafilecertifi.where()) stop_ping asyncio.Event() print(WS_OPEN wss://api.tickdb.ai/v1/realtime?api_key********) print(SEND, json.dumps(subscribe, ensure_asciiFalse)) async with websockets.connect( url, sslssl_context, ping_intervalNone, close_timeout3, ) as ws: ping_task asyncio.create_task(ping_loop(ws, stop_ping)) await ws.send(json.dumps(subscribe)) started_at time.monotonic() first_wait_started_at time.monotonic() last_seen_monotonic None try: while time.monotonic() - started_at RUN_SECONDS: try: raw await asyncio.wait_for(ws.recv(), timeout5) except asyncio.TimeoutError: if log.first_message_at is None and time.monotonic() - first_wait_started_at FIRST_MESSAGE_TIMEOUT: log.state stale log.stale_reason first_message_timeout print(STATE subscribed - stale) break if last_seen_monotonic is not None and time.monotonic() - last_seen_monotonic STALE_THRESHOLD: log.state stale log.stale_reason no_message_after_first_message print(STATE first_message_received - stale) break continue try: message json.loads(raw) except json.JSONDecodeError: log.state error log.stale_reason invalid_json log.raw_snapshot_path save_snapshot(raw, SYMBOL, invalid_json) print(STATE error) break cmd message.get(cmd) if cmd connected: log.state connected log.connected_at utc_now() print(STATE connecting - connected) print(RECV, json.dumps(message, ensure_asciiFalse, indent2)) continue if cmd subscribe: if message.get(code) 0: log.state subscribed log.subscribed_at utc_now() print(STATE connected - subscribed) else: log.state error log.stale_reason subscribe_rejected log.raw_snapshot_path save_snapshot(message, SYMBOL, subscribe_rejected) print(RECV, json.dumps(message, ensure_asciiFalse, indent2)) continue if cmd pong: continue ok, reason validate_ticker_message(message, SYMBOL) if not ok: log.state error log.stale_reason reason log.raw_snapshot_path save_snapshot(message, SYMBOL, invalid) print(STATE error) print(json.dumps(asdict(log), ensure_asciiFalse, indent2)) break now utc_now() last_seen_monotonic time.monotonic() log.last_message_at now log.message_count 1 log.raw_snapshot_path save_snapshot(message, SYMBOL, ticker) if log.first_message_at is None: log.state first_message_received log.first_message_at now print(STATE subscribed - first_message_received) print(RECV, json.dumps(message, ensure_asciiFalse, indent2)) finally: stop_ping.set() ping_task.cancel() with contextlib.suppress(asyncio.CancelledError): await ping_task print(FINAL_STATE) print(json.dumps(asdict(log), ensure_asciiFalse, indent2))ifname “main”:asyncio.run(run())这段代码做了几件事WebSocket 鉴权使用 URL query api_key订阅命令使用 cmdsubscribe 和 data.channel / data.symbols每秒发送一次 ping但不把心跳当成业务数据只把 cmdticker 当成业务消息校验 symbol、last_price、timestamp保存 ticker 原始快照第一条业务消息超时则进入 stale持续阶段消息沉寂超阈值也进入 stale。八、本次真实实测截图本次实测使用 TickDB WebSocket 订阅 600519.SH 的 ticker 频道终端记录了 connected、subscribed、first_message_received 三个状态并收到 ticker 业务消息。image.png图中可以看到WebSocket 连接地址为 wss://api.tickdb.ai/v1/realtime?api_key********API Key 已脱敏订阅频道为 ticker订阅 symbol 为 600519.SH收到 connected、subscribe、ticker 消息ticker 消息里包含 symbol、last_price、timestamp 等字段。这张图只证明采样时刻的最小订阅链路跑通不代表延迟、SLA、稳定性、全市场覆盖或交易信号。九、TickDB 在这条链路里的位置TickDB 是一个统一行情数据 API提供 REST、WebSocket、MCP 等接入方式。放到这篇文章的问题里可以这样理解WebSocket适合实时订阅和持续推送本文实测的就是这一层REST适合初始化快照、查询历史数据、在 stale 恢复后做补数据MCP适合让 AI 工具按需查询行情不替代 WebSocket 的持续推送。三种方式的鉴权不能混写接入方式 鉴权方式REST HeaderX-API-KeyWebSocket URL queryapi_keyMCP HeaderX-TickDB-Key如果你的系统要做实时行情面板或监控告警不能只看 WebSocket 是否连接成功。应用侧仍然要记录状态、保存 raw snapshot、设置 stale 阈值、处理重连和回补。TickDB 提供的是可验证的行情接入入口状态判断、日志留痕和异常处理仍然应该由你的系统自己完成。十、上线前检查清单接入任何 WebSocket 行情 API都建议先过这张表检查项 通过标准连接状态 收到 connected 控制消息订阅状态 收到 subscribe 且 code0首条业务消息 收到 cmdtickersymbol 校验 返回 symbol 与订阅 symbol 一致价格字段 last_price 可用 Decimal 解析时间字段 timestamp 可解析为整数时间戳原始留痕 ticker 和异常消息保存 raw snapshotstale 检测 首条消息和后续消息都有超时阈值非交易时段 结合交易日历或市场状态判断鉴权边界 REST、WebSocket、MCP 的 Key 写法不混用十一、FAQQ1WebSocket connected 后为什么不能直接认为行情可用因为 connected 只说明通道建立。你还没有确认订阅是否成功也没有收到业务行情消息。Q2subscribe 成功后为什么还要等 first message因为 subscribe 成功只说明订阅请求被接受。第一条 ticker 消息才证明业务数据真的进入客户端。Q3什么情况下应该标记 stale交易时段内超过阈值没有收到新业务消息时。首条消息迟迟不到也应该标记为 stale 或 first message timeout。Q4心跳正常能不能证明行情正常不能。ping/pong 只能证明连接还活着不证明 ticker 业务消息还在持续推送。Q5本次 TickDB WebSocket 实测能证明什么能证明 2026-07-08 采样时刻600519.SH 的 ticker 频道完成了 connected、subscribed、first_message_received 的最小链路。不能证明延迟、SLA、全市场覆盖、全时段可用、生产稳定性或交易信号。最后WebSocket 行情接入不要只问“连上了吗”。更应该问订阅确认了吗第一条业务消息到了吗最后一条业务消息是什么时候