docs/MARKET-DATA.md
Rama main · réplica de lectura del repositorio original.
# Market data
Sage does not have a licensed market-data feed. This document sets out exactly
what it uses instead, what that costs you, and what you see when it fails.
What it has instead is a default that works and some official alternatives
whose free tiers are small. Being straight about that is the point of this
document.
**The default is Yahoo, and it is unofficial.** With no API key set, Sage reads
prices through [`yahoo-finance2`](https://github.com/gadicc/node-yahoo-finance2),
an unofficial client for endpoints Yahoo does not publish as a public API. It has
broad coverage and costs nothing, and it can break without warning — it did on
2026-08-09, when an upstream change blanked every price in the app. Yahoo's terms
of service govern what you may do with that data; personal self-hosting is the
case Sage is built for, and if you have anything commercial in mind you should
read those terms yourself rather than take this document's word for it.
**The licensed options are official, and their free tiers are small.**
| Provider | Free tier | Notes |
| --------------------------------------------- | ------------------------- | ------------------------------------------------------------ |
| [EODHD](https://eodhd.com/pricing) | 20 API calls/day | Prices 1 call each; fundamentals are paywalled |
| [Twelve Data](https://twelvedata.com/pricing) | 8 credits/minute, 800/day | Free plan is US prices only; see [Twelve Data](#twelve-data) |
| ECB (FX only) | unrationed | Official euro reference rates, no key |
Twenty calls a day does not cover the first load of a thirty-holding portfolio,
because each symbol needs its own quote. Sage handles this by falling back to
Yahoo whenever the primary provider cannot serve a symbol — which means that in
practice, **a free-tier setup still leans on the unofficial path**, and adding a
key does not by itself change that.
On EODHD's free tier the fundamentals endpoint is paywalled outright, so company
profile data (sector, market cap, P/E) will not come from it; Sage degrades
around this rather than failing. Enrichment (`ENRICHMENT_PROVIDER`, default
`yahoo`) accepts `yahoo`, which fills those gaps from the unofficial path even
when your prices come from EODHD.
## Twelve Data
Set `MARKET_DATA_PROVIDER=twelvedata`, `TWELVEDATA_API_KEY`, and
`TWELVEDATA_CREDITS_PER_MINUTE` to your plan's per-minute credits (free 8,
Grow 55, Pro 610). Sage never sends more than that in a minute: once the
minute's credits are spent, requests go straight to Yahoo instead of queueing.
**What a key covers depends on its plan.** Checked against the live API on
2026-09-16; Twelve Data's own [pricing](https://twelvedata.com/pricing) is the
authority.
| Data | Individual plan needed | Business plan needed |
| ------------------------------------------------------------------------- | ---------------------- | -------------------- |
| US quotes and daily history | Basic (free) | Venture |
| Dividends, company profile | Grow | Venture |
| Germany, London, Euronext (Paris, Amsterdam, Brussels, Lisbon), Toronto | Grow | Venture |
| Copenhagen, Stockholm, Oslo, Helsinki, Milan, Madrid, SIX, ASX, Hong Kong | Pro | Venture |
Anything your plan does not include is served by Yahoo, silently and by design:
the degraded-prices notice does not report it, because a plan declining an
exchange is not an outage. A free key on a mostly European portfolio therefore
still leans almost entirely on the unofficial path.
Market cap, P/E, beta, dividend yield and ETF composition are not read from
Twelve Data; with `ENRICHMENT_PROVIDER=yahoo` they come from Yahoo as before.
**Licensing.** Twelve Data's individual plans are for personal, internal,
non-commercial use. Running Sage for yourself on your own key fits that. Showing
the data to other people — a hosted service — needs a business plan; the
Venture plan is the one that grants external display.
**What you see when data is unavailable.** Prices are stored in your own
Postgres, so an outage or an exhausted quota does not empty the app. You keep
seeing the last known values, with a notice naming the age and the cause
("Prices last updated 2 days ago — EODHD has spent its daily allowance"). A
symbol that has _never_ been fetched has nothing to show and will read as
unavailable until a provider answers for it. Currency conversion is unaffected:
ECB rates are stored locally too, and are neither rate-limited nor tied to your
market-data provider.
**Region comes from the country name.** Diversification's region breakdown is
derived from a two-letter country code. Yahoo and Twelve Data report a country
name, which Sage maps to a code from a table of the names those providers
actually return; a name it does not recognise stays `Unknown` rather than
guessing, because a wrong region is worse than an absent one.