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>
222 lines
9.2 KiB
Python
222 lines
9.2 KiB
Python
"""Step 36:Sharpe 8 的剩余嫌疑——中枢生效时刻的回退分支、多空对称性、时间样本外。
|
||
|
||
step35 已排除未来函数(后移入场平滑衰减)、滑点脆弱、持仓重叠虚高 t 值。
|
||
剩下三个必须查:
|
||
|
||
1 build_htf_zones 里 available_ts = sure_time or end_time。
|
||
一旦回退到 end_time 就是未来函数(笔端点早于笔确认)。统计回退比例,
|
||
并给出「只保留 sure_time 可用」的严格口径下的结果。
|
||
2 多空对称性。三个币六年整体上涨,若收益几乎全来自做多,那是 beta 不是 alpha。
|
||
3 时间样本外。tol、级别对、SL/TP 都是在全样本上挑的。
|
||
用 2019~2022 当样本内,2023~2026 当样本外,看衰减多少。
|
||
"""
|
||
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"}
|
||
SPLIT = pd.Timestamp("2023-01-01", tz="Asia/Shanghai")
|
||
|
||
|
||
def strict_zones(chan, cdf: pd.DataFrame) -> tuple[pd.DataFrame, dict]:
|
||
"""重算中枢表,区分 available_ts 来自 sure_time 还是回退到 end_time。"""
|
||
zs_list = chan.cal_bi_zs_list_pure(chan.bi_list)
|
||
ts_of = dict(zip(cdf["date"].dt.strftime("%Y-%m-%d %H:%M:%S"), cdf["timestamp"]))
|
||
rows = []
|
||
cnt = {"总数": 0, "用sure_time": 0, "回退end_time": 0, "两者皆无": 0}
|
||
for zs in zs_list:
|
||
bis = getattr(zs, "bi_list", [])
|
||
if not bis:
|
||
continue
|
||
cnt["总数"] += 1
|
||
last = bis[-1]
|
||
s_ts = ts_of.get(str(getattr(last, "sure_time", "") or ""))
|
||
e_ts = ts_of.get(str(getattr(last, "end_time", "") or ""))
|
||
if s_ts is not None:
|
||
cnt["用sure_time"] += 1
|
||
src = "sure"
|
||
avail = s_ts
|
||
elif e_ts is not None:
|
||
cnt["回退end_time"] += 1
|
||
src = "fallback"
|
||
avail = e_ts
|
||
else:
|
||
cnt["两者皆无"] += 1
|
||
continue
|
||
rows.append({"zg": float(zs.zg), "zd": float(zs.zd),
|
||
"available_ts": int(avail), "src": src})
|
||
out = pd.DataFrame(rows)
|
||
if not out.empty:
|
||
out = out.sort_values("available_ts").reset_index(drop=True)
|
||
return out, cnt
|
||
|
||
|
||
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
|
||
|
||
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, cnt = strict_zones(chan_l, cdf)
|
||
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 name, zsub in (("全部中枢", z),
|
||
("仅sure_time中枢", z[z["src"] == "sure"])):
|
||
if zsub.empty:
|
||
continue
|
||
zz = zsub.reset_index(drop=True)
|
||
zz["zone_i"] = np.arange(len(zz))
|
||
sig = find_fast_bsp3(cdf, zz, tol=-1.0)
|
||
if sig.empty or len(sig) < 20:
|
||
continue
|
||
sig = sig.merge(zz[["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"):
|
||
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}", "cnt": {"task": f"{sym} {ltf}", **cnt},
|
||
"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"{(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"[偏差审计] {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["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"]]
|
||
allt.to_csv(HERE / "out" / "step36_bias.csv", index=False)
|
||
|
||
print("=" * 110)
|
||
print("########## 1. 中枢生效时刻:有多少走了 end_time 回退分支 ##########")
|
||
c = pd.DataFrame([r["cnt"] for r in res])
|
||
c["级别"] = c["task"].str.split().str[1]
|
||
g = c.groupby("级别")[["总数", "用sure_time", "回退end_time", "两者皆无"]].sum()
|
||
g["回退占比"] = (g["回退end_time"] / g["总数"] * 100).round(1).astype(str) + "%"
|
||
print(g.to_string())
|
||
|
||
print("\n########## 2. 剔掉回退中枢后结果是否站得住 ##########")
|
||
print(pd.DataFrame([r for r in [stat(g_, m) for m, g_ in fin.groupby("mode")] if r]
|
||
).to_string(index=False))
|
||
|
||
strict = fin[fin["mode"] == "仅sure_time中枢"]
|
||
|
||
print("\n########## 3. 多空对称性(严格口径)##########")
|
||
rows = [stat(strict[strict["dir_sig"] == d], n)
|
||
for d, n in ((1, "做多(三买)"), (-1, "做空(三卖)"))]
|
||
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
||
print(" 若做空也显著为正,说明不是单纯吃上涨 beta。")
|
||
|
||
print("\n########## 4. 时间样本外:2019~2022 挑参数,2023~2026 验证 ##########")
|
||
rows = [stat(strict[strict["date"] < SPLIT], "样本内 2019~2022"),
|
||
stat(strict[strict["date"] >= SPLIT], "样本外 2023~2026")]
|
||
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
||
|
||
print("\n########## 5. 样本外分级别与分品种 ##########")
|
||
oos = strict[strict["date"] >= SPLIT]
|
||
rows = [stat(x, f"OOS {k}", minn=20) for k, x in oos.groupby("ltf")]
|
||
rows += [stat(x, f"OOS {k}", minn=20) for k, x in oos.groupby("symbol")]
|
||
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
||
|
||
print("\n########## 6. 样本外资金曲线(固定名义1倍,不复利)##########")
|
||
for label, g_ in (("样本内", strict[strict["date"] < SPLIT]),
|
||
("样本外", oos)):
|
||
g_ = g_.sort_values("date")
|
||
r = g_["gross"].to_numpy() - FEE - SLIP
|
||
yrs = (g_["date"].max() - g_["date"].min()).days / 365.25
|
||
eq = 1 + np.cumsum(r)
|
||
dd = (np.maximum.accumulate(eq) - eq).max()
|
||
print(f" {label}: n={len(r):>4} 年{len(r) / yrs:>3.0f}笔 "
|
||
f"年化 {(eq[-1] - 1) / yrs * 100:+6.1f}% 回撤 {dd * 100:5.1f}% "
|
||
f"Sharpe {r.mean() / r.std(ddof=1) * np.sqrt(len(r) / yrs):5.2f}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|