research: 二类买卖点定性为无信息,一二类线整体关闭

用户提出悖论:二类按定义依赖一类(引擎里 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 <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 22:54:31 +08:00
co-authored by Cursor
parent 7292ef5dd4
commit 380d6d61cd
2 changed files with 318 additions and 0 deletions
+61
View File
@@ -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.75m 反手 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)。差距可能来自
+257
View File
@@ -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()