Files
Chan/research/step38_pit.py
T
jackyu66gitandCursor 66061f79a1 research: 低滞后信号口径定稿与实盘前偏差审计
fast_bsp3 改用 tol=-1 + require_touch=False,信号滞后从 5.8 根降到 2.2 根。
滞后与收益严格单调(年化 370% -> 906%,同一份数据同一套成本),
这是本轮提升的主因,也意味着实盘延迟会直接侵蚀收益。

新增 step31~39 验证策略能否落地:
- 跨品种样本外——8 个未参与调参的币,PF 2.73 / t 28.5,无一为负
- 时点重建——只喂到信号那一根重算,同根命中 100%,确认无未来函数;
  1m 在 2000 根窗口即饱和,计算耗时 0.20s
- 偏差审计——多空对称、中枢生效时刻零回退、滑点稳健至 30bp、持仓几乎不重叠
- 消融——alpha 来自缠论中枢的上下文定位,而非「收盘转强」这个触发动作

补 research/HANDOFF.md:记录确切口径与参数、已排除的偏差、
已验证无效因而不必重做的方向,以及下一步用影子交易器实测执行滑点的方案。

清理 step1~20 的输出:早期方法论已被推翻(存在未来函数偏差),
其结论不再被引用;脚本保留,需要时可重跑。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 17:47:41 +08:00

