Files
Chan/research/step35_audit.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

218 lines
9.1 KiB
Python

"""Step 35:禁用回抽跳过后跑出 Sharpe 9 / 年化 906%,先当作可疑数字来审。
这个量级通常意味着某个假设塌了,本步专查三件事:
1 持仓重叠:滞后压到 2.2 根、年 334 笔后,同时在场的仓位是否变多。
若重叠严重,t=19 是虚高的(收益不独立),且实盘保证金撑不住。
2 滑点脆弱性:入场越早越接近突破根,成交价优势可能全靠那 2 根。
逐档加 5/10/20/30bp,看优势是否被吃光。
3 复利假设:年化 906% 是「每笔按 1% 风险、20 倍杠杆上限、连续复利」的产物。
改成固定名义仓位、不复利,看真实量级。
另外做一次未来函数的独立复核:把入场索引整体后移 1/2/3 根,
若收益随后移平滑衰减而非断崖归零,说明信号真实且优势来自时间价值。
"""
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 = 0.0004
BEST = {"5m": "30m", "15m": "1h", "30m": "2h"}
SLIPS = [0.0, 0.0005, 0.0010, 0.0020, 0.0030]
DELAYS = [1, 2, 3, 4]
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)
sig = find_fast_bsp3(cdf, zones, tol=-1.0) # 禁用回抽跳过
if sig.empty or len(sig) < 20:
return None
sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left")
sig = attach_htf_context(sig, cdf, tl, "h1")
sig["push"] = np.where(sig["direction"] == 1, sig["z_above"], sig["z_below"])
sig = sig[(sig["h1_agree"] == 1) & sig["push"].fillna(False).astype(bool)]
if len(sig) < 20:
return None
entries = list(zip(sig["entry_idx"].astype(int), sig["direction"].astype(int)))
bars = int(np.median(np.diff(cdf["timestamp"].to_numpy())))
out = []
for delay in DELAYS:
for slip in SLIPS:
if delay != 1 and slip != 0.0:
continue # 后移只在零滑点下扫,避免组合爆炸
tr = run_trades(cdf, entries, SL, TP, MAXB, fee=0.0,
entry_delay=delay, slippage=slip)
if tr.empty:
continue
tr["delay"], tr["slip"] = delay, slip
tr["symbol"], tr["ltf"] = sym, ltf
tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()]
tr["bar_ms"] = bars
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) -> dict:
r = g["ret"].to_numpy()
if len(r) < 25:
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"{(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 overlap(g: pd.DataFrame) -> dict:
"""按真实时间轴统计同时在场仓位数。"""
g = g.sort_values("date")
bar = g["bar_ms"].iloc[0] / 1000 / 60 # 每根多少分钟
st = pd.to_datetime(g["date"]).astype("int64") // 10**9 / 60
en = st + g["bars_held"].to_numpy() * bar
ev = np.concatenate([st.to_numpy(), en])
kind = np.concatenate([np.ones(len(g)), -np.ones(len(g))])
o = np.argsort(ev, kind="stable")
live = np.cumsum(kind[o])
# 按持续时长加权的平均并发
dur = np.diff(np.concatenate([ev[o], ev[o][-1:]]))
wt = dur[:len(live)]
return {"笔数": len(g), "峰值并发": int(live.max()),
"时长加权平均并发": round(float((live * wt).sum() / max(wt.sum(), 1e-9)), 2),
"有仓位时间占比": f"{(wt[live > 0].sum() / max(wt.sum(), 1e-9)) * 100:.1f}%"}
def main() -> None:
tasks = [(s, l) for l in BEST for s in ("BTC", "ETH", "SOL")]
print(f"[审计] {len(tasks)} 个任务\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["date"] = pd.to_datetime(allt["date"])
allt.to_csv(HERE / "out" / "step35_audit.csv", index=False)
base = allt[(allt["delay"] == 1) & (allt["slip"] == 0.0)]
print("=" * 110)
print("########## 1. 滑点脆弱性:优势是否全靠提前那两根 ##########")
rows = []
for slip in SLIPS:
g = allt[(allt["delay"] == 1) & (allt["slip"] == slip)]
rows.append(stat(g, f"次根开盘 +{slip * 1e4:.0f}bp"))
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print(" 含 4bp 手续费。若加到 20~30bp 仍显著为正,说明不是靠抢那两根。")
print("\n########## 2. 入场整体后移:真信号应平滑衰减,未来函数会断崖 ##########")
rows = []
for d in DELAYS:
g = allt[(allt["delay"] == d) & (allt["slip"] == 0.0)]
rows.append(stat(g, f"信号后 {d} 根开盘入场"))
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 3. 持仓重叠:t 值是否被虚高 ##########")
rows = []
for (sym, ltf), g in base.groupby(["symbol", "ltf"]):
rows.append({"品种": sym, "级别": ltf, **overlap(g)})
print(pd.DataFrame(rows).to_string(index=False))
print(" 单品种单级别若平均并发接近 1,说明各笔基本独立,t 值可信。")
print("\n########## 4. 组合层面并发(三品种三级别一起做)##########")
print(pd.DataFrame([{"全组合": "9 条腿", **overlap(base)}]).to_string(index=False))
print("\n########## 5. 剥掉复利与杠杆假设,看真实量级 ##########")
g = base.sort_values("date")
r = g["ret"].to_numpy()
yrs = (g["date"].max() - g["date"].min()).days / 365.25
lev = np.clip(0.01 / np.clip(g["risk_pct"].to_numpy(), 0.002, None), 0, 20)
for name, pnl, comp in (
("A 1%风险+20倍上限+复利", r * lev, True),
("B 1%风险+20倍上限+不复利", r * lev, False),
("C 固定名义1倍+不复利", r, False),
("D 固定名义3倍+不复利", r * 3, False),
):
if comp:
eq = np.cumprod(1 + pnl)
ann = (eq[-1] ** (1 / yrs) - 1) * 100
dd = (1 - eq / np.maximum.accumulate(eq)).max() * 100
else:
eq = 1 + np.cumsum(pnl)
ann = (eq[-1] - 1) / yrs * 100
dd = (np.maximum.accumulate(eq) - eq).max() * 100
sh = pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(pnl) / yrs)
print(f" {name:<26}: 年化 {ann:+8.1f}% 回撤 {dd:5.1f}% Sharpe {sh:5.2f}")
print(f" 平均单笔杠杆 {lev.mean():.1f}x(上限20),中位 {np.median(lev):.1f}x")
print(" 未含资金费率、未含同时持仓的保证金约束,C/D 才是可直接对照的量级。")
if __name__ == "__main__":
main()