"""Step 28:小级别 × 大级别的全网格扫描。 此前的配对(15m配1h、30m配2h)是拍脑袋定的,而 step26 意外发现 5m 配 1h 明显好过配 15m —— 说明「大级别该拉多远」本身是个未扫过的维度。 关键省算:大级别只充当过滤标记,不改变入场点,所以每个小级别的 中枢/信号/回测只跑一次,然后把所有候选大级别一次性挂上去。 """ 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 LTF_HTFS = { "5m": ["15m", "30m", "1h", "2h", "4h"], "15m": ["30m", "1h", "2h", "4h", "1d"], "30m": ["1h", "2h", "4h", "1d"], "1h": ["2h", "4h", "1d", "1w"], } 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 htfs = LTF_HTFS[ltf] try: df_l = fetch_ohlcv(f"{sym}/USDT:USDT", 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 sig = find_fast_bsp3(cdf, zones) if sig.empty or len(sig) < 10: return None z = zones.copy() pg, pdn = z["zg"].shift(), z["zd"].shift() z["z_above"] = z["zd"] > pg z["z_below"] = z["zg"] < pdn z["zone_i"] = np.arange(len(z)) sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left") # 所有候选大级别一次挂完,前缀就用周期名 for tf in htfs: df_h = fetch_ohlcv(f"{sym}/USDT:USDT", tf, 10**9) if df_h is None or len(df_h) < 200: continue chan_h = TF_DF(df_h, 1, tf) s = signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe)) if s.empty: continue sig = attach_htf_context(sig, cdf, htf_fx_timeline(s, chan_h.dataframe), f"p{tf}") 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: return None m = sig.drop_duplicates("entry_idx").set_index("entry_idx") tr["symbol"], tr["ltf"] = sym, ltf tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()] for c in ["z_above", "z_below"] + [f"p{tf}_agree" for tf in htfs]: tr[c] = tr["entry_idx"].map(m[c]) if c in m.columns else np.nan return {"task": f"{sym} {ltf}", "trades": tr} except Exception as e: return {"task": f"{sym} {ltf}", "error": repr(e)[:200]} def stat(r: np.ndarray) -> dict | None: if len(r) < 40: return None w, o = r[r > 0], r[r <= 0] sd = r.std(ddof=1) return { "笔数": len(r), "胜率": (r > 0).mean() * 100, "均收益": r.mean() * 100, "中位": np.median(r) * 100, "PF": w.sum() / abs(o.sum()) if len(o) else np.inf, "偏度": pd.Series(r).skew(), "t值": r.mean() / (sd / np.sqrt(len(r))), } def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--symbols", default="BTC,ETH,SOL") ap.add_argument("--ltfs", default="5m,15m,30m,1h") ap.add_argument("--workers", type=int, default=6) ap.add_argument("--reuse", action="store_true") args = ap.parse_args() cache = HERE / "out" / "step28_grid.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 args.ltfs.split(",") for s in args.symbols.split(",")] print(f"[网格] {len(tasks)} 个任务,每个内部挂多个大级别\n", flush=True) res = [] with ProcessPoolExecutor(max_workers=args.workers) 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']} — {len(r['trades'])} 笔", 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["ret_net"] = allt["gross"] - FEE - SLIP allt["push"] = np.where(allt["direction"] == 1, allt["z_above"], allt["z_below"]).astype(bool) print("=" * 120) print("########## 1. 全网格:小级别 × 大级别(仅大级别同向)##########") rows = [] for ltf, g in allt.groupby("ltf"): for tf in LTF_HTFS.get(ltf, []): col = f"p{tf}_agree" if col not in g.columns: continue s = stat(g[g[col] == 1]["ret_net"].to_numpy()) if s: rows.append({"小级别": ltf, "大级别": tf, **s}) df = pd.DataFrame(rows) if df.empty: print("样本不足") return for c, f in (("胜率", "{:.1f}%"), ("均收益", "{:+.3f}%"), ("中位", "{:+.3f}%"), ("PF", "{:.2f}"), ("偏度", "{:.2f}"), ("t值", "{:+.2f}")): df[c] = df[c].map(f.format) print(df.to_string(index=False)) print("\n########## 2. 加上顺向推进过滤后的全网格 ##########") rows = [] for ltf, g in allt[allt.push].groupby("ltf"): for tf in LTF_HTFS.get(ltf, []): col = f"p{tf}_agree" if col not in g.columns: continue s = stat(g[g[col] == 1]["ret_net"].to_numpy()) if s: rows.append({"小级别": ltf, "大级别": tf, **s}) df2 = pd.DataFrame(rows) if not df2.empty: best = df2.sort_values("PF", ascending=False).head(3) for c, f in (("胜率", "{:.1f}%"), ("均收益", "{:+.3f}%"), ("中位", "{:+.3f}%"), ("PF", "{:.2f}"), ("偏度", "{:.2f}"), ("t值", "{:+.2f}")): df2[c] = df2[c].map(f.format) print(df2.to_string(index=False)) print(f"\n PF 前三:{', '.join(f'{r.小级别}/{r.大级别}' for r in best.itertuples())}") print("\n########## 3. 每个小级别的最优大级别(按 t 值,中位需为正)##########") rows = [] for ltf, g in allt[allt.push].groupby("ltf"): cand = [] for tf in LTF_HTFS.get(ltf, []): col = f"p{tf}_agree" if col not in g.columns: continue s = stat(g[g[col] == 1]["ret_net"].to_numpy()) if s and s["中位"] > 0: cand.append((tf, s)) if cand: tf, s = max(cand, key=lambda x: x[1]["t值"]) rows.append({"小级别": ltf, "最优大级别": tf, "笔数": s["笔数"], "胜率": f"{s['胜率']:.1f}%", "中位": f"{s['中位']:+.3f}%", "PF": f"{s['PF']:.2f}", "t值": f"{s['t值']:+.2f}"}) if rows: print(pd.DataFrame(rows).to_string(index=False)) print("\n########## 4. 最优配对的组合资金曲线 ##########") picks = {r["小级别"]: r["最优大级别"] for r in rows} if rows else {} if picks: parts = [] for ltf, tf in picks.items(): g = allt[(allt.ltf == ltf) & allt.push & (allt[f"p{tf}_agree"] == 1)] parts.append(g) g = pd.concat(parts).sort_values("date") r = g["ret_net"].to_numpy() 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" 配对 {picks}") print(f" n={len(r)} 年{len(r) / yrs:.0f}笔 " f"年化 {(eq[-1] ** (1 / yrs) - 1) * 100:+.1f}% 回撤 {dd * 100:.1f}% " f"Sharpe {pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(pnl) / yrs):.2f} " f"中位 {np.median(r) * 100:+.3f}%") gg = g.copy() gg["y"] = gg["date"].dt.year yr = [(y, (x["ret_net"] > 0).mean() * 100, x["ret_net"].mean() * 100, len(x)) for y, x in gg.groupby("y") if len(x) >= 20] print(" 分年:" + " ".join(f"{y}:{m:+.2f}%({n})" for y, _, m, n in yr)) if __name__ == "__main__": main()