From 93ed315a4bd5353496d5bce353f576c470f968a6 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Fri, 28 Aug 2026 23:17:48 +0800 Subject: [PATCH] =?UTF-8?q?research:=20=E4=B8=80=E7=B1=BB=E7=BA=BF?= =?UTF-8?q?=E9=97=A8=E6=A7=9B=E9=87=8F=E5=8C=96=E2=80=94=E2=80=94=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=8F=AA=E6=9C=89=E4=B8=80=E4=B8=AA=E5=8F=98=E9=87=8F?= =?UTF-8?q?=EF=BC=8C=E7=B2=BE=E5=BA=A6=E9=9C=80=2067~73%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 §3.3992 的天花板换成实时可算的 ext_run 选样配结构止损:5m 0.51、15m 0.48, 对比上界 2.29/4.11,兑现不了。但数字对不上(Q4 真端点浓度已 44%),拆开后 得到本轮最有价值的一张表: ext_run 在真端点**内部**毫无反向选样(5m 各档 2.53/1.80/2.61/2.22 持平, 15m 单调递增到 6.10),在非真端点内部也毫无区分力(恒在 0.11~0.18)。 全局只有「是不是真端点」这一个变量在起作用,其余特征都是它的噪声代理。 这也修正了 §3.399:ext_run 的符号翻转不是拟合,它确实提纯(28.7%→44%), 只是幅度远远不够。 于是 PF 退化为两组按精度 p 的混合,解出盈亏平衡精度:5m 72.6%、15m 67.1%。 即要求实时判「这是不是那个底」的准确率达七成,而现在是 28.7%。 顺带解释了为什么 B4 能做而一类不能:B4 是突破后的延续信号,不需要判断反转; 一类的全部难度集中在这一个二分类上。 Co-authored-by: Cursor --- research/HANDOFF.md | 64 ++++++++++-- research/step67_realtime.py | 195 ++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 research/step67_realtime.py diff --git a/research/HANDOFF.md b/research/HANDOFF.md index c483e63..a26ec68 100644 --- a/research/HANDOFF.md +++ b/research/HANDOFF.md @@ -1484,10 +1484,52 @@ MFE/MAE 给出机制:真底那批**逆向行程中位 2.24 ATR(5m)/ 2.08 它证明的是**失败原因可修(选样精度 + 止损位),而非这个级别没得做**。 **由此产生的两个待跑实验:** -1. **实时过滤器 + 结构止损**(唯一值得跑的组合)。step62 的 `ext_run` 实时可算、 - 已把精度做到 47.9%,但当时配 2 ATR 止损所以只到 0.54。天花板说这个配错了。 -2. **超时从 48 根放宽**。线段中位时长 **113 根**,48 根超时在行情走完 42% 时就平仓; - 真底那批 MFE 中位仅 2.37 ATR,很可能就是被截断的(而非行情本身小)。 +1. ✅ **实时过滤器 + 结构止损** → 见 §3.3993,兑现不了,但换来一个确切门槛 +2. **超时从 48 根放宽**(仍未跑)。线段中位时长 **113 根**,48 根超时在行情走完 + 42% 时就平仓;真底那批 MFE 中位仅 2.37 ATR,可能是被截断的(而非行情本身小) + +### 3.3993 ⭐ 一类线的门槛量化:全局只有一个变量,精度要 67~73%(step67) + +把 §3.3992 的天花板换成**实时可算**的 `ext_run` 选样,配结构止损: + +| | 5m | 15m | +|---|---|---| +| 全体无过滤 | 0.34 | 0.32 | +| ext_run ≥ P75(实时) | 0.51 | 0.48 | +| ext_run≥P75 且 div≥中位 | 0.60 | 0.46 | +| ★ 真线段端点(未来函数) | **2.29** | **4.11** | + +**兑现不了。** 但数字对不上——Q4 的真端点浓度已达 44%(基线 28.7%), +若真端点值 2.29,44% 浓度不该只有 0.51。拆开看: + +| ext_run 档 | 真端点组 PF (5m / 15m) | 非真端点组 PF (5m / 15m) | +|---|---|---| +| Q1 最短 | 2.53 / 2.61 | 0.18 / 0.11 | +| Q4 最延伸 | 2.22 / **6.10** | 0.16 / 0.12 | + +**这张表是本节的结论**:`ext_run` 在真端点**内部**毫无反向选样(5m 持平、 +15m 单调递增到 6.10),在非真端点内部也毫无区分力(恒在 0.11~0.18)。 +**全局只有「是不是真端点」这一个变量在起作用**,其余特征都是它的噪声代理。 + +这同时**修正了 §3.399 的判定**:当时把 `ext_run` 的符号翻转判为拟合。 +现在看它不是拟合——它确实提纯(28.7% → 44%),只是提纯幅度远远不够。 + +于是 PF 退化成两组按精度 p 的混合,可解出门槛: + +| 实时精度 | 28.7%(当前) | 44%(ext_run) | 60% | 70% | 100%(上界) | +|---|---|---|---|---|---| +| PF (5m) | 0.34 | 0.50 | 0.73 | 0.93 | 2.29 | +| PF (15m) | 0.33 | 0.51 | 0.81 | 1.10 | 4.11 | + +**盈亏平衡精度:5m 需 72.6%,15m 需 67.1%。** 当前 28.7%,`ext_run` 做到 44%。 + +**判定:一类线的问题已完全定性,但门槛极高。** 要把「这是不是真线段底」的实时 +判准精度做到 ~70%(现在 28.7%,最好的特征到 44%),本质上是要求实时抄底摸顶的 +准确率达到七成。这是可以尝试的 ML 问题(标签、特征、评估口径本节都已给全), +但**先验上很难**,且 70% 只是毛平衡,还要再扣 §3.396 的 15 根因果滞后。 + +> **这解释了为什么 B4 能做而一类不能**:B4 是突破后的**延续**信号, +> 不需要判断反转;一类的全部难度集中在「这是不是那个底」这一个二分类上。 ### 3.4 alpha 的来源(step32 消融) @@ -2700,12 +2742,14 @@ API 限流风险隔离三个好处。 §3.396 证明 `sure_time` 对一类是后视产物(PF 2.35 → 0.92)。B4 用的是 同一个 `sure_time` 字段,**在这个回放出结果前,所有 B4 的历史 PF 都待定**。 若 B4 也栽,整条线要重估;若过了,B4 是目前唯一活着的信号 -- [ ] ⭐ **一类线已重开,跑「实时过滤器 + 结构止损」**(§3.3992)。 - §3.399 的关闭结论**已被 step66 推翻**:之前六次进攻每次只动一半, - 完美选样配 2ATR 止损是 0.70,无选样配结构止损是 0.36,**两个一起是 2.29/4.11**。 - 要跑的是 step62 的 `ext_run` 实时过滤器(精度 28.7% → 47.9%)配结构止损, - 看能兑现多少上界。注意上界含两个未来函数,兑现后必须再过因果回放 -- [ ] **一类超时从 48 根放宽**(§3.3992)。线段中位时长 113 根, +- [ ] **一类线:问题已完全定性,等一个决策**(§3.3992 / §3.3993)。 + 全局只有一个变量——**「是不是真线段端点」**:是则 PF 2.29(5m)/4.11(15m), + 否则恒为 0.13,其余特征都只是它的噪声代理。**盈亏平衡精度 5m 72.6%、 + 15m 67.1%**,当前 28.7%,最好的实时特征 `ext_run` 到 44%。 + 要继续就是做一个「实时判真底」的分类器打到 ~70%,标签/特征/评估口径 + §3.3993 已给全,但先验很难,且 70% 只是毛平衡,还要再扣 15 根因果滞后。 + **这条不阻塞任何实盘工作** +- [ ] **一类超时从 48 根放宽**(§3.3992,仍未跑,代价很低)。线段中位时长 113 根, 48 根在行情走完 42% 时平仓,真底那批 MFE 中位仅 2.37 ATR 疑似被截断 - [x] **二类关闭**(step64,§3.3991)。与一类不同,二类是**信号消失**而非信号指反: 两个方向 t 都贴零,且入场离结构止损位 4.87 ATR(一类 2.90), diff --git a/research/step67_realtime.py b/research/step67_realtime.py new file mode 100644 index 0000000..344b3a8 --- /dev/null +++ b/research/step67_realtime.py @@ -0,0 +1,195 @@ +"""把 step66 的天花板换成实时可算的选样,看能兑现多少。 + +step66 测出上界:**未来函数选样 + 结构止损**在 5m 是 PF 2.29、15m 是 4.11。 +但那个选样用了线段端点(事后才知道),不能交易。本脚本把它换成 step62 那个 +**实时可算**的 `ext_run`(极值越过中枢边界几个 ATR,当根即可算),配上结构止损。 + +这是唯一同时处理两个已知约束的组合,也是 §3.3992 列出的第一个待跑实验: + + step62 有实时过滤器(精度 28.7% -> 47.9%)但配了 2 ATR 止损 -> PF 0.54 + step58 有结构止损但没有过滤器 -> PF 0.36 + step66 两个都有,但选样是未来函数 -> PF 2.29 / 4.11 + **本脚本:实时过滤器 + 结构止损** -> ? + +判读(对照 step66 的上界): + > 1.3 兑现了相当部分,一类线值得继续,下一步做因果回放(sure_time 仍是未来函数) + ~ 1.0 过滤器精度不够,需要更好的实时特征 + < 0.8 实时特征抓不到那 28.7%,天花板兑现不了,一类线仍关闭 + +⚠️ 即使 > 1.3 也**不能算数**:入场仍在 `sure_time` 上,而 §3.396 证明实盘回放 +还要再晚 15 根。这一步只决定「值不值得再花一次因果回放的机器时间」。 +""" +from __future__ import annotations + +import argparse +import sys +import warnings +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)) + +F62 = HERE / "out" / "step62_trend_end.feather" +F64 = HERE / "out" / "step64_b2.feather" +KEY = ["sym", "tf", "type", "i_ext", "i_sure"] + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--tf", default="5m") + ap.add_argument("--rows", type=int, default=200_000) + args = ap.parse_args() + + from chanlun import TF_DF + from lib.data import fetch_ohlcv + from step58_struct_stop import simulate, stats + + a = pd.read_feather(F62)[KEY + ["ext_run", "div", "ladder_n"]] + b = pd.read_feather(F64)[KEY + ["entry_px", "own_ext", + "atr_at_entry", "hit", "dir"]] + d = a.merge(b, on=KEY, how="inner") + d = d[d.tf == args.tf].reset_index(drop=True) + d["to_ext"] = (d.entry_px - d.own_ext) * d.dir / d.atr_at_entry + + print(f"[实时过滤器 + 结构止损] {args.tf} · 一类 {len(d)} 笔 · " + f"真线段端点占比 {d.hit.mean()*100:.1f}%") + print(f"step66 上界(同止损、但用未来函数选样):" + f"{'PF 2.29' if args.tf == '5m' else 'PF 4.11'}\n") + + cache = {} + for sym in sorted(d.sym.unique()): + df = fetch_ohlcv(f"{sym}/USDT:USDT", args.tf, args.rows) + cache[sym] = TF_DF(df, 1, args.tf).dataframe + + def run(x: pd.DataFrame, margin: float | None) -> dict: + gs, rs, cs, ap_, sl_ = [], [], [], [], [] + for sym, g in x.groupby("sym"): + g = g.reset_index(drop=True) + t = pd.DataFrame({"entry_idx": g.i_sure.values, + "direction": g.dir.values, + "atr_at_entry": g.atr_at_entry.values}) + sl = (np.full(len(g), 2.0) if margin is None + else (g.to_ext.values + margin)) + res = simulate(cache[sym], t, sl, r_scale=False) + ok = res.g.notna().values + gs.append(res.g[ok]) + rs.append(res.r[ok]) + cs.append(res.c[ok]) + ap_.append((g.atr_at_entry.values / g.entry_px.values)[ok]) + sl_.append(sl[ok]) + return stats(pd.concat(gs, ignore_index=True), + pd.concat(rs, ignore_index=True), + pd.concat(cs, ignore_index=True), + np.concatenate(ap_), np.concatenate(sl_)) + + print("=" * 96) + print("【一】ext_run 分档 × 止损口径 —— 过滤器的效果依赖止损吗") + print("=" * 96) + q = d.ext_run.quantile([.25, .5, .75]).values + lab = ["Q1最短", "Q2", "Q3", "Q4最延伸"] + d["bucket"] = pd.cut(d.ext_run, [-np.inf, *q, np.inf], labels=lab) + rows = [] + for bk, g in d.groupby("bucket", observed=True): + for name, m in [("固定2ATR", None), ("结构+1.0", 1.0)]: + rows.append({"ext_run档": bk, "止损": name, "真端点占比": + f"{g.hit.mean()*100:.0f}%", **run(g, m)}) + print(pd.DataFrame(rows).to_string(index=False)) + print(""" + §3.399 判过「过滤器符号随入场时点翻转 -> 拟合」。这里换的是**止损**而非入场, + 若 Q4 在两种止损下都最好,说明 ext_run 是真信号;若又翻转,则仍是拟合。""") + + print("\n" + "=" * 96) + print("【二】实时可交易组合 vs step66 上界") + print("=" * 96) + rows = [{"选样": "全体(无过滤)", "笔数": len(d), **run(d, 1.0)}] + for p in (50, 75): + thr = np.percentile(d.ext_run, p) + g = d[d.ext_run >= thr] + rows.append({"选样": f"ext_run ≥ P{p}(实时)", "笔数": len(g), + **run(g, 1.0)}) + g = d[(d.ext_run >= np.percentile(d.ext_run, 75)) + & (d["div"] >= d["div"].median())] + rows.append({"选样": "ext_run≥P75 且 div≥中位(实时)", + "笔数": len(g), **run(g, 1.0)}) + gh = d[d.hit] + rows.append({"选样": "★真线段端点(未来函数=上界)", "笔数": len(gh), + **run(gh, 1.0)}) + print(pd.DataFrame(rows).to_string(index=False)) + print(""" + 判读:实时行若接近 ★ 那行,说明 ext_run 抓到了同一批信号,一类线值得继续; + 若仍贴近「全体」,说明实时特征抓不到那 28.7%,天花板兑现不了。 + ⚠️ 即便好也不算数——入场仍在 sure_time 上,必须再过一次因果回放。""") + + print("\n" + "=" * 96) + print("【三】数字对不上,追一下:ext_run 挑出的真端点,质量还一样吗") + print("=" * 96) + print("Q4 的真端点浓度 44%(基线 28.7%),若真端点都值 PF 2.29," + "Q4 不该只有 0.51。\n拆开看 ext_run 在真端点**内部**是帮忙还是帮倒忙——" + "这决定天花板是否可学:") + rows = [] + for k, g in d.groupby(d.hit): + for bk, gg in g.groupby("bucket", observed=True): + if len(gg) < 25: + continue + rows.append({"真端点": "是" if k else "否", "ext_run档": bk, + "笔数": len(gg), **run(gg, 1.0)}) + print(pd.DataFrame(rows).to_string(index=False)) + print(""" + 「是」组内若 Q4 明显低于 Q1 -> ext_run 在真端点里**反向选样**, + 它提高浓度的同时挑走了最差的那些,两个效应抵消,这解释了 0.51 vs 2.29。 + 若「是」组内各档持平 -> 浓度提升是真的,缺的只是更强的实时特征。""") + + print("\n" + "=" * 96) + print("【四】那需要多高的实时精度才能翻正") + print("=" * 96) + print("【三】显示只有「是不是真端点」这一个变量在起作用(组内各档持平)。" + "\n于是 PF 只是两组按精度 p 的混合,可以直接解出盈亏平衡精度:") + net = {} + for k, g in d.groupby(d.hit): + ns = [] + for sym, x in g.groupby("sym"): + x = x.reset_index(drop=True) + t = pd.DataFrame({"entry_idx": x.i_sure.values, + "direction": x.dir.values, + "atr_at_entry": x.atr_at_entry.values}) + res = simulate(cache[sym], t, x.to_ext.values + 1.0, + r_scale=False) + ok = res.g.notna().values + from lib.exit_model import fee_of + ns.append(res.g[ok].values + - fee_of(res.r[ok].values, res.c[ok].values)) + net[bool(k)] = np.concatenate(ns) + + def mix_pf(p: float) -> float: + """精度 p 时的 PF:两组按 p 加权(组内分布不变,只变权重)。""" + h, m = net[True], net[False] + w = (p * h[h > 0].sum() / len(h) + + (1 - p) * m[m > 0].sum() / len(m)) + l = (p * -h[h <= 0].sum() / len(h) + + (1 - p) * -m[m <= 0].sum() / len(m)) + return w / l if l > 0 else np.inf + + grid = [0.287, 0.35, 0.44, 0.5, 0.6, 0.7, 0.8, 1.0] + print(pd.DataFrame([{ + "实时精度": f"{p*100:.1f}%", "PF": round(mix_pf(p), 2), + "备注": {0.287: "← 当前基线", 0.44: "← ext_run Q4 已达到", + 1.0: "← step66 上界"}.get(p, "")} for p in grid] + ).to_string(index=False)) + lo, hi = 0.287, 1.0 + if mix_pf(hi) > 1 > mix_pf(lo): + for _ in range(40): + mid = (lo + hi) / 2 + lo, hi = (mid, hi) if mix_pf(mid) < 1 else (lo, mid) + print(f"\n **盈亏平衡精度 ≈ {(lo+hi)/2*100:.1f}%** " + f"(当前 28.7%,ext_run Q4 已到 44.0%)") + print(" 这就是「改识别规则」要够到的具体门槛,且它只是毛平衡;" + "\n 还要再扣 §3.396 的因果滞后(实盘比 sure_time 再晚 15 根)。") + + +if __name__ == "__main__": + main()