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

182 lines
7.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 37:跨品种样本外——在完全没参与过调参的币种上原样重跑。
到此为止所有参数(级别对、tol、SL/TP、过滤条件)都是在 BTC/ETH/SOL 上挑的,
即便 step36 的时间切分也仍是这三个币。本步换 8 个全新品种,
一个参数都不动,直接套用最终配置,看结论是否还在。
固定配置(不做任何搜索):
信号 find_fast_bsp3 默认(require_touch=False, tol=-1
过滤 大级别分型同向 + 中枢阶梯顺向推进
出场 1.5 ATR 止损 / 3.0 ATR 止盈 / 最多 48 根
成交 信号次根开盘,4bp 手续费 + 1bp 滑点
"""
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
PAIRS = [("5m", "30m"), ("15m", "1h"), ("30m", "2h")]
IS_SYMS = ["BTC", "ETH", "SOL"]
OOS_SYMS = ["BNB", "XRP", "DOGE", "ADA", "AVAX", "LINK", "LTC", "TRX"]
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, htf, group = 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 {"task": f"{sym} {ltf}", "error": "小级别数据不足"}
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 {"task": f"{sym} {ltf}", "error": "无中枢"}
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, htf, 10**9)
if df_h is None or len(df_h) < 300:
return {"task": f"{sym} {ltf}", "error": "大级别数据不足"}
chan_h = TF_DF(df_h, 1, htf)
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)
if sig.empty:
return {"task": f"{sym} {ltf}", "error": "无信号"}
sig = sig.merge(z[["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=FEE,
entry_delay=1, slippage=SLIP)
if tr.empty:
return {"task": f"{sym} {ltf}", "error": "无成交"}
m = sig.set_index("entry_idx")
tr["symbol"], tr["ltf"], tr["group"] = sym, ltf, group
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])
return {"task": f"{sym} {ltf}", "trades": tr}
except Exception as e:
return {"task": f"{sym} {ltf}", "error": repr(e)[:200]}
def stat(g: pd.DataFrame, label: str, minn: int = 25) -> dict:
r = g["ret"].to_numpy()
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)]
yrs = max((g["date"].max() - g["date"].min()).days / 365.25, 0.25)
return {"分组": label, "笔数": len(r), "年笔数": round(len(r) / yrs),
"胜率": 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, h, "样本内") for s in IS_SYMS for l, h in PAIRS]
+ [(s, l, h, "样本外") for s in OOS_SYMS for l, h in PAIRS])
print(f"[跨品种样本外] {len(tasks)} 个任务\n", flush=True)
res, skip = [], []
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 {}):
skip.append(f"{(r or {}).get('task', '?')}: {(r or {}).get('error', '')}")
continue
res.append(r)
print(f" [{i}/{len(tasks)}] {r['task']}{len(r['trades'])} 笔", flush=True)
if skip:
print(f"\n 跳过 {len(skip)} 个:")
for s in skip:
print(f" {s}")
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"]].copy()
fin.to_csv(HERE / "out" / "step37_oos.csv", index=False)
print("\n" + "=" * 116)
print("########## 1. 样本内(调参用的三个币)vs 样本外(八个全新币)##########")
rows = [stat(g, k) for k, g in fin.groupby("group")]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print(" 参数一个没动。若样本外仍显著为正,说明不是在三个币上过拟合。")
print("\n########## 2. 样本外逐个品种 ##########")
oos = fin[fin["group"] == "样本外"]
rows = [stat(g, s, minn=20) for s, g in oos.groupby("symbol")]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 3. 样本外分级别 ##########")
rows = [stat(g, f"样本外 {k}", minn=20) for k, g in oos.groupby("ltf")]
rows += [stat(g, f"样本内 {k}", minn=20)
for k, g in fin[fin["group"] == "样本内"].groupby("ltf")]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 4. 样本外多空 ##########")
rows = [stat(oos[oos["dir_sig"] == d], n, minn=20)
for d, n in ((1, "做多"), (-1, "做空"))]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 5. 样本外分年 ##########")
o = oos.copy()
o["y"] = o["date"].dt.year
rows = [stat(g, str(y), minn=20) for y, g in o.groupby("y")]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 6. 资金曲线(固定名义1倍,不复利,已扣 5bp##########")
for k, g in fin.groupby("group"):
g = g.sort_values("date")
r = g["ret"].to_numpy()
yrs = (g["date"].max() - g["date"].min()).days / 365.25
eq = 1 + np.cumsum(r)
dd = (np.maximum.accumulate(eq) - eq).max()
print(f" {k}: 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()