否掉 bis[2]:修右边缘重画会把 alpha 打成零,重画是必付代价

§5.41 发现 available_ts 取「中枢最后一笔」是右边缘重画的根因,改取「第三笔」
能把重画率从 6.5% 压到 1.2%,当时据此判断它是「唯一可能同时改善收益与稳定性」
的改动。那个判断只测了稳定性,过早了。

8 个样本外币 × 30 万根 1m,两组共用同一个 TF_DF,只切 available_ts 的取法。
实盘口径(深色 ∧ ATR≥8bp,955 vs 989 笔):

  毛 R      0.933 → -0.000
  净均 R    0.798 → -0.147
  PF        3.22  → 0.80
  滑点余量  15.07 → -2.20 bp

判决依据是毛 R 那一行:扣任何费用之前 edge 就没了,所以不是成本、门控或出场
参数的问题,是信号本身不再有预测力。逐币 8/8 全部变差。滞后确实降了
(2.16 → 2.01),但换来的是另一批交易——两组重合度只有约 30%。

原因是中枢没发育完就下注,支撑/压力还没立住。「等中枢最后一笔」那段等待不是
可以优化掉的延迟,它就是 alpha 本身。由此得一条一般规则:任何以「让信号更早
确定」为目标的改动,先测毛 R,不能只看重画率和滞后。

开关 AVAIL_BI_INDEX 保留只为可复现该 A/B,默认 -1 维持现行口径。环境变量在
调用时解析而非 import 时——fork 启动的子进程会继承已 import 的模块,import
时读会固化成父进程的值。

