SPEC 008 Market Data Service - rafaelfqueiroz/allmywallet GitHub Wiki

SPEC-008 — Market Data Service

Status Draft
PRD source F6 (FR-6.1–6.24)
Depends on SPEC-002 (blocking), SPEC-003
Required by SPEC-009, SPEC-012, SPEC-013, SPEC-015, SPEC-017, SPEC-018, SPEC-021
Milestone M4

Description

Acquisition and storage of all external market data: delayed intraday quotes for listed assets, official closing prices, Tesouro Direto prices, and the BCB index series (CDI, IPCA, Selic) plus IBOV.

The governing principle: the database is the system of record for quotes; the provider is consulted only when the database cannot answer. Every read serves from storage and calls out only on a miss, or on a stale entry during an open session, persisting whatever comes back.

Three scope rules keep spend proportional to what is actually being looked at, and are what make the free tier viable at all:

  1. Poll only while the market is open. A closed market produces no new prices.
  2. Poll only assets someone actually holds. The catalog is thousands of instruments; the polling set is the few dozen in real portfolios.
  3. Fetch anything else on demand and keep it.

SPEC-002 is blocking, not merely a dependency: BR-008-14 requires the service to change its own cadence at runtime, which is unimplementable if the interval is a constant.

Business rules

Tier and cadence

ID Rule
BR-008-01 v1 uses the provider's free tier — the most frequent free option, ~30 min delay.
BR-008-02 Refresh cadence is matched to the provider's own update interval and is configuration, not code. Polling faster than the provider updates is prohibited: it spends budget without improving freshness.
BR-008-03 Real-time (tick-by-tick) data is not used: it requires a licensed B3 distributor agreement with a variable fee per end user, per segment — a cost that scales with the user base and buys nothing this product needs.
BR-008-04 Every screen showing a current value displays the quote timestamp and the delay tier in plain language. The product never implies real-time.

Fetch scope

