Merge branch 'chan' of ssh://git.jackyu66.com:2222/jack/chan into chan

This commit is contained in:
jack
2026-08-28 04:50:16 +08:00
2 changed files with 183 additions and 0 deletions
+22
View File
@@ -315,6 +315,28 @@ push = z_above if direction == 1 else z_below
> 凡用到「这一簇共几个币」「我是第几个」「簇跨度多长」的,都含未来信息,
> 回测漂亮但实盘拿不到。
**✅ 唯一可交易的那条已通过样本外验证(step49)**:10 币 × 80 万根,
2025-02 ~ 2026-08 共 3941 笔,其中发现期(step48 用过的最近 208 天)1161 笔、
样本外 2780 笔。
| | 无前序 | 有前序 | 差 |
|---|---|---|---|
| 样本外 毛R | 1.086 | **1.275** | +0.189 |
| 样本外 余量 | 18.83bp | **24.89bp** | +6.06bp |
| 发现期 余量 | 14.14bp | 21.96bp | +7.82bp |
**逐时段 6/6 全部同向**(余量差 12.52 / 9.10 / 3.82 / 3.89 / 6.95 / 9.25bp
中位 8.03)。发现期毛R 差只有 0.085,**样本外反而放大到 0.189——不是过拟合
衰减,是发现期恰好偏保守**。占比很稳,各时段 13~19%。
ATR 混淆已排除:`有前序` 的 ATR 确实略高(中位 15.04 vs 13.13bp),但按 ATR
四分位分层后 **4/4 层同向**,层内余量差 3.29 / 8.39 / 5.16 / 4.14bp,与不分层
的 +6.06 同量级。且毛R 本身是 ATR 归一化指标,其 +0.19 不可能是 ATR 假象。
**可用方式**:这 16% 的信号能多容忍约 6bp 执行成本。可用于加仓位权重,
或对这批放宽 ATR 门控——ATR 最低层里 `有前序` 余量仍有 14.92bp(对照 11.63),
提示门控对它们可以更松。**但放宽门控尚未回测,先别改。**
> ⚠️ **「同分钟 vs 错开」是看过数据之后才划的切法,不是事先定的。**
> 208 天、158 个簇,post-hoc 切分容易切出噪声。**拿它当仓位规则前必须换一段
> 时间验证。** 目前可确定的只有「扎堆整体更好」(簇级也成立);
+161
View File
@@ -0,0 +1,161 @@
"""Step 49:簇内「有无前序信号」的样本外验证。
step48 在最近 208 天上发现:开仓时若过去 5 分钟内已有别的币发过信号,
该笔的滑点余量 21.88bp,明显高于无前序的 14.13bp+55%)。毛R 只差 8.8%
所以值得追的是余量这一条——1m 的生死线就在执行成本上。
但那是发现期内的数字,且「过去 5 分钟」这个窗口是我挑的。本步用更长的历史,
把发现期之外的时段单独拿出来看同一个差距还在不在。
只验这一个假设。158 个簇撑不起更多——每多验一个,假阳性概率就涨一截。
判据:OOS 各时段里 `有前序` 的余量应稳定高于 `无前序`,且方向一致。
"""
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", 320)
SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48
GATE_BP = 8.0
WIN_MIN = 5 # 前序窗口,与 step48 一致
OUT = HERE / "out" / "step49_cluster_oos.feather"
# 发现期:step48 用的就是最近这段
IS_START = pd.Timestamp("2026-01-30", tz="Asia/Shanghai")
def collect(sym: str, rows: int):
import warnings as _w
_w.filterwarnings("ignore")
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
from step48_signal_timing import collect as _c
return _c(sym, rows)
def split_stats(d: pd.DataFrame, label: str) -> pd.DataFrame:
from lib.exit_model import fee_of, taker_notional
d = d.sort_values("date").reset_index(drop=True)
t = d.date.values.astype("datetime64[m]").astype(np.int64)
# 只看过去:严格早于本笔、且在 WIN_MIN 分钟内。这是开仓时真正可知的信息。
prev = np.searchsorted(t, t, "left") - np.searchsorted(t, t - WIN_MIN, "left")
net = d.g.values - fee_of(d.r.values, d.c.values)
gR = d.g.values / (SL * d.atr_pct.values)
R = net / (SL * d.atr_pct.values)
tn = taker_notional(d.r.values, d.c.values)
rows = []
for m, lab in ((prev == 0, "无前序"), (prev >= 1, "有前序")):
if m.sum() < 15:
rows.append({"时段": label, "分组": lab, "笔数": int(m.sum()), "备注": "样本不足"})
continue
nn, rr, gg, tt = net[m], R[m], gR[m], tn[m]
w, o = nn[nn > 0].sum(), -nn[nn <= 0].sum()
rows.append({
"时段": label, "分组": lab, "笔数": int(m.sum()),
"占比": f"{m.mean()*100:.0f}%",
"胜率": f"{(nn > 0).mean()*100:.1f}%",
"毛R": round(gg.mean(), 3), "净均R": round(rr.mean(), 3),
"PF": round(w / o, 2) if o > 0 else np.inf,
"余量bp": round(nn.mean() / tt.mean() * 1e4, 2),
})
return pd.DataFrame(rows)
def report(d: pd.DataFrame) -> None:
print("\n" + "=" * 100)
print("########## 逐时段:有前序 vs 无前序 ##########")
tabs, deltas = [], []
d["半年"] = d.date.dt.to_period("2Q").astype(str)
periods = sorted(d["半年"].unique())
for p in periods:
g = d[d["半年"] == p]
if len(g) < 60:
continue
t = split_stats(g, p)
tabs.append(t)
if len(t) == 2 and "余量bp" in t.columns and t["余量bp"].notna().all():
a = t[t.分组 == "无前序"]["余量bp"].iloc[0]
b = t[t.分组 == "有前序"]["余量bp"].iloc[0]
ga = t[t.分组 == "无前序"]["毛R"].iloc[0]
gb = t[t.分组 == "有前序"]["毛R"].iloc[0]
deltas.append({"时段": p, "笔数": len(g),
"无前序余量": a, "有前序余量": b, "余量差": round(b - a, 2),
"无前序毛R": ga, "有前序毛R": gb, "毛R差": round(gb - ga, 3),
"方向": "✅ 同向" if b > a else "❌ 反向"})
if tabs:
print(pd.concat(tabs, ignore_index=True).to_string(index=False))
if deltas:
print("\n########## 判据:余量差是否稳定为正 ##########")
dd = pd.DataFrame(deltas)
print(dd.to_string(index=False))
ok = (dd["余量差"] > 0).sum()
print(f"\n{len(dd)} 个时段中 {ok} 个方向一致(有前序余量更高)")
print(f"余量差 中位 {dd['余量差'].median():.2f}bp、均值 {dd['余量差'].mean():.2f}bp")
print(f"毛R差 中位 {dd['毛R差'].median():.3f}、均值 {dd['毛R差'].mean():.3f}")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbols", default="BTC,BNB,ETH,SOL,LINK,LTC,AVAX,XRP,DOGE,ADA")
ap.add_argument("--rows", type=int, default=800_000)
ap.add_argument("--workers", type=int, default=3)
ap.add_argument("--reuse", action="store_true")
args = ap.parse_args()
if args.reuse and OUT.exists():
d = pd.read_feather(OUT)
else:
syms = [s.strip() for s in args.symbols.split(",")]
print(f"[簇内前序 样本外验证] {len(syms)}× {args.rows} 根 1m\n", flush=True)
parts = []
with ProcessPoolExecutor(max_workers=args.workers) as ex:
fut = {ex.submit(collect, s, args.rows): s for s in syms}
for i, f in enumerate(as_completed(fut), 1):
r = f.result()
print(f" [{i}/{len(syms)}] {fut[f]} {0 if r is None else len(r)}", flush=True)
if r is not None:
parts.append(r)
if not parts:
print("无结果")
return
d = pd.concat(parts, ignore_index=True)
d.to_feather(OUT)
d["date"] = pd.to_datetime(d["date"])
d = d[(d.htf == 1.0) & d.lad & (d.atr_bp >= GATE_BP)].copy()
print(f"实盘口径 {len(d)} 笔,{d.date.min():%Y-%m-%d} ~ {d.date.max():%Y-%m-%d}")
oos = d[d.date < IS_START]
ins = d[d.date >= IS_START]
print(f" 发现期(step48 用过): {len(ins)}")
print(f" 样本外 : {len(oos)}")
print("\n" + "=" * 100)
print("########## 样本外整体(发现期之前的全部数据)##########")
if len(oos) > 60:
print(split_stats(oos, "样本外").to_string(index=False))
print("\n########## 发现期(复现 step48##########")
if len(ins) > 60:
print(split_stats(ins, "发现期").to_string(index=False))
report(d)
if __name__ == "__main__":
main()