"""Step 29:信号为什么这么少——中枢到成交的漏斗,以及放宽参数能救回多少。 30m/2h 六年三个币只有 177 笔,年均 26 笔。先定位卡在哪一步, 再对回抽容差、回抽窗口、扫描窗口做敏感性测试,看样本量与质量的取舍。 """ from __future__ import annotations import argparse import os import sys import warnings from concurrent.futures import ProcessPoolExecutor, as_completed 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 sys.path.insert(0, str(HERE)) sys.path.insert(0, str(HERE.parent)) pd.set_option("display.width", 320) SL, TP, MAXB = 1.5, 3.0, 48 FEE, SLIP = 0.0004, 0.0001 BEST = {"5m": "30m", "15m": "1h", "30m": "2h"} # (扫描窗口, 回抽窗口, 回抽容差) GRID = [ (200, 30, 0.003), # 现状 (200, 30, 0.010), (200, 60, 0.003), (200, 60, 0.010), (400, 60, 0.010), (400, 90, 0.020), (600, 120, 0.030), ] def run_one(task: tuple) -> dict | None: import warnings as _w _w.filterwarnings("ignore") sys.path.insert(0, str(HERE)) sys.path.insert(0, str(HERE.parent)) from chanlun import TF_DF from lib.breakout import run_trades from lib.data import fetch_ohlcv from lib.fast_bsp3 import find_fast_bsp3 from lib.fx_signal import extract_fx_signals, signals_to_frame from lib.nested_bsp import attach_htf_context, htf_fx_timeline from lib.nested_level import build_htf_zones sym, ltf = task htf = BEST[ltf] pair = f"{sym}/USDT:USDT" try: df_l = fetch_ohlcv(pair, ltf, 10**9) if df_l is None or len(df_l) < 3000: return None chan_l = TF_DF(df_l, 1, ltf) cdf = chan_l.dataframe zones = build_htf_zones(cdf, ltf, chan=chan_l).reset_index(drop=True) if zones.empty: return None z = zones.copy() pg, pdn = z["zg"].shift(), z["zd"].shift() z["z_above"], z["z_below"] = z["zd"] > pg, z["zg"] < pdn z["zone_i"] = np.arange(len(z)) df_h = fetch_ohlcv(pair, htf, 10**9) if df_h is None: return None chan_h = TF_DF(df_h, 1, htf) s = signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe)) tl = htf_fx_timeline(s, chan_h.dataframe) diag0: dict = {} frames = [] for scan, pw, tol in GRID: d = diag0 if (scan, pw, tol) == GRID[0] else None sig = find_fast_bsp3(cdf, zones, scan=scan, pullback_win=pw, tol=tol, diag=d) if sig.empty: continue sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left") sig = attach_htf_context(sig, cdf, tl, "h1") entries = list(zip(sig["entry_idx"].astype(int), sig["direction"].astype(int))) tr = run_trades(cdf, entries, SL, TP, MAXB, fee=0.0, entry_delay=1) if tr.empty: continue m = sig.drop_duplicates("entry_idx").set_index("entry_idx") tr["symbol"], tr["ltf"] = sym, ltf tr["cfg"] = f"scan{scan}/pb{pw}/tol{tol * 100:g}%" tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()] for c in ("h1_agree", "z_above", "z_below", "direction"): tr[c if c != "direction" else "dir_sig"] = tr["entry_idx"].map(m[c]) frames.append(tr) if not frames: return None return {"task": f"{sym} {ltf}", "diag": diag0, "trades": pd.concat(frames, ignore_index=True)} except Exception as e: return {"task": f"{sym} {ltf}", "error": repr(e)[:250]} def stat(r: np.ndarray) -> dict: if len(r) < 30: return {} w, o = r[r > 0], r[r <= 0] sd = r.std(ddof=1) return {"笔数": len(r), "胜率": round((r > 0).mean() * 100, 1), "中位": round(np.median(r) * 100, 2), "PF": round(w.sum() / abs(o.sum()), 2) if len(o) else np.inf, "偏度": round(pd.Series(r).skew(), 2), "t值": round(r.mean() / (sd / np.sqrt(len(r))), 2)} def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--symbols", default="BTC,ETH,SOL") ap.add_argument("--reuse", action="store_true") args = ap.parse_args() cache = HERE / "out" / "step29_funnel.csv" dcache = HERE / "out" / "step29_diag.csv" if args.reuse and cache.exists(): allt = pd.read_csv(cache, parse_dates=["date"]) diag = pd.read_csv(dcache) if dcache.exists() else pd.DataFrame() print(f"[复用] {len(allt)} 笔\n") else: syms = [s.strip() for s in args.symbols.split(",")] tasks = [(s, l) for l in BEST for s in syms] print(f"[漏斗] {len(tasks)} 个任务 × {len(GRID)} 组参数\n", flush=True) res = [] with ProcessPoolExecutor(max_workers=6) as ex: futs = {ex.submit(run_one, t): t for t in tasks} for i, f in enumerate(as_completed(futs), 1): r = f.result() if r is None or "error" in (r or {}): print(f" [{i}] 跳过 {(r or {}).get('error', '')}", flush=True) continue res.append(r) print(f" [{i}/{len(tasks)}] {r['task']}", flush=True) if not res: return allt = pd.concat([r["trades"] for r in res], ignore_index=True) allt.to_csv(cache, index=False) diag = pd.DataFrame([{"task": r["task"], **r["diag"]} for r in res if r.get("diag")]) diag.to_csv(dcache, index=False) allt["date"] = pd.to_datetime(allt["date"]) allt["r"] = allt["gross"] - FEE - SLIP allt["push"] = np.where(allt["dir_sig"] == 1, allt["z_above"], allt["z_below"]) allt["push"] = allt["push"].fillna(False).astype(bool) base = allt["cfg"] == f"scan200/pb30/tol0.3%" if not diag.empty: print("=" * 118) print("########## 1. 现状参数下,每个中枢卡在哪一步 ##########") diag["级别"] = diag["task"].str.split().str[1] cols = [c for c in diag.columns if c.startswith(("×", "√", "中枢"))] g = diag.groupby("级别")[cols].sum() tot = g["中枢总数"] out = g.copy() for c in cols: if c != "中枢总数": out[c] = g[c].astype(int).astype(str) + " (" + \ (g[c] / tot * 100).round(0).astype(int).astype(str) + "%)" print(out.to_string()) print(" 成交率 = √成交 / 中枢总数。占比最大的那一项就是瓶颈。") print("\n########## 2. 放宽参数:样本量 vs 质量 ##########") for ltf, g in allt.groupby("ltf"): rows = [] for cfg, x in g.groupby("cfg"): sub = x[(x["h1_agree"] == 1) & x["push"]] s = stat(sub["r"].to_numpy()) if s: rows.append({"级别": ltf, "参数": cfg, **s}) if rows: df = pd.DataFrame(rows) df["_k"] = df["参数"].map({f"scan{a}/pb{b}/tol{c * 100:g}%": i for i, (a, b, c) in enumerate(GRID)}) print(df.sort_values("_k").drop(columns="_k").to_string(index=False)) print("\n########## 3. 最宽参数下的年频率变化 ##########") rows = [] for (ltf, cfg), g in allt.groupby(["ltf", "cfg"]): sub = g[(g["h1_agree"] == 1) & g["push"]] if len(sub) < 30: continue yrs = (sub["date"].max() - sub["date"].min()).days / 365.25 r = sub["r"].to_numpy() lev = np.clip(0.01 / np.clip(sub["risk_pct"].to_numpy(), 0.002, None), 0, 20) pnl = r * lev eq = np.cumprod(1 + pnl) rows.append({"级别": ltf, "参数": cfg, "笔数": len(r), "年笔数": round(len(r) / yrs), "PF": round( r[r > 0].sum() / abs(r[r <= 0].sum()), 2), "年化": f"{(eq[-1] ** (1 / yrs) - 1) * 100:+.0f}%", "回撤": f"{(1 - eq / np.maximum.accumulate(eq)).max() * 100:.1f}%", "Sharpe": round(pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(r) / yrs), 2)}) print(pd.DataFrame(rows).to_string(index=False)) if __name__ == "__main__": main()