Data sources
Where every number comes from: Nasdaq's delayed public data, SEC EDGAR, Schwab and your imports, plus how freshness and caching work.
On this page
Investi uses four sources: Nasdaq's public endpoints for market data, SEC EDGAR for filings and financial statements, the Schwab Trader API for your brokerage account, and files you import. Each number records which source it came from and when it was observed. See Coverage and provenance for how that is shown.
Nasdaq public endpoints#
Quotes, daily and intraday price history, the market session, index levels, earnings dates and calendars, economic events, dividends, company summaries, the listing universe for the screener, and analyst consensus all come from api.nasdaq.com. Investi labels the provider as "nasdaq.com (public, delayed)", with the entitlement "Delayed public quotes for personal research. Not real-time; not for redistribution."
Quotes are delayed about 15 minutes. Batch quotes are always treated as delayed by 15 minutes. A single-security quote uses the real-time flag the feed returns with it.
Requests are rate-limited to 6 per second, time out after 12 seconds, and retry up to 3 times with backoff on rate-limit and server errors. Unknown symbols are left out of results rather than guessed.
Daily bars#
Daily bars are regular-session, split-adjusted by the provider. Indicators use a trailing 365 calendar days and Wilder's 14-day ATR, and the report states these definitions in priceSetup.definitions. Extended-hours prints are not part of daily bars; the intraday chart includes pre-market and after-hours prints for the latest session.
The market regime reads daily bars for IWM from the same source, plus your own recent buys. See Market regime.
A scheduled refresh fetches daily bars after every close, at 21:30, 22:30 and 23:30 UTC on weekdays, for every ticker anyone holds, watches, has an alert or a working order on, or evaluated in the last 30 days. Tickers that already have the session's bar are skipped, so the later runs only retry what the provider had not published yet. Each run leaves a one-line note, such as "Bars for Nov 4: 41 refreshed, 3 already current.", which appears in freshness.barsRefresh.
The live quote as a provisional bar#
Until a session's daily bar lands, its live quote counts as a provisional bar for the 52-week figures: the low and high, the date of the low, the distance above the low, the count of new lows and the age of the low. The quote supplies the session's low when the feed has one (Schwab does; the Nasdaq quote does not, so its last price stands in). Only regular-session prints (09:30 to the 16:00 close, New York time) are folded in; extended-hours prints never are. As a result:
- The distance above the 52-week low is never negative. A price under the stored low is the new low, dated today, at 0% above it.
- A new low printed today counts in "new lows in the last 30 days".
- The age of the low is measured to today, in words: "Low set 2026-11-04 (today, provisional from the live quote...)" or "Low set 2026-07-23 (104 days ago)".
Provisional figures are always labeled: priceSetup.lowProvisional and priceSetup.provisionalNote in the report, range.lowProvisional in the intel payload, and the bars segment of the company page's freshness line ("Bars through Nov 3, Nov 4 provisional from the quote"). When the real bar lands, it replaces the provisional one.
Bid-ask spread#
The spread check runs only on a quote from the regular session. Outside it (pre-market, after hours or closed) the book is thin or stale, so the rule reads INFO, "Not applicable outside the regular session", and never WARN or FAIL.
Freshness on every dataset#
Every API response and MCP tool result that carries a dataset also carries a freshness object with one shape per dataset:
{ "observedAt": "2026-11-04T01:16:00.000Z", "source": "schwab_api", "through": "2026-11-03", "stale": false, "note": "..." }observedAt: when Investi last fetched, imported or synced the dataset, or null when never.source: the provider, or the record source (schwab_api,schwab_csv,generic_csv,manual,sample).through: the newest date the data covers: the last daily bar, the last fill, the period end. It can lagobservedAt: a CSV imported this morning may hold fills only through last Tuesday.stale: past the dataset's cadence. Daily bars are stale when a completed weekday session has no bar. Orders, balances and positions from a Schwab sync follow the connection rules: stale past 15 minutes in market hours and past 26 hours outside them. From a CSV file they are stale after 26 hours. A fills ledger is stale after 74 hours without an import or sync.
Datasets: quote, bars, fundamentals, filings, catalysts, analyst and barsRefresh, plus orders, fills, balances and positions as one entry per source, and schwab: the connection state (LIVE, DELAYED, STALE, FAILED or REAUTH_REQUIRED, computed from finished sync runs only), reason, lastSyncAt (the last good run), fillsSyncedAt, ordersSyncedAt, scheduled (a scheduled run finished in the last 26 hours), reauthRequiredAt and reauthWarningExpiresAt (from day 6 of Schwab's 7-day authorization). For orders, observedAt is the last run that read the order list (Schwab) or the oldest confirmation among working orders (files). For fills, observedAt is the last CSV import for that source or the last run that read Schwab transactions.
Quotes and share counts are never merged into one freshness: a real-time quote says nothing about whether the shares behind it are current. Portfolio shows them side by side, and the headline takes the worse of the two.
Portfolio and each company page show the same data as one line, for example "Orders confirmed Nov 4, 01:16 UTC · fills through Nov 3 (CSV)". A stale segment is drawn in the warning color.
How freshness is classified#
Every quote carries a freshness label computed from the age of the data, not from a timer in the UI (classifyFreshness in packages/core/src/live.ts). Two ages matter: how long ago Investi fetched the quote (observed age) and how old the quote itself is (market age, from the trade timestamp).
Rules, checked in order:
- UNKNOWN if the observed time cannot be read.
- STALE if Investi's own fetch is more than 15 minutes old, in any session.
- When the market is closed, the last print is expected to be old: it is labeled DELAYED for delayed feeds (LIVE only for a real-time entitlement), and the UI shows "Closed" with the print time.
- Real-time feed, open session: LIVE if the quote is at most 15 seconds old, DELAYED up to 2 minutes, STALE beyond.
- Delayed feed, open session: DELAYED while the quote's age is within the declared delay plus 2 minutes, STALE beyond. For Nasdaq's 15 minute delay, that budget is 17 minutes.
The session (PRE, REGULAR, POST or CLOSED) comes from the market status Nasdaq reports with each quote, so holidays and early closes follow the provider.
Streaming#
The live quote stream (/api/v1/stream, server-sent events) covers up to 200 symbols and depends on your feed:
- Connected Schwab account, session open: one snapshot, then ticks from the Schwab Streamer (LEVELONE_EQUITIES) as they happen, at most one update per symbol every 500 ms. Investi opens one streamer connection per account, logged in with your own token, subscribed only to the symbols on your open screens, and closes it 30 seconds after the last screen goes away. If Schwab declines the login or drops the connection, the stream falls back to one quote request for all symbols every 2 seconds and tries the streamer again after a minute. Ticks are shown only to you.
- Everyone else: Nasdaq every 5 seconds while a session is open, its real update rate, labeled delayed.
When the market is closed the stream checks every 30 seconds. It sends a quote event only when the price, change, percent change, volume, pre- or after-hours print, or trade time actually changed, each with a sequence number and its freshness label, plus heartbeats. A connection ends after about 4.5 minutes and the client reconnects (the stream asks for a 3 second retry); sequence numbers keep rising across reconnects. In the browser, a quote store drops duplicate and out-of-order sequence numbers, and an unchanged quote never animates.
SEC EDGAR#
Issuer identity, the filings timeline and financial statements come from SEC EDGAR, which is official, free and public domain.
| Dataset | Endpoint | Used for |
|---|---|---|
| Company tickers | company_tickers_exchange.json |
Resolving tickers to a CIK, name and exchange. Every symbol in Investi is resolved against this list. |
| Submissions | data.sec.gov/submissions/CIK##########.json |
The filings timeline: form, filing date, items and a link to each document. 10-K, 10-Q, 8-K, proxy and ownership forms and similar are marked material. The policy reads the same timeline for filing flags (8-K items 4.02, 4.01 and 3.01, and NT late-filing forms) and for the date of the last earnings release (8-K item 2.02, or a 6-K whose cover furnishes a results press release; for a recent 6-K the cover document is read once and cached for 30 days). |
| Company facts | data.sec.gov/api/xbrl/companyfacts/CIK##########.json |
XBRL financial statements: revenue, earnings, cash flow, debt, cash and derived figures such as free cash flow, net debt and EBITDA. |
SEC asks automated clients to identify themselves and to stay at or under 10 requests per second. Investi limits itself to 8 per second and sends the SEC_USER_AGENT environment variable as its User-Agent. Set it to a contact string:
SEC_USER_AGENT="Your Name you@domain.com"If it is not set, Investi sends a generic placeholder, which SEC may throttle or block. Securities without an SEC filer have no filings or fundamentals; rules that need them report UNKNOWN rather than passing. See Verdicts and policy.
Schwab#
Positions, balances, orders, trades, dividends, interest and transfers come from the Schwab Trader API through read-only GET calls, when you connect it. Syncs run every minute in market hours, every 5 minutes in extended hours, in full at 9:00 and 20:05 New York time, at once when Schwab reports a fill, cancel or replace while the app is open, and when you click Sync now (which also backfills five years of transactions). See Connect Schwab and Connection state.
Your imports#
Schwab History CSV and JSON exports, Schwab Positions CSV and generic CSVs become fills, cash events, splits and position snapshots. Imported data is the only source of history older than what Schwab's API returns. See Import your history.
Company logos#
Logos are looked up once per ticker on the server, stored, and served from Investi's own address (/api/logos/{ticker}). No page embeds a logo provider's URL, so the extension and desktop app show them too and your browser never contacts a logo vendor.
| Order | Source | Notes |
|---|---|---|
| 1 | Wikidata and Wikimedia Commons | The company is matched by its SEC CIK (Wikidata property P5531), or by its ticker on a US exchange. Its icon (P8972, P2910) or logo (P154) comes from Commons, mostly as SVG. Most files are public domain (simple text logos); some are CC BY-SA, which requires credit. |
| 2 | Logo.dev (optional) | Used only when the server has a LOGO_DEV_TOKEN. The free tier requires a visible "Logos provided by Logo.dev" link, which Investi then shows in Settings and on research pages. |
| 3 | The company's website | The site is taken from Wikidata (P856) or Nasdaq's company profile. Investi reads SVG icons, apple-touch icons, web-manifest icons and ICO favicons, including bitmap-only ICO files. Official icons of at least 32 px are accepted. |
| 4 | NVSTly's public logo collection | A square stock mark when the sources above have only a tiny favicon, an unreadably wide wordmark, or an unavailable website. Share-class symbols use the collection's filenames. The research page links to the source file. |
Each logo is stored as a 256 px square tile with its mark centred on a white ground (white marks get Investi's dark surface). SVG is kept as SVG after a sanitizer removes scripts, event handlers and every external reference. A ticker with no usable logo shows a monogram: its first letters on the accent color. A genuine miss is retried after a day, a source that could not be reached after 15 minutes, and a stored logo after 90 days. When the resolver improves, older negative cache entries are retried immediately. Every research page credits its logo's source and license under the report, and Settings, Data sources, lists the counts.
Visible logo tiles share their downloads. If the first response says the lookup is still pending, the tile retries a bounded number of times and updates when the image is ready, without a page reload. Off-screen tiles wait until they approach the viewport. Confirmed missing images are browser-cached for five minutes; pending images are never cached as completed logos.
The account owner can override a logo from the research page: click the logo next to the ticker, then Use monogram (remembered for that ticker, the stored logo is kept so Use logo brings it back) or Replace logo with a PNG or SVG of up to 1 MB. Uploads go through the same sanitizer and tile as downloaded logos and are never replaced by a later lookup. Visible copies in the same page update together. Other browser tabs revalidate within about five minutes of their next request; the extension keeps a logo for up to six hours.
Caching and latency#
Market and SEC data go through a two-tier cache: an in-process memory cache per server instance, and a shared Postgres table that survives restarts and is shared across instances. Concurrent requests for the same key share one upstream call, so a page with hundreds of rows does not fan out into hundreds of identical requests.
| Data | Cached for | Tiers |
|---|---|---|
| Quotes (batch and single) | 15 seconds | memory only |
| Intraday prints, index levels | 30 seconds | memory only |
| Market session status | 60 seconds | memory only |
| Daily bars, listing universe, earnings calendar | 6 hours | memory and Postgres |
| Economic events | 3 hours | memory and Postgres |
| Analyst consensus, next earnings date, company summary | 12 hours (a next earnings date within 7 days of today: 1 hour) | memory and Postgres |
| Dividends | 24 hours | memory and Postgres |
| SEC filings timeline | 3 hours | memory and Postgres |
| SEC company facts | 12 hours | memory and Postgres |
| SEC ticker list | 24 hours | memory and Postgres |
During market hours, a quote you see is therefore the provider's delay plus up to 15 seconds of caching old, plus the stream's polling interval when you watch it live.
If an upstream call fails, Investi serves the last value it has, from memory or Postgres, with its original fetch time. That older time flows into the freshness label and the provenance, so a degraded value is visibly older instead of silently presented as current. If no earlier value exists, the field is shown as unavailable.
Note: Nothing in this list is real-time. Real-time quotes, Level II, licensed news, individual analyst firm targets, options, futures and FX are not licensed and are never shown or approximated. See Coverage and provenance.