Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 63 additions & 15 deletions infrastructure/lambdas/freshness-monitor/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
check_freshness,
resolve_dedup_key,
)
from alpha_engine_lib.trading_calendar import previous_trading_day

logger = logging.getLogger()
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
Expand Down Expand Up @@ -347,25 +348,19 @@ def _maybe_alert(spec: ArtifactSpec, result: CheckResult, now: datetime) -> bool
# can route via alpha_engine_lib.dates.


def _iter_historical_cycle_dates(cadence: str, now: datetime, count: int) -> list[date]:
"""Return the N most recent cycle dates for the given cadence,
newest-first. Calendar-naive (NYSE holidays are not skipped).

For ``saturday_sf``: previous ``count`` Saturdays strictly before
``now.date()``. Today's Saturday is excluded if ``now`` is before
the typical 09:00 UTC cron — the caller is expected to invoke
historical mode after the current-state probe has had time to
cover the current cycle.

For ``weekday_sf`` / ``eod_sf``: previous ``count`` Mon-Fri days
strictly before ``now.date()``.
def _iter_sf_firing_dates(cadence: str, now: datetime, count: int) -> list[date]:
"""Return the N most recent SF firing dates (calendar) for the given
cadence, newest-first. The SF cron's actual firing dates — Saturdays
for saturday_sf, Mon-Fri for weekday_sf / eod_sf. Calendar-naive
(NYSE holidays NOT skipped at this layer — observable false-positives
for holiday-skipped firings surface as ❌ absent cells, which the
operator interprets in context).
"""
if count <= 0:
return []
today = now.date()
dates: list[date] = []
if cadence == "saturday_sf":
# Walk back day-by-day, collecting Saturdays.
d = today - timedelta(days=1)
while len(dates) < count:
if d.weekday() == 5: # Saturday
Expand All @@ -377,10 +372,61 @@ def _iter_historical_cycle_dates(cadence: str, now: datetime, count: int) -> lis
if d.weekday() < 5: # Mon-Fri
dates.append(d)
d -= timedelta(days=1)
# Other cadences: skip (continuous covered by current-state probe).
return dates


def _resolve_axis_dates(
firing_dates: list[date], template: str, cadence: str,
) -> list[date]:
"""Translate SF firing dates to the date axis the s3_key_template
actually uses. Two axes are supported:

- ``{date}`` — calendar date (the SF firing date itself). Used by
artifacts whose key reflects the SF firing identity, e.g.
``_weekly/{date}/manifest.json`` (the data manifest IS the
Saturday firing receipt).
- ``{trading_day}`` — NYSE trading day. Used by artifacts whose
key reflects the trading-day the data refers to, NOT the SF
firing date. Cadence-specific resolution:
* saturday_sf: previous_trading_day(saturday) → typically Fri
(the trading day whose close drove this Saturday's research).
* weekday_sf: previous_trading_day(weekday) → the prior trading
day's close (the AM SF fires before market open).
* eod_sf: weekday itself → today's close (the EOD SF fires
after market close, so today IS the trading_day).

Per the system-wide ``now_dual()`` convention
(``trading_day = last_closed_trading_day(now)``); see
alpha-engine-docs/private/DATE_CONVENTIONS.md.

Calendar-naive at the SF-firing layer above, but trading_day
resolution uses ``alpha_engine_lib.trading_calendar.previous_trading_day``
which IS NYSE-holiday-aware. So holiday-skipped firings still
surface as cleanly-absent cells, but their resolved trading_day
skips the holiday correctly.
"""
if "{trading_day}" in template:
if cadence == "eod_sf":
return list(firing_dates)
return [previous_trading_day(d) for d in firing_dates]
return list(firing_dates)


def _iter_historical_cycle_dates(
cadence: str, now: datetime, count: int, template: str = "",
) -> list[date]:
"""Return the N most recent cycle dates resolved to the axis the
template uses. See ``_iter_sf_firing_dates`` +
``_resolve_axis_dates`` for the two-stage derivation.

