diff --git a/research/live/deploy/start.sh b/research/live/deploy/start.sh index 1084dd4..1f193c6 100755 --- a/research/live/deploy/start.sh +++ b/research/live/deploy/start.sh @@ -17,6 +17,10 @@ NAME="${NAME:-shadow}" # verify_lean_parity.py 在影子这条路径上逐根验过(180 窗口 / 90 命中零分歧)。 # 设 0 可退回 full,用来复量两模式的耗时差。 SHADOW_LEAN="${SHADOW_LEAN:-1}" +# 币池。默认三个流动性最好的做滑点测量;十币池是实际要交易的那批(TRX 剔除, +# ATR 门控几乎全刷掉)。币数直接决定排队:所有币同一秒收盘,2 核上 10 个币 +# 需要约 640ms 墙钟才算完,最后一个币的信号会落在 800ms 哨兵线之外。 +SYMS="${SYMS:-BTC,ETH,SOL}" IMAGE="${SHADOW_IMAGE:-hummingbot/hummingbot:latest}" HOURS="${HOURS:-168}" WORKERS="${WORKERS:-2}" @@ -108,7 +112,7 @@ docker run -d --name "$NAME" -w /home/hummingbot \ -v "$OUT:/out" \ --entrypoint /opt/conda/envs/hummingbot/bin/python \ "$IMAGE" /repo/research/live/shadow_hb.py \ - --hours "$HOURS" --workers "$WORKERS" >/dev/null + --hours "$HOURS" --workers "$WORKERS" --syms "$SYMS" >/dev/null echo "已启动。等启动自检(补丁断言 + 历史回填,约 60 秒)…" sleep 45 diff --git a/research/live/shadow_hb.py b/research/live/shadow_hb.py index 5a45359..f6f771e 100644 --- a/research/live/shadow_hb.py +++ b/research/live/shadow_hb.py @@ -825,10 +825,18 @@ async def main_async(workers: int, hours: float, pool) -> None: def main() -> None: + global SYMS ap = argparse.ArgumentParser() ap.add_argument("--hours", type=float, default=24.0) ap.add_argument("--workers", type=int, default=2) + # 币数直接决定排队:所有币在同一秒收盘,worker 少于币数就必然排队, + # 最后一个币的信号要等 ceil(n/worker) 轮计算。TRX 不在默认池里—— + # 实盘口径 208 天只有 5 笔,ATR 门控几乎全刷掉(HANDOFF §step48) + ap.add_argument("--syms", default=",".join(SYMS), + help="逗号分隔。十币池:BTC,ETH,SOL,BNB,XRP,DOGE,ADA," + "AVAX,LINK,LTC") a = ap.parse_args() + SYMS = tuple(s.strip().upper() for s in a.syms.split(",") if s.strip()) # 进程池必须在事件循环和任何 WS 连接之前建好:fork 一个已带活跃 socket # 的进程会把连接状态一起复制过去,后果不可预测 with ProcessPoolExecutor(max_workers=a.workers) as pool: diff --git a/research/live/shadow_report.py b/research/live/shadow_report.py index 97fd5da..14869a0 100644 --- a/research/live/shadow_report.py +++ b/research/live/shadow_report.py @@ -149,6 +149,36 @@ def book_quality(sig: pd.DataFrame, drf: pd.DataFrame) -> None: f"不改变曲线形状,但要确认没有异常长尾") +def tick_floor(drf: pd.DataFrame) -> None: + """标出哪些币的漂移只是 tick 量化的地板,不是真实漂移。 + + 中价的最小变动是半个 tick,所以 tick 相对价格粗的币(ADA 半 tick 就有 + 2.34bp、LTC 1.00bp),漂移中位会**精确等于半 tick** 且在所有延迟点上 + 完全相同。这很容易被读成「ADA 漂移 2.34bp」,实际是「测不到更细」。 + + 方向上是安全的——真实漂移只会更小,所以这些数是上界。但必须标出来, + 否则会拿一个测量地板去和预算做比较,然后误判某个币不可做。 + """ + if drf.empty or "mid" not in drf.columns: + return + print("\n ── tick 地板检查") + hit = False + for sym, g in drf.groupby("sym"): + mid = g["mid"].median() + # 同一币在各延迟点的漂移若几乎不变,就是被量化了 + by = g.groupby("delay_label")["drift_bp_long"].apply( + lambda x: x.abs().median()) + if len(by) < 3 or not np.isfinite(mid) or mid <= 0: + continue + spread = by.max() - by.min() + if spread < 0.02 and by.median() > 0.2: + hit = True + print(f" {sym:<6}漂移在各延迟点恒为 {by.median():.2f}bp" + f" → 半 tick 地板,真实漂移低于此值") + if not hit: + print(" 没有币落在 tick 地板上,漂移数值可直接读") + + def drift_split(sig: pd.DataFrame, drf: pd.DataFrame) -> None: """条件漂移 vs 无条件漂移。两者的差就是「系统性追价」的大小。""" print("\n\n########## 三、条件漂移 vs 无条件漂移 ##########") @@ -173,6 +203,7 @@ def drift_split(sig: pd.DataFrame, drf: pd.DataFrame) -> None: f"{cm - um:>9.2f}") if len(cond) and len(cond[cond["delay_label"] == "1.0s"]) < MIN_N: print(f"\n 条件侧样本不足 {MIN_N},差值还读不出方向") + tick_floor(drf) def slippage_curve(sig: pd.DataFrame) -> None: