Files
Chan/research/step70_sure_split.py
T
jackyu66gitandCursor 7585491481 research: 订正额外信号的机制——不是重画,是中枢终点在实时不可知
我先前把 B4 的额外信号归因为「中枢重画」,用户两次指出后逐条查证,归因是错的:

① 中枢边界不重画(用户对)。zg/zd 由前三笔定死,verify_window_sens/step39 验过。

② 中枢只由已确认的笔构成,这是结构性保证:cal_bi_zs_list_pure 要求
   bi1/bi2/bi3.is_sure,延伸时要求 leave_bi/back_bi.is_sure。
   step70 实测 789 个信号全部 z_sure=True,用户说的浅色 B4 在研究路径不存在。
   因此我提的「补一道 is_sure 门」是空操作,实测 PF 0.73→0.73,已标记不要再提。

③ 真机制是 available_ts 棘轮,§5.41 早写明:改动不是已确认的笔被推翻,
   而是中枢又吸收了新笔、bis[-1] 换人。available_ts 取末笔 sure_time,
   于是往后棘轮,find_fast_bsp3 的 200 根扫描窗口整体右移。
   消失的不是笔,是中枢的终点。

顺带澄清一个伪问题:中枢跨度 > 200 根不会导致突破落不进窗口,
因为扫描起点是中枢结束(末笔确认)而非起点,此时价格已在离开中枢。

仍未对上的是数量级:中枢层面 6.5% vs 信号层面 75%。假设是 max_per_zone=1
的放大(起点棘轮越过旧入场点,同一中枢反复产出「第一个」)。
step69_mechanism.py 已写好判据但三次后台运行被中断,标为下一步前置项。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 00:31:46 +08:00

