Skip to content

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:

  1. one payout = the same ex-date and the same cash (same_amount) across sources;
  2. 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);
  3. Nasdaq, where it has the day, settles the cash (resolve); else the main source’s cash stands — the disagreement is named either way;
  4. 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;
  5. 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 | None

The 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_ · rejected · unconfirmed · announced.

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 have
removed a stored split the fetch no longer has (taken out on a rebuild)
changed the same day, another factor
moved the same factor told on another day within ``SPLIT_SLACK`` days

Each 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”.