Files
Chan/research/step64_b2.py
T
jackyu66gitandCursor 380d6d61cd 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>
2026-08-28 22:54:31 +08:00

258 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""二类买卖点:它只是一类的延迟版,还是一类里被确认过的那个子集?
用户提出的悖论:二类按定义是「一买之后回调、再继续趋势」,可一类已经证否了,
二类凭什么会好?
代码上悖论成立——`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()