From 380d6d61cdd12330d3aa53e2b10f702ae50fac7b Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Fri, 28 Aug 2026 22:54:31 +0800 Subject: [PATCH] =?UTF-8?q?research:=20=E4=BA=8C=E7=B1=BB=E4=B9=B0?= =?UTF-8?q?=E5=8D=96=E7=82=B9=E5=AE=9A=E6=80=A7=E4=B8=BA=E6=97=A0=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=EF=BC=8C=E4=B8=80=E4=BA=8C=E7=B1=BB=E7=BA=BF=E6=95=B4?= =?UTF-8?q?=E4=BD=93=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户提出悖论:二类按定义依赖一类(引擎里 B2 确实被 first_bsp_bi_div 门控), 一类既已证否,二类凭什么好。当时有个值得测的反驳——二类多要求「确实反弹」 且「回踩守住 B1 低点」,这是「底是真的」的事后确认,而 §3.397 的诊断恰恰是 一类缺这个确认。若成立,二类反而是一类里被验证过的子集。 测下来悖论成立,但机制不同:二类不是继承了一类的错,是信息为零。 一类 t = −17~−22(强烈指反,有信息只是方向被滞后翻了面),二类原方向 t = −0.8~−2.4、反手 +0.2~+1.7,两边都贴着零,连反手都没有。 另一发现是几何:二类的天然止损位是 B1 的低点,但反弹加回踩之后入场价已在 其上方 4.87 ATR(一类 2.90),2 ATR 止损有 99~100% 落在结构内侧。 step58 在一类上放宽到结构位只把 PF 从 0.22 抬到 0.36,二类缺口大 68%。 ⚠️ 报表里二类「命中线段顶点 0.0%」是我用错口径,已在文档标注:二类按定义 是反弹后的更高低点,与线段顶点不可能重合,该指标只对自称在极值的一类有效。 Co-authored-by: Cursor --- research/HANDOFF.md | 61 ++++++++++ research/step64_b2.py | 257 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 research/step64_b2.py diff --git a/research/HANDOFF.md b/research/HANDOFF.md index 052e25d..eab74b5 100644 --- a/research/HANDOFF.md +++ b/research/HANDOFF.md @@ -1386,6 +1386,58 @@ step56 的实时版把滞后压到 ≤1 根但无质量过滤(PF 0.40),ste 实盘回放还要再晚 15 根(§3.396),而这来自「等笔确认」这个机制本身, 不是可调参数。识别可以优化,但识别从来不是瓶颈。 +### 3.3991 ⛔ 二类:不是继承了一类的错,是信息为零(step64) + +用户提的悖论:二类按定义是「一买后回调、再继续趋势」,一类既已证否,二类凭什么好? + +代码上悖论成立——`find_all_bsp` 里 B2 被 B1 门控(`if first_bsp_bi_div`)。但当时 +有个反驳值得测:B2 比 B1 多要求价格**确实反弹了**、且回踩**守住了** B1 低点, +这两条是「那个底是真的」的事后确认,而 §3.397 的诊断恰恰是一类缺这个确认。 +若成立,B2 就是 B1 里被验证过的子集,是好事而非坏事。 + +**测下来是坏消息,且机制与用户说的不同。** + +⚠️ 先记一个**我用错的口径**:报表里 B2/S2「命中线段顶点 0.0%」(276~293 笔一个 +没中)**是定义决定的,不是发现**。B2 按定义是反弹后的更高低点,线段顶点在 B1 +那个极值上,两者不可能重合。这个指标对自称在极值的一类有效,对二类无效。 +若日后要给二类找标准答案,该问的是「B2 之后 B1 低点是否再未被破」,不是顶点重合。 + +两组有效结论: + +**一、二类的几何是所有信号里最差的** + +| | 入场到结构止损位 | 2ATR 止损落在结构内侧 | +|---|---|---| +| B1 | 2.90 ATR | 88.6% | +| **B2** | **4.87 ATR** | **99~100%** | + +二类的天然止损位是 B1 的低点(回踩守住的就是那点)。但反弹 + 回踩之后,入场价 +离那个低点已有 4.87 ATR。**二类没有可用的止损位**:挂 2 ATR 是纯任意止损, +挂到结构位则单笔风险近 5 ATR。step58 在一类上试过放宽到结构位,PF 仅 0.22→0.36; +二类的缺口比一类大 68%,没有理由更好。 + +**二、二类两个方向都没有边缘——这是它和一类的本质区别** + +| 5m/15m | 原方向 PF / t | 反手 PF / t | +|---|---|---| +| B1 | 0.21~0.24 / **−17 ~ −19** | 2.35~3.03 / **+10 ~ +13** | +| S1 | 0.20~0.23 / **−16 ~ −22** | 2.63~2.75 / **+9.6 ~ +12.8** | +| B2 | 0.73~0.85 / −0.79 ~ −1.42 | 1.09~1.10 / +1.67 ~ +1.70 | +| S2 | 0.75~0.82 / −1.55 ~ −2.40 | 0.85~1.13 / +0.19 ~ +1.36 | + +**一类 t = −17~−22 是强烈指反**:它有信息,只是滞后把「反转信号」变成了 +「反弹追单」(反手曾看似 PF 3.0,但 §3.396 已证那是 `sure_time` 后视产物)。 +**二类两边 t 都贴着零**,原方向 −0.8、反手 +1.7,5m 反手 S2 甚至 0.85。 +没有可提取的东西,连反手都没有。 + +其余画像与 §3.393 一致:二类 atr_z 中位 0.94~0.97(低波动,与一类的 1.22 相反), +滞后 8 根(与一类持平,§3.393 已记「三类滞后无差别」)。 + +**判定:二类关闭,一二类线整体收尾。** 一类是**信号指反**(有信息、方向错、 +错在滞后),二类是**信号消失**——在一类之上又等两笔,把仅剩的方向性磨掉, +同时把止损位推远到 4.87 ATR。用户的悖论方向对,但不是「继承错误」, +是「多等的那两笔把信息也等没了」。 + ### 3.4 alpha 的来源(step32 消融) 逐条拆掉 `fast_bsp3` 的条件后发现:**alpha 完全来自缠论中枢的上下文定位, @@ -2593,6 +2645,15 @@ API 限流风险隔离三个好处。 ## 10. 待办清单 +- [ ] ⭐ **B4 因果回放(step63,跑着)——当前唯一的关键未决项**。 + §3.396 证明 `sure_time` 对一类是后视产物(PF 2.35 → 0.92)。B4 用的是 + 同一个 `sure_time` 字段,**在这个回放出结果前,所有 B4 的历史 PF 都待定**。 + 若 B4 也栽,整条线要重估;若过了,B4 是目前唯一活着的信号 +- [x] **一二类买卖点全线关闭**(step55~62 一类、step64 二类,见 §3.393~3.3991)。 + 一类是**信号指反**(t = −17~−22,有信息但方向错,错在滞后), + 二类是**信号消失**(两个方向 t 都贴零,且入场离结构止损位 4.87 ATR)。 + 六次独立进攻全部止步 PF 1.0 以下,**含用未来函数选样也只到 0.83**。 + **不要重开**,除非能从机制上解决「等笔确认」带来的滞后 - [x] **step39**:1m 时点重建 + 窗口扫描 → **2000 根窗口饱和,计算 0.20s, 假阳性 0%,1m 回测口径可信** - [ ] **对齐 step23 与 step42 的 1m 数字**(3.91bp vs 11.06bp)。差距可能来自 diff --git a/research/step64_b2.py b/research/step64_b2.py new file mode 100644 index 0000000..59bf603 --- /dev/null +++ b/research/step64_b2.py @@ -0,0 +1,257 @@ +"""二类买卖点:它只是一类的延迟版,还是一类里被确认过的那个子集? + +用户提出的悖论:二类按定义是「一买之后回调、再继续趋势」,可一类已经证否了, +二类凭什么会好? + +代码上悖论成立——`find_all_bsp` 里 B2 确实被 B1 门控(`if first_bsp_bi_div`)。 +但 B2 比 B1 多要求两件事:价格**确实反弹了**(bounce_bi 向上), +且回踩**守住了** B1 的低点(`second_bsp_bi.end_klc.low > leave_bi.end_klc.low`)。 +**这两条正是「那个底是真的」的事后确认。** + +而 §3.397 的诊断恰恰是:一类只有 30% 落在真反转上,那 30% 的 PF 是 0.70~0.83, +另外 70% 是 0.08。所以「按确认筛掉假底」正是一类缺的东西。 + +于是悖论变成一个可测的问题: + + B2 命中线段顶点的比例,是否显著高于 B1 的 30%? + + 是 -> B2 是 B1 的**已验证子集**,悖论解除,值得继续查 + 否 -> B2 只是 B1 的延迟版,用户的悖论成立,直接关掉 + +同时量三件与一类可比的东西(口径完全对齐,才能横向比): + 几何 入场价到结构止损位有多远(一类是 2.90 ATR,止损落在结构内侧 88.6%) + 波动 atr_z(一类 1.22 偏高,二类 0.94 偏低,画像相反) + 反手 一类反手在全量口径上曾看似很好,二类是否也有这个现象 +""" +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" / "step64_b2.feather" +SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48 +BASE_WIN, TOL = 200, 2 + + +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)) + + atr = cdf["atr"].to_numpy(float) + cl = cdf["close"].to_numpy(float) + op = cdf["open"].to_numpy(float) + base = (pd.Series(atr).rolling(BASE_WIN, min_periods=50) + .median().shift(1).to_numpy()) + + 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 0 <= i < n: + (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) -> bool: + a = truth[d] + k = int(np.searchsorted(a, i)) + return any(0 <= j < len(a) and abs(int(a[j]) - i) <= TOL + for j in (k - 1, k)) + + # 一类按中枢建索引,好给二类找到它自己那个一类的低点 —— + # 二类的天然止损位是**一类的极值**,不是它自己的极值 + one_ext: dict[int, float] = {} + for b in bsp: + if b.type in (Chan_BSP_TYPE.B1, Chan_BSP_TYPE.S1) and b.zs: + d = 1 if b.type == Chan_BSP_TYPE.B1 else -1 + one_ext[id(b.zs)] = float(b.klc.low if d == 1 else b.klc.high) + + want = {Chan_BSP_TYPE.B1: ("B1", 1), Chan_BSP_TYPE.S1: ("S1", -1), + Chan_BSP_TYPE.B2: ("B2", 1), Chan_BSP_TYPE.S2: ("S2", -1)} + rec = [] + 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 + a = atr[i_ext] + if not np.isfinite(a) or a <= 0 or not np.isfinite(base[i_ext]): + continue + own = float(b.klc.low if d == 1 else b.klc.high) + # 二类的结构止损用一类的极值(回踩不破的就是那个点); + # 一类用自己的极值。这样两者的「入场离结构位多远」才可比 + struct = own + if name in ("B2", "S2") and b.zs is not None: + struct = one_ext.get(id(b.zs), own) + rec.append({ + "sym": sym, "tf": tf, "type": name, "dir": d, + "i_ext": i_ext, "i_sure": i_sure, + "lag_bars": i_sure - i_ext, + "hit": near(i_ext, d), + "atr_z": a / base[i_ext], + "own_ext": own, "struct_px": struct, + "entry_px": op[min(i_sure + 1, n - 1)], + }) + if not rec: + return None + r = pd.DataFrame(rec) + r = r[(r.i_sure < n - 2) & np.isfinite(atr[r.i_sure.values]) + & (atr[r.i_sure.values] > 0)].reset_index(drop=True) + if r.empty: + return None + r["atr_at_entry"] = atr[r.i_sure.values] + r["atr_pct"] = atr[r.i_sure.values] / cl[r.i_sure.values] + r["to_struct"] = ((r.entry_px - r.struct_px) * r.dir + / r.atr_at_entry) + + cfg = cfg_name(SL, RUNNER, MAXB, RSTOP) + for sfx, sgn in (("", 1), ("f_", -1)): + res = walk_exits(cdf, pd.DataFrame({ + "entry_idx": r.i_sure.values, + "direction": sgn * r.dir.values}), [SL], [RUNNER], [MAXB], + scale_at=SCALE_AT, runners=(RUNNER,), runner_stops=(RSTOP,)) + if len(res) != len(r): + return None + for c in ("g", "r", "c", "b"): + r[sfx + c] = res[f"{cfg}_{c}"].to_numpy() + return r + except Exception as e: # noqa: BLE001 + print(f" {sym} {tf} 失败: {type(e).__name__}: {e}", flush=True) + return None + + +def perf(g: pd.DataFrame, pre: str = "") -> dict: + from lib.exit_model import fee_of, taker_notional + gg, rr, cc = g[pre + "g"].values, g[pre + "r"].values, g[pre + "c"].values + net = gg - fee_of(rr, cc) + gR = gg / (SL * g.atr_pct.values) + tn = taker_notional(rr, cc) + w, o = net[net > 0].sum(), -net[net <= 0].sum() + return { + "笔数": 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), + } + + +def report(d: pd.DataFrame) -> None: + for tf, x in d.groupby("tf"): + print("\n" + "#" * 96) + print(f"########## {tf} ##########") + + print("\n【一】悖论的判据:二类命中线段顶点的比例是否高于一类") + rows = [] + for t in ["B1", "S1", "B2", "S2"]: + g = x[x.type == t] + if len(g) < 30: + continue + rows.append({ + "类型": t, "样本": len(g), + "命中线段顶点": f"{g.hit.mean()*100:.1f}%", + "atr_z中位": round(g.atr_z.median(), 3), + "滞后中位": int(g.lag_bars.median()), + "入场到结构位(ATR)": round(g.to_struct.median(), 2), + "止损2ATR落在结构内侧": f"{(g.to_struct > 2).mean()*100:.0f}%", + }) + print(pd.DataFrame(rows).to_string(index=False)) + print("\n 「入场到结构位」是入场价离天然止损位多少个 ATR。二类的结构位取" + "\n 它那个一类的极值(回踩不破的就是那点)。>2 表示 2ATR 的止损挂在" + "\n 结构位以内,价格不用回踩到前低就出局 —— 一类实测 88.6%。") + + print("\n【二】按原方向做(抄底/摸顶)") + print(pd.DataFrame([{"类型": t, **perf(g)} + for t, g in x.groupby("type") + if len(g) >= 30]).to_string(index=False)) + + print("\n【三】反手做") + print(pd.DataFrame([{"类型": t, **perf(g, "f_")} + for t, g in x.groupby("type") + if len(g) >= 30]).to_string(index=False)) + + print("\n【四】二类拆命中/未命中 —— 一类的对应数字是 0.08 vs 0.70") + two = x[x.type.isin(["B2", "S2"])] + if len(two) >= 60: + print(pd.DataFrame([ + {"命中线段顶点": "是" if k else "否", **perf(g)} + for k, g in two.groupby(two.hit) if len(g) >= 20 + ]).to_string(index=False)) + + +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=2) + 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()