"""Step 4:中枢来源 A/B —— cal_bi_zs(seg_list) vs cal_bi_zs_list_pure(bi_list)。 假设:seg 口径下笔中枢必须等所属线段成形,多叠了一层确认滞后; pure 口径直接在扁平笔序列上滚动,应当显著更快出信号。 用同一套 walk-forward 重放,只切换中枢来源,对比滞后与真实收益。 """ from __future__ import annotations import argparse import sys from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parent)) from lib.bsp_eval import baseline_stats, run_pipeline from lib.data import fetch_ohlcv from lib.walkforward import analyze_stability, replay pd.set_option("display.width", 240) from chanlun.core.ChanEnum import Chan_BSP_DIR HORIZONS = (1, 3, 5, 10, 20, 40) def evaluate(df: pd.DataFrame, tf: str, symbol: str, zs_source: str, window: int, step: int): _, final_bsp = run_pipeline(df, tf, zs_source=zs_source) final_keys = { (str(b.type).replace("Chan_BSP_TYPE.", ""), 1 if b.dir == Chan_BSP_DIR.BUY else -1, str(b.end_time)) for b in final_bsp if b.is_sure and b.sure_time is not None } result = replay( df, tf, window=window, step=step, zs_source=zs_source, cache_key=f"{symbol.replace('/', '_').replace(':', '-')}_{tf}", ) life = analyze_stability(result, final_keys, df, window=window) valid = life[~life["truncated"]].dropna(subset=["fx_idx"]).copy() valid["fx_idx"] = valid["fx_idx"].astype(int) mature = valid[~valid["immature"]] return final_keys, valid, mature def rt_returns(valid: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame: """以实时首见时刻入场的方向调整收益。""" closes = df["close"].to_numpy(dtype=float) n = len(df) rows = [] for _, r in valid.iterrows(): i, d = int(r["first_seen_idx"]), int(r["direction"]) row = {"bsp_type": r["bsp_type"], "direction": d} for h in HORIZONS: j = i + h row[f"ret_{h}"] = d * (closes[j] - closes[i]) / closes[i] if j < n else np.nan rows.append(row) return pd.DataFrame(rows) def stats_block(fwd: pd.DataFrame, df: pd.DataFrame, mask=None, label="") -> list[dict]: base = baseline_stats(df, HORIZONS).set_index("horizon") g = fwd if mask is None else fwd[mask] out = [] for h in HORIZONS: r = g[f"ret_{h}"].dropna().to_numpy() if len(r) == 0: continue dirs = g.loc[g[f"ret_{h}"].notna(), "direction"].to_numpy() sd = r.std(ddof=1) if len(r) > 1 else np.nan out.append({ "group": label, "horizon": h, "n": len(r), "mean": r.mean(), "winrate": (r > 0).mean(), "excess": r.mean() - float(np.mean(dirs) * base.loc[h, "base_mean_long"]), "tstat": r.mean() / (sd / np.sqrt(len(r))) if sd else np.nan, }) return out def fmt(rows: list[dict]) -> str: d = pd.DataFrame(rows) if d.empty: return "(无数据)" d["mean"] = d["mean"].map(lambda v: f"{v * 100:+.2f}%") d["excess"] = d["excess"].map(lambda v: f"{v * 100:+.2f}%") d["winrate"] = d["winrate"].map(lambda v: f"{v * 100:.0f}%") d["tstat"] = d["tstat"].map(lambda v: f"{v:+.2f}") return d.to_string(index=False) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--symbol", default="BTC/USDT:USDT") ap.add_argument("--tf", default="1h") ap.add_argument("--window", type=int, default=3000) ap.add_argument("--step", type=int, default=4) args = ap.parse_args() df = fetch_ohlcv(args.symbol, args.tf, 50000) print(f"[data] {args.symbol} {args.tf} rows={len(df)} " f"{df['date'].iloc[0]} -> {df['date'].iloc[-1]}\n") store = {} for src in ("seg", "pure"): print(f"===== 中枢来源: {src} =====") final_keys, valid, mature = evaluate(df, args.tf, args.symbol, src, args.window, args.step) fwd = rt_returns(valid, df) store[src] = (final_keys, valid, mature, fwd) print(f" 全量口径买卖点 = {len(final_keys)}") print(f" 重放有效信号 = {len(valid)}(已成熟 {len(mature)})") print(f" 实时首见滞后 = 中位数 {valid['observed_lag'].median():.0f} 根 / " f"均值 {valid['observed_lag'].mean():.1f} 根") print(f" 幻影率 = {(~mature['in_final']).mean() * 100:.1f}%") print(f" 存活度 = {mature['persist_ratio'].mean():.3f}\n") print("\n########## 滞后对比 ##########") for src in ("seg", "pure"): v = store[src][1] q = v["observed_lag"].quantile([0.25, 0.5, 0.75]) print(f" {src:5s} P25={q[0.25]:5.0f} 中位数={q[0.5]:5.0f} P75={q[0.75]:5.0f} n={len(v)}") print("\n########## 分类型滞后中位数 ##########") comp = pd.DataFrame({ src: store[src][1].groupby("bsp_type")["observed_lag"].median() for src in ("seg", "pure") }) comp["改善(根)"] = comp["seg"] - comp["pure"] print(comp.to_string()) print("\n########## 分类型信号数量与幻影率 ##########") cnt = pd.DataFrame({ f"{src}_n": store[src][1].groupby("bsp_type").size() for src in ("seg", "pure") }) ph = pd.DataFrame({ f"{src}_幻影": store[src][2].groupby("bsp_type")["in_final"].apply(lambda s: (1 - s.mean()) * 100) for src in ("seg", "pure") }) print(pd.concat([cnt, ph], axis=1).round(1).to_string()) print("\n########## 无未来函数收益:全部信号 ##########") for src in ("seg", "pure"): print(f"--- {src} ---") print(fmt(stats_block(store[src][3], df, None, "ALL"))) print("\n########## 无未来函数收益:仅做多 ##########") for src in ("seg", "pure"): fwd = store[src][3] print(f"--- {src} ---") print(fmt(stats_block(fwd, df, fwd.direction == 1, "LONG"))) print("\n########## 无未来函数收益:仅做空 ##########") for src in ("seg", "pure"): fwd = store[src][3] print(f"--- {src} ---") print(fmt(stats_block(fwd, df, fwd.direction == -1, "SHORT"))) print("\n########## pure 口径分类型(持有 5 / 20 根)##########") fwd = store["pure"][3] rows = [] for t in sorted(fwd["bsp_type"].unique()): rows += stats_block(fwd, df, fwd.bsp_type == t, t) d = pd.DataFrame(rows) d = d[d["horizon"].isin([5, 20])] print(fmt(d.to_dict("records"))) if __name__ == "__main__": main()