用 7 年长样本复核 step54:时段坐实无效,周日效应符号翻转、撤回

用户追问"你用的是长时间周期分析的?"。原答案是 1m / 18.5 个月 / 3941 笔,
而上一轮的结论是"没效果"——没效果最怕样本不够,这个追问戳到了点子上。

改用 step41 那份 5m/15m/30m 复核:2019-09 → 2026-08,2525 天,8168 笔。
口径能对上——keep = (h1_agree==1) & push 就是深色过滤,出场配置
s2_so8_k2_m48 正是实盘那套 2/3/8/2/48(cfg_name 的分批命名,scale_at 默认 3)。
唯一差异是没加 ATR≥8bp 门控,而 §3.5 实测它在这些周期上几乎不触发
(30m 0%、15m 0.16%、5m 2.6%)。

① 时段坐实无效。段间毛R极差的置换 p:5m 0.6185、15m 0.6687、30m 0.3105、
合并 0.6445,全部远离显著。1m 那个 p=0.0895 现在看清楚了,就是零分布里运气
略好的一条尾巴。

② 周末效应没复现,而且符号翻转。1m 上周末−工作日是 -0.132(p=0.0246),
长样本上 5m +0.014、15m +0.142、30m -0.006、合并 +0.041;周日从 -0.199
(p=0.0079)变成 +0.033。这不是严格的样本外复现——1m 与 5m 是不同的信号
总体——但若"周末流动性薄所以吃亏"是真的市场结构效应,它没有理由只在 1m 上
出现、在 5m 上还反号。

所以上一提交里"周日效应统计上真实"那句要撤回。本步一共跑了约 35 个分组比较
(24 小时 + 两套时段定义 + 7 个星期 + 周末/周日),冒出一个 p=0.008 恰是多重
比较的期望产物。HANDOFF 里已把该结论标为撤回并记下两条教训:报告"无效"之前
先确认样本量撑得起这个"无";在几十个分组里挑出的最显著那个,默认它是噪声,
除非能在另一个总体上复现。

实践结论不变且更硬:不加任何时间维度的过滤,mom60≥7 仍是唯一值得上的开关。

复核走 --long,复用 step41 已有 feather,未重跑采集。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 19:29:26 +08:00
co-authored by Cursor
parent 1ffc4fbb18
commit 6d93975e67
2 changed files with 126 additions and 7 deletions
+79
View File
@@ -299,11 +299,90 @@ def decision(d: pd.DataFrame, cuts: dict) -> None:
print(pd.DataFrame(rows).to_string(index=False))
SRC41 = HERE / "out" / "step41_exit_tp.feather"
# step41 的分批出场配置名,与实盘完全一致:SL 2 / 减半 3(scale_at 默认)/
# runner 8 / runner 止损 2 / 48 根。见 lib.exit_model.cfg_name
CFG41 = "s2_so8_k2_m48"
def long_history() -> None:
"""用 5m/15m/30m 的 7 年数据复核时段与星期几。
为什么要这一步:1m 只能追到 18.5 个月(3941 笔),而时段结论是"没效果"
**没效果最怕的就是样本不够**。step41 那份跨 2019-09 → 2026-085m 单周期
就有 5434 笔,且过滤口径相同(`h1_agree==1 & push` 就是深色),出场配置
也能对上实盘那套。若长样本上仍然测不出时段效应,"无效"才站得住。
⚠️ 口径差一处:这里没加 ATR≥8bp 门控。§3.5 实测该门控在这些周期上几乎
不触发(30m 0%、15m 0.16%、5m 2.6%),所以影响可忽略,但要记着。
"""
from lib.exit_model import fee_of, taker_notional
d = pd.read_feather(SRC41)
d["date"] = pd.to_datetime(d["date"])
g, r, c = (d[f"{CFG41}_{k}"].to_numpy() for k in ("g", "r", "c"))
d["net"] = g - fee_of(r, c)
d["gR"] = g / (SL * d.atr_pct.values)
d["R"] = d.net / (SL * d.atr_pct.values)
d["tn"] = taker_notional(r, c)
d["atr_bp"] = d.atr_pct * 1e4
utc = d.date.dt.tz_convert("UTC")
d["utc_h"], d["utc_dow"] = utc.dt.hour, utc.dt.dayofweek
print("\n" + "#" * 100)
print("########## 长样本复核:5m/15m/30m × 7 年 ##########")
print(f"{len(d)} 笔 · {d.symbol.nunique()} 币 · "
f"{d.date.min():%Y-%m}{d.date.max():%Y-%m}"
f"{(d.date.max()-d.date.min()).days} 天)")
print(f"出场配置 {CFG41}= 实盘的 2/3/8/2/48)· 深色已过滤 · 无 ATR 门控")
for tf in ("5m", "15m", "30m"):
x = d[d.ltf == tf].copy()
if len(x) < 300:
continue
x["seg"] = label_session(x.utc_h, SESSIONS)
print(f"\n--- {tf}{len(x)} 笔)时段 ---")
rows = []
for n, _, _ in SESSIONS:
s = x[x.seg == n]
ins, oos = s[s.group == "样本内"], s[s.group == "样本外"]
row = stat(s, n, len(x))
row["样本内毛R"] = round(ins.gR.mean(), 3) if len(ins) >= 30 else None
row["样本外毛R"] = round(oos.gR.mean(), 3) if len(oos) >= 30 else None
rows.append(row)
print(pd.DataFrame(rows).to_string(index=False))
permutation(x, SESSIONS, n_iter=10_000)
we = (x.utc_dow >= 5).to_numpy()
print(f"\n--- {tf} 工作日 vs 周末 ---")
print(pd.DataFrame([stat(x[~we], "工作日", len(x)),
stat(x[we], "周末", len(x))]).to_string(index=False))
perm_binary(x, we, f"{tf} 周末 vs 工作日", n_iter=10_000)
print("\n--- 三周期合并(每笔等权)---")
d["seg"] = label_session(d.utc_h, SESSIONS)
print(pd.DataFrame([stat(d[d.seg == n], n, len(d))
for n, _, _ in SESSIONS]).to_string(index=False))
permutation(d, SESSIONS, n_iter=10_000)
we = (d.utc_dow >= 5).to_numpy()
print(pd.DataFrame([stat(d[~we], "工作日", len(d)),
stat(d[we], "周末", len(d))]).to_string(index=False))
perm_binary(d, we, "合并 周末 vs 工作日", n_iter=10_000)
perm_binary(d, (d.utc_dow == 6).to_numpy(), "合并 周日 vs 其余",
n_iter=10_000)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--src", default=str(SRC))
ap.add_argument("--long", action="store_true",
help="只跑 5m/15m/30m 的 7 年长样本复核")
args = ap.parse_args()
if args.long:
long_history()
return
from step50_volume import prep
d = pd.read_feather(args.src)