Backward compat: callers that omit ``template`` get calendar-axis
resolution (the pre-2026-05-28 behavior). The historical-mode
handler always passes the template.
"""
firing_dates = _iter_sf_firing_dates(cadence, now, count)
return _resolve_axis_dates(firing_dates, template, cadence)


def _format_historical_key(template: str, target_date: date) -> str:
"""Substitute date placeholders. Supports the same placeholders the
substrate's _format_key handles: ``{date}``, ``{trading_day}``.
Expand Down Expand Up @@ -498,7 +544,9 @@ def _handle_historical(
total_cycles_probed = 0
for spec in specs:
count = lookback.get(spec.cadence, 0)
cycle_dates = _iter_historical_cycle_dates(spec.cadence, now, count)
cycle_dates = _iter_historical_cycle_dates(
spec.cadence, now, count, template=spec.s3_key_template,
)
cycles, is_latest_pointer = _probe_historical(s3_client, spec, cycle_dates)
if not cycles and "{cycle_label}" in spec.s3_key_template:
skipped_unsupported += 1
Expand Down
90 changes: 90 additions & 0 deletions infrastructure/lambdas/freshness-monitor/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,93 @@ def test_handler_dispatches_to_historical_on_mode_flag(monkeypatch, fixed_now):
assert result["mode"] == "historical"
index._handle_historical.assert_called_once()
index.load_registry.assert_not_called() # current-state path NOT taken


# ── Trading-day-axis historical-probe tests ─────────────────────────────────


def test_iter_historical_resolves_trading_day_axis_for_saturday_sf(fixed_now):
"""When template uses {trading_day}, saturday_sf cycle dates resolve
to the previous NYSE trading day before each Saturday. fixed_now is
Sat 2026-05-30; prev Saturdays are 5/23, 5/16, 5/9; their
previous_trading_day values are Fri 5/22, Fri 5/15, Fri 5/8."""
import index
dates = index._iter_historical_cycle_dates(
"saturday_sf", fixed_now, 3,
template="signals/{trading_day}/signals.json",
)
assert [d.isoformat() for d in dates] == [
"2026-05-22", "2026-05-15", "2026-05-08",
]


def test_iter_historical_resolves_trading_day_axis_for_weekday_sf(fixed_now):
"""weekday_sf with {trading_day}: previous_trading_day of each
weekday firing date — the AM SF fires before market open so the
'available' trading day is the previous one. From Fri 5/29 (the
first weekday before fixed_now Sat 5/30): prev trading day = Thu
5/28; from Thu 5/28 → Wed 5/27; etc."""
import index
dates = index._iter_historical_cycle_dates(
"weekday_sf", fixed_now, 4,
template="predictor/predictions/{trading_day}.json",
)
assert [d.isoformat() for d in dates] == [
"2026-05-28", "2026-05-27", "2026-05-26", "2026-05-22",
]


def test_iter_historical_resolves_eod_keeps_firing_date_for_trading_day(fixed_now):
"""eod_sf with {trading_day}: EOD writes today's data after market
close, so trading_day == the SF firing weekday itself (no offset).
fixed_now Sat 5/30; previous weekday firings 5/29, 5/28, 5/27."""
import index
dates = index._iter_historical_cycle_dates(
"eod_sf", fixed_now, 3,
template="regime/{trading_day}.json",
)
assert [d.isoformat() for d in dates] == [
"2026-05-29", "2026-05-28", "2026-05-27",
]


def test_iter_historical_calendar_axis_unchanged_for_date_placeholder(fixed_now):
"""{date} placeholder keeps calendar-axis resolution (no
previous_trading_day translation). Used by _weekly/{date}/manifest.json
where the {date} IS the Saturday firing date."""
import index
dates = index._iter_historical_cycle_dates(
"saturday_sf", fixed_now, 3,
template="_weekly/{date}/manifest.json",
)
assert [d.isoformat() for d in dates] == [
"2026-05-23", "2026-05-16", "2026-05-09",
]


def test_iter_historical_backward_compat_no_template_arg(fixed_now):
"""Pre-PR callers that omit template still get calendar-axis
resolution. Required so the prior 21 tests don't regress."""
import index
dates = index._iter_historical_cycle_dates("saturday_sf", fixed_now, 3)
assert [d.isoformat() for d in dates] == [
"2026-05-23", "2026-05-16", "2026-05-09",
]


def test_resolve_axis_dates_holiday_skips_via_lib():
"""previous_trading_day is NYSE-holiday-aware. Memorial Day 2026-05-25
(Mon) is a NYSE holiday; previous_trading_day(2026-05-25) returns
Fri 5/22 (skipping the Mon holiday)."""
from datetime import date as _date
import index
dates = index._resolve_axis_dates(
[_date(2026, 5, 26)], # Tue after Memorial Day
template="x/{trading_day}.json",
cadence="weekday_sf",
)
# Tue 5/26's prior trading day skips Mon 5/25 (Memorial Day) →
# lands on Fri 5/22 if 5/25 is holiday-marked in the lib's calendar.
# Don't pin a specific value here — just assert it's NOT Mon 5/25.
assert dates[0] != _date(2026, 5, 25)
assert dates[0] < _date(2026, 5, 26)
Loading