"""把两侧 t_data − t_close 对齐,并折算成 bp,与滑点余量对照。 为什么要折算成 bp 才有意义:延迟本身不花钱,花钱的是延迟期间价格的漂移。 按随机游走,t 秒的价格标准差是 σ_1m · √(t/60),其中 σ_1m 是本币 1m 收益 的标准差。入场方向上还有系统性追价(信号触发往往伴随同向动量),所以随机 漂移只是下限,真实成本更高——这也是为什么最终仍要用真实盘口测滑点。 余量(bitget_baseline.py 得出,Bitget 原生基线减去手续费后剩下的空间): BTC -0.13bp ETH +4.02bp SOL +2.92bp BTC 本就为负,留着只作延迟测量的参照物,不作交易标的。 .venv/bin/python research/live/latency_compare.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd HERE = Path(__file__).resolve().parent RESEARCH = HERE.parent sys.path.insert(0, str(RESEARCH)) OUT = RESEARCH / "out" BUDGET_BP = {"BTC": -0.13, "ETH": 4.02, "SOL": 2.92} SYMS = ("BTC", "ETH", "SOL") def vol_bp_per_min() -> dict[str, float]: """从 Bitget 缓存算 1m 收益标准差,单位 bp。""" out = {} for s in SYMS: f = HERE / "cache" / f"bitget_{s}_1m_30d.feather" if not f.exists(): f = HERE / "cache" / f"bitget_{s}_1m_210d.feather" if not f.exists(): continue df = pd.read_feather(f) r = np.log(df["close"].to_numpy(dtype=float)) out[s] = float(np.nanstd(np.diff(r)) * 1e4) return out def drift_bp(lag_ms: float, vol: float) -> float: """随机游走下,lag 毫秒对应的价格漂移标准差(bp)。""" return vol * np.sqrt(max(lag_ms, 0) / 60_000.0) def load(tag: str) -> pd.DataFrame | None: f = OUT / f"latency_{tag}.csv" if not f.exists(): return None df = pd.read_csv(f) return df if not df.empty else None def describe(df: pd.DataFrame, name: str, vols: dict) -> None: print(f"\n########## {name}:t_data − t_close ##########") print(f"{'币':<5}{'n':>5}{'中位ms':>9}{'P90ms':>9}{'最大ms':>9}" f"{'中位漂移bp':>12}{'余量bp':>9}{'占余量':>9}") for s in SYMS: v = df[df["sym"] == s]["lag_ms"].to_numpy(dtype=float) if not len(v): continue med, p90 = float(np.median(v)), float(np.percentile(v, 90)) vol = vols.get(s) d = drift_bp(med, vol) if vol else float("nan") b = BUDGET_BP[s] share = f"{d / b * 100:.0f}%" if b > 0 else "—(负)" print(f"{s:<5}{len(v):>5}{med:>9.0f}{p90:>9.0f}{v.max():>9.0f}" f"{d:>12.2f}{b:>9.2f}{share:>9}") def main() -> None: vols = vol_bp_per_min() print("1m 收益标准差(bp/分钟,Bitget 缓存实测):") for s, v in vols.items(): print(f" {s}: {v:.2f}") a, b = load("ccxt"), load("hummingbot") if a is None or b is None: print(f"\n数据未就绪:ccxt={'有' if a is not None else '无'} " f"hummingbot={'有' if b is not None else '无'}") if a is not None: describe(a, "ccxt.pro", vols) if b is not None: describe(b, "Hummingbot", vols) return describe(a, "ccxt.pro", vols) describe(b, "Hummingbot", vols) # 逐根配对才能消掉「不同分钟市场活跃度不同」的干扰 m = a.merge(b, on=["kline_ts", "sym"], suffixes=("_ccxt", "_hb")) print(f"\n########## 逐根配对(重叠 {len(m)} 根)##########") if m.empty: print(" 两侧无重叠 K 线,无法配对;检查采集时间窗是否错开") return print(f"{'币':<5}{'n':>5}{'ccxt中位':>10}{'HB中位':>10}" f"{'差值中位':>10}{'HB更慢占比':>12}{'差值→bp':>10}") for s in SYMS: g = m[m["sym"] == s] if g.empty: continue d = (g["lag_ms_hb"] - g["lag_ms_ccxt"]).to_numpy(dtype=float) vol = vols.get(s) # 差值转 bp:比较两条路径各自漂移的差,而非直接对差值开方 extra = (drift_bp(float(np.median(g["lag_ms_hb"])), vol) - drift_bp(float(np.median(g["lag_ms_ccxt"])), vol)) if vol else float("nan") print(f"{s:<5}{len(g):>5}{np.median(g['lag_ms_ccxt']):>10.0f}" f"{np.median(g['lag_ms_hb']):>10.0f}{np.median(d):>10.0f}" f"{(d > 0).mean() * 100:>11.0f}%{extra:>10.2f}") print("\n判读:差值 → bp 若显著小于余量,说明选哪个运行时不影响结论," "可直接用 Hummingbot(生产路径一致);若接近或超过余量," "则运行时本身就是成本项,需要单独优化或放弃 1m。") if __name__ == "__main__": main()