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

174 lines
6.7 KiB
Python
Raw Permalink 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 31:引擎 B3/S3 的方向是不是反的。
引擎原版在完全相同条件下跑出 27.4% 胜率、t=-18.76,这不是滞后能解释的幅度,
稳定做反才会有这种数字。本步只做一件事:把引擎信号的方向翻转再跑一遍。
若翻转后由显著负转为显著正,说明 Chan_BSP_DIR 与实际交易方向的映射存在问题,
而不是三类买卖点本身无效——这会影响引擎所有下游使用者,不只是本次研究。
同时打印几个样本的价格上下文,用价格自己来判定哪个方向才是对的。
"""
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
def run_one(sym: str) -> 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 chanlun.core.ChanEnum import Chan_BSP_DIR
from lib.breakout import run_trades
from lib.data import fetch_ohlcv
ltf = "30m"
try:
df_l = fetch_ohlcv(f"{sym}/USDT:USDT", ltf, 10**9)
if df_l is None:
return None
chan = TF_DF(df_l, 1, ltf)
cdf = chan.dataframe
zs_list = chan.cal_bi_zs_list_pure(chan.bi_list)
if not zs_list:
return None
bsp_list = chan.find_all_bsp(chan.bi_list, zs_list) or []
idx_map = {k: i for i, k in
enumerate(cdf["date"].dt.strftime("%Y-%m-%d %H:%M:%S"))}
close = cdf["close"].to_numpy(dtype=float)
n = len(cdf)
rows = []
for b in bsp_list:
t = str(b.type).replace("Chan_BSP_TYPE.", "")
if t not in ("B3", "S3") or not b.is_sure or b.sure_time is None:
continue
ek = str(b.sure_time)
if ek not in idx_map:
continue
i = idx_map[ek]
zs = getattr(b, "zs", None)
rows.append({
"entry_idx": i,
"type": t,
"dir_enum": "BUY" if b.dir == Chan_BSP_DIR.BUY else "SELL",
"dir_mapped": 1 if b.dir == Chan_BSP_DIR.BUY else -1,
"zg": float(zs.zg) if zs is not None else np.nan,
"zd": float(zs.zd) if zs is not None else np.nan,
"entry_px": close[i],
# 入场后 12 根的实际涨跌,让价格自己说话
"fwd12": (close[min(i + 12, n - 1)] - close[i]) / close[i],
})
sig = pd.DataFrame(rows).drop_duplicates("entry_idx")
if len(sig) < 30:
return None
out = []
for name, flip in (("原方向", 1), ("反方向", -1)):
entries = [(int(i), int(d) * flip)
for i, d in zip(sig["entry_idx"], sig["dir_mapped"])]
tr = run_trades(cdf, entries, SL, TP, MAXB, fee=0.0, entry_delay=1)
if tr.empty:
continue
tr["mode"], tr["symbol"] = name, sym
m = sig.set_index("entry_idx")
for c in ("type", "dir_enum", "fwd12", "zg", "zd", "entry_px"):
tr[c] = tr["entry_idx"].map(m[c])
out.append(tr)
return {"sym": sym, "trades": pd.concat(out, ignore_index=True),
"sig": sig.assign(symbol=sym)}
except Exception as e:
return {"sym": sym, "error": repr(e)[:250]}
def stat(g: pd.DataFrame, label: str) -> dict:
r = g["gross"].to_numpy() - FEE - SLIP
if len(r) < 15:
return {}
w, o = r[r > 0], r[r <= 0]
sd = r.std(ddof=1)
return {"口径": label, "笔数": len(r),
"胜率": f"{(r > 0).mean() * 100:.1f}%",
"均收益": f"{r.mean() * 100:+.3f}%",
"中位": 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}"}
def main() -> None:
syms = ["BTC", "ETH", "SOL"]
res = []
with ProcessPoolExecutor(max_workers=3) as ex:
futs = {ex.submit(run_one, s): s for s in syms}
for f in as_completed(futs):
r = f.result()
if r is None or "error" in (r or {}):
print(f" {futs[f]} 跳过 {(r or {}).get('error', '')}", flush=True)
continue
res.append(r)
print(f" {r['sym']} ok", flush=True)
if not res:
return
allt = pd.concat([r["trades"] for r in res], ignore_index=True)
sig = pd.concat([r["sig"] for r in res], ignore_index=True)
print("\n" + "=" * 100)
print("########## 1. 原方向 vs 反方向(30m,引擎 B3/S3##########")
print(pd.DataFrame([r for r in
[stat(g, m) for m, g in allt.groupby("mode")] if r]
).to_string(index=False))
print("\n########## 2. 分类型看 ##########")
rows = []
for (m, t), g in allt.groupby(["mode", "type"]):
rows.append(stat(g, f"{m} {t}"))
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 3. 让价格自己说话:入场后12根的实际涨跌 ##########")
print(" B3 若真是买点,其后价格应偏涨(fwd12 均值>0")
rows = []
for t, g in sig.groupby("type"):
f = g["fwd12"].to_numpy()
sd = f.std(ddof=1)
rows.append({"类型": t, "枚举方向": g["dir_enum"].iloc[0], "笔数": len(f),
"后12根均涨跌": f"{f.mean() * 100:+.3f}%",
"上涨占比": f"{(f > 0).mean() * 100:.1f}%",
"t值": f"{f.mean() / (sd / np.sqrt(len(f))):+.2f}"})
print(pd.DataFrame(rows).to_string(index=False))
print("\n########## 4. 入场价相对中枢的位置(B3 应在中枢上方)##########")
rows = []
for t, g in sig.groupby("type"):
above = (g["entry_px"] > g["zg"]).mean() * 100
below = (g["entry_px"] < g["zd"]).mean() * 100
rows.append({"类型": t, "笔数": len(g),
"入场价>中枢上沿zg": f"{above:.1f}%",
"入场价<中枢下沿zd": f"{below:.1f}%",
"在中枢内": f"{100 - above - below:.1f}%"})
print(pd.DataFrame(rows).to_string(index=False))
print("\n 若 B3 大量落在中枢下方、S3 落在上方,则标签与几何位置矛盾。")
if __name__ == "__main__":
main()