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:
@@ -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.7,5m 反手 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)。差距可能来自
|
||||
|
||||
@@ -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