Skip to content

data_updater_v2.data_clients.general

GeneralClient — the sources that are neither FMP nor EODHD: Bitstamp (crypto, exchange-direct), OpenFIGI (identifiers), Nasdaq and Nasdaq Trader (its listings’ own record), Coin Metrics (a coin’s supply, valuation, network and exchange metrics), FINRA (the listed short interest and the daily short-sale volume), IB’s shortable file (the borrow fee and the shares available, today) with the history companiesmarketcap republishes of it, and the NYSE calendar from pandas_market_calendars (mcal). EODHD is EODHDClient (eodhd.py), its own; the DataClient combines all three.

Its sections match FMPClient’s names and parameters so the DataClient combines them one to one:

2 · UNIVERSE cryptos() — Bitstamp's USD pairs · figis() / coin_figis() —
OpenFIGI ids
3 · CALENDAR sessions() · closures() (mcal; no HTTP) · nyse_calendar()
4 · PRICES daily() / intraday() — Bitstamp, for the pairs it lists
5 · CORPORATE ACTIONS nasdaq_dividends() / nasdaq_dividends_calendar() /
nasdaq_splits_calendar() — Nasdaq's own record · nasdaq_events()
/ nasdaq_today() — Nasdaq Trader's listings, delistings, renames
7 · METRICS coin_catalog() · coin_metrics(table) · coin_metric_definitions()
— Coin Metrics' community API, the four tables, built or updated ·
short_interest() / short_interest_dates() / short_volume() — FINRA
· ib_shortable() — IB's file, today · borrow_slugs() /
borrow_history() — companiesmarketcap, IB's numbers since 2023

The HTTP vendors sit underneath, each a BaseClient of its own (one base URL, one quota); Nasdaq’s facts are on NasdaqClient, FINRA’s, IB’s and companiesmarketcap’s on their blocks below:

BitstampClient https://www.bitstamp.net/api/v2/, public, no key (live-verified by nc-feeds 2026-08-10). ohlc/{pair}/?step&limit&start&end: at most 1,000 bars a call; with both start and end the server anchors to the END (the last <= 1,000 bars up to end), so a window exactly 1,000 bars wide comes back whole, and a narrower one or a sparse era returns bars from BEFORE its start — every walk is clipped to its ask (seen live 2026-10-03: 120 bars before the start). Bars are stamped at their START in UTC epoch seconds; volume is in COINS of the base currency (BTC for btcusd: ~1,900 a day on this one venue), served as strings. History from 2011-08-18 (BTC’s first print). Rates: no published limit; nc-feeds’ guidance is ~8,000 calls per 10 minutes. CoinMetricsClient https://community-api.coinmetrics.io/v4/, public, no key: the facts sit on the COIN METRICS block below (routes, paging, the 403 on a metric an asset does not carry, the completion stamp, the flash status, 6,000 calls a 20-second window). OpenFIGIClient https://api.openfigi.com/v3/mapping, POST a JSON list of jobs, one answer per job in order. Without a key: 25 requests a minute (ratelimit-* headers), 10 jobs a request; a key (header X-OPENFIGI-APIKEY) raises it to 250 a minute and 100 jobs (ratelimit-policy: 250;w=60, probed 2026-10-05). Probed 2026-10-04: a share class is spelled with a SLASH (BRK/B; BRK-B and BRK.B find nothing); an ISIN or CUSIP returns every US venue record (US = the composite, UA/UC/… the venues), so the composite is kept; a dead ticker (KOL) is found only with includeUnlistedEquities; there is no CIK id type.

Classes

BitstampClient

Bases: BaseClient

Bitstamp’s public market-data API. Carried state: the base’s only.

fetch_bars

fetch_bars(pair: str, interval: str, start: dt.datetime, end: dt.datetime) -> RawResponse

One pair’s bars of one width whose START lies in [start, end] (aware instants), as Bitstamp serves them (timestamp, OHLC and coin volume as strings), oldest first, clipped to the ask.

fetch_currencies

fetch_currencies() -> RawResponse

Bitstamp’s currency catalog: name, type (crypto / fiat), decimals, networks.

fetch_pairs

fetch_pairs() -> RawResponse

Every pair Bitstamp lists (~300): name (BTC/USD), url_symbol (btcusd), description, trading (Enabled/Disabled), decimals, minimum order.

CoinMetricsClient

Bases: BaseClient

Coin Metrics’ community API. Carried state: the base’s only.

fetch_catalog

