"""查 Bitget candle1m 每条推送里到底带几根 K 线、最新的在哪一端。 归因结果显示 ccxt.pro 比原始 WS 快约 1.4 秒,而两者订阅的是同一条频道。 Hummingbot 的解析取 data["data"][0],我的探针也取 [0],两者一致地慢—— 若 Bitget 一条消息里带多根、最新在末尾,取 [0] 就会系统性落后一个滑动窗口。 对每条消息记录:元素个数、首末元素的时间戳、以及首末之差。 若普遍 len>1 且末元素更新,则 [0] 就是那 1.4 秒的来源,且可用一行覆盖修好。 .venv/bin/python research/live/probe_ws_payload.py --minutes 4 """ from __future__ import annotations import argparse import asyncio import json import time from collections import Counter, defaultdict WSS = "wss://ws.bitget.com/v2/ws/public" SYMS = ("BTCUSDT", "ETHUSDT", "SOLUSDT") async def run(minutes: int) -> None: import aiohttp payload = {"op": "subscribe", "args": [{"instType": "USDT-FUTURES", "channel": "candle1m", "instId": s} for s in SYMS]} n_elems = Counter() action_elems = Counter() # 取 [0] 相对取 [-1] 的落后量:同一根 K 线,两种读法各自首次见到的时刻 first_seen_head: dict = {} first_seen_tail: dict = {} n_msg = 0 print(f"[载荷探针] {SYMS} · 跑 {minutes} 分钟", flush=True) deadline = time.time() + minutes * 60 while time.time() < deadline: try: async with aiohttp.ClientSession() as sess, \ sess.ws_connect(WSS, heartbeat=20) as ws: await ws.send_str(json.dumps(payload)) while time.time() < deadline: msg = await ws.receive(timeout=30) # 断开后 receive() 立刻返回 CLOSED,只 continue 会变成空转 if msg.type is not aiohttp.WSMsgType.TEXT: print(f" 非文本消息 {msg.type},重连", flush=True) break if msg.data == "pong": continue d = json.loads(msg.data) if "data" not in d or "arg" not in d: continue arr = d["data"] if not arr: continue n_msg += 1 sym = d["arg"]["instId"].replace("USDT", "") act = d.get("action") n_elems[len(arr)] += 1 action_elems[(act, len(arr))] += 1 now_ms = int(time.time() * 1000) head_ts, tail_ts = int(arr[0][0]), int(arr[-1][0]) first_seen_head.setdefault((sym, head_ts), now_ms) first_seen_tail.setdefault((sym, tail_ts), now_ms) if n_msg <= 6 or len(arr) > 1: print(f" {sym} action={act} 元素数={len(arr)} " f"首ts={head_ts} 末ts={tail_ts} " f"跨度={(tail_ts - head_ts) // 1000}s", flush=True) except asyncio.CancelledError: raise except Exception as e: print(f" 连接异常 {type(e).__name__}: {e},1s 后重连", flush=True) if time.time() < deadline: await asyncio.sleep(1) print(f"\n########## 每条消息的元素个数分布(共 {n_msg} 条)##########") for k in sorted(n_elems): print(f" {k} 根: {n_elems[k]} 条") print("\n按 action 拆分:") for k in sorted(action_elems, key=lambda x: (str(x[0]), x[1])): print(f" action={k[0]} 元素数={k[1]}: {action_elems[k]} 条") # 同一根 K 线,用 [-1] 读比用 [0] 读早多少 lead = defaultdict(list) for (sym, ts), t_tail in first_seen_tail.items(): t_head = first_seen_head.get((sym, ts)) if t_head is not None: lead[sym].append(t_head - t_tail) print("\n########## 取 [-1] 比取 [0] 提前多少(ms)##########") for s in SYMS: v = sorted(lead[s.replace("USDT", "")]) if not v: print(f" {s}: 无配对") continue print(f" {s}: n={len(v)} 中位 {v[len(v) // 2]}ms 最大 {v[-1]}ms") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--minutes", type=int, default=4) asyncio.run(run(ap.parse_args().minutes)) if __name__ == "__main__": main()