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
+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()