fetch_catalog(assets: Sequence[str]) -> RawResponse

The catalog of the asked assets as served: each asset’s metrics and frequencies.

fetch_metrics

fetch_metrics(assets: Sequence[str], metrics: Sequence[str], start: dt.date | None = None, end: dt.date | None = None) -> RawResponse

The daily rows of many assets and metrics in one walk, as served ({asset, time, <metric>: "number", <metric>-status ...}), every page.

fetch_reference

fetch_reference(metrics: Sequence[str] | None = None) -> RawResponse

The metric definitions as served, all of them or the asked ones.

CompaniesMarketCapClient

Bases: BaseClient

companiesmarketcap.com’s pages. Carried state: the base’s only.

fetch_borrow

fetch_borrow(slug: str) -> RawResponse

One company’s cost-to-borrow page as served (HTML); no such company: 404, empty.

fetch_page

fetch_page(n: int) -> RawResponse

One ranking page as served (HTML); a page past the last is 404: empty.

FinraBearer

Bases: BearerAuthenticator

common-auth’s bearer with FINRA’s token admitted: the token FINRA mints carries * (2,331 characters, 2026-10-19), which the RFC 6750 grammar the package enforces refuses, so the one check is widened to the header’s token characters (RFC 7230) here; validity (not before, expiry) and the token type are checked as the package checks them. A gap to close in common-auth, named here until it is.

FinraClient

Bases: BaseClient

FINRA’s Query API. Carried state: the base’s only (the bearer, when one is handed in, is the authenticator’s).

fetch_data

fetch_data(dataset: tuple[str, str], start: dt.date | None = None, end: dt.date | None = None, offset: int = 0, limit: int | None = None, fields: Sequence[str] | None = None, equal: Mapping[str, str] | None = None, date_field: str = 'settlementDate') -> RawResponse

One page of one dataset as served (a JSON list of rows; a window with nothing answers 204: ok, no rows): start / end as a dateRangeFilter on date_field, equal as compareFilters, fields the columns asked.

fetch_partitions

fetch_partitions(dataset: tuple[str, str]) -> RawResponse

One dataset’s partitions document as served.

FinraFilesClient

Bases: BaseClient

FINRA’s Reg SHO daily files on its CDN. Carried state: the base’s only.

fetch_short_volume

fetch_short_volume(day: dt.date) -> RawResponse

One day’s consolidated NMS short-sale volume file as served (text).

GeneralClient

Bitstamp, NYSE, Nasdaq, Nasdaq Trader, OpenFIGI, Coin Metrics, FINRA, IB’s file, companiesmarketcap and mcal behind FMPClient’s section names (EODHD is EODHDClient, its own):

2 · UNIVERSE cryptos() · coin_figis() · figis()
3 · CALENDAR sessions() · nyse_calendar() (mcal, the NYSE page)
4 · PRICES daily() · intraday() (Bitstamp)
5 · CORPORATE ACTIONS nasdaq_events() · nasdaq_dividends() · the Nasdaq calendars
7 · METRICS coin_catalog() · coin_metrics(table) · coin_metric_definitions()
(Coin Metrics: the four tables, built or updated) ·
short_interest() · short_interest_dates() · short_volume()
(FINRA) · ib_shortable() (IB's file) · borrow_slugs() ·
borrow_history() (companiesmarketcap)

Carried state: the nine vendor clients it OWNS (built from the borrowed limiter it is handed, or given ready-made) and closes; the mcal NYSE calendar object. Nothing about any call is kept.

aclose

aclose() -> None

aopen

aopen() -> GeneralClient

borrow_history

borrow_history(symbols: str | Iterable[str], start: dt.date | None = None, end: dt.date | None = None, slugs: Mapping[str, Mapping[str, Any]] | None = None) -> dict[str, RawResponse]

IB’s borrow fee and shares available per day as companiesmarketcap republishes them, our symbol -> answer with rows {symbol, date, borrow_fee (annual fraction: 25 bps -> 0.0025), shortable_shares} oldest first, one row a day (the day’s last reading, UTC) in [start, end] (None: from BORROW_FLOOR; None: today). One page a symbol, CONCURRENCY at a time; slugs (borrow_slugs; read here when not handed in) says the page. A symbol the site does not rank, or whose page carries no borrow data (Berkshire), answers ok and empty with the reason named; a failed page comes back as it is.

borrow_slugs

borrow_slugs() -> dict[str, dict[str, Any]]

The site’s slug for every company it ranks, symbol (as the site spells it: BRK-B as we do; a foreign one with its suffix, 2222.SR) -> {slug, name}: its ranking pages read one after another until one is past the last (114 pages of 100 on 2026-10-19, about two minutes at the declared pace). FetchError on a failed page (a catalog: no holes).

closures

closures(start: dt.date, end: dt.date) -> list[dict[str, Any]]

The NYSE’s weekday closures and shortened weekday sessions in [start, end], oldest first: date, is_closed, open_et / close_et (the shortened session’s wall-clock times; None for a closure). Saturdays are sessions of their own, never holidays.

coin_catalog

coin_catalog(symbols: str | Iterable[str]) -> dict[str, dict[str, dict[str, Any]]]

Each coin’s daily metrics on the community tier, our symbol -> {metric: {first, last}} (the first and last day served); a coin Coin Metrics does not know is absent. One call per COINS_PER_CALL coins. FetchError when a catalog call fails (a catalog: no holes).

coin_figis

coin_figis(bases: Iterable[str] | None = None) -> list[dict[str, Any]]

OpenFIGI’s coin records (securityType2 CRYPTO; the pairs it calls CROSS and FORWARD left out): figi, ticker (the coin’s code on its “Crypto” records, Bloomberg’s on its 47 SPOT ones: XBT for bitcoin), name. bases None: every one (the filter list, ~4 minutes); else only those its mapping finds under each code (one job each, 100 a request keyed). A code is not a coin: OpenFIGI’s TIA is Tianhe, not Celestia; BB is BlackBerry’s — the caller matches on the name too.

coin_metric_definitions

coin_metric_definitions(metrics: Iterable[str] | None = None) -> list[dict[str, Any]]

Coin Metrics’ own definitions of its metrics (metric, full_name, description, category, unit, data_type), all of them or the asked ones; FetchError on a failed call.

coin_metrics

coin_metrics(symbols: str | Iterable[str], table: str, start: dt.date | None = None, end: dt.date | None = None, catalog: Mapping[str, Mapping[str, Mapping[str, Any]]] | None = None) -> dict[str, RawResponse]

One table’s days per coin (COIN_TABLES: supply, valuation, network, exchange), our symbol -> answer with rows {symbol, date, <column>..., complete, final} oldest first, values as floats (None where the day carries none). complete is Coin Metrics’ own word that the day’s end-of-day values were all computed (COIN_COMPLETE, set 01:30-04:10 UTC the following day; None for an asset without the stamp, the ones with no node); final is False while any value carries the flash status — the exchange flows keep it for days (btc’s of 10-15 still flash on 10-19), so a flash value is a provisional number to land and replace, never one to wait for. start None is the whole history (COIN_FLOOR; the API serves from each metric’s first day), end None yesterday UTC, the last complete day: the build and the update are this one verb windowed, and an update re-asking the last days replaces what was provisional. The catalog (coin_catalog; read here when not handed in) says which of the table’s metrics each coin carries: coins are grouped by that set and asked together, COINS_PER_CALL a call, so no call names a metric an asset lacks (a 403 on the whole call); a coin carrying none of the table’s metrics answers ok and empty, the reason named; a day served with no value at all drops by reason. A coin Coin Metrics does not know is a failed answer only when asked alone; grouped, it is absent from the catalog and so asked for nothing.

cryptos

cryptos(trading_only: bool = False) -> list[dict[str, Any]]

Bitstamp’s crypto/USD pairs: name ending /USD whose base is a CRYPTO currency in Bitstamp’s own catalog (EUR/USD, GBP/USD… are fiat pairs and dropped), each with symbol added in FMP’s spelling (BTCUSD) so the catalogs join and the base currency’s catalog row as currency; disabled pairs kept unless trading_only.

daily

daily(symbols: str | Symbols, start: dt.date | None = None, end: dt.date | None = None) -> RawResponse | dict[str, RawResponse]

Bitstamp’s daily candles (UTC days), one pair or many, oldest first, finished days only (bars.settled). start None = Bitstamp’s first day; end None = today. Coin volume served, dollars = coins x the day’s typical price (Bitstamp has no daily VWAP); a candle with no coins traded is no bar (named).

figis

figis(listings: Iterable[Mapping[str, Any]], unlisted: bool = False) -> dict[str, list[dict[str, Any]]]

OpenFIGI’s US-composite records for catalog rows (symbol, plus isin / cusip / figi when known): symbol -> the records (figi, compositeFIGI, shareClassFIGI, ticker, name, securityType, securityType2, marketSector, exchCode); unlisted for delisted eras (figi_jobs). Each row is asked by its surest id first and by the next only while nothing is found (figi_jobs), one batch per step over the rows still empty. A figi from FMP’s sec-profile is the ISSUER’s for notes and preferreds (STRC, STRD -> MicroStrategy’s common; SOJC -> Southern’s; probed 2026-10-05), so what it finds counts only under the row’s own ticker. Each record also says what it IS (papers.openfigi_kind): asset_class, security_type, refused. An empty list = OpenFIGI found nothing. A failed request raises FetchError (ids are a catalog: no holes).

ib_shortable

ib_shortable(country: str = 'usa') -> dict[str, Any]

IB’s shortable file for one country read (_ib_file): as_of (the file’s day) and time, declared (the trailer’s count, checked against the rows: ValueError when they differ), rows — one per USD line, in our shape: symbol (ours: papers.spell_ib), symbol_as_told, date, name, conid, isin (as IB masks it), figi, borrow_fee and rebate_rate (annual FRACTIONS: 0.2782% -> 0.002782; None for IB’s NA), shortable_shares (IB_AVAILABLE_CAP for “>10000000”, capped True) — and dropped: a line in another currency, a spelling that is no US ticker (a dead era’s .OLD marker, a bond’s CUSIP), named. The DataClient lands the fee and the shares (the rebate is told, not stored).

intraday

intraday(symbols: str | Symbols, start: dt.date | None = None, end: dt.date | None = None, interval: Interval = '1min') -> RawResponse | dict[str, RawResponse]

Bitstamp’s intraday bars of the UTC days [start, end], one pair or many, oldest first, in the common shape, every width; coins served, dollars = coins x the bar’s typical price. start None = INTRADAY_FLOOR; end None = today.

nasdaq_dividends

nasdaq_dividends(symbols: str | Symbols, start: dt.date | None = None, end: dt.date | None = None, asset: Asset) -> RawResponse | dict[str, RawResponse]

Nasdaq listings’ dividend histories (asset etf or equity), oldest first, in the common shape: the payouts with cash (Cash, and Cash/Stock beside a stock dividend: CBSH’s December 0.27), amount as paid, period ‘unknown’ (Nasdaq names none), currency as stated. A symbol Nasdaq does not list comes back failed, named. start / end bound the ex-dates.

nasdaq_dividends_calendar

nasdaq_dividends_calendar(start: dt.date, end: dt.date) -> RawResponse

Every dividend Nasdaq’s calendar lists with its ex-date in [start, end] (its own listings), one call per calendar day, in the common shape; a future day lists what is announced. Symbols in our spelling (BRK.B -> BRK-B).

nasdaq_events

nasdaq_events(start: dt.date, end: dt.date) -> RawResponse

Nasdaq’s own listing events dated in [start, end] (its status history: Nasdaq listings only, from 2024-01-01 — an earlier start is clipped), every page, oldest first: symbol (ours), date, name, event (NASDAQ_EVENTS: listed, delisted, suspended, listing_expected, delisting_expected, renamed — that one names no old or new ticker —, else Nasdaq’s words, lower-cased: financial status changes, market class changes; 617 events 2026-09-01..10-09). A row it cannot read is dropped, named. The pages are asked one at a time (the client’s quota paces them: Nasdaq’s site serves about a dozen, then rests); a page that comes back blank is asked again after a wait (_nasdaq_status_page). A failed page fails the answer.

nasdaq_splits_calendar

nasdaq_splits_calendar(since: dt.date | None = None) -> RawResponse

Every split Nasdaq’s calendar lists — every US venue, not its listings only — in splits’ shape: symbol (ours: BRK.B -> BRK-B), date (the effective day), split_to / split_from, split_type None. It reaches back about six months whatever since says (probed 2026-10-08: since 2020-01-01 answered from 2026-04-08) and lists announced splits weeks ahead. Ratios are to : from (“3 : 1” forward, “1 : 10” reverse) or a stock dividend “2%” (102 for 100); a row it cannot read is dropped, named. Its ratio is not the authority: on CDT 2026-09-29 it said 1:10 where the price moved 25x, as FMP and EODHD had it (DataClient judges on the price).

nasdaq_today

nasdaq_today() -> RawResponse

Today’s adds and deletes on Nasdaq’s trading systems, every primary market (its newest day only): symbol (ours), date, name, event (listed / delisted), market (Nasdaq’s code of the primary listing market: Q Nasdaq, N NYSE, P NYSE Arca, A NYSE American, Z Cboe BZX …). A failed answer comes back as it is.

nyse_calendar

nyse_calendar() -> dict[str, Any]

NYSE’s own announced calendar (nyse_calendar): the years it covers, its closures and its early closes. FetchError when the page does not come back, ValueError when it cannot be read whole.

sessions

sessions(start: dt.date, end: dt.date) -> list[dict[str, Any]]

Every NYSE session in [start, end], Saturdays included (the NYSE traded them until 1952): date, open/close as UTC instants and on the US/Eastern wall clock, and whether it is shortened (half day) against ITS era’s regular hours — mcal’s own early closes and late opens (10:00-15:00 until 1952, 15:30 until 1974, 9:30 from 1985), so a 1960 session closing 15:30 is a full day, and so is a Saturday’s 10:00-12:00 (its regular hours).

short_interest

short_interest(symbols: Iterable[str] | None = None, start: dt.date | None = None, end: dt.date | None = None) -> RawResponse

FINRA’s consolidated short interest settled in [start, end] (None: from SHORT_INTEREST_FLOOR; None: today), every venue, oldest first, in our shape: symbol (ours: papers.spell_finra reads the glued ABRPRD as ABR-PD, BRKB on the NYSE as BRK-B), symbol_as_told, date (the settlement date), short_interest (shares), previous_short_interest, change, change_percent, average_daily_volume, days_to_cover, market_class (FINRA’s code), name, split, revised. symbols keeps those of ours (every symbol otherwise: 22,581 a settlement date). Asked a calendar month at a time, every page (5,000 rows; two settlement dates a month run to nine pages); a spelling that is no US ticker is dropped, named (one in 22,581: a when-issued right). A failed page comes back as it is.

short_interest_dates

short_interest_dates() -> list[dt.date]

The settlement dates FINRA serves short interest for, oldest first (the partitions: a new one is the update’s signal). FetchError on a failed call.

short_volume

short_volume(start: dt.date, end: dt.date | None = None) -> RawResponse

FINRA’s consolidated daily short-sale volume for the weekdays in [start, end] (None: start alone), one file a day (SHORT_VOLUME_FLOOR), oldest first, in our shape: symbol (ours), symbol_as_told, date, short_volume, short_exempt_volume, total_volume (shares, fractional as FINRA serves them), markets (the venues the volume printed on: B, Q, N). A day with no file (a holiday; today before the file is cut) is noted in dropped, never a failure; a file whose trailer count differs from its rows fails the answer; a failed call comes back as it is.

IBFilesClient

IB’s public shortable files over FTP (no HTTP: ftplib in a worker thread). Carried state: none; fetcher is handed in for tests (a callable from a file name to its bytes).

fetch_countries

fetch_countries() -> list[str]

The countries IB publishes a file for (usa, canada …).

fetch_shortable

fetch_shortable(country: str = 'usa') -> str

One country’s file as served (text).

NYSEClient

Bases: BaseClient

NYSE’s hours-and-calendars page: the exchange’s own announced holidays and early closes (three years ahead). Carried state: the base’s only.

NasdaqClient

Bases: BaseClient

Nasdaq’s public quote API (api.nasdaq.com): the exchange’s own dividend record for the securities it lists. Keyless; it answers only a browser’s headers. Probed 2026-10-07: amounts are cash AS PAID (AAPL 2020-08-07 $0.82, before its 4:1); only Nasdaq listings are carried (SPY, KO: an empty answer; BRK-B: “Symbol not exists”); the daily calendar lists that day’s ex-dates for its listings, and had same-day ETF payouts (CEPI, FEPI, MST on 10-07) before FMP and EODHD did. Carried state: the base’s only.

fetch_dividends

fetch_dividends(symbol: str, assetclass: str) -> RawResponse

One listing’s dividend record as served (assetclass etf or stocks).

fetch_dividends_calendar

fetch_dividends_calendar(day: str) -> RawResponse

One day’s ex-dates as served (YYYY-MM-DD).

fetch_splits_calendar

fetch_splits_calendar(since: str) -> RawResponse

Every split Nasdaq’s calendar lists from since (an ISO day) on, as served.

NasdaqTraderClient

Bases: BaseClient

Nasdaq Trader (www.nasdaqtrader.com): the exchange’s own record of what it lists and stops listing. Keyless, a browser’s headers. Probed 2026-10-10:

Trader.aspx?id=nasdaq-security-status-updates&from=MM/DD/YYYY&to=MM/DD/YYYY[&page=N] an HTML table, dated, Nasdaq listings only, from 2024-01-01: Effective Date, Symbol, Company Name, Issue Event (Security Additions, Security Deletions, Anticipated Security Additions / Deletions, Name/Symbol Changes — with no old or new ticker —, Financial Status Changes …), and the financial statuses; pages linked beneath it (~50 rows each) dynamic/SymDir/TradingSystemAddsDeletes.txt today’s adds and deletes on Nasdaq’s three trading systems, every primary market (Symbol|Company Name|NASDAQ Action|BX Action|PSX Action|Effective Date|Primary Listing Market): the newest day only, no history

Carried state: the base’s only.

OpenFIGIClient

Bases: BaseClient

OpenFIGI’s mapping API: keyless unless the flow hands in an authenticator for its key (AUTH), which raises both the quota and the jobs a request carries. Carried state: the base’s only (jobs_per_request reads whether a key was handed in).

filter_all

filter_all(body: Mapping[str, Any]) -> list[dict[str, Any]]

Every record OpenFIGI’s filter holds for body (e.g. every coin: {“marketSecDes”: “Curncy”, “securityType2”: “CRYPTO”} — 8,168 on 2026-10-10, 82 pages of 100), page after page by its next cursor, spaced to FILTER_PER_MINUTE (the quota paces mapping’s 25 or 250 a minute, not filter’s 20). A failed page raises FetchError (a catalog: no holes).

map_jobs

map_jobs(jobs: list[dict[str, Any]]) -> list[RawResponse]

Map OpenFIGI jobs ({"idType": ..., "idValue": ..., "exchCode": ...}), sent jobs_per_request at a time as one common-http batch. One response per REQUEST: its rows are the per-job answers in job order ({"data": [...]}, or {"warning": "No identifier found."}, or {"error": ...}).

Functions

figi_jobs

figi_jobs(listing: Mapping[str, Any], unlisted: bool = False) -> list[dict[str, Any]]

The OpenFIGI jobs for one catalog row, surest first — each asked only when the ones before it found nothing: its ISIN, its CUSIP, its ticker on the US composite (LISTED instruments only), and a FIGI handed in as figi (FMP’s sec-profile openFigiComposite). The ticker is always asked when the ids find nothing: a reissued ISIN can be missing from OpenFIGI (CPOP after its reverse split, 2026-10-05). Unlisted instruments are asked for only with unlisted (a delisted era: a dead ticker is found only with includeUnlistedEquities): for an active listing that flag would also return every dead instrument that once carried the ticker (89 of 5,725 active equities on 2026-10-05: WBD -> Wimm-Bill-Dann’s ADR beside Warner Bros. Discovery, NXT -> an ETF).

figi_ticker

figi_ticker(symbol: str) -> str

A share class in Bloomberg’s spelling, which OpenFIGI requires (BRK/B found; BRK-B and BRK.B not, probed): a one-letter class after a dash (everyone’s, FMP’s), a dot (FMP lines that kept Nasdaq’s), or already a slash. Anything else is passed as it is (the equity universe holds common stock only: no preferreds here).

nasdaq_adds_deletes

nasdaq_adds_deletes(text: str) -> list[dict[str, str]]

Nasdaq’s adds-and-deletes file as rows (its header’s columns). ValueError when the header is not the one expected.

nasdaq_page_blank

nasdaq_page_blank(page: str) -> bool

Whether a status page came back without its results grid at all — Nasdaq’s throttle (a 212-byte blank under a 200), never a page with no events: that one still carries the grid.

nasdaq_status_page

nasdaq_status_page(page: str) -> tuple[list[dict[str, str]], int]

One page of Nasdaq Trader’s status history: its rows (the header’s columns) and the last page it links to. ValueError when the table is not the one expected.

nyse_calendar

nyse_calendar(page: str) -> dict[str, Any]

NYSE’s announced calendar from its hours-and-calendars page (the holiday table it embeds as JSON cells, and the early closes it states in footnotes): years it covers, closed {date: holiday name}, early {date: close time, ET}. A year whose cell is a dash observes no such holiday (2028: New Year’s Day falls on a Saturday). Raises ValueError when the page cannot be read whole (a missing year header, a holiday row, a cell without a date, no early close) — never a partial calendar.