Files
Chan/research/step71_trace.py
T
jackyu66gitandCursor f14ef540c1 research: 订正——「额外信号 74%」是按条数统计的放大,按中枢是 52%
用户指出「原来的计算是没有问题的,是你算错了」。改用逐笔对账(不做统计,
把两边中间量全摆出来),结论一半一半。

引擎与我的口径都没问题:36 个额外信号 100% 归为 A 类(全量扫描区间不覆盖
入场根,即 available_ts 棘轮),0% 属于我的 bug;索引逐行对齐(5001=5001,
时间戳全同);额外信号的中枢在全量里全都存在且 (zg,zd) 完全一致,中枢不重画。
棘轮幅度实测 32~234 根。

但同一张表暴露了真正算错的地方:全量每个中枢恰好产出 1.00 个信号,回放里
同一中枢平均触发 2.12 次、最多 5 次。max_per_zone=1 只保证每次扫描返回一个,
但扫描起点随棘轮后移、越过旧入场点,同一中枢会重新产出「第一个」。

所以先前的「额外信号占 74%」不成立——那是按信号条数统计的,按中枢算是
17/33≈52%。这也把数量级对上了:§5.41 的 6.5% 分母是全部中枢,而会产出信号的
中枢里被改过的占比自然更高,两个数不矛盾,先前直接对比也是错的。

仍成立:棘轮机制、额外信号确实亏钱。已推翻:「实盘会多开 74% 的仓」。
真实倍数取决于执行层是否按中枢去重/冷却,该层从未审计,现已提为最高优先级
的前置项——查清前不要据 PF 0.73 动实盘参数。

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