209 lines
8.7 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.
"""Step 38point-in-time 重建——回测的中枢,在当时那一刻真的存在吗。
incremental.py 开头写明「最后一笔 is_sure 允许收回」。而中枢生效时刻
available_ts 取的正是笔的 sure_time,那是全量重算得出的。
此前所有审计(后移入场、剔尾、多空、时间样本外、跨品种)都共用同一份
全量结构,无法发现这类偏差。
本步对每个信号做严格的时点重建:只喂到信号那一根为止的数据,
重跑 TF_DF -> 中枢 -> fast_bsp3,看该信号是否真的在那一刻出现。
召回 全量口径的信号,在时点口径下同一根也出现
偏移 出现了但不在同一根(早/晚几根)
消失 时点口径下完全没有
另外在随机非信号点上做同样重建,量化时点口径的假阳性——
那是实盘会真的下单、而回测里根本不存在的交易。
"""
from __future__ import annotations
import argparse
import os
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")
for v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(v, "1")
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
pd.set_option("display.width", 320)
BEST = {"15m": "1h", "30m": "2h"}
WINDOW = 4000 # 时点重建时回看多少根,实盘也不会带六年历史
TOLERANCE = 3 # 判定「同一时刻」允许的根数偏移
def pit_signals(df_slice: pd.DataFrame, ltf: str) -> set[int]:
"""只用给定切片重建,返回该切片内的信号在切片中的下标集合。"""
from chanlun import TF_DF
from lib.fast_bsp3 import find_fast_bsp3
from lib.nested_level import build_htf_zones
chan = TF_DF(df_slice, 1, ltf)
cdf = chan.dataframe
zones = build_htf_zones(cdf, ltf, chan=chan)
if zones.empty:
return set()
sig = find_fast_bsp3(cdf, zones.reset_index(drop=True))
if sig.empty:
return set()
return set(sig["entry_idx"].astype(int).tolist())
def run_one(task: tuple) -> dict | None:
import warnings as _w
_w.filterwarnings("ignore")
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
from chanlun import TF_DF
from lib.data import fetch_ohlcv
from lib.fast_bsp3 import find_fast_bsp3
from lib.fx_signal import extract_fx_signals, signals_to_frame
from lib.nested_bsp import attach_htf_context, htf_fx_timeline
from lib.nested_level import build_htf_zones
sym, ltf, n_probe = task
pair = f"{sym}/USDT:USDT"
try:
df_l = fetch_ohlcv(pair, ltf, 10**9)
if df_l is None or len(df_l) < 3000:
return None
chan_l = TF_DF(df_l, 1, ltf)
cdf = chan_l.dataframe
zones = build_htf_zones(cdf, ltf, chan=chan_l).reset_index(drop=True)
if zones.empty:
return None
z = zones.copy()
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))
df_h = fetch_ohlcv(pair, BEST[ltf], 10**9)
chan_h = TF_DF(df_h, 1, BEST[ltf])
s = signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe))
tl = htf_fx_timeline(s, chan_h.dataframe)
# 全量口径(回测用的那一份)
full = find_fast_bsp3(cdf, zones)
full = full.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left")
full = attach_htf_context(full, cdf, tl, "h1")
full["push"] = np.where(full["direction"] == 1, full["z_above"], full["z_below"])
fin = full[(full["h1_agree"] == 1) & full["push"].fillna(False).astype(bool)]
idx_all = fin["entry_idx"].astype(int).to_numpy()
idx_all = idx_all[idx_all >= WINDOW]
if len(idx_all) < 20:
return None
rng = np.random.default_rng(7)
probe = (rng.choice(idx_all, size=min(n_probe, len(idx_all)), replace=False)
if len(idx_all) > n_probe else idx_all)
rows = []
for i in sorted(probe):
sl = cdf.iloc[i - WINDOW + 1: i + 1].reset_index(drop=True)
pit = pit_signals(sl, ltf)
last = len(sl) - 1 # 信号那一根在切片中的位置
hit_exact = last in pit
near = [p - last for p in pit if abs(p - last) <= TOLERANCE]
rows.append({"idx": int(i), "exact": hit_exact,
"near": bool(near), "shift": min(near, key=abs) if near else np.nan})
# 假阳性:随机非信号点,看时点口径是否在当根给出信号
pool = np.setdiff1d(np.arange(WINDOW, len(cdf) - 1), idx_all)
fp_probe = rng.choice(pool, size=min(len(probe), len(pool)), replace=False)
fp = 0
for i in sorted(fp_probe):
sl = cdf.iloc[i - WINDOW + 1: i + 1].reset_index(drop=True)
if (len(sl) - 1) in pit_signals(sl, ltf):
fp += 1
return {"task": f"{sym} {ltf}", "sym": sym, "ltf": ltf,
"probe": pd.DataFrame(rows), "n_full": len(idx_all),
"fp": fp, "n_fp": len(fp_probe)}
except Exception as e:
return {"task": f"{sym} {ltf}", "error": repr(e)[:250]}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbols", default="BTC,ETH,SOL,BNB,ADA,LINK")
ap.add_argument("--probe", type=int, default=120)
ap.add_argument("--workers", type=int, default=6)
args = ap.parse_args()
syms = [s.strip() for s in args.symbols.split(",")]
tasks = [(s, l, args.probe) for l in BEST for s in syms]
print(f"[时点重建] {len(tasks)} 个任务 × 每个抽 {args.probe} 个信号"
f"(窗口 {WINDOW} 根)\n", flush=True)
res = []
with ProcessPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(run_one, t): t for t in tasks}
for i, f in enumerate(as_completed(futs), 1):
r = f.result()
if r is None or "error" in (r or {}):
print(f" [{i}] 跳过 {(r or {}).get('error', '')}", flush=True)
continue
res.append(r)
p = r["probe"]
print(f" [{i}/{len(tasks)}] {r['task']} 命中 "
f"{p['exact'].mean() * 100:.0f}% / 容差内 {p['near'].mean() * 100:.0f}%",
flush=True)
if not res:
return
print("\n" + "=" * 104)
print("########## 1. 全量口径的信号,在时点口径下还在吗 ##########")
rows = []
for r in res:
p = r["probe"]
rows.append({"品种": r["sym"], "级别": r["ltf"], "全量信号数": r["n_full"],
"抽检": len(p),
"同根命中": f"{p['exact'].mean() * 100:.1f}%",
f{TOLERANCE}根内": f"{p['near'].mean() * 100:.1f}%",
"完全消失": f"{(~p['near']).mean() * 100:.1f}%",
"中位偏移": (f"{np.nanmedian(p['shift']):+.1f}"
if p["near"].any() else "—")})
d = pd.DataFrame(rows).sort_values(["级别", "品种"])
print(d.to_string(index=False))
print("\n########## 2. 分级别汇总 ##########")
for ltf in BEST:
sub = [r for r in res if r["ltf"] == ltf]
if not sub:
continue
p = pd.concat([r["probe"] for r in sub], ignore_index=True)
print(f" {ltf}: 抽检 {len(p)} 同根命中 {p['exact'].mean() * 100:.1f}% "
f{TOLERANCE}根内 {p['near'].mean() * 100:.1f}% "
f"完全消失 {(~p['near']).mean() * 100:.1f}%")
print("\n########## 3. 假阳性:实盘会下、回测里没有的单 ##########")
rows = []
for r in res:
rows.append({"品种": r["sym"], "级别": r["ltf"], "抽检非信号点": r["n_fp"],
"当根却给信号": r["fp"],
"假阳性率": f"{r['fp'] / max(r['n_fp'], 1) * 100:.1f}%"})
print(pd.DataFrame(rows).sort_values(["级别", "品种"]).to_string(index=False))
p_all = pd.concat([r["probe"] for r in res], ignore_index=True)
fp_tot = sum(r["fp"] for r in res)
fp_n = sum(r["n_fp"] for r in res)
print("\n########## 结论 ##########")
print(f" 同根命中 {p_all['exact'].mean() * 100:.1f}%"
f{TOLERANCE}根内 {p_all['near'].mean() * 100:.1f}%"
f"完全消失 {(~p_all['near']).mean() * 100:.1f}%"
f"假阳性 {fp_tot / max(fp_n, 1) * 100:.1f}%")
print(" 同根命中率若接近 100%,说明结构在当时就已确定,回测口径可信;")
print(" 若大量消失或偏移,则回测收益里有一部分实盘永远拿不到。")
if __name__ == "__main__":
main()