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>
174 lines
6.7 KiB
Python
174 lines
6.7 KiB
Python
"""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()
|