"""影子交易器的报表:首日延迟门槛 + 滑点对延迟曲线。 两份产物对应计划里的两件事。 ### 延迟门槛(提前止损用) 跑满 24 小时先看这个。若**总延迟已令预期漂移超过余量**,说明方案在这台机器 上就不成立,不必等两周样本再停。余量取 bitget_baseline.py 的实测值: BTC −0.13bp(本就为负,只作参照)、ETH +4.02bp、SOL +2.92bp。 漂移按随机游走折算:σ_1m · √(t/60)。这是下限——入场条件是「收盘突破转强」, 那一刻价格正朝我们方向跑,延迟造成的是系统性追价,不会正负抵消。所以实测 滑点理应比这个折算值更差,两者对照本身就是个校验。 ### 滑点对延迟曲线 把延迟当自变量:0.5s / 1s / 2s / 5s 各一个滑点值,外加「actual」= 本机实际 算完的时刻。这样即便本机算得慢,也能读出「若延迟压到 X 秒,滑点是多少」, 决策不被当前实现拖累。 .venv/bin/python research/live/shadow_report.py """ from __future__ import annotations from pathlib import Path import numpy as np import pandas as pd HERE = Path(__file__).resolve().parent OUT = HERE.parent / "out" SYMS = ("BTC", "ETH", "SOL") BUDGET_BP = {"BTC": -0.13, "ETH": 4.02, "SOL": 2.92} def vol_bp() -> dict[str, float]: v = {} 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 c = np.log(pd.read_feather(f)["close"].to_numpy(float)) v[s] = float(np.nanstd(np.diff(c)) * 1e4) return v def latency_gate(lat: pd.DataFrame, vols: dict) -> None: print("########## 一、延迟门槛 ##########") span_h = (lat["t_close_ms"].max() - lat["t_close_ms"].min()) / 3.6e6 print(f"样本跨度 {span_h:.1f} 小时 · 共 {len(lat)} 根\n") print(f"{'币':<5}{'根数':>6}{'数据ms':>9}{'计算ms':>9}{'总延迟ms':>10}" f"{'P90ms':>8}{'折算漂移bp':>12}{'余量bp':>9}{'占余量':>9}") verdicts = {} for s in SYMS: g = lat[lat["sym"] == s] if g.empty: continue d = float(np.median(g["lag_data_ms"])) c = float(np.median(g["compute_ms"])) t = float(np.median(g["lag_signal_ms"])) p90 = float(np.percentile(g["lag_signal_ms"], 90)) vol = vols.get(s) drift = vol * np.sqrt(t / 60_000) if vol else float("nan") b = BUDGET_BP[s] share = drift / b if b > 0 else float("nan") verdicts[s] = (drift, b) txt = f"{share * 100:.0f}%" if b > 0 else "—(负)" print(f"{s:<5}{len(g):>6}{d:>9.0f}{c:>9.0f}{t:>10.0f}{p90:>8.0f}" f"{drift:>12.2f}{b:>9.2f}{txt:>9}") print("\n判读:") for s, (drift, b) in verdicts.items(): if b <= 0: print(f" {s}: 余量本就为负,不作交易标的,仅作延迟参照") elif drift > b: print(f" {s}: 折算漂移 {drift:.2f}bp 已超余量 {b:.2f}bp —— 停下改方案") elif drift > b * 0.6: print(f" {s}: 折算漂移 {drift:.2f}bp 吃掉余量 {b:.2f}bp 的六成以上," f"需要压延迟或放弃") else: print(f" {s}: 折算漂移 {drift:.2f}bp 对余量 {b:.2f}bp 尚有空间,继续收集") def slippage_curve(sig: pd.DataFrame) -> None: print("\n\n########## 二、滑点对延迟曲线 ##########") if sig.empty: print(" 尚无信号样本") return n_sig = sig.groupby(["sym", "kline_ts", "direction"]).ngroups n_agree = sig[sig["h1_agree"] == 1].groupby( ["sym", "kline_ts", "direction"]).ngroups print(f"信号总数 {n_sig}(其中 h1_agree=1 的 {n_agree} 个)\n") order = ["0.5s", "1.0s", "2.0s", "5.0s", "actual"] for scope, sub in (("全部信号", sig), ("仅 h1_agree=1(主口径)", sig[sig["h1_agree"] == 1])): if sub.empty: continue print(f"--- {scope} ---") print(f"{'延迟':<8}{'仓位':>9}{'n':>5}{'滑点均值bp':>12}" f"{'中位bp':>9}{'漂移bp':>9}{'价差bp':>9}{'冲击bp':>9}") for lb in order: g0 = sub[sub["delay_label"] == lb] if g0.empty: continue for nt in sorted(sub["notional"].unique()): g = g0[g0["notional"] == nt] if g.empty: continue print(f"{lb:<8}{int(nt):>9}{len(g):>5}" f"{g['slip_bp'].mean():>12.2f}" f"{g['slip_bp'].median():>9.2f}" f"{g['drift_bp'].mean():>9.2f}" f"{g['spread_bp'].mean():>9.2f}" f"{g['impact_bp'].mean():>9.2f}") print() print("--- 分币种(仅 h1_agree=1,仓位 5000)---") m = sig[(sig["h1_agree"] == 1) & (sig["notional"] == 5000.0)] if m.empty: print(" 尚无样本") return print(f"{'币':<5}{'延迟':<8}{'n':>5}{'滑点均值bp':>12}{'余量bp':>9}") for s in SYMS: for lb in order: g = m[(m["sym"] == s) & (m["delay_label"] == lb)] if g.empty: continue print(f"{s:<5}{lb:<8}{len(g):>5}{g['slip_bp'].mean():>12.2f}" f"{BUDGET_BP[s]:>9.2f}") def main() -> None: vols = vol_bp() print("1m 收益标准差(bp/分钟,Bitget 实测):" + " ".join(f"{s} {v:.2f}" for s, v in vols.items()) + "\n") f_lat = OUT / "shadow_latency.csv" if f_lat.exists() and f_lat.stat().st_size > 0: lat = pd.read_csv(f_lat) if not lat.empty: latency_gate(lat, vols) else: print("尚无延迟数据") f_sig = OUT / "shadow_signals.csv" if f_sig.exists() and f_sig.stat().st_size > 0: sig = pd.read_csv(f_sig) slippage_curve(sig) else: print("\n尚无信号数据") if __name__ == "__main__": main()