212 lines
8.6 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.
"""逐笔追「额外信号」:全量那一遍到底为什么没产出它。
用户说「原来的计算是没有问题的,是你算错了」。之前几轮都是统计口径,
容易把自己的 bug 说成市场现象。这次不做统计,挑具体信号逐个对账。
对每个额外信号,把两边的中间量全摆出来:
回放侧 中枢 (zg,zd)、available_ts、扫描起点、入场根
全量侧 同一个中枢是否存在、它的 available_ts、扫描区间 [start, start+200]
入场根落不落在这个区间里、`diag` 记的拒绝原因
四种可能的结论,指向完全不同的处理:
A 全量里那个中枢的扫描区间**不覆盖**入场根
-> available_ts 棘轮,机制成立,不是 bug
B 中枢在全量里**不存在**
-> 中枢集合本身有差异,要查是不是我窗口用错了
C 区间覆盖了、全量却仍没出信号
-> 两边输入不同(我传错了 df/zones),**是我的 bug**
D 入场根索引对不上(差几根)
-> 索引口径错,`chan.dataframe` 与原始 df 不是 1:1**是我的 bug**
D 尤其要查:回放里我用原始 df 的下标 i 当 key,全量用的是 `cdf` 的 entry_idx
两者只有在 `TF_DF.dataframe` 与输入逐行对齐时才等价。
"""
from __future__ import annotations
import argparse
import sys
import warnings
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))
WIN, MAX_GROW, SCAN = 2001, 500, 200
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--sym", default="BTC")
ap.add_argument("--tf", default="5m")
ap.add_argument("--rows", type=int, default=45_000)
ap.add_argument("--steps", type=int, default=3_000)
ap.add_argument("--show", type=int, default=8)
args = ap.parse_args()
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
df = fetch_ohlcv(f"{args.sym}/USDT:USDT", args.tf, args.rows)
df = df.iloc[-(WIN + args.steps):].reset_index(drop=True)
full = TF_DF(df, 1, args.tf)
cdf = ensure_timestamp(full.dataframe)
print("=" * 92)
print("【0】先查 D:索引口径是否 1:1")
print("=" * 92)
print(f"原始 df 行数 {len(df)} · TF_DF.dataframe 行数 {len(cdf)} "
f"-> {'一致' if len(df) == len(cdf) else '**不一致,索引口径有问题**'}")
if len(df) == len(cdf):
t_df = pd.to_datetime(df["date"])
t_cd = pd.to_datetime(cdf["date"])
if t_df.dt.tz is not None:
t_df = t_df.dt.tz_localize(None)
if t_cd.dt.tz is not None:
t_cd = t_cd.dt.tz_localize(None)
same = int((t_df.values == t_cd.values).sum())
print(f"逐行时间戳相同 {same}/{len(df)} "
f"-> {'逐行对齐' if same == len(df) else '**有错位**'}")
zsf = full.cal_bi_zs_list_pure(full.bi_list)
zf = zones_from_zs_list(zsf, cdf)
diag_full: dict = {}
sig_full = find_fast_bsp3(cdf, zf, diag=diag_full)
fkeys = {(int(r.entry_idx), int(r.direction))
for r in sig_full.itertuples()}
ts = cdf["timestamp"].to_numpy()
print(f"\n全量:中枢 {len(zf)} 个,信号 {len(sig_full)} 个")
print(f"全量 diag{diag_full}")
# ---- 回放 ----
rec = []
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, args.tf)
chan.init_stream(w, 1, args.tf)
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
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:
continue
except Exception: # noqa: BLE001
continue
for r in s.itertuples():
zi = int(r.zone_i)
rec.append({
"i": i, "d": int(r.direction),
"in_full": (i, int(r.direction)) in fkeys,
"zg": float(z.zg.iloc[zi]), "zd": float(z.zd.iloc[zi]),
"avail_rt": int(z.available_ts.iloc[zi]),
"win_len": len(sub),
"bo": int(r.bo_idx), "last": last,
})
rp = pd.DataFrame(rec)
if rp.empty:
print("回放无信号")
return
print(f"\n回放:信号 {len(rp)} 个 · 其中全量也有 "
f"{int(rp.in_full.sum())} · 额外 {int((~rp.in_full).sum())}")
# ---- 逐笔对账 ----
print("\n" + "=" * 92)
print(f"【1】抽 {args.show} 个额外信号逐笔对账")
print("=" * 92)
fzg, fzd = zf.zg.to_numpy(float), zf.zd.to_numpy(float)
fav = zf.available_ts.to_numpy()
n = len(cdf)
rows = []
for r in rp[~rp.in_full].head(args.show).itertuples():
m = (np.abs(fzg - r.zg) < 1e-9) & (np.abs(fzd - r.zd) < 1e-9)
if not m.any():
rows.append({"入场根": r.i, "中枢在全量": "**不存在**",
"结论": "B 中枢集合有差异"})
continue
j = int(np.argmax(m))
start = int(np.searchsorted(ts, fav[j], side="left"))
end = min(start + SCAN, n)
cover = start <= r.i < end
rows.append({
"入场根": r.i, "中枢在全量": "存在",
"回放avail根": int(np.searchsorted(ts, r.avail_rt, side="left")),
"全量avail根": start,
"棘轮(根)": start - int(np.searchsorted(ts, r.avail_rt,
side="left")),
"全量扫描区间": f"[{start},{end})",
"覆盖入场根": "是" if cover else "否",
"结论": "C **是我的bug**" if cover else "A 棘轮,机制成立",
})
print(pd.DataFrame(rows).to_string(index=False))
print("""
A = 全量扫描区间不覆盖入场根(available_ts 棘轮后移),机制成立
B = 中枢在全量里不存在 -> 中枢集合有差异,要查窗口
C = 区间覆盖了全量却没出信号 -> 两边输入不同,是我的 bug""")
print("\n" + "=" * 92)
print("【2】全体额外信号按结论归类")
print("=" * 92)
cnt = {"A 棘轮": 0, "B 中枢不存在": 0, "C 我的bug": 0}
for r in rp[~rp.in_full].itertuples():
m = (np.abs(fzg - r.zg) < 1e-9) & (np.abs(fzd - r.zd) < 1e-9)
if not m.any():
cnt["B 中枢不存在"] += 1
continue
j = int(np.argmax(m))
start = int(np.searchsorted(ts, fav[j], side="left"))
cnt["C 我的bug" if start <= r.i < min(start + SCAN, n)
else "A 棘轮"] += 1
tot = max(sum(cnt.values()), 1)
print(pd.DataFrame([{"结论": k, "个数": v, "占比": f"{v/tot*100:.1f}%"}
for k, v in cnt.items()]).to_string(index=False))
print("\n" + "=" * 92)
print("【3】按信号条数统计是否被同一中枢的重复触发放大了")
print("=" * 92)
print("上表可见 2691/2707/2750 的全量 avail 同为 2767,是**同一个中枢**触发三次。")
print("`max_per_zone=1` 只保证「每次扫描返回一个」,但扫描起点随棘轮后移,")
print("越过旧入场点后同一中枢会重新产出「第一个」——于是按条数统计被放大。\n")
rows = []
for nm, x in [("额外", rp[~rp.in_full]), ("全量也有", rp[rp.in_full])]:
if x.empty:
continue
nz = x.groupby(["zg", "zd"]).size()
rows.append({"分组": nm, "信号条数": len(x), "不同中枢数": len(nz),
"每中枢触发": round(len(x) / len(nz), 2),
"最多触发": int(nz.max())})
print(pd.DataFrame(rows).to_string(index=False))
print("""
若「额外」组每中枢触发 >> 1 而「全量也有」组 ≈ 1,则先前那个「额外信号占 74%」
是**按条数**统计的放大结果,不等于实盘会多开 74% 的仓。
真实多开多少,取决于执行层对同一中枢是否去重/冷却 —— 那一层仍未审计。""")
if __name__ == "__main__":
main()