281 lines
11 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.
"""把 B4 按「中枢末笔是否已确认」拆开——之前的统计把两种混在一起了。
用户指出:B4 有两种,**没确认的会出现然后消失,确认的不会**。而我在 step63/68
里统计的是 `find_fast_bsp3` 的全部输出,它的返回列里根本没有确认标志
entry_idx/direction/bo_idx/pb_idx/lag/depth/zg/zd/width_pct/occ/zone_i),
**两种被混在一起了**,所以「额外信号 74%」这个数字不能直接拿来说实盘。
确认状态在更上游,`zones_from_zs_list`
sure_key = str(getattr(key_bi, "sure_time", "") or "")
end_key = str(getattr(key_bi, "end_time", "") or "")
avail = ts_of.get(sure_key) or ts_of.get(end_key) # ← 静默退回 end_time
`ChanBI` 初始 `is_sure=False / sure_time=None`,确认时才 `set_is_sure(True, ...)`。
所以**末笔未确认时 available_ts 退回 end_time,而 end_time 随笔延伸而移动**
中枢的可用时刻跟着漂 —— 这正是「出现然后消失」的那一种。笔一旦确认,
`sure_time` 固定,中枢不再动。
这也解释了 §5.41 的数量级:中枢层面确认时刻只被改 6.5%,而我在信号层面看到 74%。
本脚本按 `zs.bi_list[-1].is_sure` 把信号拆成两组,分别看:
额外率 未确认组应显著高(会出现然后消失),已确认组应接近 0
收益 若亏损集中在未确认组,那么修法就是**信号侧加一道 is_sure 门**
而不是动出场参数或放弃 B4
⚠️ 同时要查的第二件事:**实盘路径到底交易哪一种。** `shadow_signal.py` 的三道
滤网是同向 + 阶梯 + ATR 门控,**没有 is_sure 这一道**。若未确认组确实是亏损源,
且实盘没有挡它,那这道门就是要补的东西。
"""
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" / "step70_sure.feather"
SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48
WIN, MAX_GROW, GATE_BP = 2001, 500, 8.0
def _sure_map(zs_list) -> dict:
"""(zg, zd) -> 末笔是否已确认。zones_from_zs_list 会按 available_ts 重排,
索引对不上,所以用中枢边界当键接回去。"""
m = {}
for zs in zs_list:
bis = getattr(zs, "bi_list", []) or []
if not bis:
continue
m[(round(float(zs.zg), 10), round(float(zs.zd), 10))] = \
bool(getattr(bis[-1], "is_sure", False))
return m
def replay(sym: str, ltf: str, htf: str, rows: int,
steps: int) -> pd.DataFrame | None:
from chanlun import TF_DF
from chanlun.analysis.fast_bsp import (
ensure_timestamp,
find_fast_bsp3,
zones_from_zs_list,
)
from lib.data import fetch_ohlcv
from lib.fx_signal import extract_fx_signals, signals_to_frame
from lib.nested_bsp import attach_htf_context, htf_fx_timeline
try:
df = fetch_ohlcv(f"{sym}/USDT:USDT", ltf, rows)
if df is None or len(df) < WIN + steps + 100:
return None
df = df.iloc[-(WIN + steps):].reset_index(drop=True)
full = TF_DF(df, 1, ltf)
cdf = ensure_timestamp(full.dataframe)
zsf = full.cal_bi_zs_list_pure(full.bi_list)
if not zsf:
return None
sig_full = find_fast_bsp3(cdf, zones_from_zs_list(zsf, cdf))
full_keys = {(int(r.entry_idx), int(r.direction))
for r in sig_full.itertuples()} if not sig_full.empty \
else set()
dh = fetch_ohlcv(f"{sym}/USDT:USDT", htf, rows)
tl = None
if dh is not None and len(dh) > 500:
ch = TF_DF(dh, 1, htf)
tl = htf_fx_timeline(
signals_to_frame(extract_fx_signals(ch, ch.dataframe)),
ch.dataframe)
rec: dict[tuple, dict] = {}
chan, anchor = None, 0
for i in range(WIN, len(df)):
if chan is None or (i - anchor) >= MAX_GROW:
w = df.iloc[i - WIN + 1:i + 1].copy()
chan = TF_DF(w, 1, ltf)
chan.init_stream(w, 1, ltf)
anchor = i
else:
chan.append_bar(df.iloc[i])
try:
zl = chan.cal_bi_zs_list_pure(chan.bi_list)
if not zl:
continue
sub = ensure_timestamp(chan.dataframe)
z = zones_from_zs_list(zl, sub)
if z is None or z.empty:
continue
sm = _sure_map(zl)
pg, pdn = z["zg"].shift(), z["zd"].shift()
z["z_above"], z["z_below"] = z["zd"] > pg, z["zg"] < pdn
z["zone_i"] = np.arange(len(z))
z["z_sure"] = [
sm.get((round(float(a), 10), round(float(b), 10)), False)
for a, b in zip(z["zg"], z["zd"])]
s = find_fast_bsp3(sub, z)
if s is None or s.empty:
continue
last = len(sub) - 1
s = s[s["entry_idx"].astype(int) == last]
if s.empty or "zone_i" not in s.columns:
continue
s = s.merge(z[["zone_i", "z_above", "z_below", "z_sure"]],
on="zone_i", how="left")
if tl is not None:
s = attach_htf_context(s, sub, tl, "h1")
except Exception: # noqa: BLE001
continue
for r in s.itertuples():
k = (i, int(r.direction))
if k in rec:
continue
push = getattr(r, "z_above" if r.direction == 1
else "z_below", None)
ag = getattr(r, "h1_agree", 0)
rec[k] = {
"sym": sym, "entry_idx": i, "direction": int(r.direction),
"in_full": k in full_keys,
"z_sure": bool(getattr(r, "z_sure", False)),
"ladder_ok": int(bool(pd.notna(push) and bool(push))),
"h1_agree": int(ag) if pd.notna(ag) else 0,
}
if not rec:
return None
r = pd.DataFrame(list(rec.values()))
from lib.exit_model import cfg_name, walk_exits
atr = cdf["atr"].to_numpy(float)
cl = cdf["close"].to_numpy(float)
r = r[(r.entry_idx < len(cdf) - 2)
& np.isfinite(atr[r.entry_idx.values])
& (atr[r.entry_idx.values] > 0)].reset_index(drop=True)
if r.empty:
return None
res = walk_exits(cdf, pd.DataFrame({
"entry_idx": r.entry_idx.values,
"direction": r.direction.values}), [SL], [RUNNER], [MAXB],
scale_at=SCALE_AT, runners=(RUNNER,), runner_stops=(RSTOP,))
cfg = cfg_name(SL, RUNNER, MAXB, RSTOP)
if len(res) != len(r):
return None
for c in ("g", "r", "c"):
r[c] = res[f"{cfg}_{c}"].to_numpy()
r["atr_pct"] = atr[r.entry_idx.values] / cl[r.entry_idx.values]
r["gate_ok"] = (r.atr_pct * 1e4 >= GATE_BP).astype(int)
r["pass_all"] = ((r.h1_agree == 1) & (r.ladder_ok == 1)
& (r.gate_ok == 1)).astype(int)
return r
except Exception as e: # noqa: BLE001
print(f" {sym} 失败: {type(e).__name__}: {e}", flush=True)
return None
def perf(g: pd.DataFrame) -> dict | None:
from lib.exit_model import fee_of, taker_notional
if len(g) < 15:
return None
net = g.g.values - fee_of(g.r.values, g.c.values)
gR = g.g.values / (SL * g.atr_pct.values)
w, o = net[net > 0].sum(), -net[net <= 0].sum()
return {
"笔数": len(g), "胜率": f"{(net > 0).mean()*100:.1f}%",
"PF": round(w / o, 2) if o > 0 else np.inf,
"余量bp": round(net.mean()
/ taker_notional(g.r.values, g.c.values).mean() * 1e4, 2),
"t值": round(gR.mean() / (gR.std(ddof=1) / np.sqrt(len(g))), 2),
}
def report(d: pd.DataFrame) -> None:
print("\n" + "=" * 92)
print("【一】判据:额外信号是不是集中在「末笔未确认」那一组")
print("=" * 92)
rows = []
for k, g in d.groupby(d.z_sure):
rows.append({"中枢末笔": "已确认" if k else "未确认", "信号数": len(g),
"额外(全量没有)": int((~g.in_full).sum()),
"额外率": f"{(~g.in_full).mean()*100:.1f}%"})
print(pd.DataFrame(rows).to_string(index=False))
print("""
用户的判断:「没确认的会出现然后消失,确认的不会」。
若已确认组额外率接近 0 -> 判断成立,之前 74% 是把两种混在一起统计的结果。""")
print("\n" + "=" * 92)
print("【二】亏损是不是也集中在未确认组")
print("=" * 92)
rows = []
for k, g in d.groupby(d.z_sure):
nm = "已确认" if k else "未确认"
for lab, x in [("全部", g), ("三道滤网后", g[g.pass_all == 1])]:
s = perf(x)
if s:
rows.append({"中枢末笔": nm, "口径": lab, **s})
print(pd.DataFrame(rows).to_string(index=False))
print("\n" + "=" * 92)
print("【三】若补一道 is_sure 门,实盘口径会变成什么样")
print("=" * 92)
rows = []
for nm, x in [
("现状:三道滤网", d[d.pass_all == 1]),
("**加 is_sure 门**", d[(d.pass_all == 1) & d.z_sure]),
(" 对照:仅未确认", d[(d.pass_all == 1) & ~d.z_sure]),
]:
s = perf(x)
if s:
rows.append({"口径": nm, **s})
print(pd.DataFrame(rows).to_string(index=False))
print("""
⚠️ `shadow_signal.py` 的三道滤网是同向 + 阶梯 + ATR 门控,**没有 is_sure**。
若加上这道门 PF 明显回升,那它就是要补进信号路径的东西。""")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbols", default="BTC,ETH,SOL,LINK,DOGE")
ap.add_argument("--ltf", default="5m")
ap.add_argument("--htf", default="30m")
ap.add_argument("--rows", type=int, default=45_000)
ap.add_argument("--steps", type=int, default=20_000)
ap.add_argument("--workers", type=int, default=5)
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(",")]
print(f"[确认态拆分] {len(syms)}× {args.steps} 根 · {args.ltf}\n",
flush=True)
parts = []
with ProcessPoolExecutor(max_workers=args.workers) as ex:
fut = {ex.submit(replay, s, args.ltf, args.htf, args.rows,
args.steps): s for s in syms}
for i, f in enumerate(as_completed(fut), 1):
r = f.result()
print(f" [{i}/{len(syms)}] {fut[f]} "
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()