"""Step 42:1m 单独定出场(SL × TP × MAX_BARS × 分批 + 真实费率模型)。 用户指出的问题:`SL1.5/TP3.0/MAXB48` 是在 5m/15m/30m 上定的,1m 不该混用。 查证成立,而且原因比成本更根本——**1m 以 ATR 计的右尾是其他级别的两倍** (MFE 中位 5.39 ATR vs 3.94~4.29),因为 1m 的 ATR 量的是分钟级噪声, 信号一旦成立抓到的却是几小时级别的趋势。所以「TP=3 ATR」在两个级别上 根本不是一回事,这是量纲问题不是调参问题。 费率按出场原因分别计(入场 taker / 止盈 maker / 止损超时 taker), 实现在 lib/exit_model.py,与 step41 共用。 配对按 §1.5:1m 的大级别是 5m。 ⚠️ 1m 全量 366 万根,单币 TF_DF 约 370s / 峰值 24.5GB,最多开 3 并行。 """ 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", 400) LTF, HTF = "1m", "5m" SLS = [1.0, 1.5, 2.0] TPS = [2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0] MAXBS = [48, 96] RUNNERS = [5.0, 6.0, 8.0, None] SCALE_AT = 3.0 # 减仓后剩余半仓的止损位(开仓价下方几个 ATR)。0 = 保本损,等于初始 SL 即不动 RUNNER_STOPS = [0.0, 0.5, 1.0, 1.5, 2.0] IS_SYMS = ["BTC", "ETH", "SOL"] OOS_SYMS = ["BNB", "XRP", "DOGE", "ADA", "AVAX", "LINK", "LTC", "TRX"] def cfgs() -> list[tuple[str, str, float]]: """所有配置的 (列名, 可读标签, 初始 SL)。""" from lib.exit_model import cfg_name out = [] for sl in SLS: for b in MAXBS: for t in TPS: out.append((cfg_name(sl, t, b), f"SL{sl:g} 整仓TP{t:g} {b}根", sl)) for rn in RUNNERS: tgt = f"{rn:g}ATR" if rn else "不设目标" for k in RUNNER_STOPS: kk = "保本" if k == 0 else f"留损{k:g}" out.append((cfg_name(sl, rn, b, k), f"SL{sl:g} 分批→{tgt} {kk} {b}根", sl)) return out 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.data import fetch_ohlcv from lib.exit_model import walk_exits 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, group = task pair = f"{sym}/USDT:USDT" try: df_l = fetch_ohlcv(pair, LTF, 10 ** 9) if df_l is None or len(df_l) < 3000: return {"task": sym, "error": "1m 数据不足"} chan_l = TF_DF(df_l, 1, LTF) cdf = chan_l.dataframe del df_l zones = build_htf_zones(cdf, LTF, chan=chan_l).reset_index(drop=True) if zones.empty: return {"task": sym, "error": "无中枢"} z = zones.copy() pg, pdn = z["zg"].shift(), z["zd"].shift() z["z_above"], z["z_below"] = z["zd"] > pg, z["zg"] < pdn z["zone_i"] = np.arange(len(z)) df_h = fetch_ohlcv(pair, HTF, 10 ** 9) chan_h = TF_DF(df_h, 1, HTF) tl = htf_fx_timeline( signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe)), chan_h.dataframe) del df_h, chan_h sig = find_fast_bsp3(cdf, zones) if sig.empty: return {"task": sym, "error": "无信号"} sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left") sig = attach_htf_context(sig, cdf, tl, "h1") push = np.where(sig["direction"] == 1, sig["z_above"], sig["z_below"]) keep = (sig["h1_agree"] == 1) & pd.Series(push, index=sig.index).fillna(False).astype(bool) sig = sig[keep] if len(sig) < 25: return {"task": sym, "error": f"过滤后仅 {len(sig)} 笔"} r = walk_exits(cdf, sig, SLS, TPS, MAXBS, SCALE_AT, RUNNERS, RUNNER_STOPS) r["symbol"], r["group"] = sym, group r["date"] = cdf["date"].to_numpy()[r["sig_idx"].to_numpy()] return {"task": sym, "rows": r} except Exception as e: return {"task": sym, "error": repr(e)[:200]} def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--symbols", default="all") ap.add_argument("--workers", type=int, default=3) ap.add_argument("--reuse", action="store_true") args = ap.parse_args() from lib.exit_model import (all_taker_cost, cfg_name, cost_of, flat_cost, rstat, stat) cache = HERE / "out" / "step42_exit_tp_1m.feather" if args.reuse and cache.exists(): allr = pd.read_feather(cache) print(f"[复用] {len(allr)} 笔\n") else: if args.symbols == "all": tasks = [(s, "样本内") for s in IS_SYMS] + [(s, "样本外") for s in OOS_SYMS] elif args.symbols == "is": tasks = [(s, "样本内") for s in IS_SYMS] else: picked = [s.strip() for s in args.symbols.split(",")] tasks = [(s, "样本内" if s in IS_SYMS else "样本外") for s in picked] print(f"[1m 出场全扫] {len(tasks)} 个币 × {len(cfgs())} 个配置," f"{args.workers} 并行(单币约 6.5 分钟 / 24GB)\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 not r or "error" in r: print(f" [{i}/{len(tasks)}] 跳过 {(r or {}).get('task', '')} " f"{(r or {}).get('error', '')}", flush=True) continue res.append(r["rows"]) print(f" [{i}/{len(tasks)}] {r['task']} n={len(r['rows'])}", flush=True) if not res: return allr = pd.concat(res, ignore_index=True) allr.to_feather(cache) oos = allr[allr["group"] == "样本外"] ins = allr[allr["group"] == "样本内"] base = "s1.5_tp3_m48" print("=" * 170) print(f"1m 出场全扫 样本内 {len(ins)} 笔 / 样本外 {len(oos)} 笔") print("\n########## 1. 费率模型的影响(样本外,SL1.5/TP3/48根)##########") gr = oos[f"{base}_g"].to_numpy() rs, sc = oos[f"{base}_r"].to_numpy(), oos[f"{base}_c"].to_numpy() for fn, name in ((all_taker_cost, "旧 全taker双边"), (cost_of, "新 入场taker+止盈maker"), (flat_cost, "研究口径 固定5bp")): print(f" {name:26s} 平均成本 {fn(rs, sc).mean() * 10000:5.2f}bp " f"净均收益 {(gr - fn(rs, sc)).mean() * 100:+.3f}%") print(f" 止盈成交占比 {(rs == 0).mean() * 100:.0f}%(只有这部分是 maker)") print("\n########## 2. 全网格排名(样本外,按剔10%PF)##########") rank = [r for r in (stat(oos, c, lab) for c, lab, _ in cfgs()) if r] rk = pd.DataFrame(rank) rk["_k"] = rk["剔10%PF"].astype(float) print(rk.sort_values("_k", ascending=False).drop(columns="_k").head(15).to_string(index=False)) print("\n########## 3. 减仓后剩余半仓的止损位(样本外,分批→8ATR,48根)##########") print(" 0 = 保本损;等于初始 SL 即不动;更大 = 减仓后主动放宽") for sl in SLS: rows = [] for k in RUNNER_STOPS: kk = "保本损" if k == 0 else f"留损{k:g}ATR" rows.append(stat(oos, cfg_name(sl, 8.0, 48, k), f"初始SL{sl:g} → {kk}")) rows = [r for r in rows if r] if rows: print(pd.DataFrame(rows).to_string(index=False)) print() print("########## 4. 初始 SL:R 倍数口径(唯一可比)##########") for name, frame in (("样本外", oos), ("样本内", ins)): rows = [] for sl in SLS: rows.append(rstat(frame, cfg_name(sl, 3.0, 48), f"SL{sl:g} 整仓TP3/48", sl)) rows.append(rstat(frame, cfg_name(sl, 8.0, 48, sl), f"SL{sl:g} 分批→8ATR 留损{sl:g}/48", sl)) print(f" 【{name}】") print(pd.DataFrame(rows).to_string(index=False)) print() print("########## 5. 现用 vs 最优:样本内外对照 ##########") best_lab = rk.sort_values("_k", ascending=False).iloc[0]["口径"] lut = {lab: (c, sl) for c, lab, sl in cfgs()} best_cfg, best_sl = lut[best_lab] rows = [] for name, frame in (("样本内", ins), ("样本外", oos)): rows += [stat(frame, base, f"{name} 现用 SL1.5/TP3/48根"), stat(frame, best_cfg, f"{name} 最优 {best_lab}")] print(pd.DataFrame([r for r in rows if r]).to_string(index=False)) print("\n 同一对比的 R 倍数口径:") rows = [] for name, frame in (("样本内", ins), ("样本外", oos)): rows += [rstat(frame, base, f"{name} 现用 SL1.5/TP3/48根", 1.5), rstat(frame, best_cfg, f"{name} 最优 {best_lab}", best_sl)] print(pd.DataFrame(rows).to_string(index=False)) print(f"\n########## 6. {best_lab} 的逐币与分年稳定性(样本外)##########") f = oos.assign(year=pd.to_datetime(oos["date"]).dt.year) for key, name in (("symbol", "币"), ("year", "年")): rows = [] for k, g in f.groupby(key): a, b = stat(g, base, str(k)), stat(g, best_cfg, str(k)) if a and b: rows.append({name: k, "笔数": a["笔数"], "现用 净收益": a["净均收益"], "最优 净收益": b["净均收益"], "现用 PF": a["PF"], "最优 PF": b["PF"], "现用 剔10%": a["剔10%PF"], "最优 剔10%": b["剔10%PF"], "现用 余量bp": a["滑点余量bp"], "最优 余量bp": b["滑点余量bp"]}) print(pd.DataFrame(rows).to_string(index=False)) print() if __name__ == "__main__": main()