"""Step 11:突破跟随(分型失败 / 中枢突破),叠加已验证的波动率信号。 前十步确立:方向不可预测(AUC 0.52),但大波动可预测(AUC 0.689)。 反转逻辑已被否定,本步测其镜像——分型转折失败即为趋势延续, 以及缠论正统的「离开中枢做趋势」。 """ 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 find_breakout_entries, run_trades, summarize_trades from lib.data import fetch_ohlcv from lib.fx_signal import add_forward_returns, extract_fx_signals, signals_to_frame from lib.nested_level import annotate_position, build_htf_zones from step9_endpoint_predictability import build_features from step10_direct_return_label import ALL_FEATS, walk_forward sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from chanlun import TF_DF pd.set_option("display.width", 260) HORIZONS = (3, 5, 10, 20) def zs_breakout_entries(df: pd.DataFrame, zones: pd.DataFrame, window: int = 200): """中枢突破:价格自内部向外突破 zg/zd 的第一根。""" if zones.empty: return [] ts = df["timestamp"].to_numpy() close = df["close"].to_numpy(dtype=float) n = len(df) entries = [] for _, z in zones.iterrows(): start = int(np.searchsorted(ts, z["available_ts"], side="left")) was_inside = False for j in range(start, min(start + window, n)): c = close[j] if z["zd"] <= c <= z["zg"]: was_inside = True continue if not was_inside: continue entries.append((j, 1 if c > z["zg"] else -1)) break return entries def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--symbol", default="BTC/USDT:USDT") ap.add_argument("--tf", default="1h") ap.add_argument("--htf", default="4h") ap.add_argument("--window", type=int, default=10, help="分型后等待突破的K线数") args = ap.parse_args() df = fetch_ohlcv(args.symbol, args.tf, 10**9) chan = TF_DF(df, 1, args.tf) cdf = chan.dataframe sig = signals_to_frame(extract_fx_signals(chan, cdf)) sig = add_forward_returns(sig, cdf, HORIZONS) zones = build_htf_zones(fetch_ohlcv(args.symbol, args.htf, 10**9), args.htf) sig = annotate_position(sig, cdf, zones, tol=0.015) print(f"[样本] {len(cdf)} 根K线 {cdf['date'].iloc[0]} -> {cdf['date'].iloc[-1]}") print(f"[分型] {len(sig)} [大级别中枢] {len(zones)}\n") bo = find_breakout_entries(sig, cdf, window=args.window, mode="fail") rev = find_breakout_entries(sig, cdf, window=args.window, mode="reverse") zsb = zs_breakout_entries(cdf, zones) print(f"[入场点] 分型失败突破 {len(bo)} 反转对照 {len(rev)} 中枢突破 {len(zsb)}\n") print("########## 1. 突破跟随 vs 反转对照(sl=1.5ATR tp=3ATR max=48)##########") rows = [ summarize_trades(run_trades(cdf, bo, 1.5, 3.0, 48), "分型失败突破"), summarize_trades(run_trades(cdf, rev, 1.5, 3.0, 48), "分型反转(对照)"), ] if zsb: rows.append(summarize_trades(run_trades(cdf, zsb, 1.5, 3.0, 48), "中枢突破")) print(pd.DataFrame(rows).to_string(index=False)) print("\n########## 2. 止损止盈网格(分型失败突破)##########") rows = [] for sl in (1.0, 1.5, 2.0): for tp in (2.0, 3.0, 5.0): rows.append(summarize_trades( run_trades(cdf, bo, sl, tp, 48), f"sl{sl} tp{tp}")) print(pd.DataFrame(rows).to_string(index=False)) print("\n########## 3. 移动止损(trailing)##########") rows = [] for sl in (1.0, 1.5, 2.0, 3.0): rows.append(summarize_trades( run_trades(cdf, bo, sl, 99.0, 96, trail=True), f"trail {sl}ATR")) print(pd.DataFrame(rows).to_string(index=False)) # ---- 叠加波动率模型:只在预测到大波动时才跟随突破 ---- print("\n########## 4. 叠加波动率过滤(标签C:3根内 >1% 波动,AUC~0.69)##########") f = build_features(sig, cdf) for c in ("near_support", "near_resistance", "inside_zone"): f[c] = f[c].astype(float) f = f.dropna(subset=[f"ret_{h}" for h in HORIZONS]).reset_index(drop=True) y = (f["ret_3"].to_numpy() > 0.01).astype(int) X = f[ALL_FEATS].to_numpy(dtype=float) oof = walk_forward(X, y, folds=5, purge=60) f["volp"] = oof ev = f.dropna(subset=["volp"]) print(f" 样本外 {len(ev)} 个分型带波动率预测") # 把预测概率挂回突破入场点(按其来源分型) prob_by_confirm = dict(zip(ev["confirm_idx"].astype(int), ev["volp"])) bo_with_p = [] for _, r in sig.iterrows(): c0 = int(r["confirm_idx"]) if c0 not in prob_by_confirm: continue sub = find_breakout_entries(pd.DataFrame([r]), cdf, args.window, "fail") for e, d in sub: bo_with_p.append((e, d, prob_by_confirm[c0])) rows = [] if bo_with_p: probs = np.array([p for _, _, p in bo_with_p]) for q, name in [(0.0, "全部"), (0.5, "top50%"), (0.7, "top30%"), (0.9, "top10%")]: thr = np.quantile(probs, q) sel = [(e, d) for e, d, p in bo_with_p if p >= thr] if len(sel) < 30: continue rows.append(summarize_trades(run_trades(cdf, sel, 1.5, 3.0, 48), f"波动率{name}")) print(pd.DataFrame(rows).to_string(index=False)) print("\n########## 5. 多空拆分(分型失败突破,sl1.5 tp3)##########") rows = [] for d, name in [(1, "做多"), (-1, "做空")]: sel = [(e, dd) for e, dd in bo if dd == d] if sel: rows.append(summarize_trades(run_trades(cdf, sel, 1.5, 3.0, 48), name)) print(pd.DataFrame(rows).to_string(index=False)) if __name__ == "__main__": main()