Big-candle gate (default-OFF) + M5 relaxation campaign + --set tooling
Gate 6h (big-candle entry gate): blocks all XAU entries (core, XAU_PB, XAU_CP) when the primary TF's last 24h contains a bar with range >= K x prior-bar ATR. Ships default-OFF: validation rejected every threshold (K 3-6 on M5/M15 12mo) — removed trades were net-POSITIVE at every cell; M5 quiet-window trades are the only losing bucket. Config knobs big_candle_atr_multiple/lookback_hours, --big-candle backtest flag, Module 17 tests. M5 relaxation SHIPPED (validated 2026-07-30, Oanda bars, $0.30 spread): m5_min_abs_score 30->25, m5_min_confirmation_score 10->5, hour-21 UTC unblocked M5-only. Beats control in ALL 6 calendar years of the 5yr replay (+230.9R/2319 PF 1.26 DD 21.3 vs +168.0R/2048 PF 1.21 DD 27.0), 12mo +107.4R PF 1.36 vs +84.6R PF 1.33, OOS +26.1R vs +17.2R, $0.70 spread +86.9R vs +66.4R. M15: every relaxation retested negative — config unchanged. Declined-but-open: M5 hour-22 block, M15 hour-16 unblock (both fail 5yr/OOS). Evidence in the factory docstring. Tooling: generic --set PATH=JSON research override in gpu_backtest (typo-proof, logged); diagnose_filters filter_tags refreshed for gates 6d-6i with a staleness-guard test (Module 18); research scripts hour_analysis/burst_profile/book_diff. Suite: 268 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jqHpmNYmwYMowF6bPZyeD
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""Per-hour P&L breakdown of a backtest trade book.
|
||||
|
||||
Usage:
|
||||
python backtest/research/hour_analysis.py BOOK.csv BAR_MINUTES
|
||||
|
||||
Reports, per UTC hour (both entry hour and DECISION hour = entry_time
|
||||
minus one bar, the clock blocked_hours_utc evaluates): trade count,
|
||||
total R, mean R, win rate, profit factor. Also a per-(hour, direction)
|
||||
table. Analysis tooling only — never imported by the live code.
|
||||
"""
|
||||
import sys
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def hour_tables(book_csv: str, bar_minutes: int):
|
||||
df = pd.read_csv(book_csv)
|
||||
et = pd.to_datetime(df["entry_time"], utc=True, format="mixed")
|
||||
df["entry_hour"] = et.dt.hour
|
||||
df["decision_hour"] = (et - pd.Timedelta(minutes=bar_minutes)).dt.hour
|
||||
|
||||
def table(group_cols):
|
||||
g = df.groupby(group_cols)["r_multiple"]
|
||||
out = g.agg(count="count", total_r="sum", mean_r="mean",
|
||||
wr=lambda r: (r > 0).mean() * 100)
|
||||
out["pf"] = g.apply(
|
||||
lambda r: r[r > 0].sum() / max(-r[r <= 0].sum(), 1e-9))
|
||||
return out.round(2)
|
||||
|
||||
return (table("decision_hour"), table(["decision_hour", "direction"]),
|
||||
table("entry_hour"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
book, mins = sys.argv[1], int(sys.argv[2])
|
||||
by_dec, by_dec_dir, by_ent = hour_tables(book, mins)
|
||||
print(f"\n── {book} · by DECISION hour (bar-close clock) ──")
|
||||
print(by_dec.to_string())
|
||||
print(f"\n── by (decision hour, direction) ──")
|
||||
print(by_dec_dir.to_string())
|
||||
Reference in New Issue
Block a user