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:
@@ -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()
|
||||
Reference in New Issue
Block a user