"""Step 33:去掉「回抽必须触及中枢边界」这一条,在最优配置下完整验证。 step32 的消融实验里,不要求回抽触及边界的档位(C)信号多 43%、PF 持平、 中位数几乎翻倍。原因是它把「突破后一去不回头」的强势段也收了进来。 本步在三个最优级别对上、叠加大级别同向与中枢阶梯过滤后完整对比, 并检查稳健性(分年、分品种、尾部),确认不是样本波动。 """ 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"} 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 or len(df_h) < 300: 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) out = [] for name, req in (("要求回抽触边界(现用)", True), ("不要求回抽触边界", False)): sig = find_fast_bsp3(cdf, zones, require_touch=req) if sig.empty or len(sig) < 20: continue sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left") sig = attach_htf_context(sig, cdf, tl, "h1") sig = sig.drop_duplicates("entry_idx") 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.set_index("entry_idx") tr["mode"], tr["symbol"], tr["ltf"] = name, sym, ltf tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()] for c in ("h1_agree", "z_above", "z_below", "direction", "lag", "depth"): if c in m.columns: tr[c if c != "direction" else "dir_sig"] = tr["entry_idx"].map(m[c]) out.append(tr) if not out: return None return {"task": f"{sym} {ltf}", "trades": pd.concat(out, ignore_index=True)} except Exception as e: return {"task": f"{sym} {ltf}", "error": repr(e)[:250]} def stat(g: pd.DataFrame, label: str, minn: int = 25) -> dict: r = g["gross"].to_numpy() - FEE - SLIP if len(r) < minn: return {} w, o = r[r > 0], r[r <= 0] sd = r.std(ddof=1) t10 = r[r <= np.quantile(r, 0.90)] return {"分组": label, "笔数": len(r), "滞后": f"{g['lag'].mean():.1f}" if "lag" in g else "—", "胜率": f"{(r > 0).mean() * 100:.1f}%", "均收益": f"{r.mean() * 100:+.3f}%", "中位": f"{np.median(r) * 100:+.3f}%", "PF": f"{w.sum() / abs(o.sum()):.2f}" if len(o) else "inf", "偏度": f"{pd.Series(r).skew():.2f}", "t值": f"{r.mean() / (sd / np.sqrt(len(r))):+.2f}", "剔10%PF": f"{t10[t10 > 0].sum() / abs(t10[t10 <= 0].sum()):.2f}"} def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--reuse", action="store_true") args = ap.parse_args() cache = HERE / "out" / "step33_notouch.csv" if args.reuse and cache.exists(): allt = pd.read_csv(cache, parse_dates=["date"]) print(f"[复用] {len(allt)} 笔\n") else: tasks = [(s, l) for l in BEST for s in ("BTC", "ETH", "SOL")] print(f"[验证] {len(tasks)} 个任务\n", flush=True) res = [] with ProcessPoolExecutor(max_workers=5) 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) allt["date"] = pd.to_datetime(allt["date"]) allt["push"] = np.where(allt["dir_sig"] == 1, allt["z_above"], allt["z_below"]) allt["push"] = allt["push"].fillna(False).astype(bool) fin = allt[(allt["h1_agree"] == 1) & allt["push"]] print("=" * 118) print("########## 1. 最终配置对比(同向+阶梯,全级别合并)##########") print(pd.DataFrame([r for r in [stat(g, m) for m, g in fin.groupby("mode")] if r] ).to_string(index=False)) print("\n########## 2. 分级别 ##########") for ltf, g in fin.groupby("ltf"): rows = [stat(x, f"{ltf} {m}") for m, x in g.groupby("mode")] print(pd.DataFrame([r for r in rows if r]).to_string(index=False)) print("\n########## 3. 分年(看是否某一年独大)##########") for m, g in fin.groupby("mode"): g = g.copy() g["y"] = g["date"].dt.year rows = [stat(x, str(y), minn=20) for y, x in g.groupby("y")] rows = [r for r in rows if r] if rows: print(f"-- {m} --") print(pd.DataFrame(rows).to_string(index=False)) print("\n########## 4. 分品种 ##########") for m, g in fin.groupby("mode"): rows = [stat(x, s, minn=20) for s, x in g.groupby("symbol")] rows = [r for r in rows if r] if rows: print(f"-- {m} --") print(pd.DataFrame(rows).to_string(index=False)) print("\n########## 5. 资金曲线 ##########") for m, g in fin.groupby("mode"): g = g.sort_values("date") r = g["gross"].to_numpy() - FEE - SLIP lev = np.clip(0.01 / np.clip(g["risk_pct"].to_numpy(), 0.002, None), 0, 20) pnl = r * lev eq = np.cumprod(1 + pnl) yrs = (g["date"].max() - g["date"].min()).days / 365.25 dd = (1 - eq / np.maximum.accumulate(eq)).max() print(f" {m:>18}: n={len(r):>4} 年{len(r) / yrs:>3.0f}笔 " f"年化 {(eq[-1] ** (1 / yrs) - 1) * 100:+7.1f}% 回撤 {dd * 100:5.1f}% " f"Sharpe {pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(pnl) / yrs):5.2f}") if __name__ == "__main__": main()