Files
Chan/research/step56_fast_bsp1.py
T
jackyu66gitandCursor 7292ef5dd4 research: 低滞后+质量过滤组合失败,过滤器符号翻转,一类线关闭
把 step62 的 ext_run 质量过滤接到 step56 的低滞后版上——两者各解决一半约束,
是唯一同时处理滞后和识别精度的路径。

失败,且失败方式本身是判据:过滤器符号反了。5m 上延伸度分档在滞后版是
Q1 0.12 / Q4 0.46(越延伸越好),在低滞后版是 Q1 0.58 / Q4 0.33(越短越好)。
叠加后 PF 从 0.37 掉到 0.30~0.33,比不过滤更差。div 同样翻转。

同一批信号、同一个特征,换个入场时点最优方向就反过来,说明这些过滤效果是
入场时点的交互产物而非信号的稳定属性,继续挑阈值就是拟合噪声。

一类线六次独立进攻全部止步 1.0 以下:引擎原生 0.24、反手 0.92(因果回放后)、
实时重写 0.41、结构止损 0.36、用未来函数选样 0.83、质量过滤 0.54、
低滞后+过滤 0.33。第五项尤其说明问题——即使选样做到完美也只到 0.83。

判定关闭。病根是入场价已在结构底上方 2.9 ATR、实盘再晚 15 根,来自「等笔确认」
机制本身而非参数。识别可以优化(精度能翻倍),但识别从来不是瓶颈。

fast_bsp1 顺带补 ext_atr 字段,用 ATR 归一才与 step62 的 ext_run 同口径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 22:03:59 +08:00

