币池可配,实测十币的排队代价;标出漂移的 tick 地板

加 --syms(start.sh 传 SYMS),默认仍是三币。TRX 不进十币池:实盘口径 208 天
只有 5 笔,ATR 门控几乎全刷掉。

十币实测(对比三币):

  queue_ms        2 → 169ms(P90 519ms,每刻最大排队中位 533ms)
  inner_ms      144 → 196ms(CPU 争抢也拖慢了纯计算)
  lag_signal_ms 621 → 958ms;同一收盘时刻最后算完的币中位 1376ms

排队从可忽略变成主项,与「10 币 × 196ms ÷ 2 核 ≈ 640ms 突发」吻合。这也是
之前「加核没用」那个结论唯一会翻转的场景:CPU 占用率只有约 3%,问题纯粹是
所有币同一秒收盘的突发,加核压的是并行度而非单币耗时。

但 800ms 哨兵不受影响——它喂的是 t_data − kline_ts(数据腿),十币下逐币
154~532ms 全在线内。加币不碰那道闸。

另外发现一个会被静默误读的东西:ADA/AVAX/DOGE/LINK/LTC 的漂移中位精确等于
半个 tick 且在四个延迟点上完全相同。那不是漂移,是中价的最小变动量——ADA 半
tick 就有 2.34bp。拿这个数去比预算会误判某币不可做。shadow_report 加了
tick_floor() 标注;方向上安全(真实漂移只会更小,这些是上界)。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jack
2026-08-28 04:50:09 +08:00
co-authored by Cursor
parent 7c13781f90
commit ef2a85dd8c
3 changed files with 44 additions and 1 deletions
+5 -1
View File
@@ -17,6 +17,10 @@ NAME="${NAME:-shadow}"
# verify_lean_parity.py 在影子这条路径上逐根验过(180 窗口 / 90 命中零分歧)。 # verify_lean_parity.py 在影子这条路径上逐根验过(180 窗口 / 90 命中零分歧)。
# 设 0 可退回 full,用来复量两模式的耗时差。 # 设 0 可退回 full,用来复量两模式的耗时差。
SHADOW_LEAN="${SHADOW_LEAN:-1}" SHADOW_LEAN="${SHADOW_LEAN:-1}"
# 币池。默认三个流动性最好的做滑点测量;十币池是实际要交易的那批(TRX 剔除,
# ATR 门控几乎全刷掉)。币数直接决定排队:所有币同一秒收盘,2 核上 10 个币
# 需要约 640ms 墙钟才算完,最后一个币的信号会落在 800ms 哨兵线之外。
SYMS="${SYMS:-BTC,ETH,SOL}"
IMAGE="${SHADOW_IMAGE:-hummingbot/hummingbot:latest}" IMAGE="${SHADOW_IMAGE:-hummingbot/hummingbot:latest}"
HOURS="${HOURS:-168}" HOURS="${HOURS:-168}"
WORKERS="${WORKERS:-2}" WORKERS="${WORKERS:-2}"
@@ -108,7 +112,7 @@ docker run -d --name "$NAME" -w /home/hummingbot \
-v "$OUT:/out" \ -v "$OUT:/out" \
--entrypoint /opt/conda/envs/hummingbot/bin/python \ --entrypoint /opt/conda/envs/hummingbot/bin/python \
"$IMAGE" /repo/research/live/shadow_hb.py \ "$IMAGE" /repo/research/live/shadow_hb.py \
--hours "$HOURS" --workers "$WORKERS" >/dev/null --hours "$HOURS" --workers "$WORKERS" --syms "$SYMS" >/dev/null
echo "已启动。等启动自检(补丁断言 + 历史回填,约 60 秒)…" echo "已启动。等启动自检(补丁断言 + 历史回填,约 60 秒)…"
sleep 45 sleep 45
+8
View File
@@ -825,10 +825,18 @@ async def main_async(workers: int, hours: float, pool) -> None:
def main() -> None: def main() -> None:
global SYMS
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--hours", type=float, default=24.0) ap.add_argument("--hours", type=float, default=24.0)
ap.add_argument("--workers", type=int, default=2) 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() a = ap.parse_args()
SYMS = tuple(s.strip().upper() for s in a.syms.split(",") if s.strip())
# 进程池必须在事件循环和任何 WS 连接之前建好:fork 一个已带活跃 socket # 进程池必须在事件循环和任何 WS 连接之前建好:fork 一个已带活跃 socket
# 的进程会把连接状态一起复制过去,后果不可预测 # 的进程会把连接状态一起复制过去,后果不可预测
with ProcessPoolExecutor(max_workers=a.workers) as pool: with ProcessPoolExecutor(max_workers=a.workers) as pool:
+31
View File
@@ -149,6 +149,36 @@ def book_quality(sig: pd.DataFrame, drf: pd.DataFrame) -> None:
f"不改变曲线形状,但要确认没有异常长尾") 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: def drift_split(sig: pd.DataFrame, drf: pd.DataFrame) -> None:
"""条件漂移 vs 无条件漂移。两者的差就是「系统性追价」的大小。""" """条件漂移 vs 无条件漂移。两者的差就是「系统性追价」的大小。"""
print("\n\n########## 三、条件漂移 vs 无条件漂移 ##########") print("\n\n########## 三、条件漂移 vs 无条件漂移 ##########")
@@ -173,6 +203,7 @@ def drift_split(sig: pd.DataFrame, drf: pd.DataFrame) -> None:
f"{cm - um:>9.2f}") f"{cm - um:>9.2f}")
if len(cond) and len(cond[cond["delay_label"] == "1.0s"]) < MIN_N: if len(cond) and len(cond[cond["delay_label"] == "1.0s"]) < MIN_N:
print(f"\n 条件侧样本不足 {MIN_N},差值还读不出方向") print(f"\n 条件侧样本不足 {MIN_N},差值还读不出方向")
tick_floor(drf)
def slippage_curve(sig: pd.DataFrame) -> None: def slippage_curve(sig: pd.DataFrame) -> None: