data_updater_v2.events
events.py — the corporate actions’ pure pieces: one listing’s dividends from three sources combined into one row per real payout, and what could not be settled.
Used by DataClient.build_dividends / update_dividends; nothing here calls a vendor.
What the sources are (probed 2026-10-07: 373 liquid US ETFs and stocks, ~23,600 payouts, and every payout of the 67 Nasdaq-listed ones checked against Nasdaq’s own record):
FMP the main source. Its served amount is right on 99.2% of the payouts Nasdaq checks;
its repeats (the same payout under two labels, an undeclared amount beside a
declared correction, a split on two days) are folded in the client
(data_clients/actions.py).
EODHD the second: agrees with FMP on 94% of history and 99% of the last 30 days. Where
the two agree they are right on 3,551 of 3,556 checked payouts; where they disagree
FMP is right 129 times, EODHD 13, neither 10. Its ex-date is sometimes a day late
(FMP’s was right every time a third source could tell). It has payouts FMP lacks.
Nasdaq exchange-direct, cash as paid, Nasdaq listings only: the referee wherever it lists
the symbol, and first with some same-day payouts.
The rules, in order, per listing:
- one payout = the same ex-date and the same cash (
same_amount) across sources; - the main source’s payouts are matched to the second’s: same day and cash (agree), then the same cash a few days apart (dates: FMP’s day kept), then the same day and other cash (differs);
- Nasdaq, where it has the day, settles the cash (
resolve); else the main source’s cash stands — the disagreement is named either way; - a payout only one vendor has: where the other vendor has no record at all (does not
carry the listing, or failed), that vendor’s payouts are the listing’s, checked by
Nasdaq only; inside the other’s record it is named, Nasdaq confirms or rejects it, and
unsettled: FMP’s is kept, EODHD’s only while recent (
RECENT_DAYS); a payout only Nasdaq has is kept and named; - the period in FMP’s words (
actions.period_word); the other dates filled from whichever source has them.
Nothing here disqualifies a listing: a cash disagreement no third source settles keeps FMP’s cash (right 129 times of 152 where Nasdaq could tell), named; a date apart keeps FMP’s day.
resolve False keeps the main source’s rows as served and names every disagreement and
every payout the others alone have, without settling or adding any — the input of a later
targeted run.
Functions
adjusted_step
adjusted_step(bars: Mapping[dt.date, Any] | None, adjusted: Mapping[dt.date, Any] | None, day: dt.date) -> float | NoneThe factor a split-adjusted series applied on day: the trades over its closes, the
median of the three sessions before against the three from day on; None without
both around it.
combine_delistings
combine_delistings(fmp: Sequence[Mapping[str, Any]] | None, nasdaq: Sequence[Mapping[str, Any]] | None) -> list[dict[str, Any]]The delistings both tell, one row per delisting: the delistings table’s told
columns. FMP’s day is the delisting’s (the day it stops trading); Nasdaq’s suspension
and formal deletion of that ticker within DELISTING_DAYS are its suspended_on /
deleted_on; a delisting only Nasdaq tells is dated by its suspension, else its
deletion.
combine_dividends
combine_dividends(main: Sequence[Mapping[str, Any]], second: Sequence[Mapping[str, Any]] | None, nasdaq: Sequence[Mapping[str, Any]] | None, main_source: str = 'fmp', second_source: str = 'eodhd', resolve: bool = True, today: dt.date | None = None, record_from: dt.date | None = None) -> tuple[list[dict[str, Any]], dict[str, list[dict[str, Any]]]]One listing’s payouts from its main source, its second and Nasdaq (None = not asked,
or failed) as one row per real payout (the dividends table’s columns plus sources), and the
issues found: differs · dates · only_<main> · only_<second> ·
only_nasdaq · rejected · invalid — each a list of named cases (empty lists
dropped). A payout one vendor alone has is confirmed (and no longer named) when Nasdaq
has it, and rejected (not kept) when it falls inside Nasdaq’s record and Nasdaq does not.
A vendor’s record starts at its first payout; record_from (a calendar window’s start)
says each vendor that answered (main, and second unless None) covered every day
from then — an empty answer is then “no payout”, not “no record”.
combine_listings
combine_listings(fmp: Sequence[Mapping[str, Any]] | None, nasdaq: Sequence[Mapping[str, Any]] | None) -> list[dict[str, Any]]The listings both tell (FMP’s IPO calendar, data_clients/listings.py; Nasdaq’s
events, GeneralClient.nasdaq_events / nasdaq_today), one row per listing: the
listings table’s told columns. One listing = one ticker on FMP’s day and Nasdaq’s
addition within LISTING_DAYS; the day is FMP’s (the day it trades, or is expected
to), Nasdaq’s where FMP has none; added_on Nasdaq’s addition (its listing systems,
up to days before the first trade); status FMP’s word, listed for one only
Nasdaq tells. FMP telling one listing on two lines within LISTING_DAYS (expected,
then priced: the same day, or moved on) is one listing, its latest line kept.
combine_splits
combine_splits(sources: Mapping[str, Sequence[Mapping[str, Any]] | None], bars: Mapping[dt.date, Any] | None = None, adjusted: Mapping[dt.date, Any] | None = None, sessions: Sequence[dt.date] | None = None, today: dt.date | None = None, resolve: bool = True) -> tuple[list[dict[str, Any]], dict[str, list[dict[str, Any]]]]One listing’s splits from every source (sources: fmp / eodhd / nasdaq -> their
processed rows, None = not asked or failed) as one row per real split (the splits
table’s columns plus sources), and the issues found.
Per row: a date off the trading calendar moves to the session it takes effect on
(moved). Across sources: rows of the same factor within SPLIT_SLACK days, or on
the same day, are one event. An event every answering source tells alike (two or more,
one day, one factor) stands. Any other — a ratio or a day told two ways, a split one
source alone has — is judged on the listing’s own trades (bars: its raw daily bars,
as traded; price_says): the told (day, factor) the price moved by stands; a factor
large enough to see that no price move shows is not landed (rejected) — unless
Nasdaq, the exchange, lists it that day (the vendors’ daily can show a split’s move
weeks early); a factor too small to see, a day with no prices, or one still ahead is
kept on the vendors’ word (unconfirmed / announced): the factor EODHD tells for a
ratio told two ways (it was right 8 times in 11 where the price could tell), the day
EODHD tells for a day told two ways. resolve False lands only what every source
agrees on and names the rest. One source telling one factor on two days within an event
is one split or two as the trades moved once or twice (_repeated). Issue keys: moved
· dates · differs · repeated · only_
combine_symbol_changes
combine_symbol_changes(fmp: Sequence[Mapping[str, Any]] | None, eodhd: Sequence[Mapping[str, Any]] | None) -> tuple[list[dict[str, Any]], dict[str, list[dict[str, Any]]]]The renames both vendors tell (each in the common shape, data_clients/changes.py;
None = that vendor not read), one row per rename: the symbol-change table’s told columns
(old_symbol, new_symbol, date, name, in_fmp, in_eodhd, disagree, conflict).
One rename = the same old and new ticker on both lists (neither list ever repeats a pair:
5,511 and 1,224 lines, 2026-10-09); where a pair does repeat, each of EODHD’s dates goes
to FMP’s nearest. The dates: the same on 1,213 of the 1,215 both tell; where they differ
the earlier is kept and the row is disagree (GRAY -> CALC: FMP 2023-03-21, the day
CALC began trading, EODHD 85 days later; HGSH -> GGE a day apart). The name is FMP’s,
else EODHD’s. conflict: another rename on the same day leaves the same old ticker or
takes the same new one (EODHD’s AVB -> VMRK beside FMP’s EQR -> VMRK, 2026-08-18) — told
as it is, never applied to the catalog. Beside a rename into a ticker, a line from that
ticker (or the other line’s) plus V / VV the same day is the when-issued paper
turning regular: taken out (when_issued), so it never blocks the rename.
Returns (rows oldest first, what was found: disagree, conflict and
when_issued, each named).
price_says
price_says(bars: Mapping[dt.date, Any] | None, day: dt.date, factor: float) -> tuple[str, float]What the listing’s own trades say of a split of factor taking effect on day
(bars: day -> its raw daily bar, as traded): confirmed (the price moved by the
factor on the day, up to EARLY_SESSIONS before it — EODHD’s daily shows the last
pre-split sessions already adjusted, ARMH 2026-07-13 at 12.70 while its minutes traded
~127 — or the session after), rejected
(it did not, and the factor is large enough to see), too small or no prices; with
how far the closest move was from the factor (log).
split_changes
split_changes(stored: Iterable[Any], fetched: Iterable[Any], ids: Iterable[str], window: tuple[dt.date | None, dt.date | None] | None = None, today: dt.date | None = None) -> list[dict[str, Any]]What a fetch changes against the stored splits, for the listings it answered for
(ids) over the days it covered (window; None: all) — read the stored ones
(read_splits) before writing the fetched ones:
added a split the store did not haveremoved a stored split the fetch no longer has (taken out on a rebuild)changed the same day, another factormoved the same factor told on another day within ``SPLIT_SLACK`` daysEach change: id, symbol, change, date (the last day it touches: prices up
to it were put on the as-traded scale with the old list), before / after (date,
split_to, split_from; None for added / removed), announced (every day it touches is
after today, default today: no stored price leans on it yet). A listing outside
ids (its fetch failed) is never compared: its stored splits are not “removed”.