225 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""实时版一类买卖点(fast B1/S1)能否翻正。
step55 已判定引擎的 B1/S1 不可用(5m PF 0.24/0.20、胜率 18.6%/14.8%、
t −17/−22),且病根是滞后 8~9 根带来的几何劣势,不是参数。B3 当年同病
(PF 0.66),靠重新定义成实时判据翻到 1.59(B4)。本脚本对一类做同样的尝试。
对照组三条,缺一不可:
引擎 B1/S1 step55 的数,说明「不改判据」是什么下场
fast B1/S1 本次
fast B3/S3 同一份数据、同一套出场跑 B4,确认管线本身能跑出正数
—— 少了它,fast B1 若为负就分不清是判据不行还是管线接错了
"""
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" / "step56_fast_bsp1.feather"
SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48
def collect(sym: str, rows: int, tf: str) -> pd.DataFrame | None:
from chanlun import TF_DF
from chanlun.analysis.fast_bsp import (
ensure_timestamp,
find_fast_bsp3,
zones_from_zs_list,
)
from lib.data import fetch_ohlcv
from lib.exit_model import cfg_name, walk_exits
from lib.fast_bsp1 import find_fast_bsp1, zones_with_enter_area
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)
cdf = ensure_timestamp(chan.dataframe)
zs_list = chan.cal_bi_zs_list_pure(chan.bi_list)
if not zs_list:
return None
atr = cdf["atr"].to_numpy(float)
cl = cdf["close"].to_numpy(float)
cfg = cfg_name(SL, RUNNER, MAXB, RSTOP)
parts = []
d1 = {}
s1 = find_fast_bsp1(cdf, zones_with_enter_area(zs_list, cdf), diag=d1)
if not s1.empty:
s1["kind"] = "fastB1"
parts.append(s1)
# 同数据同出场跑一遍 B4,作为「管线能出正数」的存在性证明
s3 = find_fast_bsp3(cdf, zones_from_zs_list(zs_list, cdf))
if not s3.empty:
s3["kind"] = "fastB3"
parts.append(s3)
if not parts:
return None
r = pd.concat(parts, ignore_index=True)
r = r[(r.entry_idx < len(cdf) - 2)
& np.isfinite(atr[r.entry_idx.values])
& (atr[r.entry_idx.values] > 0)].reset_index(drop=True)
if r.empty:
return None
res = walk_exits(cdf, r[["entry_idx", "direction"]], [SL], [RUNNER],
[MAXB], scale_at=SCALE_AT, runners=(RUNNER,),
runner_stops=(RSTOP,))
if len(res) != len(r):
return None
for k in ("g", "r", "c", "b"):
r[k] = res[f"{cfg}_{k}"].to_numpy()
r["sym"], r["tf"] = sym, tf
r["atr_pct"] = atr[r.entry_idx.values] / cl[r.entry_idx.values]
r["date"] = cdf["date"].to_numpy()[r.entry_idx.values]
r["diag"] = str(d1)
return r
except Exception as e: # noqa: BLE001
print(f" {sym} {tf} 失败: {type(e).__name__}: {e}", flush=True)
return None
def stats(g: pd.DataFrame) -> dict:
from lib.exit_model import fee_of, taker_notional
net = g.g.values - fee_of(g.r.values, g.c.values)
gR = g.g.values / (SL * g.atr_pct.values)
R = net / (SL * g.atr_pct.values)
tn = taker_notional(g.r.values, g.c.values)
w, o = net[net > 0].sum(), -net[net <= 0].sum()
return {
"笔数": len(g), "胜率": f"{(net > 0).mean()*100:.1f}%",
"毛R": round(gR.mean(), 3), "净均R": round(R.mean(), 3),
"PF": round(w / o, 2) if o > 0 else np.inf,
"R夏普": round(R.mean() / R.std(ddof=1), 3),
"余量bp": round(net.mean() / tn.mean() * 1e4, 2),
"中位持仓": int(np.median(g.b.values)),
"t值": round(gR.mean() / (gR.std(ddof=1) / np.sqrt(len(g))), 2),
}
def report(d: pd.DataFrame) -> None:
for tf, x in d.groupby("tf"):
span = (pd.to_datetime(x.date).max()
- pd.to_datetime(x.date).min()).total_seconds() / 86400
print("\n" + "#" * 92)
print(f"########## {tf} · {x.sym.nunique()} 币 · 跨 {span:.0f} 天"
f" ##########")
rows = []
for (kind, dirn), g in x.groupby(["kind", "direction"]):
if len(g) < 40:
continue
nm = {"fastB1": ("一买", "一卖"), "fastB3": ("三买", "三卖")}[kind]
rows.append({"信号": f"{kind} {nm[0 if dirn == 1 else 1]}",
"每币每天": round(len(g) / span / x.sym.nunique(), 3),
**stats(g)})
if rows:
print(pd.DataFrame(rows).to_string(index=False))
print("\n对照 · step55 引擎原生(同周期同出场):")
print(" 5m B1 PF 0.24 胜率 18.6% t 17.3 S1 PF 0.20 胜率 14.8% "
"t 22.3")
print(" 15m B1 PF 0.21 t 18.9 S1 PF 0.23 t 15.9")
f1 = x[x.kind == "fastB1"]
if len(f1) >= 120 and "ext_atr" in f1.columns:
y = f1.dropna(subset=["ext_atr"])
print("\n延伸度分档(ext_atr,step62 在滞后版上实测的唯一单调特征:"
"\n命中线段顶点的比例 12%→44%PF 0.12→0.46):")
q = pd.qcut(y["ext_atr"], 4,
labels=["Q1最短", "Q2", "Q3", "Q4最延伸"],
duplicates="drop")
print(pd.DataFrame([{"档": k, **stats(v)}
for k, v in y.groupby(q, observed=True)
if len(v) >= 30]).to_string(index=False))
print("\n⭐ 组合:低滞后(本模块)+ 延伸过滤(step62)—— "
"唯一同时处理两个约束的路径")
p50, p75 = y["ext_atr"].quantile(.50), y["ext_atr"].quantile(.75)
dm = y["div"].median()
rows = [{"过滤器": "无", **stats(y)}]
for nm, g in [
(f"ext≥P50({p50:.1f})", y[y["ext_atr"] >= p50]),
(f"ext≥P75({p75:.1f})", y[y["ext_atr"] >= p75]),
(f"ext≥P75 且 div≥中位", y[(y["ext_atr"] >= p75)
& (y["div"] >= dm)]),
(f"ext≥P50 且 div≥中位", y[(y["ext_atr"] >= p50)
& (y["div"] >= dm)]),
]:
if len(g) >= 30:
rows.append({"过滤器": nm, **stats(g)})
print(pd.DataFrame(rows).to_string(index=False))
print("对照 · 同过滤器在滞后版(step62,5m):无 PF 0.22 → "
"ext≥P75且div≥中位 PF 0.54")
if len(f1) >= 60:
print("\n背驰强度分档(div = 离开段面积/进入段面积,越小背驰越强):")
q = pd.qcut(f1["div"], 4, labels=["Q1最强", "Q2", "Q3", "Q4最弱"],
duplicates="drop")
print(pd.DataFrame([{"档": k, **stats(v)}
for k, v in f1.groupby(q, observed=True)
if len(v) >= 20]).to_string(index=False))
print("\n滞后分档(lag = 入场根 − 离开段极值根):")
b = pd.cut(f1["lag"], [-1, 1, 3, 6, 12, 1e9],
labels=["≤1根", "2-3根", "4-6根", "7-12根", ">12根"])
print(pd.DataFrame([{"档": k, **stats(v)}
for k, v in f1.groupby(b, observed=True)
if len(v) >= 20]).to_string(index=False))
dg = d[d.kind == "fastB1"].diag.dropna()
if len(dg):
print("\n" + "=" * 92)
print("fast B1 漏斗(首个中枢样本):", dg.iloc[0])
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():
d = pd.read_feather(OUT)
else:
syms = [s.strip() for s in args.symbols.split(",")]
tfs = [t.strip() for t in args.tfs.split(",")]
print(f"[实时版一类] {len(syms)}× {tfs} × {args.rows}\n",
flush=True)
parts = []
with ProcessPoolExecutor(max_workers=args.workers) as ex:
fut = {ex.submit(collect, s, args.rows, t): (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()