顺带交叉验证:现行口径本次算出滑点余量 15.07bp,与用优化前代码算的同组同期
15.19bp 吻合。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 04:05:01 +08:00
co-authored by Cursor
parent 0b4d7693b8
commit d2fcb27d01
5 changed files with 1157 additions and 13 deletions
+195
View File
@@ -0,0 +1,195 @@
"""Step 47:中枢可用时刻取 bis[-1] 还是 bis[2] —— 按收益判,不按重画率判。
§5.41 发现 available_ts 取「中枢最后一笔」是右边缘重画的根因,改取「第三笔」
(中枢成立即固定)能把重画率从 6.5% 压到 1.2%。但那只测了稳定性。
小样本预检(60k 根 × 5 个币/周期)显示这不是一次「稳定性修补」:
信号数 +17% ~ +44%,而两组的**重合度只有约 30%**。
即 bis[2] 丢掉了原信号的多数,又换进来一批新的。
换句话说它是另一个策略,不是同一个策略的低延迟版。所以判据必须是扣费后的
净 R / PF / 滑点预算,重画率只能作为次要参考。
口径与 step44 一致(当前 1m 最优):
出场 SL 2.0 / 3 ATR 减半 / runner 目标 8 ATR / runner 止损留原位 / 48 根超时
成本 taker 2bp、maker 0.8bp,滑点只加在 taker 腿
门控 ATR >= 8bp
过滤 深色(同向 ∧ 阶梯)——实盘只做这一档,见 §3.38
两组共用同一个 TF_DF,只切 available_ts 的取法,确保差异只来自这一处。
"""
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", 340)
LTF, HTF = "1m", "5m"
SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48
GATE_BP = 8.0
VARIANTS = (("bis[-1] 现行", -1), ("bis[2] 中枢成立", 2))
def collect(sym: str, rows: int) -> pd.DataFrame | 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.analysis.fast_bsp import (
add_zone_ladder, attach_htf_agree, attach_zone_ladder,
build_htf_zones, find_fast_bsp3, htf_fx_timeline,
)
from lib.data import fetch_ohlcv
from lib.exit_model import cfg_name, walk_exits
try:
df = fetch_ohlcv(f"{sym}/USDT:USDT", LTF, rows)
if df is None or len(df) < 50_000:
return None
# lean:只需要笔/中枢/信号,跳过线段与 MACD 状态机(见 §5.6,已验证等价)
chan = TF_DF(df, 1, LTF, lean=True)
cdf = chan.dataframe
df_h = fetch_ohlcv(f"{sym}/USDT:USDT", HTF, 10 ** 9)
chan_h = TF_DF(df_h, 1, HTF, lean=True)
tl = htf_fx_timeline(chan_h, chan_h.dataframe)
idx_all, parts = None, []
for label, avail_bi in VARIANTS:
zones = build_htf_zones(cdf, LTF, chan=chan, avail_bi=avail_bi)
if zones.empty:
continue
zl = add_zone_ladder(zones.reset_index(drop=True))
sig = find_fast_bsp3(cdf, zl)
if sig.empty:
continue
sig = attach_zone_ladder(attach_htf_agree(sig, cdf, tl), zl)
res = walk_exits(cdf, sig, [SL], [RUNNER], [MAXB],
scale_at=SCALE_AT, runners=(RUNNER,), runner_stops=(RSTOP,))
cfg = cfg_name(SL, RUNNER, MAXB, RSTOP)
need = [f"{cfg}_g", f"{cfg}_r", f"{cfg}_c", f"{cfg}_b"]
if any(c not in res.columns for c in need):
continue
idx = sig["entry_idx"].to_numpy().astype(int)
atr = cdf["atr"].to_numpy(float)[idx]
close = cdf["close"].to_numpy(float)[idx]
out = res[need].copy()
out.columns = ["g", "r", "c", "b"]
out["sym"] = sym
out["variant"] = label
out["atr_pct"] = atr / close
out["lag"] = sig["lag"].to_numpy()
out["entry_idx"] = idx
out["htf_agree"] = sig["htf_agree"].to_numpy()
out["ladder_ok"] = sig["ladder_ok"].to_numpy()
parts.append(out)
return pd.concat(parts, ignore_index=True) if parts else None
except Exception as e:
print(f" {sym} 失败: {e!r}", flush=True)
return None
def describe(g: pd.DataFrame, label: str) -> dict:
from lib.exit_model import fee_of, taker_notional
if len(g) < 40:
return {"口径": label, "笔数": len(g), "备注": "样本不足"}
gross = g["g"].to_numpy()
reason, scaled = g["r"].to_numpy(), g["c"].to_numpy()
net = gross - fee_of(reason, scaled) # 未扣滑点:剩下的就是滑点余量
denom = SL * g["atr_pct"].to_numpy()
R, gR = net / denom, gross / denom
w, o = net[net > 0], -net[net <= 0].sum()
q = np.quantile(net[net > 0], 0.90) if (net > 0).any() else 0.0
t10 = net[(net > 0) & (net <= q)].sum()
tn = taker_notional(reason, scaled)
return {
"口径": label, "笔数": len(g),
"滞后": round(float(g["lag"].mean()), 2),
"胜率": f"{(net > 0).mean() * 100:.1f}%",
"毛R": round(gR.mean(), 3),
"净均R": round(R.mean(), 3),
"R夏普": round(R.mean() / R.std(ddof=1), 3),
"PF": round(w.sum() / o, 2) if o > 0 else np.inf,
"剔10%PF": round(t10 / o, 2) if o > 0 else np.inf,
"滑点余量bp": round(net.mean() / tn.mean() * 1e4, 2),
}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbols", default="BTC,ETH,SOL,XRP,DOGE,LINK,ADA,LTC")
ap.add_argument("--rows", type=int, default=300_000)
ap.add_argument("--workers", type=int, default=3)
args = ap.parse_args()
syms = [s.strip() for s in args.symbols.split(",")]
print(f"[available_ts A/B] {len(syms)}× {args.rows}{LTF}\n"
f"出场 SL{SL}/减半{SCALE_AT}/runner{RUNNER}/rstop{RSTOP}/{MAXB}根,"
f"ATR 门控 {GATE_BP:g}bp\n", flush=True)
parts = []
with ProcessPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(collect, s, args.rows): s for s in syms}
for i, f in enumerate(as_completed(futs), 1):
r = f.result()
if r is None:
print(f" [{i}/{len(syms)}] {futs[f]} 跳过", flush=True)
continue
parts.append(r)
n = r.groupby("variant").size().to_dict()
print(f" [{i}/{len(syms)}] {futs[f]} {n}", flush=True)
if not parts:
print("无结果")
return
d = pd.concat(parts, ignore_index=True)
d["a_bp"] = d["atr_pct"] * 1e4
d.to_feather(HERE / "out" / "step47_avail_bi.feather")
dark = (d["htf_agree"] == 1.0) & d["ladder_ok"]
for lab, dd in (("全部信号", d), ("深色(实盘口径)", d[dark])):
for gate_lab, ddd in (("未门控", dd), (f"ATR>={GATE_BP:g}bp", dd[dd.a_bp >= GATE_BP])):
print("\n" + "=" * 118)
print(f"########## {lab} / {gate_lab} ##########")
print(pd.DataFrame([describe(ddd[ddd.variant == v], v)
for v, _ in [(a, b) for a, b in VARIANTS]]).to_string(index=False))
print("\n########## 逐币(深色 + 门控)##########")
dd = d[dark & (d.a_bp >= GATE_BP)]
rows = []
for s, g in dd.groupby("sym"):
r = {"": s}
for v, _ in VARIANTS:
x = describe(g[g.variant == v], v)
tag = "现行" if "-1" in v else ""
r[f"{tag}_笔数"] = x.get("笔数")
r[f"{tag}_净均R"] = x.get("净均R")
r[f"{tag}_余量bp"] = x.get("滑点余量bp")
rows.append(r)
print(pd.DataFrame(rows).to_string(index=False))
print("\n判据:净均R 与滑点余量bp 同时不劣于现行,才值得换。"
"\n信号数变多本身不是好处——重合度只有约 30%,换的是另一批交易。")
if __name__ == "__main__":
main()