All commands default to data/pyvalue.db unless --database is provided.
The stage commands from fundamentals ingestion onward use one shared scope model. If you omit all scope selectors, the command defaults to the full supported universe. When you do provide a selector, provide at most one of:
--symbols <symbols...>: one or more fully qualified symbols such asAAPL.USorSHEL.LSE--exchange-codes <codes...>: one or more canonical exchange codes such asUSorLSE--all-supported: the full current supported-ticker catalog
Provider rules:
refresh-supported-exchanges,refresh-supported-tickers,ingest-fundamentals,reconcile-listing-status,normalize-fundamentals, andupdate-market-dataaccept--providerand default it toEODHDcompute-metrics,run-screen,report-fact-freshness,report-metric-status, andreport-screen-failuresare provider-agnostic and operate on canonical symbols
For EODHD-backed symbols, downstream stage commands and canonical-scope
commands filter by canonical primary-listing classification: listings
classified as secondary are excluded from
normalization, market-data refresh, metric, screening, metadata-refresh, and
canonical reporting scopes. Classification applies an ordered rule set over
several EODHD fields (see docs/providers/eodhd.md); a listing no rule can
settle is unknown, which stays eligible — only secondary is excluded.
That classification is written only by
ingest-fundamentals (in the same transaction that stores each raw payload) and
by reconcile-listing-status; every other command reads the cached
listing.primary_listing_status and never reconciles as a side effect.
Classification writes only that status column -- a secondary listing keeps its
stored data and is excluded from universe work by the primary-only scopes. A
one-time migration (078) resolves any leftover unknown listing that already
has stored fundamentals, so reads can trust the cache.
Refresh and persist the provider-supported exchange catalog.
Key options:
--provider {EODHD}- default provider:
EODHD --allow-mass-drop--database <path>
Notes:
EODHDrefreshes the live exchange list from EODHD- an exchange absent from the provider's list is dropped from
provider_exchangetogether with its provider layer (provider_listingmappings plus their raw fundamentals and fetch/normalization state); each drop is printed with its purge size. Canonical rows (exchange,listing,issuer) and canonical data are never deleted -- listings that lose their mapping become unreachable through the provider-joined scopes, nothing more - a payload that would drop at least 5 provider exchanges and more than
half of the catalog looks like a truncated response: the sync is rolled
back untouched and exits
1unless--allow-mass-dropis passed - run this after any EODHD plan change so
refresh-supported-tickers(which iterates the provider's cataloged exchanges) stops visiting dropped venues
Refresh and persist the provider-supported ticker catalog.
Key options:
--provider {EODHD}- default provider:
EODHD --exchange-codes <codes...>--all-supported--allow-mass-delisting--database <path>
Notes:
- omitting both
--exchange-codesand--all-supporteddefaults to the full supported exchange catalog for the provider EODHDreadsexchange-symbol-list/<EXCHANGE_CODE>and keeps onlyCommon Stock,Preferred Stock, andStock- Refreshes never delete canonical data. Removed provider symbols lose
only their provider layer: the
provider_listingmapping, their raw fundamentals, and the relevant fetch/normalization-state tables. Canonical rows (listing,issuer) and canonical data (facts, market data, metrics, compute/refresh state) are provider-independent and are retained; a listing left with no provider mapping is reported as orphaned and simply becomes unreachable, because every scope resolver and catalog view joins throughprovider_listing - an exchange the EODHD plan no longer covers answers the symbol list with HTTP 404; the run warns, skips it with stored data untouched, and continues with the remaining exchanges
- a payload that would remove at least 20 provider listings and more than
half of the exchange's existing mappings looks like a truncated response or
a plan change: the slice is rolled back untouched and skipped unless
--allow-mass-delistingis passed - exit code:
0when every exchange refreshed or was only skipped as not-in-plan;1when any exchange hit the mass-delisting guard or another provider error (so cron jobs surface it)
Download fundamentals for supported tickers from the chosen provider.
Key options:
--provider {EODHD}- default provider:
EODHD - optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --rate <float>--max-symbols <int>--max-age-days <int>default30--retry-failed-now--database <path>
Notes:
EODHDrate is symbols per minuteEODHDuses the stored supported-ticker catalog plus daily quota checks, a concurrent worker pool, and retry backoff for multi-day runs- EODHD raw writes do not store or infer listing currency from
General.CurrencyCode; listing currency remains catalog metadata onlisting - storing an EODHD raw payload also refreshes cached primary-vs-secondary listing classification for that symbol
- retry backoff is respected by default; use
--retry-failed-nowto ignore it - the default EODHD fundamentals rate is
950 req/min, leaving a small buffer under the1000 req/minprovider limit - omitted
--max-age-daysnow means the same 30-day freshness window used by the other CLI freshness filters - when nothing in a valid scope is eligible the command says which of the two
causes applies — everything is inside the freshness window (rerun with
--max-age-days 0to force a re-fetch), or everything stale is serving retry backoff (rerun with--retry-failed-now)
Group listings of the same legal entity onto one issuer row, from the ISIN and
LEI already stored on each listing.
Key options:
--database <path>
Notes:
- takes no scope selector on purpose: grouping is only as good as the set it sees, so a partial view would split entities rather than merge them
- makes no provider calls, but it does read the stored payloads: the LEI lives
in
fundamentals_raw, deliberately not cached onlistingwhere it would duplicateissuer.lei. ~97s on the full catalog — it seeks each payload by key rather than scanning the set, so it costs far less thanreconcile-listing-status - the sole writer of
issuer.lei, and the only path that merges issuer rows in bulk; it repoints listings onto the group's lowestissuer_idand deletes the emptied rows, all in one transaction - safe to repeat: a catalog already matching the derived shape issues no writes
- it does not link a depositary receipt to its underlying — they are different securities with different ISINs and receipts rarely carry an LEI
Backfill canonical EODHD primary-vs-secondary listing classification from stored raw fundamentals only.
Key options:
--provider {EODHD}- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --database <path>
Notes:
- this command does not download fundamentals or market data
- it reads existing
fundamentals_rawpayloads and writeslisting.primary_listing_status - a repair tool, not part of the normal flow:
ingest-fundamentalsruns the same pass per write batch and leaves both derived values final. On a healthy catalog this reports zero changes, which is how you confirm that - it settles issuer identity too — the two cannot be run independently, since grouping names a merged issuer after its primary listing
- run it after a rule change, or to repair a database that drifted some other way
- a narrow
--symbols/--exchange-codesrun is still correct: the scope expands to the whole ISIN/LEI/issuer neighbourhood and every listing in it is re-evaluated and written, so a narrow run reaches the same verdict a full pass would - reclassification writes only the status column. A listing that flips to
secondarykeeps its facts, market data, metrics and refresh state; exclusion is purely scope-side, so a later flip back to primary restores it intact - the run prints the resulting status counts, which rule decided each listing, and the before/after distribution across the database
- a missing
General.PrimaryTickeryieldsunknown, notprimary— reading absence as proof of primacy was the original defect.unknownstays eligible for primary-only scopes
Report EODHD fundamentals ingest progress across supported tickers.
Key options:
--provider {EODHD}--exchange-codes <codes...>--max-age-days <int>default30--missing-only--database <path>
Notes:
Storedmeans a raw fundamentals payload exists in the databaseFreshmeans the symbol currently satisfies the selected completeness rule--max-age-days 0is honoured literally: every stored payload counts as stale- status is strict:
COMPLETE: no missing, stale, or blocked symbols remainBLOCKED_BY_BACKOFF: only retry-blocked failures remainINCOMPLETE: missing or stale symbols remain
Normalize stored fundamentals into canonical financial_facts.
Key options:
--provider {EODHD}- default provider:
EODHD - optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --forceto re-normalize even when stored raw fundamentals are already up to date--database <path>
Notes:
- bulk runs over
--exchange-codesor--all-supportedparallelize automatically - only symbols with stored raw fundamentals are normalized
- EODHD listings already classified as secondary are excluded from the requested scope before normalization starts
- EODHD normalization resolves its target from base(
listing.currency);listing.currencyitself preserves the catalog quote unit, including subunits such asGBX,ZAC, andILA - raw payload currencies are used only as fact source currencies
- fact source-currency lookup uses entry-level currency keys, then direct
statement-level currency, then payload-level
General.CurrencyCode - by default, normalization skips symbols whose raw
fundamentals_raw.payload_hashmatches the payload hash recorded by the last successful normalization for that provider - bulk runs with
--forceskip the freshness scan and start re-normalizing the requested symbol set immediately - normalization never fetches FX from the network; run
refresh-fx-ratesfirst when you need currency conversion coverage - bulk normalization resolves direct, inverse, and configured-pivot
triangulated rates (default pivots
USD, EUR, GBP) from the stored canonical series, loading each pair's history lazily per worker process and caching it in memory - if a required conversion still cannot be resolved from stored FX, the affected monetary fact is skipped with a structured warning
- the console shows progress lines only; every warning and error (missing FX,
quarantined periods, per-symbol failures, ...) is written to
data/logs/pyvalue.loginstead, and the final summary points at the log file whenever failures occurred
Fetch latest market data for supported tickers and dual-write each observation:
the provider layer (provider_market_data, keyed by the provider listing) and
the canonical market_data series, in one transaction.
Key options:
--provider {EODHD}- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --rate <float>--max-symbols <int>--max-age-days <int>default30--retry-failed-now--database <path>
Notes:
- market-data refreshes use hybrid EODHD accounting: per-symbol requests cost
1, while exchange-bulk refreshes cost100for the exchange - the command is freshness-based by default and selects missing symbols first, then the oldest stale symbols
- EODHD listings already classified as secondary are excluded before refresh planning and progress accounting
- retry backoff is respected by default; use
--retry-failed-nowto ignore it - large exchange and all-supported runs may use exchange-bulk fetches and then fall back to individual symbols when needed
- progress across multiple days is tracked through
market_data_fetch_state - no price-anomaly guard runs before persistence; per-symbol fetch errors are
stored as fetch failures in
market_data_fetch_state - when nothing in a valid scope is eligible the command says which of the two
causes applies — everything is inside the freshness window (rerun with
--max-age-days 0to force a refresh), or everything stale is serving retry backoff (rerun with--retry-failed-now)
Report EODHD market-data refresh progress across supported tickers.
Key options:
--provider {EODHD}--exchange-codes <codes...>--max-age-days <int>default30--database <path>
Notes:
Storedmeans a market-data snapshot exists in the databaseFreshmeans the latest snapshot satisfies the selected freshness window--max-age-days 0is honoured literally: every stored snapshot counts as staleRecent failureslists API/network failures
Compute one or more metrics for a canonical ticker scope.
Key options:
- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --metrics <metric-ids...>default all registered metrics- console output defaults to periodic symbol progress like
Progress: 1234/75848 symbols complete (1.6%) - per-listing metric/data-quality diagnostics (warnings plus INFO notices such as documented-cap emissions) are suppressed on the console by default but still written to
data/logs/pyvalue.log --show-metric-warningsto show those diagnostics on the console again--database <path>
Notes:
- stored metric rows now include explicit
unit_kind, optionalcurrency, and optionalunit_label - canonical metric/screen/report scopes exclude EODHD listings already classified as secondary from raw fundamentals
- monetary and per-share metrics are FX-aware; ratio, percent, multiple, and count metrics remain non-monetary outputs
- every metric attempt also updates
metric_compute_status, which stores the latest success or failure plus the input watermarks used for freshness checks
Fetch and store direct FX rates for the configured provider.
Key options:
--database <path>--start-date <YYYY-MM-DD>optional historical backfill start--end-date <YYYY-MM-DD>optional end date, default today
Notes:
- with the default
EODHDprovider, the command syncs the FOREX catalog intofx_supported_pairsfirst - EODHD refresh iterates canonical six-letter pairs only; three-letter
shorthand aliases such as
EURare tracked as aliases toUSDEURand are not refreshed separately - dual-writes each rate: the provider row in
provider_fx_ratesand the canonical provider-free rate infx_rates - EODHD stores per-pair coverage and retry state in
fx_refresh_state - the first EODHD run backfills full available history per canonical pair when
--start-dateis omitted; later runs top up only missing older/newer outer ranges --start-datelimits the first requested window, but a later unbounded run can still complete the older missing history- progress is reported pair-by-pair on the console
- later runtime lookups can use direct, inverse, or triangulated conversion from those stored rows
List missing or stale financial facts required by metrics for the requested canonical scope.
Key options:
- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --metrics <metric-ids...>--max-age-days <int>default400--output-csv <path>--show-all--database <path>
Notes:
- when the selection includes any
uses_market_datametric, the report ends with a one-line market-data seam summary (fresh/stale/missing price snapshots over the scope) — concept coverage alone cannot explain NAs caused by a missing or stale price
Rank metrics by persisted NA share (failed or never-attempted) for the
requested canonical scope, and break the failures down by reason — the survey
side of the diagnostics. A pure read: nothing is recomputed and nothing is
written; run compute-metrics first to refresh the underlying state.
Key options:
- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --metrics <metric-ids...>or--config <path>(mutually exclusive);--configrestricts the report to the screen's criteria metrics — the set whose NA excludes a symbol from that screen--reasonsbreaks each metric down into per-reason buckets with a representative example--output-csv <path>--database <path>
Notes:
- the summary is a SQL aggregate over
metric_compute_status, so a full-universe ranking finishes in seconds;na_share = (failures + never_attempted) / total_symbols, i.e. the share of the scope with no usable persisted value — what a screen effectively sees never_attemptedcounts scope listings with no persisted attempt at all for the metric (how a newly registered metric looks before its firstcompute-metricsrun)--reasonsclassifies every (listing, metric) persisted state against the current input watermarks — the same staleness lensrun-screenapplies before trusting a stored value:- fresh failures bucket by their persisted
reason_code; the example is the largest-market-cap listing in the bucket and appendsdetail=<reason_detail>(truncated on the console) when the attempt carried an untemplated detail — the CSV keeps the full text inexample_reason_detail - pairs whose persisted state no longer matches the current inputs bucket
under
stale_inputs (run compute-metrics), with the example detail summarizing the last (now untrustworthy) attempt - pairs with no persisted attempt bucket under
never_attempted (run compute-metrics) - a large stale or never-attempted bucket means the summary counts are out
of date — rerun
compute-metricsbefore reading the reason mix
- fresh failures bucket by their persisted
reason_codeis the first templated warning of the last failed attempt;reason_detailcarries the same first warning untemplated (real years, counts, dates) for guard failures, and the invariant/exception text for raised failures — useexplain-metricfor a per-symbol live recompute with every guard warning untemplated
Explain per (symbol, metric) why the metric computes or comes out NA — the microscope next to the scope-wide report commands.
Key options:
--symbols <symbols...>required (deliberately symbol-scoped; exchange and all-supported scopes are not accepted)--metrics <metric-ids...>or--config <path>(exactly one);--configexpands to the screen's criteria metrics--max-age-days <int>default400--database <path>
Each (symbol, metric) block prints:
- the persisted attempt state — stored value or failure
reason_codeplus the otherwise-buriedreason_detail, with a staleness verdict - per required concept: latest stored point (end date, fiscal period, filing date, value, currency), fresh/STALE, and FY/quarterly/total row depth
- the market-data seam (latest price snapshot or its absence) for
uses_market_datametrics - a write-free live recompute: SUCCESS with the would-be value, or FAILURE
with the templated
reason_codeplus every guard warning untemplated (real listing ids, dates, and counts)
Notes:
- never persists recomputed attempts, so it is safe to run mid-investigation without changing what screens or the report commands see
Rank which screen groups and missing metrics exclude the most symbols for the requested canonical scope — the fallout analyzer. A pure read: nothing is recomputed and nothing is written.
Key options:
--config <path>required- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --output-csv <path>(columns:metric_id,missing_symbols,affected_criteria_count,affected_criteria)--database <path>
Notes:
- evaluates every group for every symbol, so group ranking is not biased by YAML order
- metric NA counts are deduplicated by
(symbol, metric_id), even when the same metric appears in multiple groups - screen evaluation treats fresh failed or stale metric status as
unavailable, even if an older raw row still exists in
metrics - for OR / K-of-N groups a missing metric is attributed to NA fallout only when it actually blocked the group — i.e. no other arm produced a real answer
- the console report has two sections:
Metric NA impact: missing stored metrics ranked by affected-symbol count, with the groups each gap affectsCriterion fallout: per-group fail counts split intona_fails(the group was NA-blocked — no arm had data) versusthreshold_fails(an arm had data and missed its bar) — the "relax the threshold or fix the data?" signal
- per-reason NA root causes intentionally live elsewhere: the report ends its
NA-impact section with a
hint: pyvalue report-metric-status --config <screen> --reasonsdrill-down instead of duplicating that survey; runcompute-metricsfirst if statuses are missing or stale
Evaluate a YAML screen against a canonical ticker scope.
Key options:
--config <path>required- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) - console output defaults to periodic symbol progress like
Progress: 1234/75848 symbols complete (1.6%) - per-listing metric/data-quality diagnostics (warnings plus INFO notices such as documented-cap emissions) are suppressed on the console by default but still written to
data/logs/pyvalue.log --show-metric-warningsto show those diagnostics on the console again--output-csv <path>--database <path>
Notes:
- screen reads use the latest metric status when available; a fresh failed status or stale success status hides older raw metric rows until the metric is recomputed
- when the scope is a single symbol, output includes entity details and
group-by-group pass/fail rows (a multi-member group also prints an indented
per-member breakdown); when a group is NA-blocked, each missing metric is
followed by its persisted NA reason (
reason_code, never-attempted, or stale-state note) and the output ends with a paste-readyhint: pyvalue explain-metric --symbols <symbol> --metrics <ids>line - when the scope contains multiple symbols, output lists only passing symbols
- the multi-symbol console view is a compact preview with one passing symbol per row, a truncated description, and ranking columns when present
- large passing sets are previewed in the console and the command tells you how to save or inspect the full result set
--output-csvwrites a row-oriented file with one passing symbol per row, base columns such assymbol,entity,description,price, andprice_currency, then ranking columns and one column per group.price_currencyis the listing quote unit; monetary metrics and market-cap values use base(listing.currency)- if the screen YAML defines a
rankingblock, multi-symbol output also adds ranking columns such asqarp_rankandqarp_score, and sorts passing symbols by the configured ranking rules - ranked multi-symbol screens load ranking-only metrics only for passers after the initial criteria filter
- monetary and per-share comparisons apply FX only where needed; ratio-like metrics are compared directly
- monetary constants can optionally declare a currency in YAML
Refresh canonical security metadata from stored raw fundamentals without rewriting normalized facts.
Key options:
- optional scope selector:
--symbols,--exchange-codes, or--all-supported(defaults to the full supported universe) --database <path>
Notes:
- offline only; makes no network requests
- updates canonical
issuermetadata such as entity name, description, sector, and industry - intended for metadata backfills after ingesting raw fundamentals
Delete all normalized facts, financial-facts refresh state, and metric attempt status.
Delete all stored raw fundamentals.
Delete all stored metric rows and metric attempt status.
Delete all stored market-data snapshots, both layers (provider_market_data
and canonical market_data) in one transaction.
All clear commands take:
--database <path>