"""延迟基准 B:Hummingbot 侧的数据到手时刻。 在 Hummingbot 容器内运行,测的事件与 latency_ccxt.py 完全相同: **我们在什么时候得知第 N 根 1m 已经收盘**。 为什么必须在 Hummingbot 里测而不是复用 ccxt 的数字:如果最终执行走 Hummingbot,那决定成交价的是它的 WS 处理与事件循环延迟。用别的运行时测出 的滑点,换到 Hummingbot 上就不成立了。 用 max_records=20 让历史回填快速完成——这里只关心增量推送的时刻,不需要 2000 根窗口。 输出 out/latency_hummingbot.csv,字段与 A 侧一致,事后按 kline_ts join。 """ from __future__ import annotations import argparse import asyncio import csv import time from pathlib import Path SYMS = ("BTC", "ETH", "SOL") CONNECTOR = "bitget_perpetual" OUT = Path("/out/latency_hummingbot.csv") async def main_async(minutes: int) -> None: from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory from hummingbot.data_feed.candles_feed.data_types import CandlesConfig feeds = {} for s in SYMS: cfg = CandlesConfig(connector=CONNECTOR, trading_pair=f"{s}-USDT", interval="1m", max_records=20) feeds[s] = CandlesFactory.get_candle(cfg) print(f"[Hummingbot 延迟] {SYMS} · connector={CONNECTOR} · " f"计划跑 {minutes} 分钟", flush=True) for s, f in feeds.items(): if hasattr(f, "start"): f.start() else: await f.start_network() print(f" {s} 已启动订阅", flush=True) # 等历史回填完成,否则 deque 尾部时间戳还在跳变 t0 = time.time() while time.time() - t0 < 120: if all(f.ready for f in feeds.values()): break await asyncio.sleep(0.5) ready = {s: f.ready for s, f in feeds.items()} print(f" 回填状态 {ready}({time.time() - t0:.1f}s)", flush=True) OUT.parent.mkdir(parents=True, exist_ok=True) fh = OUT.open("w", newline="") writer = csv.DictWriter(fh, fieldnames=["kline_ts", "sym", "t_data_ms", "lag_ms"]) writer.writeheader() last = {} for s, f in feeds.items(): c = f._candles last[s] = int(c[-1][0]) if len(c) else None stats: dict = {} deadline = time.time() + minutes * 60 while time.time() < deadline: for s, f in feeds.items(): c = f._candles if not len(c): continue newest = int(c[-1][0]) if last[s] is None: last[s] = newest continue if newest > last[s]: now_ms = int(time.time() * 1000) # Hummingbot 的时间戳是秒,统一成毫秒后再与本地钟相减 closed_ms = newest * 1000 if newest < 1e12 else newest lag_ms = now_ms - closed_ms writer.writerow({"kline_ts": closed_ms, "sym": s, "t_data_ms": now_ms, "lag_ms": lag_ms}) fh.flush() stats.setdefault(s, []).append(lag_ms) n = len(stats[s]) if n % 5 == 1: med = sorted(stats[s])[n // 2] print(f" {s}: 第 {n} 根,本次 lag {lag_ms}ms,中位 {med}ms", flush=True) last[s] = newest await asyncio.sleep(0.01) for f in feeds.values(): f.stop() fh.close() print("\n########## t_data − t_close(ms)##########") for s in SYMS: v = sorted(stats.get(s, [])) if not v: print(f" {s}: 无样本") continue print(f" {s}: n={len(v)} 中位 {v[len(v) // 2]}ms " f"P90 {v[int(len(v) * .9)]}ms 最大 {v[-1]}ms") print(f"\n产物写入 {OUT}") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--minutes", type=int, default=20) args = ap.parse_args() asyncio.run(main_async(args.minutes)) if __name__ == "__main__": main()