diff --git a/AGENTS.md b/AGENTS.md index 5e47b56..2ee05c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,107 +87,57 @@ export FINDATA_BD_BILLING_PROJECT_ID="" ## Graphics and chart generation standards -Agents will sometimes use this repo to generate exploratory charts for users. -Those charts should be clear, source-backed, and reproducible without turning the -library into a plotting package. - -### Dependency policy +Agents may use this repo to generate exploratory charts for users. Treat the +Chart Lab (`/charts`, `src/findata/web/templates/charts.html`, +`src/findata/web/static/chart-explorer.js`) as the canonical visual and +informational reference, but not as a mandatory renderer. + +Short runbook for any one-off chart request: + +1. Generate an audit data file next to the visual artifact, preferably tidy CSV. +2. Generate a clear visual artifact (SVG, PNG, HTML, or Lightweight Charts HTML) + with the same minimum information block used by Chart Lab. +3. Save or print the script/route used, data path, visual output path, and + renderer. +4. Keep temporary scripts, CSVs, PNGs, SVGs, and HTML files outside the repo + unless the user explicitly asks for a committed example. + +Minimum information contract for every chart: + +- clear title stating exactly what is compared; +- frequency and period; +- primary source/curation: `Dados Financeiros Abertos (findata-br)`; +- extraction timestamp in BRT; +- effective data cutoff: first and last date actually plotted; +- original source subsets/series identifiers, such as `BCB SGS 432` or + `B3 IndexStatisticsProxy`; +- final technical line with audit data path, script/route path, renderer, and + relevant transformations; +- audit data saved next to the visual artifact for one-off work. + +Dependency and renderer policy: - Do **not** add plotting libraries such as matplotlib, seaborn, plotly, altair, bokeh, or pandas as project dependencies just to make a chart. -- Prefer no-library outputs when possible: - 1. CSV data export. - 2. Hand-authored SVG using Python standard library string templates or - `xml.etree.ElementTree`. - 3. Single-file HTML with inline SVG when interactivity or browser rendering is - useful. -- If a one-off local script uses a plotting library already installed on the - machine, keep that script and its PNG/SVG outputs outside the repo unless the - user explicitly asks to add an example. -- If a chart becomes a committed example, keep it dependency-light and place - reusable code under `examples/` or docs assets only after confirming scope. - -### Reproducibility contract - -Every generated chart should make it possible to audit the result later: - -- Save or print the exact script path, data path, and output path. -- Export the tidy source data as CSV next to the image for one-off work. -- Use the project APIs/CLI for data retrieval instead of ad hoc scraping when - this repo already exposes the dataset. -- Include source names and series identifiers in the subtitle or footer, for - example `BCB SGS 432` or `B3 IndexStatisticsProxy`. -- Include the consultation date or data cutoff date. Prefer ISO dates in file - metadata and human labels in pt-BR (`Consulta em 2026-04-29`). -- Do not interpolate, forward-fill, or resample silently. State the frequency and - transformation in the subtitle: monthly, daily close, 12-month sum, end-of-month, - forward-filled policy rate, etc. - -### Visual style - -Default chart style for financial/economic time series: - -- Canvas: 16:9 or wide report format. Good defaults: `1600x900` or SVG - `viewBox="0 0 1600 900"`. -- Background: white or near-white (`#ffffff` / `#f8fafc`). -- Text color: dark slate (`#111827` for titles, `#4b5563` for secondary text). -- Grid: light horizontal grid only (`#e5e7eb`), no heavy chart junk. -- Borders/spines: thin and low-contrast (`#d1d5db`) or omitted. -- Typography: system sans-serif stack (`Inter`, `SF Pro`, `Segoe UI`, `Arial`, - `sans-serif`). Title should be bold and larger than every other label. -- Use line widths thick enough for screenshots (`3px` to `5px` in SVG). -- Use color intentionally and consistently: - - BCB / interest-rate series: orange `#d97706`. - - B3 / equity-index series: blue `#2563eb`. - - Neutral comparison: slate `#64748b`. - - Negative or warning: red `#dc2626`. -- Do not rely on color alone. Use legend labels, direct labels, or line style - differences where possible. - -### Axes, labels, and formatting - -- Titles should say exactly what is compared: `Selic meta vs Ibovespa`. -- Subtitles should include frequency, date range, and source mapping: - `Dados mensais: jan/2016 a abr/2026. Selic = BCB SGS 432; Ibovespa = B3.` -- Axis labels must include units: - - `Selic meta (% a.a.)` - - `Ibovespa (mil pontos)` - - `Valor (R$ bilhões)` - - `Variação (% a.m.)` -- Prefer pt-BR month labels in user-facing charts: `jan/2026`, `abr/2026`. -- Use Brazilian numeric conventions in annotations when the output is for a - Brazilian audience: decimal comma in prose, `R$`, `% a.a.`, `% a.m.`. -- Dual axes are acceptable only for unlike units. If used, color each axis label - and tick labels to match its series, and mention both units in the legend. -- For policy rates such as Selic meta, prefer a step line when the data changes - discretely. For market indexes, prefer a continuous line. -- Bars should normally start at zero. Line charts may use a narrowed y-axis, but - the scale must remain visible and honest. - -### Annotations and footers - -- Add a compact final-value annotation when it improves readability, e.g. - `Último ponto (abr/2026): Selic 14,75% a.a. | Ibovespa 188.619 pts`. -- Footer should include sources and consultation date, not generic claims. -- Keep legends inside unused whitespace or above the plot; avoid covering data. -- Avoid decorative logos, watermarks, shadows, gradients, and 3D effects unless - explicitly requested. - -### File naming and placement - -For one-off user-requested graphics, prefer a temporary work directory outside -this repo and use snake_case names: - -```text -selic_meta_vs_ibovespa.py -selic_meta_vs_ibovespa_mensal.csv -selic_meta_vs_ibovespa.svg -selic_meta_vs_ibovespa.png # optional, only if specifically needed -``` - -Only commit chart artifacts when they are part of documented examples or a docs -page. If committed, include the script or generation instructions next to the -artifact so future agents can regenerate it. +- Prefer dependency-light outputs: tidy CSV plus hand-authored SVG or + single-file HTML/inline SVG. +- PNG is acceptable for screenshot/raster delivery, but keep the CSV/script so it + is not the only artifact. +- TradingView Lightweight Charts is the interactive Chart Lab renderer and the + canonical visual/informational reference. Use it for interactive HTML when it + helps, not as a universal obligation. +- If HTML/Lightweight Charts is used, keep attribution/copyright behavior aligned + with `docs/CHART_STANDARDS.md` and the visible footer/link pattern. + +Use the Chart Lab color tokens consistently: + +- text principal: `#07132c`; +- juros/macro: orange `#ff7a1a`; +- mercado/B3: blue `#0050ff`; +- fonte/validação: green `#00a859`. + +See `docs/CHART_STANDARDS.md` for the detailed specification, renderer decision +matrix, templates, and examples of source/technical lines. ## Release and handoff guardrails diff --git a/README.md b/README.md index b8bc355..dbc601c 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,15 @@ curl 'http://localhost:8000/registry/lookup?q=PETR4' ``` - Site local: `http://localhost:8000/` +- Chart Lab: `http://localhost:8000/charts` - Swagger: `http://localhost:8000/api/docs` - ReDoc: `http://localhost:8000/redoc` - MCP: `http://localhost:8000/mcp` +O Chart Lab é a referência visual e informacional para gráficos do projeto, mas +o renderer pode ser SVG, PNG, HTML inline ou TradingView Lightweight Charts. O +contrato detalhado fica em [docs/CHART_STANDARDS.md](docs/CHART_STANDARDS.md). + ## Fontes suportadas O README mantém só o mapa de alto nível. A matriz completa de fontes, endpoints, diff --git a/docs/CHART_STANDARDS.md b/docs/CHART_STANDARDS.md index 21a5fd0..d459d2b 100644 --- a/docs/CHART_STANDARDS.md +++ b/docs/CHART_STANDARDS.md @@ -1,64 +1,188 @@ # Padrão oficial de gráficos Este padrão vale para gráficos gerados pelo Dados Financeiros Abertos, -incluindo páginas interativas, screenshots e artefatos avulsos. +incluindo o Chart Lab (`/charts`), screenshots, SVG/PNG/HTML avulsos e qualquer +artefato criado por agentes a partir dos dados do projeto. -## Superfícies públicas +A regra central é simples: o Chart Lab com TradingView Lightweight Charts é a +referência visual e informacional canônica, mas o renderer não é obrigatório. O +mesmo contrato mínimo deve aparecer quando o resultado final for SVG, PNG, HTML +inline ou Lightweight Charts. -Gráficos no site devem ser tratados como camada de visualização. Enquanto o -explorador for experimental, usar linguagem de `Labs`, não CTA principal do -produto. O produto principal continua sendo API REST, biblioteca Python, CLI e -MCP. +## Papel do Chart Lab + +O Chart Lab é a referência de estrutura: + +- cabeçalho com fonte/série, título e estado; +- bloco de metadados no topo; +- gráfico limpo com grade horizontal leve; +- bloco de fontes abaixo do gráfico; +- linha técnica final com dados auditáveis e renderer. + +O produto principal continua sendo API REST, biblioteca Python, CLI e MCP. O +Chart Lab é uma vitrine experimental de visualização, não uma exigência para todo +script ou exemplo. Presets públicos devem priorizar fontes oficiais/auditáveis já integradas no projeto. Fontes experimentais ou indiretas, como Yahoo/yfinance, não entram na vitrine principal. -## Identidade visual +## Contrato mínimo de informação + +Todo gráfico precisa carregar, de forma visível ou em metadados diretamente +anexos ao artefato, os itens abaixo. + +1. **Título claro**: dizer exatamente o que está sendo comparado ou medido, por + exemplo `Selic meta vs Ibovespa`. +2. **Frequência e período**: informar a periodicidade e o intervalo apresentado, + por exemplo `Dados mensais: jan/2016 a abr/2026`. +3. **Fonte primária/curadoria do repo**: + `Dados Financeiros Abertos (findata-br)` apontando para + `https://github.com/robertoecf/findata-br`. +4. **Extração**: timestamp da extração em BRT. Use formato explícito, por + exemplo `2026-05-11 14:32:05 BRT` ou `11/05/2026, 14:32:05 BRT`. +5. **Recorte efetivo dos dados**: primeira e última data realmente plotadas após + filtros, normalização, agregação ou perda de pontos, não apenas o período + solicitado. +6. **Fontes originais/subsets**: identificadores auditáveis das fontes externas + usadas, como `BCB SGS 432`, `B3 IndexStatisticsProxy`, + `IBGE Agregados 7060/63` ou `IPEA Data BM12_TJOVER12`. +7. **Transformações**: frequência, agregação, reamostragem, preenchimento, + normalização ou conversão de unidade. Não interpolar, forward-fill ou + resamplear silenciosamente. +8. **Linha técnica**: caminho do CSV/JSON auditável, script/rota usada, + renderer, versão/biblioteca quando relevante e contagem de pontos. +9. **Dados auditáveis**: em gráficos one-off, salvar o CSV/JSON de base junto do + visual. Em páginas do projeto, expor o endpoint JSON ou caminho reproduzível. + +## Anatomia recomendada + +### Cabeçalho + +- Rótulo curto da série/fonte principal, por exemplo `BCB SGS 432`. +- Título em destaque. +- Status ou observação curta quando o gráfico é interativo. +- Subtítulo com frequência, período e mapeamento de fontes quando couber. + +Exemplo: + +```text +Selic meta vs Ibovespa +Dados mensais: jan/2016 a abr/2026. Selic = BCB SGS 432; Ibovespa = B3. +``` -- Fundo claro com glow discreto nas cores do projeto. -- Texto principal em `#07132c`. -- Azul de mercado: `#0050ff`. -- Laranja de juros/macroeconomia: `#ff7a1a`. -- Verde de fonte/validação: `#00a859`. -- Grade horizontal leve; evitar grade vertical e excesso de molduras. +### Metadados no topo -## Metadados no topo +Todo gráfico deve mostrar ou anexar estes campos: -Todo gráfico deve mostrar, de forma visível no topo: +```text +Fonte primária: Dados Financeiros Abertos (findata-br) +Extração: 2026-05-11 14:32:05 BRT +Recorte dos dados: 2016-01-31 -> 2026-04-30 +``` -- `Fonte primária`: `Dados Abertos de Mercado (findata-br)`. -- `Extração`: timestamp da extração em BRT. -- `Recorte dos dados`: primeira e última data efetivamente plotadas. +Se o usuário pediu um período diferente do recorte efetivo, explicite ambos: +`período solicitado` e `recorte efetivo`. -## Fontes no rodapé +### Fontes no rodapé -O bloco `Fontes dos dados` deve ficar abaixo do gráfico e acima da linha de -artefatos técnicos (`CSV auditável`, biblioteca de gráfico etc.). +O bloco `Fontes dos dados` deve ficar abaixo do gráfico e acima da linha técnica. -Formato: +Formato recomendado: ```text Fontes dos dados. Fonte primária/curadoria: -Dados Abertos de Mercado (findata-br). -Subsets originais: para ; para . +Dados Financeiros Abertos (findata-br). +Subsets originais: BCB SGS 432 para Selic meta; B3 IndexStatisticsProxy para Ibovespa. ``` -`Dados Abertos de Mercado (findata-br)` deve apontar para: +A fonte primária deve apontar para: ```text https://github.com/robertoecf/findata-br ``` -## Linha técnica final +### Linha técnica final -A última linha deve ficar compacta: +A última linha deve ser compacta e auditável: -- CSV auditável ou caminho de dados reproduzível. -- Biblioteca de visualização, se houver exigência de atribuição. +```text +CSV auditável: ./selic_meta_vs_ibovespa_mensal.csv · Script: ./selic_meta_vs_ibovespa.py · Renderer: SVG stdlib · Pontos: 124 · Transformações: mês-fim, sem interpolação +``` -Quando usar TradingView Lightweight Charts com `attributionLogo: false`, manter -o aviso de copyright no código-fonte e um link visível para: +Para páginas do próprio projeto, o caminho reproduzível pode ser um endpoint JSON +em vez de um CSV materializado: + +```text +JSON auditável: /bcb/series/code/432?start=2024-05-11&end=2026-05-11 · Script: src/findata/web/static/chart-explorer.js · Renderer: TradingView Lightweight Charts 5.2.0 +``` + +## Identidade visual + +Use os tokens do Chart Lab como padrão visual: + +- fundo: `#ffffff` ou near-white (`#f8fbff` / `#f8fafc`); +- texto principal: `#07132c`; +- texto secundário: `#42526f`; +- linha/borda: `rgba(7, 19, 44, 0.12)` ou `rgba(0, 39, 118, 0.16)`; +- grade horizontal: `rgba(0, 39, 118, 0.10)`; +- mercado/B3: blue `#0050ff`; +- juros/macro: orange `#ff7a1a`; +- fonte/validação: green `#00a859`; +- negativo/erro: red `#ef4444`. + +Estilo padrão para séries financeiras/econômicas: + +- canvas 16:9 ou formato largo. Bons defaults: `1600x900` ou SVG + `viewBox="0 0 1600 900"`; +- tipografia system sans-serif (`Inter`, `SF Pro`, `Segoe UI`, `Arial`, + `sans-serif`); +- título maior e bold; +- grade horizontal leve, sem grade vertical pesada; +- bordas/spines finas ou omitidas; +- linhas com espessura suficiente para screenshots (`3px` a `5px` em SVG); +- sem logos decorativos, watermarks, sombras, gradientes fortes ou 3D, salvo + pedido explícito. + +Não dependa apenas de cor. Use legenda, labels diretos, estilo de linha ou eixo +colorido quando houver mais de uma série. + +## Eixos, labels e formatação + +- Títulos devem dizer exatamente o que é comparado. +- Subtítulos devem incluir frequência, período e mapeamento de fontes. +- Labels de eixo precisam incluir unidade: + - `Selic meta (% a.a.)`; + - `Ibovespa (mil pontos)`; + - `Valor (R$ bilhões)`; + - `Variação (% a.m.)`. +- Preferir meses em pt-BR para gráficos voltados a público brasileiro: + `jan/2026`, `abr/2026`. +- Usar convenções numéricas brasileiras em anotações: vírgula decimal, `R$`, + `% a.a.`, `% a.m.`. +- Eixos duplos são aceitáveis apenas para unidades diferentes. Se usar dois + eixos, colorir label e ticks com a cor da série e explicitar as unidades. +- Para taxas de política monetária, preferir linha em degrau quando a série muda + discretamente. Para índices de mercado, preferir linha contínua. +- Barras normalmente começam em zero. Linhas podem usar eixo y estreito, desde + que a escala fique visível e honesta. + +## Escolha de renderer + +O renderer é uma decisão operacional, não o padrão mínimo em si. + +| Renderer | Quando usar | Requisitos | +| --- | --- | --- | +| SVG estático | Default para one-off auditável e sem dependências pesadas. | Salvar CSV/JSON base, script e SVG juntos. Usar tokens do Chart Lab. | +| PNG | Quando o usuário pede imagem raster, preview rápido ou screenshot. | Não entregar só PNG; manter CSV/JSON e script. Preferir gerar a partir de SVG/HTML reproduzível. | +| HTML inline/SVG | Quando interatividade leve, tooltip ou renderização em navegador ajuda. | Manter tudo em arquivo único quando possível; incluir fontes, metadados e linha técnica. | +| TradingView Lightweight Charts | Chart Lab, interatividade temporal rica, crosshair/zoom ou HTML interativo. | Não tratar como dependência universal. Manter atribuição e link visível conforme abaixo. | +| Bibliotecas locais de plotting | Apenas para script descartável fora do repo quando já disponíveis ou explicitamente aceitas. | Não adicionar como dependência do projeto só para gráfico; reportar dependências usadas. | + +## TradingView Lightweight Charts + +Quando usar Lightweight Charts com `attributionLogo: false`, manter o aviso de +copyright no código-fonte e um link visível para: ```text https://www.tradingview.com/lightweight-charts/ @@ -70,23 +194,62 @@ Texto recomendado no rodapé da página: Gráfico: TradingView Lightweight Charts™ ``` +O uso de Lightweight Charts não dispensa o contrato mínimo: metadados, fontes, +recorte efetivo e linha técnica continuam obrigatórios. + +## One-off chart runbook + +Para pedidos simples como `gere um gráfico da Selic meta vs Ibovespa`: + +1. Usar o repo como fonte de consulta quando a série já estiver exposta por API, + CLI ou módulo Python. Não fazer scraping ad hoc se houver wrapper do projeto. +2. Se a fonte necessária ainda não estiver exposta, declarar o gap e registrar + follow-up separado. Não misturar implementação de fonte nova com o gráfico. +3. Criar um diretório temporário fora do repo para artefatos descartáveis. +4. Salvar nomes em snake_case: + +```text +selic_meta_vs_ibovespa.py +selic_meta_vs_ibovespa_mensal.csv +selic_meta_vs_ibovespa.svg +selic_meta_vs_ibovespa.png # opcional, se raster for necessário +selic_meta_vs_ibovespa.html # opcional, se HTML/interatividade for necessário +``` + +5. Exportar dados tidy com datas, valores já normalizados e colunas de origem + suficientes para auditoria. +6. Gerar visual com o contrato mínimo completo. +7. Ao entregar, reportar script, dados, artefato visual, renderer, fontes e data + de extração. + +Só commitar artefatos de gráfico quando forem parte de exemplos/documentação +aprovados. Nesse caso, incluir script ou instruções de geração ao lado do +artefato para permitir regeneração. + ## Regra de fonte primária Para artefatos gerados pelo projeto, a fonte primária/curadoria é sempre o -Dados Abertos de Mercado (`findata-br`). As fontes externas aparecem como +Dados Financeiros Abertos (`findata-br`). As fontes externas aparecem como subsets originais, com identificadores auditáveis, por exemplo: -- `BCB SGS 432` para Selic meta. -- `B3 IndexStatisticsProxy` para Ibovespa. -- `IBGE Agregados 7060/63` para IPCA mensal. +- `BCB SGS 432` para Selic meta; +- `B3 IndexStatisticsProxy` para Ibovespa; +- `IBGE Agregados 7060/63` para IPCA mensal; - `IPEA Data BM12_TJOVER12` para Selic over mensal. +Não inserir credenciais, tokens, caminhos privados de credenciais ou projetos de +billing privados em código, docs, exemplos ou artefatos gerados. Quando Base dos +Dados/BigQuery for usado, seguir o runbook de `AGENTS.md`. + ## Reprodutibilidade Todo gráfico exportado deve preservar: - script ou rota usada para geração; - CSV ou JSON base; -- timestamp de extração; -- recorte de dados; -- identificadores das fontes originais. +- timestamp de extração em BRT; +- período solicitado, quando relevante; +- recorte efetivo dos dados; +- frequência e transformações; +- identificadores das fontes originais; +- renderer e biblioteca/versão quando aplicável. diff --git a/docs/SOURCES_AND_ENDPOINTS.md b/docs/SOURCES_AND_ENDPOINTS.md index 362d707..1823d67 100644 --- a/docs/SOURCES_AND_ENDPOINTS.md +++ b/docs/SOURCES_AND_ENDPOINTS.md @@ -18,7 +18,7 @@ Para testar interativamente, rode `findata serve` e abra `/api/docs` ou `/redoc` | IBGE | Indicadores econômicos, IPCA e grupos/subitens | `/ibge/indicators`, `/ibge/indicators/{name}`, `/ibge/ipca/breakdown`, `/ibge/ipca/groups` | Não | | IPEA Data | Catálogo e séries macroeconômicas OData | `/ipea/catalog`, `/ipea/search`, `/ipea/series/{sercodigo}`, `/ipea/metadata/{sercodigo}` | Não | | Open Finance Brasil | Diretório público, participantes, recursos, JWKS e Portal de Dados | `/openfinance/resources`, `/openfinance/participants`, `/openfinance/endpoints`, `/openfinance/directory/api-resources`, `/openfinance/portal/datasets` | Não para dados públicos | -| B3 | Cotações, COTAHIST oficial e composição teórica de índices | `/b3/quote/{ticker}`, `/b3/history/{ticker}`, `/b3/quotes`, `/b3/cotahist/year/{year}`, `/b3/indices`, `/b3/indices/{symbol}` | Não | +| B3 | Cotações, COTAHIST oficial, composição teórica e evolução mensal de índices | `/b3/quote/{ticker}`, `/b3/history/{ticker}`, `/b3/quotes`, `/b3/cotahist/year/{year}`, `/b3/indices`, `/b3/indices/{symbol}`, `/b3/indices/{symbol}/monthly` | Não | | Yahoo Finance | Endpoint experimental de gráfico de preços | `/yahoo/chart/{symbol}` | Não; fonte não oficial | | ANBIMA | IMA, ETTJ e debêntures via arquivos públicos | `/anbima/ima`, `/anbima/ettj`, `/anbima/debentures` | Não para os arquivos usados | | Receita Federal | Arrecadação por período, UF e tributo | `/receita/arrecadacao`, `/receita/tributos` | Não | @@ -35,6 +35,7 @@ curl 'http://localhost:8000/bcb/focus/annual?indicator=IPCA&top=3' curl 'http://localhost:8000/cvm/companies/search?q=petrobras' curl 'http://localhost:8000/cvm/funds/daily?cnpj=00.280.302/0001-60&limit=5' curl 'http://localhost:8000/b3/quote/PETR4' +curl 'http://localhost:8000/b3/indices/IBOV/monthly?start=2026-01-01&end=2026-05-11' curl 'http://localhost:8000/b3/cotahist/year/2025?limit=5' curl 'http://localhost:8000/openfinance/participants?role=DADOS&limit=20' curl 'http://localhost:8000/registry/lookup?q=PETR4' @@ -52,6 +53,7 @@ findata ipea search desemprego findata openfinance participants --role DADOS -n 20 findata cvm search Petrobras findata b3 quote PETR4 +findata b3 index-monthly IBOV --start 2026-01-01 --end 2026-05-11 findata anbima ima -i IMA-B findata registry lookup "33.000.167/0001-01" ``` diff --git a/src/findata/api/app.py b/src/findata/api/app.py index bb04c42..56f53fc 100644 --- a/src/findata/api/app.py +++ b/src/findata/api/app.py @@ -65,7 +65,7 @@ def _resolve_version() -> str: "ibge": "IBGE (economic indicators)", "ipea": "IPEA Data (~8k macro series, long historical coverage)", "openfinance": "Open Finance Brasil (public Directory + indicator Portal)", - "b3": "B3 (stock quotes via yfinance)", + "b3": "B3 (official COTAHIST, indices, and optional stock quotes)", "yahoo": "Yahoo Finance chart endpoint (experimental, unofficial)", "anbima": "ANBIMA (IMA family, ETTJ, debêntures — public file downloads)", "receita": "Receita Federal (federal tax collection)", diff --git a/src/findata/api/routers/b3.py b/src/findata/api/routers/b3.py index a89a8bc..be062a3 100644 --- a/src/findata/api/routers/b3.py +++ b/src/findata/api/routers/b3.py @@ -150,3 +150,23 @@ async def index_portfolio(symbol: str) -> indices.IndexPortfolio: quantity. Refreshed quarterly by B3. """ return await indices.get_index_portfolio(symbol) + + +@router.get("/indices/{symbol}/monthly") +async def index_monthly_evolution( + symbol: str, + start: date | None = Query(default=None, description="Initial date, YYYY-MM-DD"), + end: date | None = Query(default=None, description="Final date, YYYY-MM-DD"), + months: int = Query(120, ge=1, le=360, description="Window size when start is omitted"), +) -> list[indices.IndexMonthlyPoint]: + """Monthly closing levels for a B3 index via IndexStatisticsProxy. + + Useful for charting Ibovespa (`IBOV`) or other B3 index levels without + relying on unofficial market-data providers. + """ + try: + return await indices.get_index_monthly_evolution( + symbol, start=start, end=end, months=months + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc diff --git a/src/findata/cli.py b/src/findata/cli.py index c68d988..f4b6871 100644 --- a/src/findata/cli.py +++ b/src/findata/cli.py @@ -299,6 +299,41 @@ def b3_index( rprint(table) +@b3_app.command("index-monthly") +def b3_index_monthly( + symbol: str = typer.Argument(help="Index symbol (e.g. IBOV, IBXX, SMLL, IFIX)"), + start: str | None = typer.Option(None, "--start", help="Initial date (YYYY-MM-DD)"), + end: str | None = typer.Option(None, "--end", help="Final date (YYYY-MM-DD)"), + months: int = typer.Option(120, "--months", "-n", min=1, max=360), +) -> None: + """Show monthly B3 index closing levels from IndexStatisticsProxy.""" + from findata.sources.b3 import get_index_monthly_evolution + + start_date = date.fromisoformat(start) if start else None + end_date = date.fromisoformat(end) if end else None + rows = _run(get_index_monthly_evolution(symbol, start=start_date, end=end_date, months=months)) + if not rows: + rprint(f"[yellow]No monthly data for index {symbol}.[/yellow]") + return + + table = Table(title=f"{symbol.upper()} — evolução mensal") + table.add_column("Date", style="cyan") + table.add_column("Period") + table.add_column("Close", justify="right", style="bold") + table.add_column("Partial", justify="center") + max_shown = 60 + for point in rows[-max_shown:]: + table.add_row( + point.date, + point.period, + f"{point.close:,.2f}", + "yes" if point.partial_month else "", + ) + rprint(table) + if len(rows) > max_shown: + rprint(f"[dim](showing last {max_shown} of {len(rows)} monthly points)[/dim]") + + # ── Tesouro commands ─────────────────────────────────────────────── tesouro_app = typer.Typer(help="Tesouro Direto treasury bonds", no_args_is_help=True) diff --git a/src/findata/sources/b3/__init__.py b/src/findata/sources/b3/__init__.py index 36bbbf3..a9e9224 100644 --- a/src/findata/sources/b3/__init__.py +++ b/src/findata/sources/b3/__init__.py @@ -4,7 +4,7 @@ |---------------|-----------------------------------------|----------------------| | `quotes.py` | Yahoo Finance (via yfinance) | live + intraday | | `cotahist.py` | B3 SerHist fixed-width archive (1986+) | annual / month / day | -| `indices.py` | B3 indexProxy JSON portfolio endpoint | live (current quart) | +| `indices.py` | B3 indexProxy/indexStatisticsProxy JSON | portfolios + monthly | The optional ``[b3]`` extra installs ``yfinance`` for live quotes. ``cotahist`` and ``indices`` use only stdlib + httpx (already in core). @@ -19,7 +19,9 @@ from findata.sources.b3.indices import ( KNOWN_INDICES, IndexConstituent, + IndexMonthlyPoint, IndexPortfolio, + get_index_monthly_evolution, get_index_portfolio, list_known_indices, ) @@ -35,6 +37,7 @@ "KNOWN_INDICES", "CotahistTrade", "IndexConstituent", + "IndexMonthlyPoint", "IndexPortfolio", "StockHistoryPoint", "StockQuote", @@ -42,6 +45,7 @@ "get_cotahist_month", "get_cotahist_year", "get_history", + "get_index_monthly_evolution", "get_index_portfolio", "get_multiple_quotes", "get_quote", diff --git a/src/findata/sources/b3/indices.py b/src/findata/sources/b3/indices.py index d6172bd..f30bb38 100644 --- a/src/findata/sources/b3/indices.py +++ b/src/findata/sources/b3/indices.py @@ -29,12 +29,18 @@ import base64 import json +from calendar import monthrange +from datetime import date +from typing import Any from pydantic import BaseModel from findata.http_client import get_json INDEX_PROXY = "https://sistemaswebb3-listados.b3.com.br/indexProxy/indexCall/GetPortfolioDay" +INDEX_STATISTICS_PROXY = ( + "https://sistemaswebb3-listados.b3.com.br/indexStatisticsProxy/IndexCall/GetMonthlyEvolution" +) # Indices known to ship via this endpoint. The list is open — B3 publishes # many more; we curate the ones investors actually trade against. @@ -104,8 +110,22 @@ class IndexPortfolio(BaseModel): componentes: list[IndexConstituent] +class IndexMonthlyPoint(BaseModel): + """Monthly closing level for a B3 index.""" + + date: str # period-end date, clipped to requested end for partial current month + period: str # YYYY-MM + year: int + month: int + close: float + indice: str + provider_index: str + partial_month: bool = False + + _DEFAULT_PAGE_SIZE = 200 _MAX_PAGES = 10 # safety cap: largest index has ~250 issues +_MONTHS_IN_YEAR = 12 def _encode_query(index: str, page_size: int = _DEFAULT_PAGE_SIZE, page_number: int = 1) -> str: @@ -119,6 +139,75 @@ def _encode_query(index: str, page_size: int = _DEFAULT_PAGE_SIZE, page_number: return base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii") +def _encode_monthly_evolution_query(index: str, start: date, end: date) -> str: + payload = { + "language": "pt-br", + "index": index.upper(), + "dateInitial": start.isoformat(), + "dateFinal": end.isoformat(), + } + return base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii") + + +def _coerce_date(value: date | str | None) -> date | None: + if value is None: + return None + if isinstance(value, date): + return value + return date.fromisoformat(value) + + +def _month_window_start(end: date, months: int) -> date: + if months < 1: + raise ValueError("months must be >= 1") + month_index = end.year * 12 + (end.month - 1) - (months - 1) + if month_index < 12: + return date.min + return date(month_index // 12, month_index % 12 + 1, 1) + + +def _period_end(year: int, month: int, requested_end: date) -> tuple[date, bool]: + last = date(year, month, monthrange(year, month)[1]) + partial = year == requested_end.year and month == requested_end.month and requested_end < last + return (requested_end, True) if partial else (last, False) + + +def _f_number(value: Any) -> float | None: + if isinstance(value, bool) or value is None: + return None + if isinstance(value, int | float): + return float(value) + if not isinstance(value, str) or not value.strip(): + return None + try: + return float(value.replace(".", "").replace(",", ".")) + except ValueError: + return None + + +def _row_to_monthly_point( + row: dict[str, Any], sym: str, requested_end: date +) -> IndexMonthlyPoint | None: + year = row.get("year") + month = row.get("month") + close = _f_number(row.get("indexClosingRate")) + if not isinstance(year, int) or not isinstance(month, int) or close is None: + return None + if month not in range(1, _MONTHS_IN_YEAR + 1): + return None + period_end, partial = _period_end(year, month, requested_end) + return IndexMonthlyPoint( + date=period_end.isoformat(), + period=f"{year:04d}-{month:02d}", + year=year, + month=month, + close=close, + indice=sym, + provider_index=sym, + partial_month=partial, + ) + + def _f_redutor(s: str | None) -> float | None: """Parse B3's redutor (Brazilian decimal with thousand separators).""" if s is None or not s.strip(): @@ -179,6 +268,47 @@ async def get_index_portfolio(index: str) -> IndexPortfolio: ) +async def get_index_monthly_evolution( + index: str, + start: date | str | None = None, + end: date | str | None = None, + months: int = 120, +) -> list[IndexMonthlyPoint]: + """Fetch monthly closing levels from B3's IndexStatisticsProxy endpoint. + + Args: + index: B3 index symbol accepted by the statistics endpoint, for example + ``"IBOV"``/``"IBOVESPA"``, ``"SMLL"``, ``"IFIX"`` or ``"IBXX"``. + start: Optional initial date. If omitted, the last ``months`` calendar + buckets ending at ``end`` are requested. + end: Optional final date. Defaults to today. If the final month is + partial, the returned point date is clipped to this value. + months: Number of monthly buckets to request when ``start`` is omitted. + """ + sym = index.strip().upper() + if not sym: + raise ValueError("index symbol is required") + + end_date = _coerce_date(end) or date.today() + start_date = _coerce_date(start) or _month_window_start(end_date, months) + if start_date > end_date: + raise ValueError("start date must be on or before end date") + + encoded = _encode_monthly_evolution_query(sym, start_date, end_date) + payload = await get_json(f"{INDEX_STATISTICS_PROXY}/{encoded}", cache_ttl=3600) + if not isinstance(payload, list): + raise ValueError("B3 monthly evolution response was not a list") + + points = [ + point + for row in payload + if isinstance(row, dict) + for point in [_row_to_monthly_point(row, sym, end_date)] + if point is not None + ] + return sorted(points, key=lambda point: (point.year, point.month)) + + async def list_known_indices() -> dict[str, str]: """Return ``symbol → friendly name`` map of indices we know how to fetch.""" return dict(KNOWN_INDICES) diff --git a/src/findata/web/static/chart-explorer.js b/src/findata/web/static/chart-explorer.js index a280936..1014205 100644 --- a/src/findata/web/static/chart-explorer.js +++ b/src/findata/web/static/chart-explorer.js @@ -19,13 +19,16 @@ red: "#ef4444", white: "#ffffff", }; - const PRIMARY_SOURCE = "Dados Abertos de Mercado (findata-br)"; + const PRIMARY_SOURCE = "Dados Financeiros Abertos (findata-br)"; + const CHART_SCRIPT = "src/findata/web/static/chart-explorer.js"; + const CHART_RENDERER = "TradingView Lightweight Charts 5.2.0"; const MAX_POINTS = 5000; const REQUEST_TIMEOUT_MS = 15000; const ALLOWED_ENDPOINT_PREFIXES = [ "/bcb/series/", "/ibge/indicators/", "/ipea/series/", + "/b3/indices/", ]; const isoDate = (date) => date.toISOString().slice(0, 10); @@ -67,6 +70,7 @@ field: "valor", title: "Taxa Selic", source: "BCB SGS 432", + frequency: "diária", color: BRAND.orange, }, { @@ -76,7 +80,8 @@ field: "valor", title: "IPCA mensal", source: "BCB SGS 433", - color: BRAND.blue, + frequency: "mensal", + color: BRAND.orange, }, { id: "ipea-selic-over", @@ -85,6 +90,7 @@ field: "valor", title: "Selic over acumulada no mês", source: "IPEA Data BM12_TJOVER12", + frequency: "mensal", color: BRAND.orange, }, { @@ -94,6 +100,21 @@ field: "valor", title: "IPCA mensal — IBGE", source: "IBGE Agregados 7060/63", + frequency: "mensal", + color: BRAND.orange, + }, + { + id: "b3-ibov-monthly", + label: "B3 — Ibovespa mensal", + endpoint: () => { + const start = isoDate(monthsAgo(120)); + const end = isoDate(new Date()); + return `/b3/indices/IBOV/monthly?start=${start}&end=${end}`; + }, + field: "close", + title: "Ibovespa mensal", + source: "B3 IndexStatisticsProxy", + frequency: "mensal", color: BRAND.blue, }, ]; @@ -418,7 +439,7 @@ const usesPresetEndpoint = endpoint === presetEndpoint; const options = usesPresetEndpoint ? { ...preset, endpoint: presetEndpoint, field: field || preset.field || "" } - : { endpoint, field, title: endpoint, source: "Endpoint findata-br" }; + : { endpoint, field, title: endpoint, source: "Endpoint findata-br", frequency: "não inferida" }; setStatus("Buscando endpoint…"); const controller = new AbortController(); @@ -461,7 +482,7 @@ nodes.summary.replaceChildren( auditLink, document.createTextNode( - ` · ${normalized.data.length} pontos · ${first} a ${last} · data=${normalized.dateKey} · valor=${normalized.valueKey || "OHLC"}`, + ` · Script: ${CHART_SCRIPT} · Renderer: ${CHART_RENDERER} · Frequência: ${options.frequency || "não inferida"} · ${normalized.data.length} pontos · ${first} a ${last} · data=${normalized.dateKey} · valor=${normalized.valueKey || "OHLC"}`, ), ); setStatus("Série plotada.", "ok"); diff --git a/src/findata/web/templates/charts.html b/src/findata/web/templates/charts.html index 79ff781..624e7df 100644 --- a/src/findata/web/templates/charts.html +++ b/src/findata/web/templates/charts.html @@ -89,7 +89,7 @@

Taxa Selic

Fonte primária - Dados Abertos de Mercado (findata-br) + Dados Financeiros Abertos (findata-br)
Extração @@ -103,7 +103,7 @@

Taxa Selic

Fontes dos dados. Fonte primária/curadoria: - Dados Abertos de Mercado (findata-br). + Dados Financeiros Abertos (findata-br). Subsets originais: BCB SGS 432.