"""一类买卖点到底有没有找到真正的趋势反转点?用线段顶点当标准答案。 用户提出:线段的终点就是该级别的趋势反转点,虽然它是未来函数,但可以拿来当 **标签**验证检测器,而不是拿来交易。这能把两件事分开: 检测器对不对 B1 是否真的落在趋势反转底上 能不能交易 step59 已证否(实盘首现比 sure_time 晚中位 15 根,PF 塌到 0.92) 若检测器对而只是慢,那问题是延迟,还有救;若检测器本身就没找到反转点, 这条线整个是死的。 ⚠️ **对照组是这个测试的全部意义**。B1 按构造就长在笔的低点上,而笔低点本来 就有一定概率撞上线段底。所以要问的不是「B1 命中率多少」,而是 **「在所有同向笔端点里,B1 这个标签把命中率提高了多少倍」**。 没有这个基准,任何绝对数字都可以随便解读。 (同样的坑 fast_bsp 文档头踩过:回抽极值命中笔端点 19.3%,看着不低, 但随机基准是 22%,其实是负贡献。) """ from __future__ import annotations import argparse 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") HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) sys.path.insert(0, str(HERE.parent)) OUT = HERE / "out" / "step60_seg_truth.feather" SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48 TOL = [0, 1, 2, 3, 5] def collect(sym: str, tf: str, rows: int) -> pd.DataFrame | None: from chanlun import TF_DF from chanlun.core.ChanEnum import Chan_BSP_TYPE, Chan_SEG_DIR from lib.data import fetch_ohlcv from lib.exit_model import cfg_name, walk_exits try: df = fetch_ohlcv(f"{sym}/USDT:USDT", tf, rows) if df is None or len(df) < 5_000: return None chan = TF_DF(df, 1, tf, lean=False) cdf = chan.dataframe bz = chan.cal_bi_zs_list_pure(chan.bi_list) if not bz: return None bsp = chan.find_all_bsp(chan.bi_list, bz) or [] dser = pd.to_datetime(cdf["date"]) if dser.dt.tz is not None: dser = dser.dt.tz_localize(None) didx = pd.DatetimeIndex(dser) n = len(cdf) def to_i(ts) -> int: t = pd.Timestamp(ts) return int(didx.searchsorted(t.tz_localize(None) if t.tz else t)) # ---- 标准答案:线段终点。下降线段终点=真底,上升线段终点=真顶 ---- seg_bot, seg_top = [], [] for sg in getattr(chan, "seg_list", []) or []: if sg.end_time is None: continue i = to_i(sg.end_time) if not (0 <= i < n): continue (seg_bot if sg.dir == Chan_SEG_DIR.DOWN else seg_top).append(i) if not seg_bot or not seg_top: return None truth = {1: np.array(sorted(seg_bot)), -1: np.array(sorted(seg_top))} def near(i: int, d: int, tol: int) -> bool: a = truth[d] k = int(np.searchsorted(a, i)) for j in (k - 1, k): if 0 <= j < len(a) and abs(int(a[j]) - i) <= tol: return True return False rec = [] # ---- 对照组:所有笔端点。B1 本就长在笔低点上,基准必须同源 ---- for bi in chan.bi_list: if not getattr(bi, "is_sure", False) or bi.end_klc is None: continue d = 1 if str(bi.dir).endswith("DOWN") else -1 # 下降笔终点=低点 i = to_i(bi.end_klc.end_time) if not (0 <= i < n): continue rec.append({"kind": "笔端点", "dir": d, "i_ext": i, "i_sure": -1}) want = {Chan_BSP_TYPE.B1: ("B1", 1), Chan_BSP_TYPE.S1: ("S1", -1), Chan_BSP_TYPE.B3: ("B3", 1), Chan_BSP_TYPE.S3: ("S3", -1)} for b in bsp: tag = want.get(b.type) if tag is None or b.sure_time is None: continue name, d = tag i_ext, i_sure = to_i(b.klc.end_time), to_i(b.sure_time) if not (0 <= i_ext < n and 0 <= i_sure < n): continue rec.append({"kind": name, "dir": d, "i_ext": i_ext, "i_sure": i_sure}) r = pd.DataFrame(rec) for tol in TOL: r[f"hit{tol}"] = [near(i, d, tol) for i, d in zip(r.i_ext, r.dir)] # 命中线段顶点的那批一类,按原方向(抄底)做能不能赚 atr = cdf["atr"].to_numpy(float) cl = cdf["close"].to_numpy(float) sig = r[(r.kind.isin(["B1", "S1"])) & (r.i_sure >= 0) & (r.i_sure < n - 2)].copy() sig = sig[np.isfinite(atr[sig.i_sure.values]) & (atr[sig.i_sure.values] > 0)] if not sig.empty: cfg = cfg_name(SL, RUNNER, MAXB, RSTOP) res = walk_exits(cdf, pd.DataFrame({ "entry_idx": sig.i_sure.values, "direction": sig.dir.values}), [SL], [RUNNER], [MAXB], scale_at=SCALE_AT, runners=(RUNNER,), runner_stops=(RSTOP,)) if len(res) == len(sig): for c in ("g", "r", "c"): sig[c] = res[f"{cfg}_{c}"].to_numpy() sig["atr_pct"] = (atr[sig.i_sure.values] / cl[sig.i_sure.values]) r = r.merge(sig[["i_ext", "kind", "g", "r", "c", "atr_pct"]], on=["i_ext", "kind"], how="left") r["sym"], r["tf"] = sym, tf return r except Exception as e: # noqa: BLE001 print(f" {sym} {tf} 失败: {type(e).__name__}: {e}", flush=True) return None def report(d: pd.DataFrame) -> None: from lib.exit_model import fee_of, taker_notional for tf, x in d.groupby("tf"): print("\n" + "#" * 92) print(f"########## {tf} · 线段顶点作为标准答案 ##########") print("\n【命中率】i_ext 落在同向线段终点 ±tol 根内的比例") rows = [] for kind in ["笔端点", "B1", "S1", "B3", "S3"]: g = x[x.kind == kind] if len(g) < 30: continue row = {"信号": kind, "样本": len(g)} for tol in TOL: row[f"±{tol}根"] = f"{g[f'hit{tol}'].mean()*100:.1f}%" rows.append(row) t = pd.DataFrame(rows) print(t.to_string(index=False)) base = x[x.kind == "笔端点"] print("\n【提升倍数】相对「所有同向笔端点」这个基准。" "≈1 就是没有信息量") rows = [] for kind in ["B1", "S1", "B3", "S3"]: g = x[x.kind == kind] if len(g) < 30: continue row = {"信号": kind} for tol in TOL: b = base[base.dir.isin(g.dir.unique())][f"hit{tol}"].mean() row[f"±{tol}根"] = (round(g[f"hit{tol}"].mean() / b, 2) if b > 0 else np.nan) rows.append(row) print(pd.DataFrame(rows).to_string(index=False)) if "g" not in x.columns: continue print("\n【命中 vs 未命中】一类按原方向(抄底/摸顶)做的表现," "tol=±2 根") y = x[x.kind.isin(["B1", "S1"])].dropna(subset=["g"]).copy() if len(y) < 60: continue rows = [] for hit, g in y.groupby(y.hit2): net = g.g.values - fee_of(g.r.values, g.c.values) gR = g.g.values / (SL * g.atr_pct.values) tn = taker_notional(g.r.values, g.c.values) w, o = net[net > 0].sum(), -net[net <= 0].sum() rows.append({ "命中线段顶点": "是" if hit else "否", "笔数": len(g), "胜率": f"{(net > 0).mean()*100:.1f}%", "毛R": round(gR.mean(), 3), "PF": round(w / o, 2) if o > 0 else np.inf, "余量bp": round(net.mean() / tn.mean() * 1e4, 2), "t值": round(gR.mean() / (gR.std(ddof=1) / np.sqrt(len(g))), 2), }) print(pd.DataFrame(rows).to_string(index=False)) print("\n判读:若「命中」那组按原方向做显著为正,说明检测器是对的、" "只是掺了太多噪声,\n值得找实时可判的过滤器;若两组都为负," "说明即使真站在线段底上,\n这个入场时点也已经太晚了。") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--symbols", default="BTC,ETH,SOL,LINK,DOGE") ap.add_argument("--tfs", default="5m,15m") ap.add_argument("--rows", type=int, default=200_000) ap.add_argument("--workers", type=int, default=3) ap.add_argument("--reuse", action="store_true") args = ap.parse_args() if args.reuse and OUT.exists(): report(pd.read_feather(OUT)) return syms = [s.strip() for s in args.symbols.split(",")] tfs = [t.strip() for t in args.tfs.split(",")] parts = [] with ProcessPoolExecutor(max_workers=args.workers) as ex: fut = {ex.submit(collect, s, t, args.rows): (s, t) for s in syms for t in tfs} for i, f in enumerate(as_completed(fut), 1): r = f.result() s, t = fut[f] print(f" [{i}/{len(fut)}] {s} {t} " f"{0 if r is None else len(r)}", flush=True) if r is not None: parts.append(r) if not parts: print("无结果") return d = pd.concat(parts, ignore_index=True) OUT.parent.mkdir(exist_ok=True) d.to_feather(OUT) report(d) if __name__ == "__main__": main()