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:
Harwinder Sekhon
2026-08-04 20:21:34 -04:00
parent 8eca2f4044
commit 45ce97ed13
5 changed files with 129 additions and 28 deletions
+26 -9
View File
@@ -10,9 +10,12 @@ import os
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Dict, List, Optional
from zoneinfo import ZoneInfo
import pandas as pd
_NY_TZ = ZoneInfo("America/New_York")
from chart_patterns import detect_chart_pattern
from config import PlatformConfig, SignalStrength, Timeframe
from providers import BaseDataProvider, make_data_provider
@@ -23,15 +26,19 @@ logger = logging.getLogger(__name__)
def _is_forex_weekend(now_utc: datetime) -> bool:
"""Forex market closed: Friday 22:00 UTC → Sunday 22:00 UTC (Sydney open)."""
wd = now_utc.weekday()
hour = now_utc.hour
if wd == 4 and hour >= 22: # Friday after 22:00
return True
if wd == 5: # All Saturday
return True
if wd == 6 and hour < 22: # Sunday before 22:00
"""Gold/FX market closed for the weekend: Friday 17:00 → Sunday 18:00
NEW YORK time (= Fri 21:00 UTC in US summer, 22:00 in winter —
matches measured Oanda feed behavior; 2026-08-04 audit I43).
Deliberate copy of providers.base._is_forex_weekend — a Module 21
test asserts the copies agree across DST transitions."""
ny = now_utc.astimezone(_NY_TZ)
wd = ny.weekday()
if wd == 4: # Friday
return ny.hour >= 17
if wd == 5: # Saturday
return True
if wd == 6: # Sunday
return ny.hour < 18
return False
@@ -1253,7 +1260,17 @@ class SignalAggregator:
ema200 = primary.key_levels.get("ema_trend")
if ema200 and ema200 > 0 and report.current_price > 0:
ema_dist_pct = abs(report.current_price - ema200) / ema200 * 100.0
_lo = float(os.environ.get("FB2_EMA_LO", "0.5"))
# FB2_EMA_LO: research override for the EMA200-proximity
# penalty band; the tuned default is 0.5. An unparseable
# value must degrade to the default, never crash the scan
# (2026-08-04 audit; hardened per owner delegation).
try:
_lo = float(os.environ.get("FB2_EMA_LO", "0.5"))
except (TypeError, ValueError):
logger.warning(
"FB2_EMA_LO=%r is not a number — using tuned "
"default 0.5", os.environ.get("FB2_EMA_LO"))
_lo = 0.5
if ema_dist_pct < _lo:
report.warnings.append(f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk")
penalties.append(("ema200_near", 5))