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:
Harwinder Sekhon
2026-07-30 11:30:20 -04:00
parent a46aa9ccd9
commit 0396970d29
9 changed files with 621 additions and 36 deletions
+53
View File
@@ -30,6 +30,7 @@ Usage:
from __future__ import annotations
import argparse
import json
import logging
import multiprocessing as mp
import os
@@ -2117,6 +2118,19 @@ def _parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
p.add_argument("--cp-patterns", default=None,
help="Comma-separated pattern subset for --cp (e.g. "
"double_bottom,bull_flag). Default: all detectors.")
p.add_argument("--big-candle", type=float, default=None,
help="Force xau_strategy.big_candle_atr_multiple=K for this "
"run (no entries while the lookback window contains a "
"bar with range >= K x prior ATR) — for validating the "
"candidate.")
p.add_argument("--big-candle-hours", type=float, default=None,
help="Override big_candle_lookback_hours for --big-candle "
"runs (default 24).")
p.add_argument("--set", action="append", default=[], metavar="PATH=JSON",
help="Override any PlatformConfig leaf by dotted path; the "
"value is JSON-parsed for typing (research knob; "
"repeatable). e.g. --set xau_strategy.m5_min_abs_score=25 "
"--set 'xau_strategy.sell_min_quality=[\"A+\",\"A\",\"B\",\"C\"]'")
p.add_argument("--no-cache", action="store_true", help="Skip parquet cache (force API fetch)")
p.add_argument("--spread", type=float, default=None,
help="Round-trip cost in price units per trade (default: pair's "
@@ -2148,6 +2162,39 @@ def _resolve_tf(s: str) -> Timeframe:
raise SystemExit(f"Unknown timeframe '{s}'. Valid: {[t.value for t in Timeframe]}")
def _apply_config_overrides(cfg: PlatformConfig, overrides: List[str]) -> None:
"""Apply repeatable --set PATH=JSON research overrides onto the config.
Unknown paths are a hard error: dataclass instances silently accept
setattr of any name, so a typo'd path would otherwise produce a
no-op run that looks like 'the relaxation had no effect'. Every
applied override is logged — the run log is the config audit trail
required by instructions_for_backtest.txt.
"""
for item in overrides:
path, sep, raw = item.partition("=")
if not sep:
raise SystemExit(f"--set expects PATH=VALUE, got '{item}'")
try:
value = json.loads(raw)
except json.JSONDecodeError:
value = raw # bare-string convenience ("oanda" etc.)
obj = cfg
parts = path.strip().split(".")
for attr in parts[:-1]:
if not hasattr(obj, attr):
raise SystemExit(f"--set: unknown config path '{path}'")
obj = getattr(obj, attr)
leaf = parts[-1]
if not hasattr(obj, leaf):
raise SystemExit(f"--set: unknown config path '{path}'")
old = getattr(obj, leaf)
if hasattr(old, "__dataclass_fields__"):
raise SystemExit(f"--set: '{path}' is a config section, not a leaf")
setattr(obj, leaf, value)
logger.info(f"--set override: {path} = {value!r} (was {old!r})")
def main(argv: Optional[List[str]] = None) -> int:
args = _parse_args(argv)
@@ -2197,6 +2244,12 @@ def main(argv: Optional[List[str]] = None) -> int:
platform_cfg.xau_strategy.cp_patterns = [
s.strip() for s in args.cp_patterns.split(",") if s.strip()
]
if args.big_candle is not None:
platform_cfg.xau_strategy.big_candle_atr_multiple = args.big_candle
if args.big_candle_hours is not None:
platform_cfg.xau_strategy.big_candle_lookback_hours = args.big_candle_hours
if args.set:
_apply_config_overrides(platform_cfg, args.set)
confirm_tfs = list(platform_cfg.confirmation_timeframes)
end = (