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:
+67
-2
@@ -306,6 +306,44 @@ class SignalAggregator:
|
||||
return None
|
||||
return float(adx.iloc[-1])
|
||||
|
||||
def _detect_big_candle(self, primary_df: pd.DataFrame) -> Optional[tuple]:
|
||||
"""Largest abnormal candle in the recent primary-TF window.
|
||||
|
||||
Returns (timestamp, ratio) of the biggest bar whose range is
|
||||
>= big_candle_atr_multiple x the PRIOR bar's ATR within the last
|
||||
big_candle_lookback_hours of primary-TF bars, else None. The
|
||||
prior-bar ATR (shift 1) matters: a huge candle inflates the
|
||||
Wilder ATR immediately, so comparing against the concurrent ATR
|
||||
under-detects the very bars this gate exists to catch.
|
||||
|
||||
Pure pandas on the caller-sliced df, so the live path and the
|
||||
backtest replay (which calls analyze_from_dataframes on prefix
|
||||
slices) compute bit-identical answers. The tail slice keeps the
|
||||
per-call cost bounded (~n + 42 rows); min_periods NaNs and short
|
||||
histories degrade to "no trip".
|
||||
"""
|
||||
cfg = self.config.xau_strategy
|
||||
bar_secs = BaseDataProvider.CANDLE_SECONDS[self.config.primary_timeframe]
|
||||
n = max(1, int(cfg.big_candle_lookback_hours * 3600 // bar_secs))
|
||||
period = self.config.technical.atr_period
|
||||
tail = primary_df.iloc[-(n + 3 * period):]
|
||||
high, low, close = tail["High"], tail["Low"], tail["Close"]
|
||||
tr = pd.concat(
|
||||
[high - low,
|
||||
(high - close.shift()).abs(),
|
||||
(low - close.shift()).abs()],
|
||||
axis=1,
|
||||
).max(axis=1)
|
||||
atr = tr.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
|
||||
ratio = (high - low) / atr.shift(1)
|
||||
window = ratio.iloc[-n:]
|
||||
if window.empty:
|
||||
return None
|
||||
max_ratio = window.max()
|
||||
if pd.isna(max_ratio) or max_ratio < cfg.big_candle_atr_multiple:
|
||||
return None
|
||||
return window.idxmax(), float(max_ratio)
|
||||
|
||||
def _apply_m5_filters(self, report: SignalReport) -> None:
|
||||
"""Extra selectivity for M5 — noisy TF needs stronger confirmation."""
|
||||
if report.signal_quality not in ("A+", "A", "B", "C"):
|
||||
@@ -957,6 +995,30 @@ class SignalAggregator:
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 6h. Big-candle entry gate (opt-in via per-TF factories) ──
|
||||
# An abnormally large candle in the recent window = institutional
|
||||
# burst; price may reverse violently, so NO entry of any kind may
|
||||
# fire. Detection runs for ANY grade: the warning is the
|
||||
# suppression channel for the XAU_PB path (which accepts D) and
|
||||
# the XAU_CP path (which accepts even F). The D-downgrade itself
|
||||
# touches actionable grades only — F stays F (same convention as
|
||||
# 6e).
|
||||
if (self.config.xau_strategy.enabled
|
||||
and report.display_name == "XAU/USD"
|
||||
and self.config.xau_strategy.big_candle_atr_multiple > 0):
|
||||
big_candle = self._detect_big_candle(primary_df)
|
||||
if big_candle is not None:
|
||||
bc_ts, bc_ratio = big_candle
|
||||
report.warnings.append(
|
||||
f"BIG CANDLE FILTER: {bc_ratio:.1f}x ATR bar at {bc_ts} "
|
||||
f"within "
|
||||
f"{self.config.xau_strategy.big_candle_lookback_hours:g}h "
|
||||
f"— no entries"
|
||||
)
|
||||
if report.signal_quality in ("A+", "A", "B", "C"):
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 6i. M5 noise gate (gold-tuned thresholds — XAU only) ──
|
||||
if (self.config.primary_timeframe == Timeframe.M5
|
||||
and report.display_name == "XAU/USD"):
|
||||
@@ -985,7 +1047,9 @@ class SignalAggregator:
|
||||
and report.display_name == "XAU/USD"
|
||||
and self.config.xau_strategy.m5_pullback_enabled
|
||||
and report.trade_setup is None
|
||||
and report.signal_quality != "F"):
|
||||
and report.signal_quality != "F"
|
||||
and not any(w.startswith("BIG CANDLE FILTER")
|
||||
for w in report.warnings)):
|
||||
self._try_m5_pullback(report, primary_df, pip_value)
|
||||
|
||||
# ── 10. Chart-pattern entry (XAU_CP) ────────────────────────
|
||||
@@ -1002,7 +1066,8 @@ class SignalAggregator:
|
||||
and self.config.xau_strategy.cp_enabled
|
||||
and report.trade_setup is None
|
||||
and not any(w.startswith(("SESSION FILTER", "DAY FILTER",
|
||||
"WEEKEND FILTER"))
|
||||
"WEEKEND FILTER",
|
||||
"BIG CANDLE FILTER"))
|
||||
for w in report.warnings)):
|
||||
self._try_chart_pattern(report, primary_df, pip_value)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user