audit: NY-anchored weekend calendar (all 3 copies) + FB2_EMA_LO crash-proofing

Owner decisions from the 2026-08-04 audit report:
- spread-in-exit-triggers: DECLINED (books stay comparable; ledger in
  CLAUDE.md records the measured ~4-6% optimism).
- weekend clock: FIXED. The market week is Fri 17:00 -> Sun 18:00 New
  York time, not Fri/Sun 22:00 UTC — during US DST the Oanda feed stops
  at 21:00 UTC (measured: 159/162 summer weekends). All three weekend
  implementations (providers/base, signal_aggregator, gpu_backtest
  calendar filter) now share the NY-anchored rule; a new test sweeps
  them hourly across both DST transitions and asserts agreement. No NTP
  needed — the host clock was fine, the constant was wrong.
  Ends the weekly false-"STALE DATA / feed lagging" hours (summer Fri
  21-22 UTC, winter Sun 22-23 UTC).
- FB2_EMA_LO (delegated): kept as a research knob but crash-proofed —
  an unparseable value degrades to the tuned default 0.5 with a warning
  instead of killing every scan. Documented in CLAUDE.md.

Book gates: M15 12mo pinned book byte-identical (+78.7111R, 397
trades). M5 12mo drops exactly ONE trade (556 -> 555, +97.56 ->
+98.64R): a phantom whose signal fired on the final summer-Friday bar
(decision 21:00 UTC = NY 17:00 market close) and was "filled" at
Sunday's reopen 49 hours later, -1.08R — an entry live trading can
never take. The backtest no longer contains it; live == backtest
parity improved. M15 unaffected because hour 21 is in its blocked set.

Suite: 341/341 green.

Claude-Session: https://claude.ai/code/session_01UnZkzeFcTu9agFg8BDh2tn
This commit is contained in:
Harwinder Sekhon
2026-08-04 20:21:34 -04:00
parent 5b3f47f3e1
commit 7dbcbc6c82
5 changed files with 129 additions and 28 deletions
+59
View File
@@ -6397,6 +6397,65 @@ def _():
assert [p for p, _ in _gb._apply_cooldown(cands, 1)] == [10, 12, 15, 21, 24]
@test("weekend calendar is NY-anchored: summer close 21:00 UTC, winter reopen 23:00")
def _():
# 2026-08-04 audit I43, fixed per owner decision: the market week is
# Fri 17:00 -> Sun 18:00 NEW YORK time (measured on 5yr of Oanda
# bars), not a fixed UTC hour.
import providers.base as pbase
T = lambda s: pd.Timestamp(s, tz="UTC")
# US DST 2026: Mar 8 – Nov 1.
assert pbase._is_forex_weekend(T("2026-07-31 21:30")) is True, \
"summer Friday 21:30 UTC = 17:30 ET → market CLOSED (old code said open)"
assert pbase._is_forex_weekend(T("2026-07-31 20:30")) is False
assert pbase._is_forex_weekend(T("2026-01-02 21:30")) is False, \
"winter Friday 21:30 UTC = 16:30 ET → still open"
assert pbase._is_forex_weekend(T("2026-01-02 22:30")) is True
assert pbase._is_forex_weekend(T("2026-01-04 22:30")) is True, \
"winter Sunday 22:30 UTC = 17:30 ET → still closed (old code said open)"
assert pbase._is_forex_weekend(T("2026-01-04 23:30")) is False
assert pbase._is_forex_weekend(T("2026-08-02 21:30")) is True # summer Sun 17:30 ET
assert pbase._is_forex_weekend(T("2026-08-02 22:30")) is False # summer Sun 18:30 ET
assert pbase._is_forex_weekend(T("2026-08-01 03:00")) is True # Saturday
@test("weekend calendar: all three copies agree hourly across DST transitions")
def _():
import providers.base as pbase
import signal_aggregator as sa_mod
import gpu_backtest as _gb
cfg = PlatformConfig()
cfg.xau_strategy.weekend_filter = True
cfg.xau_strategy.allowed_weekdays = [] # isolate the weekend gate
# Sweep spring-forward (2026-03-06..11) and fall-back (2026-10-30..11-04).
for start in ("2026-03-06", "2026-10-30"):
for ts in pd.date_range(start, periods=5 * 24, freq="1h", tz="UTC"):
a = pbase._is_forex_weekend(ts)
b = sa_mod._is_forex_weekend(ts)
c = _gb._is_xau_blocked_by_calendar(ts, cfg) == "WEEKEND_FILTER"
assert a == b == c, f"{ts}: base={a} aggregator={b} backtest={c}"
@test("FB2_EMA_LO: garbage value degrades to tuned default, scan survives")
def _():
os.environ["FB2_EMA_LO"] = "not-a-number"
try:
config = PlatformConfig()
config.primary_timeframe = Timeframe.H1
config.confirmation_timeframes = [Timeframe.M15, Timeframe.H4]
agg = SignalAggregator(config)
dfs = {
Timeframe.H1: make_ohlcv(300, seed=42),
Timeframe.M15: make_ohlcv(300, seed=43),
Timeframe.H4: make_ohlcv(300, seed=44),
}
report = agg.analyze_from_dataframes("XAU/USD", dfs, use_news=False)
assert isinstance(report, SignalReport), \
"analysis must survive an unparseable FB2_EMA_LO"
finally:
os.environ.pop("FB2_EMA_LO", None)
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════