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>
This commit is contained in:
jackyu66git
2026-08-27 17:47:41 +08:00
co-authored by Cursor
parent b286cb0137
commit 66061f79a1
45 changed files with 107878 additions and 80745 deletions
+170
View File
@@ -0,0 +1,170 @@
"""Step 34require_touch=False 之后,回抽容差 tol 应该压到多小。
step29 发现放宽 tol 会让三个级别全面恶化。原因是语义变了:
不再要求回抽触边界后,tol 唯一的作用是「哪些K线算回抽中、从而跳过转强判定」,
所以 tol 越大入场越晚。若这个推断成立,tol 应当一路压到 0 乃至完全禁用。
tol<0 表示禁用跳过:突破后每一根都检查转强,入场最早。
同时输出滞后分布,确认改善确实来自入场提前而非别的原因。
"""
from __future__ import annotations
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)
SL, TP, MAXB = 1.5, 3.0, 48
FEE, SLIP = 0.0004, 0.0001
BEST = {"5m": "30m", "15m": "1h", "30m": "2h"}
TOLS = [(-1.0, "禁用跳过"), (0.0, "tol 0"), (0.001, "tol 0.1%"),
(0.003, "tol 0.3%(现用)"), (0.010, "tol 1%")]
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.breakout import run_trades
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 = 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)
if df_h is None or len(df_h) < 300:
return None
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)
out = []
for tol, name in TOLS:
sig = find_fast_bsp3(cdf, zones, tol=tol)
if sig.empty or len(sig) < 20:
continue
sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left")
sig = attach_htf_context(sig, cdf, tl, "h1")
entries = list(zip(sig["entry_idx"].astype(int),
sig["direction"].astype(int)))
tr = run_trades(cdf, entries, SL, TP, MAXB, fee=0.0, entry_delay=1)
if tr.empty:
continue
m = sig.set_index("entry_idx")
tr["mode"], tr["symbol"], tr["ltf"] = name, sym, ltf
tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()]
for c in ("h1_agree", "z_above", "z_below", "direction", "lag"):
tr[c if c != "direction" else "dir_sig"] = tr["entry_idx"].map(m[c])
out.append(tr)
if not out:
return None
return {"task": f"{sym} {ltf}", "trades": pd.concat(out, ignore_index=True)}
except Exception as e:
return {"task": f"{sym} {ltf}", "error": repr(e)[:250]}
def stat(g: pd.DataFrame, label: str, minn: int = 25) -> dict:
r = g["gross"].to_numpy() - FEE - SLIP
if len(r) < minn:
return {}
w, o = r[r > 0], r[r <= 0]
sd = r.std(ddof=1)
t10 = r[r <= np.quantile(r, 0.90)]
return {"分组": label, "笔数": len(r),
"滞后": f"{g['lag'].mean():.1f}",
"胜率": f"{(r > 0).mean() * 100:.1f}%",
"中位": f"{np.median(r) * 100:+.3f}%",
"PF": f"{w.sum() / abs(o.sum()):.2f}" if len(o) else "inf",
"t值": f"{r.mean() / (sd / np.sqrt(len(r))):+.2f}",
"剔10%PF": f"{t10[t10 > 0].sum() / abs(t10[t10 <= 0].sum()):.2f}"}
def main() -> None:
tasks = [(s, l) for l in BEST for s in ("BTC", "ETH", "SOL")]
print(f"[tol 扫描] {len(tasks)} 个任务 × {len(TOLS)}\n", flush=True)
res = []
with ProcessPoolExecutor(max_workers=5) 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)
print(f" [{i}/{len(tasks)}] {r['task']}", flush=True)
if not res:
return
allt = pd.concat([r["trades"] for r in res], ignore_index=True)
allt.to_csv(HERE / "out" / "step34_tol.csv", index=False)
allt["push"] = np.where(allt["dir_sig"] == 1, allt["z_above"], allt["z_below"])
allt["push"] = allt["push"].fillna(False).astype(bool)
fin = allt[(allt["h1_agree"] == 1) & allt["push"]]
order = {n: i for i, (_, n) in enumerate(TOLS)}
print("=" * 118)
print("########## 1. 全级别合并(同向+阶梯)##########")
rows = [stat(g, m) for m, g in fin.groupby("mode")]
df = pd.DataFrame([r for r in rows if r])
print(df.assign(_k=df["分组"].map(order)).sort_values("_k")
.drop(columns="_k").to_string(index=False))
print("\n########## 2. 分级别 ##########")
for ltf, g in fin.groupby("ltf"):
rows = [stat(x, f"{ltf} {m}") for m, x in g.groupby("mode")]
rows = [r for r in rows if r]
if rows:
d = pd.DataFrame(rows)
d["_k"] = d["分组"].str.split(" ", n=1).str[1].map(order)
print(d.sort_values("_k").drop(columns="_k").to_string(index=False))
print("\n########## 3. 资金曲线 ##########")
for _, name in TOLS:
g = fin[fin["mode"] == name].sort_values("date")
if len(g) < 30:
continue
r = g["gross"].to_numpy() - FEE - SLIP
lev = np.clip(0.01 / np.clip(g["risk_pct"].to_numpy(), 0.002, None), 0, 20)
pnl = r * lev
eq = np.cumprod(1 + pnl)
yrs = (pd.to_datetime(g["date"]).max() - pd.to_datetime(g["date"]).min()).days / 365.25
dd = (1 - eq / np.maximum.accumulate(eq)).max()
print(f" {name:>14}: n={len(r):>4}{len(r) / yrs:>3.0f}"
f"年化 {(eq[-1] ** (1 / yrs) - 1) * 100:+7.1f}% 回撤 {dd * 100:5.1f}% "
f"Sharpe {pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(pnl) / yrs):5.2f}")
if __name__ == "__main__":
main()