research: 一类线门槛量化——全局只有一个变量,精度需 67~73%

把 §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 <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 23:17:48 +08:00
co-authored by Cursor
parent ed741c16b8
commit 93ed315a4b
2 changed files with 249 additions and 10 deletions
+54 -10
View File
@@ -1484,10 +1484,52 @@ MFE/MAE 给出机制:真底那批**逆向行程中位 2.24 ATR5m/ 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),
+195
View File
@@ -0,0 +1,195 @@
"""把 step66 的天花板换成实时可算的选样,看能兑现多少。
step66 测出上界**未来函数选样 + 结构止损** 5m PF 2.2915m 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()