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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnZkzeFcTu9agFg8BDh2tn
This commit is contained in:
+59
@@ -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
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user