"""对照实验:信号集对微小数据差异有多敏感。 venue_parity 量到 Bitget 与 Binance 的 1m 信号同根重合率只有 16%~41%, 而两家的 close 中位差仅 0.3bp、P95 约 2.5bp。在断言「换交易所会换掉一批信号」 之前,必须先排除另一种解释:**信号定义本身就对任何 2bp 级别的扰动极度敏感**。 这两种解释的后果完全不同: venue 差异 -> 换成 Bitget 自己的回测基线即可归因 内在脆弱 -> Binance 回测的那份逐笔清单根本不可复现,滑点无从对照 做法:拿 Binance 原始数据当基线,注入不同幅度的 iid 噪声后重跑同一管线, 看重合率随噪声幅度的衰减曲线。噪声 0 必须给出 100%,否则说明管线不确定。 顺带单独测一档「tick 粗化」:把 Bitget SOL 的 0.001 精度四舍五入到 Binance 的 0.01,看重合率是否回升——若回升,SOL 的低重合就主要是精度差异造成的。 输出 out/signal_sensitivity.csv。 """ from __future__ import annotations import argparse import os import sys import warnings from pathlib import Path import numpy as np import pandas as pd warnings.filterwarnings("ignore") for v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"): os.environ.setdefault(v, "1") HERE = Path(__file__).resolve().parent RESEARCH = HERE.parent sys.path.insert(0, str(RESEARCH)) sys.path.insert(0, str(RESEARCH.parent)) pd.set_option("display.width", 240) sys.path.insert(0, str(HERE)) from venue_parity import overlap, pipeline # noqa: E402 SYMS = ("BTC", "ETH", "SOL") LEVELS = (0.0, 0.25, 0.5, 1.0, 2.0, 4.0) # bp TICK = {"BTC": 0.1, "ETH": 0.01, "SOL": 0.01} def perturb(df: pd.DataFrame, bp: float, tick: float, seed: int) -> pd.DataFrame: """给 OHLC 各自注入 iid 噪声,再修复 high/low 的包含关系并按 tick 归整。""" if bp <= 0: return df out = df.copy() rng = np.random.default_rng(seed) sd = bp / 1e4 for c in ("open", "high", "low", "close"): v = out[c].to_numpy(dtype=float) out[c] = v * (1.0 + rng.normal(0.0, sd, size=len(v))) o, h, l, c = (out[x].to_numpy(dtype=float) for x in ("open", "high", "low", "close")) out["high"] = np.maximum.reduce([h, o, c]) out["low"] = np.minimum.reduce([l, o, c]) for x in ("open", "high", "low", "close"): out[x] = np.round(out[x] / tick) * tick return out def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--symbols", default="BTC,ETH,SOL") ap.add_argument("--seeds", type=int, default=2) ap.add_argument("--warmup", type=int, default=2000) args = ap.parse_args() from lib.data import load_local syms = [s.strip() for s in args.symbols.split(",")] period_ms = 60_000 print(f"[信号敏感性] {syms} · 噪声档 {LEVELS} bp · 每档 {args.seeds} 个种子\n", flush=True) rows = [] for sym in syms: # 与 venue_parity 用同一段窗口,便于两组数字直接对照 cache = HERE / "cache" / f"bitget_{sym}_1m_30d.feather" if not cache.exists(): print(f"{sym}: 缺 {cache.name},先跑 venue_parity.py") continue bg = pd.read_feather(cache) lo, hi = int(bg.timestamp.min()), int(bg.timestamp.max()) bn_l = load_local(f"{sym}/USDT:USDT", "1m") bn_h = load_local(f"{sym}/USDT:USDT", "5m") bn_l = bn_l[(bn_l.timestamp >= lo) & (bn_l.timestamp <= hi)].reset_index(drop=True) bn_h = bn_h[(bn_h.timestamp >= lo) & (bn_h.timestamp <= hi)].reset_index(drop=True) base = pipeline(bn_l, bn_h) cut = bn_l["timestamp"].to_numpy()[min(args.warmup, len(bn_l) - 1)] base = base[base.entry_ts >= cut] base_f = base[base["h1_agree"] == 1] print(f"── {sym} 基线 原始 {len(base)} 笔 / 过滤后 {len(base_f)} 笔", flush=True) for bp in LEVELS: n_seeds = 1 if bp == 0 else args.seeds acc = {"原始": [], "5m同向后": []} cnt = {"原始": [], "5m同向后": []} for k in range(n_seeds): pert = pipeline(perturb(bn_l, bp, TICK[sym], 1000 + k), bn_h) if pert.empty: continue pert = pert[pert.entry_ts >= cut] pert_f = pert[pert["h1_agree"] == 1] for tag, a, b in (("原始", base, pert), ("5m同向后", base_f, pert_f)): o = overlap(a, b, period_ms) acc[tag].append(o["同根"]) cnt[tag].append(o["b"]) for tag in ("原始", "5m同向后"): if not acc[tag]: continue rows.append({"品种": sym, "口径": tag, "噪声bp": bp, "基线笔数": len(base if tag == "原始" else base_f), "扰动后笔数": round(float(np.mean(cnt[tag])), 1), "同根重合": float(np.mean(acc[tag]))}) print(f" 噪声 {bp:>4.2f}bp: 原始 {np.mean(acc['原始']) * 100:5.1f}% · " f"过滤后 {np.mean(acc['5m同向后']) * 100:5.1f}%", flush=True) if not rows: print("无结果") return tb = pd.DataFrame(rows) print("\n" + "=" * 100) print("########## 噪声幅度 → 同根重合率 ##########") piv = tb[tb["口径"] == "5m同向后"].pivot_table( index="噪声bp", columns="品种", values="同根重合") print((piv * 100).round(1).to_string()) print(" 行是注入的 iid 噪声幅度(bp),值是与无噪声基线的同根重合率。") print("\n########## 与 venue_parity 的实测对照 ##########") print(" Bitget↔Binance 实测:close 中位差 0.05~0.33bp、P95 2.0~3.0bp,") print(" 过滤后同根重合 BTC 40.9% / ETH 25.0% / SOL 15.9%。") print(" 若上表在 1~2bp 档就掉到同一水平,说明主因是信号定义的内在脆弱,") print(" 而不是 Bitget 这家交易所特殊。") out = RESEARCH / "out" / "signal_sensitivity.csv" tb.to_csv(out, index=False) print(f"\n产物写入 {out}") if __name__ == "__main__": main()