"""Step 31:引擎 B3/S3 的方向是不是反的。 引擎原版在完全相同条件下跑出 27.4% 胜率、t=-18.76,这不是滞后能解释的幅度, 稳定做反才会有这种数字。本步只做一件事:把引擎信号的方向翻转再跑一遍。 若翻转后由显著负转为显著正,说明 Chan_BSP_DIR 与实际交易方向的映射存在问题, 而不是三类买卖点本身无效——这会影响引擎所有下游使用者,不只是本次研究。 同时打印几个样本的价格上下文,用价格自己来判定哪个方向才是对的。 """ from __future__ import annotations 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 def run_one(sym: str) -> 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 chanlun.core.ChanEnum import Chan_BSP_DIR from lib.breakout import run_trades from lib.data import fetch_ohlcv ltf = "30m" try: df_l = fetch_ohlcv(f"{sym}/USDT:USDT", ltf, 10**9) if df_l is None: return None chan = TF_DF(df_l, 1, ltf) cdf = chan.dataframe zs_list = chan.cal_bi_zs_list_pure(chan.bi_list) if not zs_list: return None bsp_list = chan.find_all_bsp(chan.bi_list, zs_list) or [] idx_map = {k: i for i, k in enumerate(cdf["date"].dt.strftime("%Y-%m-%d %H:%M:%S"))} close = cdf["close"].to_numpy(dtype=float) n = len(cdf) rows = [] for b in bsp_list: t = str(b.type).replace("Chan_BSP_TYPE.", "") if t not in ("B3", "S3") or not b.is_sure or b.sure_time is None: continue ek = str(b.sure_time) if ek not in idx_map: continue i = idx_map[ek] zs = getattr(b, "zs", None) rows.append({ "entry_idx": i, "type": t, "dir_enum": "BUY" if b.dir == Chan_BSP_DIR.BUY else "SELL", "dir_mapped": 1 if b.dir == Chan_BSP_DIR.BUY else -1, "zg": float(zs.zg) if zs is not None else np.nan, "zd": float(zs.zd) if zs is not None else np.nan, "entry_px": close[i], # 入场后 12 根的实际涨跌,让价格自己说话 "fwd12": (close[min(i + 12, n - 1)] - close[i]) / close[i], }) sig = pd.DataFrame(rows).drop_duplicates("entry_idx") if len(sig) < 30: return None out = [] for name, flip in (("原方向", 1), ("反方向", -1)): entries = [(int(i), int(d) * flip) for i, d in zip(sig["entry_idx"], sig["dir_mapped"])] tr = run_trades(cdf, entries, SL, TP, MAXB, fee=0.0, entry_delay=1) if tr.empty: continue tr["mode"], tr["symbol"] = name, sym m = sig.set_index("entry_idx") for c in ("type", "dir_enum", "fwd12", "zg", "zd", "entry_px"): tr[c] = tr["entry_idx"].map(m[c]) out.append(tr) return {"sym": sym, "trades": pd.concat(out, ignore_index=True), "sig": sig.assign(symbol=sym)} except Exception as e: return {"sym": sym, "error": repr(e)[:250]} def stat(g: pd.DataFrame, label: str) -> dict: r = g["gross"].to_numpy() - FEE - SLIP if len(r) < 15: return {} w, o = r[r > 0], r[r <= 0] sd = r.std(ddof=1) return {"口径": label, "笔数": len(r), "胜率": 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", "t值": f"{r.mean() / (sd / np.sqrt(len(r))):+.2f}"} def main() -> None: syms = ["BTC", "ETH", "SOL"] res = [] with ProcessPoolExecutor(max_workers=3) as ex: futs = {ex.submit(run_one, s): s for s in syms} for f in as_completed(futs): r = f.result() if r is None or "error" in (r or {}): print(f" {futs[f]} 跳过 {(r or {}).get('error', '')}", flush=True) continue res.append(r) print(f" {r['sym']} ok", flush=True) if not res: return allt = pd.concat([r["trades"] for r in res], ignore_index=True) sig = pd.concat([r["sig"] for r in res], ignore_index=True) print("\n" + "=" * 100) print("########## 1. 原方向 vs 反方向(30m,引擎 B3/S3)##########") print(pd.DataFrame([r for r in [stat(g, m) for m, g in allt.groupby("mode")] if r] ).to_string(index=False)) print("\n########## 2. 分类型看 ##########") rows = [] for (m, t), g in allt.groupby(["mode", "type"]): rows.append(stat(g, f"{m} {t}")) print(pd.DataFrame([r for r in rows if r]).to_string(index=False)) print("\n########## 3. 让价格自己说话:入场后12根的实际涨跌 ##########") print(" B3 若真是买点,其后价格应偏涨(fwd12 均值>0)") rows = [] for t, g in sig.groupby("type"): f = g["fwd12"].to_numpy() sd = f.std(ddof=1) rows.append({"类型": t, "枚举方向": g["dir_enum"].iloc[0], "笔数": len(f), "后12根均涨跌": f"{f.mean() * 100:+.3f}%", "上涨占比": f"{(f > 0).mean() * 100:.1f}%", "t值": f"{f.mean() / (sd / np.sqrt(len(f))):+.2f}"}) print(pd.DataFrame(rows).to_string(index=False)) print("\n########## 4. 入场价相对中枢的位置(B3 应在中枢上方)##########") rows = [] for t, g in sig.groupby("type"): above = (g["entry_px"] > g["zg"]).mean() * 100 below = (g["entry_px"] < g["zd"]).mean() * 100 rows.append({"类型": t, "笔数": len(g), "入场价>中枢上沿zg": f"{above:.1f}%", "入场价<中枢下沿zd": f"{below:.1f}%", "在中枢内": f"{100 - above - below:.1f}%"}) print(pd.DataFrame(rows).to_string(index=False)) print("\n 若 B3 大量落在中枢下方、S3 落在上方,则标签与几何位置矛盾。") if __name__ == "__main__": main()