research: 实时版一类(step56),证明滞后不是病根,一类没有边
step55 判定引擎 B1/S1 不可用后,照 B3→B4 的路子做了实时版:把「中枢已成 / 向下离开 / 背驰 / 触发」四步全换成当根可判的代理,滞后从 8~9 根压到 ≤1 根。 背驰能实时判的关键是进入段面积在中枢确认时已是历史,只有离开段需要逐根累加。 没救回来。5m 上 fastB1 一买 PF 0.38 / 一卖 0.36,15m 同量级,比引擎版的 0.20~0.24 有改善但离 1.0 很远。 真正的价值在对照组和滞后分档两处: - 同数据同出场同管线,fastB3 跑出 PF 1.83~2.06 / t +8.5~+10.7,排除了 「管线接错所以为负」。 - lag ≤1 根(≈买在极值上)那一档 PF 仍只有 0.40。即滞后清零也救不活, §3.393 里「等 8 根导致几何劣势」的解释只对了一半。 结论与 §3.4 消融、§3.391 的 mom60 相互印证:这族信号的 alpha 在顺势延续上, 不在反转上。把反转信号提前,它还是反转信号。一类到此为止。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"""实时版一类买卖点(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) >= 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()
|
||||
Reference in New Issue
Block a user