"""Step 13:中枢突破的三关稳健性验证。 Step 12 发现中枢突破跨周期单调为正(PF 1.18/1.52/2.17),但样本偏小、 且切过多个维度,必须过三关才能当真: 关一 多品种 BTC / ETH / SOL —— 信号是否只存在于 BTC 关二 分时段 按年切 —— 是否只靠某一段行情 关三 参数面 sl/tp 网格 —— 是否只在某个参数点成立 任何一关塌掉,都说明 Step 12 是数据挖掘的产物。 """ from __future__ import annotations import argparse import sys import warnings from pathlib import Path import numpy as np import pandas as pd warnings.filterwarnings("ignore") sys.path.insert(0, str(Path(__file__).resolve().parent)) from lib.breakout import run_trades, summarize_trades from lib.data import fetch_ohlcv from lib.nested_level import build_htf_zones from step12_zs_breakout import collect_breakouts sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from chanlun import TF_DF pd.set_option("display.width", 260) SYMBOLS = ["BTC/USDT:USDT", "ETH/USDT:USDT", "SOL/USDT:USDT"] TFS = ["15m", "1h", "4h"] def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--sl", type=float, default=1.5) ap.add_argument("--tp", type=float, default=3.0) ap.add_argument("--max-bars", type=int, default=48) args = ap.parse_args() pool: list[pd.DataFrame] = [] print("########## 关一:多品种 × 多周期(突破即入,sl1.5 tp3)##########") rows = [] for sym in SYMBOLS: for tf in TFS: try: df = fetch_ohlcv(sym, tf, 10**9) except Exception: continue if df is None or len(df) < 2000: continue chan = TF_DF(df, 1, tf) cdf = chan.dataframe zones = build_htf_zones(df, tf) bo = collect_breakouts(cdf, zones) if bo.empty or len(bo) < 15: continue entries = list(zip(bo["bo_idx"].astype(int), bo["dir"].astype(int))) tr = run_trades(cdf, entries, args.sl, args.tp, args.max_bars) if tr.empty: continue s = summarize_trades(tr, f"{sym.split('/')[0]:>4} {tf:>3}") s["假突破率"] = f"{bo['is_fake'].mean() * 100:.0f}%" rows.append(s) tr = tr.copy() tr["symbol"] = sym.split("/")[0] tr["tf"] = tf tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()] pool.append(tr) print(pd.DataFrame(rows).to_string(index=False)) if not pool: print("无足够样本") return allt = pd.concat(pool, ignore_index=True) print(f"\n 合并 {len(allt)} 笔:", end="") r = allt["ret"].to_numpy() sd = r.std(ddof=1) win, loss = r[r > 0], r[r <= 0] print(f"胜率 {(r > 0).mean() * 100:.1f}% 均收益 {r.mean() * 100:+.3f}% " f"PF {win.sum() / abs(loss.sum()):.2f} t值 {r.mean() / (sd / np.sqrt(len(r))):+.2f}") print("\n########## 关二:按年分段(合并全部品种周期)##########") allt["year"] = pd.to_datetime(allt["date"]).dt.year rows = [] for y, g in allt.groupby("year"): if len(g) < 25: continue s = summarize_trades(g, str(y)) rows.append(s) print(pd.DataFrame(rows).to_string(index=False)) pos_years = sum(1 for _, g in allt.groupby("year") if len(g) >= 25 and g["ret"].mean() > 0) tot_years = sum(1 for _, g in allt.groupby("year") if len(g) >= 25) print(f"\n 盈利年份 {pos_years}/{tot_years}") print("\n########## 关三:参数网格(合并全部品种周期,1h 为主)##########") rows = [] for sl in (1.0, 1.5, 2.0, 2.5): for tp in (2.0, 3.0, 4.0): sub = [] for sym in SYMBOLS: for tf in TFS: try: df = fetch_ohlcv(sym, tf, 10**9) except Exception: continue if df is None or len(df) < 2000: continue chan = TF_DF(df, 1, tf) cdf = chan.dataframe bo = collect_breakouts(cdf, build_htf_zones(df, tf)) if bo.empty: continue e = list(zip(bo["bo_idx"].astype(int), bo["dir"].astype(int))) t = run_trades(cdf, e, sl, tp, args.max_bars) if not t.empty: sub.append(t) if sub: rows.append(summarize_trades(pd.concat(sub, ignore_index=True), f"sl{sl} tp{tp}")) grid = pd.DataFrame(rows) print(grid.to_string(index=False)) pos = sum(1 for v in grid["均收益"] if v.startswith("+")) print(f"\n 正收益参数点 {pos}/{len(grid)}") print("\n########## 补充:合并样本的方向与周期拆分 ##########") rows = [] for d, nm in [(1, "向上突破"), (-1, "向下突破")]: g = allt[allt["direction"] == d] if len(g) >= 25: rows.append(summarize_trades(g, nm)) for tf in TFS: g = allt[allt["tf"] == tf] if len(g) >= 25: rows.append(summarize_trades(g, f"周期 {tf}")) print(pd.DataFrame(rows).to_string(index=False)) out = Path(__file__).parent / "out" / "step13_all_trades.csv" out.parent.mkdir(exist_ok=True) allt.to_csv(out, index=False) print(f"\n明细已写入 {out}") if __name__ == "__main__": main()