ID Rule
BR-008-05 Scheduled polling runs only while the B3 regular session is open, for only those assets held in at least one user's non-zero position.
BR-008-06 Outside the session — pre-open, post-close, weekends, B3 holidays — no quote request is made at all. The stored value is served as-is and is the correct answer.
BR-008-07 The trading calendar, including half-sessions, is data driven by configuration, never hardcoded hours.
BR-008-08 The polling set is derived, not configured: exactly the distinct assets in at least one non-zero position. An asset entering any portfolio joins on the next cycle; an asset nobody holds drops out.
BR-008-09 The one deliberate call outside session hours is the post-close capture of the official closing price. It supersedes the last intraday quote and is what gets written to history. The official close is B3's own: the PREULT of B3's public historical-quotes file (COTAHIST), read once B3 publishes the day. A quote from the intraday provider is never written to history as a close. (Amended 2026-09-28, #171, #144 — see DL-008-14.)
BR-008-10 Intraday quotes update current value only. They never rewrite a historical data point.
BR-008-11 Fixed income (CDB/LCI/LCA) has no intraday behaviour and is never polled; it accrues once daily in SPEC-009.
BR-008-12 Tesouro Direto prices and BCB series are fetched once daily — as often as those sources publish. Tesouro Transparente republishes every title's whole history, and each run stores every published day not yet stored, so a missed run loses nothing. A Tesouro title is catalogued under the name B3 gives it (product and year), translated from Tesouro Transparente's product and maturity date by each product's published structure: the name year is the maturity year, except Educa+ (maturity 15/12, four years on) and Renda+ Aposentadoria Extra (maturity 15/12, nineteen years on), named for the year payments start. A name two maturities would share is not assigned to either. (Amended 2026-09-27, #152, #161, #164 — see DL-008-12.)

On-demand read-through

ID Rule
BR-008-13 If a user looks up an asset with no stored quote, the system fetches it, persists it, and serves it. Subsequent lookups by any user are answered from the database.
BR-008-14 An on-demand fetch does not add the asset to the polling set. It is polled only if it later enters a portfolio (BR-008-08).
BR-008-15 A stored quote is re-fetched on read only when the session is open and the entry is older than the cadence interval. Outside the session a stored quote is never stale, however old.
BR-008-16 An unknown ticker is validated against the asset catalog before any provider call, so a typo cannot spend budget.
BR-008-17 On-demand lookups are rate-limited per user.
BR-008-18 A failed on-demand fetch, or a ticker the provider does not have, tells the user plainly that no quote is available. The system does not retry in a loop or fabricate a value.

Budget

ID Rule
BR-008-19 The free plan allows 15,000 requests/month at one ticker per call. That combination, not the delay, sets the ceiling.
BR-008-20 A configured share of the quota (default 10%) is reserved for on-demand lookups, so user-driven searches can never starve the scheduled polling that keeps real portfolios valued.
BR-008-21 Budget consumption is tracked continuously with projected month-end burn visible to operators; crossing the alert threshold (default 70%) raises an alert.
BR-008-22 Cadence degrades automatically (30 → 60 → 120 min) when the distinct-asset universe outgrows what the current cadence and quota allow, rather than exhausting the budget mid-month. Operators are alerted; it is the signal to consider a paid tier.
BR-008-23 An autonomous degradation is stored as runtime state, distinct from operator config, and survives a deploy (SPEC-002, BR-002-05/06).
BR-008-24 If the quota is exhausted anyway, the last known quote is retained and clearly marked stale. Portfolio value is never shown as current when it is not.
BR-008-25 Quotes are shared across all tenants and stored once — they are not personal data. The ceiling therefore counts distinct assets held across all users, not per user.

Reliability

ID Rule
BR-008-26 Providers sit behind a provider interface. Changing vendor or delay tier must not touch valuation logic.
BR-008-30 Closes for any past trading day come from B3's COTAHIST, fetched without credentials: the daily file for recent days, the annual file for deep history. The intraday provider's history window never bounds which closes can be loaded, and no close is taken from it. (2026-09-28, DL-008-14.)
BR-008-31 B3's historical and end-of-day data are used only to value and chart the user's own portfolio. Under B3's Política de Consumo de Market Data (in force from 2026-01-01, §3.3.2 and §12) they may build informational charts and tables. Selling them, redistributing them, offering them for download, storing them to build a product, or deriving an index or financial product from them needs a B3 licence. A release beyond the personal instance is reviewed against that policy first. (2026-09-28, DL-008-14.)
BR-008-27 Failures retry with backoff; persistent failure alerts operators and marks the data stale in the UI.
BR-008-28 A user can manually refresh their portfolio's valuation, rate-limited. Outside session hours this recomputes from stored quotes with no provider call, and says so rather than appearing to do nothing.

Corporate event factors

ID Rule
BR-008-29 B3's published share-ratio factor for a split (desdobramento), reverse split (grupamento) or bonus (bonificação) is read from B3's public listed-companies data — no key, no credential, no user data sent — by issuer and event date, persisted and shared across tenants like quotes (BR-008-25). It is used only to confirm a ratio derived from custody data (SPEC-007 BR-007-04a); it never creates, moves or classifies a transaction. A missing or unreachable factor leaves the event unconfirmed rather than guessed. Factor semantics differ by event type (a multiplier for grupamento, a percentage added for desdobramento and bonificação) and are normalised in the adapter. (Amended 2026-09-15, #113 — see DL-008-11.)

Budget reference

Cadence Calls/asset/day Calls/asset/month Distinct held assets supported
30 min 14 intraday + 1 close = 15 315 ~42
60 min 8 168 ~80
120 min 4 84 ~160

(~7-hour session, 21 trading days, 13,500 scheduled + 1,500 on-demand calls/month)

Acceptance Criteria

  • During an open session, held assets refresh at the configured cadence.
  • Outside session hours, weekends and B3 holidays, zero provider requests are made — asserted by counting provider calls over a simulated weekend.
  • Only assets in a non-zero position are polled; an asset in the catalog but held by nobody is never polled.
  • Buying a new asset adds it to the polling set on the next cycle with no manual step.
  • Selling a position to zero removes the asset from the polling set.
  • The post-close job captures the official closing price and it supersedes the last intraday quote in history.
  • Every close in history equals B3's COTAHIST PREULT for that asset and day, including days older than the intraday provider's history window, and none is sourced from an intraday quote.
  • An intraday quote never alters a stored historical close.
  • Looking up an unheld asset fetches once, persists, and serves from the database on the second lookup by a different user.
  • An on-demand fetch does not add the asset to the polling set.
  • Outside the session, a stored quote of any age triggers no re-fetch.
  • A typo'd ticker is rejected against the catalog with no provider call.
  • On-demand lookups are rate-limited per user.
  • On-demand spend is capped at the reserve and cannot consume scheduled polling budget.
  • Cadence degrades automatically when the asset universe outgrows the quota, alerts operators, and survives a deploy.
  • With the quota exhausted, values display with an explicit stale marker.
  • Every current-value screen shows the quote timestamp and delay tier.
  • Manual refresh outside session hours makes no provider call and explains why.
  • Swapping the provider in config changes the delay tier without touching valuation code.
  • Tesouro Transparente and BCB SGS series (12, 433, 11) load daily and backfill history.
  • A published split, grupamento or bonificação factor is fetched without credentials, persisted once for all tenants, normalised to a ratio, and never creates or alters a transaction; an unreachable source leaves the event unconfirmed.

Out of Scope

  • Real-time or tick-by-tick data, and any licensed B3 distributor agreement.
  • Order books, intraday OHLC bars, technical indicators.
  • Options, futures, crypto or FX rates.
  • Fixed income accrual and valuation (SPEC-009).
  • Sector and fundamental data beyond what Composition needs (PRD Q5).
  • Forward-looking dividend calendars — the free tier excludes dividend data (PRD Q8).
  • Corporate event sourcing — events arrive via B3 extracts (SPEC-005). The published factor that confirms a derived ratio (BR-008-29) is reference data, not an event source.
  • Redistributing B3 historical data, or letting users download it as data (BR-008-31).

Decision Logs

# Decision Alternatives considered Rationale
DL-008-01 Free tier, ~30 min delay. Paid tiers at ~R$100/mo (15 min) or ~R$117/mo (5 min); EOD only The most frequent free option. For a long-horizon tracker where no order is placed, a 30-minute delay changes no decision. Upgrading is a config change (BR-008-26), so the cost can be deferred until real usage justifies it.
DL-008-02 No real-time. Licensed B3 distributor agreement The fee is charged per end user, so cost scales with the user base — a poor structure for a subscription product — and buys nothing here. Paying per user so a number is five minutes fresher is spending real money on false precision.
DL-008-03 Poll only during market hours. Poll on a fixed schedule regardless A closed market produces no new prices, so an overnight request is pure waste against a hard quota. Requires the corollary in BR-008-15: outside the session, no stored quote is ever considered stale.
DL-008-04 Poll only held assets; polling set derived from positions. Poll a curated list; poll the whole catalog The catalog is thousands of instruments and the quota is 15,000 calls/month — polling it is impossible. Deriving from positions makes the set self-maintaining in both directions, with no operator upkeep.
DL-008-05 On-demand read-through with persistence. Fetch without storing; refuse lookups for unheld assets Storing means one user's search benefits everyone, since quotes are shared across tenants. Refusing lookups would block browsing an asset before buying it — a normal thing to want to do.
DL-008-06 On-demand fetches do not join the polling set. Add any looked-up asset to the polling set Otherwise idle curiosity permanently inflates recurring spend against a tight quota. Holding it is the signal that it matters.
DL-008-07 Reserved budget share for on-demand. One shared pool Without a partition, a burst of searching could starve the scheduled polling that keeps real portfolios valued — the more important of the two. The reserve makes the priority explicit.
DL-008-08 Post-close official price captured despite the market-hours rule. Use the last intraday quote as the close A 30-minute-delayed final quote is not the official close, and every historical return computed from it would be permanently and invisibly wrong. Worth 21 calls/asset/month.
DL-008-09 Degrade cadence automatically rather than exhaust the quota. Alert only and let it run out; hard-fail An exhausted quota means a dead dashboard mid-month for every user. Lengthening the interval keeps the product working, visibly degraded, and turns a hard outage into a business signal.
DL-008-10 Quotes shared across tenants, exempt from RLS. Per-tenant quote storage Quotes are not personal data. Sharing means the ceiling counts distinct assets rather than users — forty users holding overlapping blue chips cost the same as one. This is what makes the free tier survivable. Exemption is declared and reviewed (SPEC-003, BR-003-06).
DL-008-11 B3's public corporate-event factors confirm derived split ratios. (2026-09-15, #113.) Derive ratios from custody data alone; a paid corporate-events API; manual ratio entry only B3's Movimentação states the quantity after or added by the event, never the ratio, and deriving it silently fails when earlier history is incomplete. B3's listed-companies data states the factor publicly, at no cost and without credentials, so it fits PRD §4's automatic half and SPEC-003 BR-003-08. Confirmation-only keeps the extract the single source of custody truth.
DL-008-12 Tesouro titles are catalogued under B3's name, and the sync stores the whole published history. (2026-09-27, #152, #161, #164.) Catalogue under Tesouro Transparente's product and maturity date; keep only the latest published day B3's name is what both extracts carry — Movimentação states no maturity — and what the ledger already holds, so translating at the sync moves market data only. The source republishes every title's history daily; storing only the latest day lost every day a run was missed and never loaded anything before the first run.
DL-008-13 The year in an Educa+ or Renda+ name is the year payments start. (2026-09-27, #164; acknowledged by the owner after research.) Take it as the maturity year; confirm it against Posição's Vencimento on import Tesouro Direto sells the titles under the conversion year — Tesouro Educa+ 2026 matures on 15/12/2030 (Status Invest, B3 Educação) — and B3, which operates Tesouro Direto, writes the commercial names (Tesouro RendA+, Tesouro Educa+, OC 055/2025) as it does for every other title. Confirming against Vencimento means a Posição parser change and a re-verified B3 guide (SPEC-020) to guard a naming no source uses.
DL-008-14 Official and historical closes come from B3's COTAHIST; the quote provider serves intraday only. (2026-09-28, #171, #144.) Keep brapi for closes and history; a paid brapi tier (Startup: 1 year of history, Pro: 10+); the nearest later stored close brapi's free plan serves only three months of history (brapi.dev/pricing), so no close older than that can be loaded, which blocks #144's 2024 subscription closes. Its live quote taken at 17:05 is ~30 minutes delayed, so the 'official close' written to history was an intraday price (PETR4 48,10 against B3's 47,99 on 2026-09-25), which is exactly what DL-008-08 exists to prevent. COTAHIST is B3's own record of every spot-market security since 1986. It is public and needs no credentials, so it fits PRD §4's automatic half and SPEC-003 BR-003-08, like DL-008-11. Its PREULT is B3's own closing price, and it is authoritative even where a Posição shows another figure. On 2026-09-25 it matched the Posição for PETR4, VALE3 and MGLU3. SAUD3's Posição showed 13,53, which matches no COTAHIST field that day, against a PREULT of 13,46 (#171 D14; acknowledged by the owner). B3's consumption policy limits historical data to informational charts and tables, which is what the product does with it; BR-008-31 keeps that boundary visible for any release.