core_data_updater.jobs.events
jobs/events.py — EventsFlow: splits · dividends · symbol_changes.
Design record: EVENTS_FLOW.md (pre-stamped from the records; §7 decisions taken per recommendation, 2026-08-16). Shape: UPDATE events_update 3 whole-market calendar sweeps (3 HTTP calls, no symbols) — sensor-fired (events_ready), manual=True BUILD events_build full survivorship-free history: dividends/splits per-symbol over the STORED universe, symbol_changes global — manual, max_concurrent=1, replace scoped BACKFILL events_backfill wide re-sweep (‘30d’) + targeted repull of the RECENT NAMED GAPS — nightly 09:30 UTC, born STOPPED
Laws carried here: configs read LIVE per step (per-step rows, §7-D1; enabled comes back VISIBLE — a disabled row skips, named); every landing through CoreDataRepo.land (the EVENTS policy: dividends ride KEY_UPGRADE so a period-named row upgrades its ‘unknown’ placeholder in place, period None → ‘unknown’ is a policy fill); state-as-params (the build roster is READ from raw.equities ∪ raw.etfs in fetch_symbols and passed; an unreadable roster RAISES — metadata outage blocks loudly); the run ledger opens idempotently at the first landing (sweep jobs have no fetch step); fulfill-or-name; zero SQL/wire-parsing/conversion in this file.
Sensor (§5.3): events_ready probes every 15m inside probe_window_utc — the
target is the latest session whose close+45m has passed, READ from
raw.market_sessions via CoreDataStateRepo.sessions (never weekday math; empty calendar =
named skip, fail closed); ONE dividends-calendar hit is the vendor-up canary;
run_key dedupes one run per session. It reads through
CoreDataStateRepo.sessions and gates on metadata’s market_sessions
product through requires: the sensor does not run before the calendar has
ever materialized, then reads its current rows. This is bootstrap/existence,
not an asset-age promise. No cron fallback
(§7-D4 rec) — the sensor is primary, and its conditions live INSIDE it;
manual=True keeps the UI lever.
Classes
EventsFlow
Bases: CoreDataFlow
The events flow — EVENTS_FLOW.md §5, method for method.
backfill_dividends
backfill_dividends(cfg) -> Nonebackfill_splits
backfill_splits(cfg) -> Nonebackfill_symbol_changes
backfill_symbol_changes(cfg) -> NoneRe-sweep ONLY — the rename stream is GLOBAL (one vendor call for the
whole market; no per-symbol verb exists and none should be added), so a
wide window IS the targeted repair here. And ‘wide’ for THIS stream is
the FULL ARCHIVE (window NULL → get_symbol_changes, the whole 1969→
history in ONE call, which is exactly what the nightly update already
takes): a ‘30d’ window would do strictly LESS than the sweep it’s
meant to backstop.
build_dividends
build_dividends(cfg, fetch_symbols: dict) -> Nonebuild_splits
build_splits(cfg, fetch_symbols: dict) -> Nonebuild_symbol_changes
build_symbol_changes(cfg) -> NoneGlobal stream — no symbols, ever (FMP full archive to 1969 in ONE call + EODHD fill; §7-D2 rec: upsert, inserted-count IS the diff).
events_ready
events_ready(ctx, upstream_done)Probe-then-fire (§5.3): events publish IN ADVANCE, so the fire signal is a SESSION CLOCK — target = the latest session whose close+45m has passed, read from raw.market_sessions (never weekday math; empty calendar = named skip, fail closed) — with ONE dividends- calendar hit as the vendor-up canary. run_key dedupes per session.
The requires gate confirms market_sessions has been bootstrapped;
the sensor then reads the current table to compute its target.
fetch_symbols
fetch_symbols(cfg) -> dictThe stored survivorship-free universe (raw.equities ∪ raw.etfs), read ONCE and passed (state-as-params) — {“all”: active ∪ delisted, “active”: active only}; the build steps pick per their row’s include_delisted and symbols override. A FAILED roster read RAISES — a metadata outage must block the build loudly, never masquerade as an empty universe. Opens the run ledger (prices fetch_universe precedent).
update_dividends
update_dividends(cfg) -> NoneThe policy op is KEY_UPGRADE: a period-named row matching a stored ‘unknown’ row’s (symbol, ex_date, amount) upgrades it in place — never a phantom second payout (the census rule, §7-D2).
update_splits
update_splits(cfg) -> Noneupdate_symbol_changes
update_symbol_changes(cfg) -> NoneFULL REPULL by default (§7-D2 rec): the archive is 5.4k rows in ONE call and upsert on the PK makes the repull the diff — the row’s window stays NULL; seeding one flips this to a windowed sweep.