Receitas de acesso: CDA e BACEN IF.DATA
Estas receitas saem do catálogo e chegam a uma consulta reproduzível sem
copiar dados reais para o repositório. Elas usam DuckDB sobre Parquet; a
conexão DuckDB é efêmera e somente para leitura. Antes de executar, localize o
dataset_id, a tabela_id, a competência/era e o chave_status na
catalogo/cvm.yml ou no
catalogo/bacen.yml. O contrato operacional
completo está em AGENTS.md e os comandos de operação em
README.md.
IDs estáveis usados nas receitas
Use estes IDs completos para localizar a página correta e para comunicar uma
consulta a outra ferramenta. O nome curto (blc_7, por exemplo) não é um
identificador suficiente fora do dataset:
- CDA:
cvm.fundos.cda_fi/blc_1ecvm.fundos.cda_fi/blc_7. - BACEN IF.DATA:
bacen.instituicoes.ifdata_valores/valoresebacen.instituicoes.ifdata_cadastro/cadastro. - BACEN gold:
publish.gold_bacen/fct_balance_sheet.
1. Preparação e regra de leitura
Defina CIANO_LAKE_ROOT para a raiz do lake que já contém raw/, bronze/,
silver/ e, quando aplicável, gold/. O valor abaixo é propositalmente um
placeholder; não substitua por um host, token ou caminho privado no exemplo.
export CIANO_LAKE_ROOT="<CAMINHO_DO_LAKE>"
No PowerShell:
$env:CIANO_LAKE_ROOT = "<CAMINHO_DO_LAKE>"
Os comandos da CLI carregam essa mesma variável. Uma leitura com DuckDB ou
Python não materializa nada; já ingest, bronze, silver, run,
timeseries e gold-fcts escrevem artefatos no lake. Não aponte um serviço
para um arquivo .duckdb: ele é scratch; consumidores devem ler Parquet ou um
serving explicitamente publicado.
O que cada camada significa
| Camada | Uso nesta receita | Grain/contrato | Pode ser consultada sem materializar? |
|---|---|---|---|
raw |
Arquivo fonte imutável, versionado por baixado= |
Zip/JSON original; não é tabela analítica | Sim, mas normalmente não é o ponto de consulta |
bronze |
Auditoria e inspeção fiel da fonte | Todas as colunas como VARCHAR, mais metadados e _hash_linha; quality-gated |
Sim, por Parquet |
silver |
Tipagem e normalização de negócio | Grain declarado pelo transform; pode aplicar keep_first documentado |
Sim, por Parquet |
gold |
Fatos e dimensões analíticas BACEN | Grain específico de cada fato/junção | Sim, por Parquet |
| Postgres/Metabase | Serving, somente se a página indicar disponibilidade operacional | Cópia publicada a partir do gold; nunca fonte primária | Só quando publicado e validado |
Camada não é sinônimo de prontidão. Leia chave_status, comentário de
medição, frescor (_baixado/competência) e o resultado de qualidade antes de
agregar.
2. Receita CDA: cda_fi / blc_7
Identidade antes da consulta
- Dataset:
cda_fi(fonte: cvm, domíniofundos, periodicidade mensal). - Parte/era: para uma competência moderna como
202605, o roteamento écorrente_202605; confirme a era no catálogo para a competência escolhida. - Tabela:
blc_7(cda_fi_BLC_7_{aaaamm}.csv). - Camada recomendada: bronze para inspeção da publicação; silver se a transformação tipada da mesma partição estiver disponível.
- Grão: uma posição de ativo por classe de fundo e competência. A chave
declarada na parte moderna é
(tp_fundo_classe, cnpj_fundo_classe, dt_comptc, tp_aplic, tp_ativo, cd_ativo_bv_merc, ds_ativo_exterior, cd_bv_merc, cd_pais, dt_venc, emissor). - Competência/filtro:
competencia=AAAAMMno caminho eDT_COMPTCno filtro da linha. Não misture meses numa mesma soma. - Colunas de identificação úteis:
CNPJ_FUNDO_CLASSE,DT_COMPTC,TP_APLIC,TP_ATIVO,CD_ATIVO_BV_MERC,CD_BV_MERC,CD_PAIS,DT_VENC,EMISSOReTP_NEGOC(a publicação moderna traz a dimensão de negociação). - Medida comum:
VL_MERC_POS_FINALé monetária; no bronze ainda é texto.
O comentário atual do catálogo registra excess=0 para blc_7 nas
competências modernas medidas. DS_ATIVO_EXTERIOR e DT_VENC podem ser
estruturalmente nulos e estão em chaves_permitem_nulos; NULL não significa
zero nem ausência de posição. Se a entrada não tiver chave_status explícito,
trate isso como alerta: o gate interpreta campo ausente como verificada, mas
a medição ainda deve ser confirmada com probe.
Verificar sem escrever dados
uv run python -m ciano_lake catalog-status
uv run python -m ciano_lake probe \
--dataset cda_fi --competencia 202605 --tabela blc_7
catalog-status lê o catálogo. probe lê a partição raw correspondente e
reporta excesso de linhas, grupos duplicados e verdict de NULL nas colunas
da chave; não corrige, deduplica nem promove Parquet. Se a fonte for
tolerada, os duplicados precisam ser byte-idênticos dentro do cap. Em
tolerada_com_residuo, os grupos não exatos permitidos ficam auditáveis no
_residuos.json; ultrapassar qualquer cap é falha, não um convite a aumentar a
tolerância.
O gate de bronze não deduplica: uma duplicidade é evidência a investigar. A
deduplicação keep_first, quando declarada, pertence ao silver e precisa ser
considerada ao interpretar a contagem de linhas.
Consulta DuckDB sobre Parquet
O caminho abaixo é montado a partir do root e da competência; portanto a
receita não depende de uma máquina específica. Bronze é VARCHAR, logo o
exemplo seleciona e filtra texto e deixa a soma tipada para a camada silver.
import os
from pathlib import Path
import duckdb
lake = Path(os.environ.get("CIANO_LAKE_ROOT", "<CAMINHO_DO_LAKE>"))
competencia = os.environ.get("CDA_COMPETENCIA", "202605")
tabela = "blc_7"
parquet = (
lake / "bronze" / "cvm" / "cda_fi"
/ f"competencia={competencia}" / f"tabela={tabela}" / "data.parquet"
)
if not parquet.exists():
raise SystemExit(f"Partição não encontrada: {parquet}")
path_sql = parquet.as_posix().replace("'", "''")
mes = f"{competencia[:4]}-{competencia[4:]}"
con = duckdb.connect()
try:
rows = con.execute(
f"""
SELECT CNPJ_FUNDO_CLASSE, DT_COMPTC, TP_APLIC, TP_ATIVO,
CD_ATIVO_BV_MERC, CD_BV_MERC, CD_PAIS, DT_VENC, EMISSOR,
TP_NEGOC, VL_MERC_POS_FINAL
FROM read_parquet('{path_sql}')
WHERE DT_COMPTC LIKE ?
ORDER BY CNPJ_FUNDO_CLASSE, DT_COMPTC
LIMIT 50
""",
[f"{mes}-%"],
).fetchall()
for row in rows:
print(row)
finally:
con.close()
Se houver silver para a mesma competência/tabela, ele fica em
silver/fundos/cda_fi/competencia=AAAAMM/tabela=blc_7/data.parquet, com nomes
normalizados em minúsculas e tipos explícitos. Uma soma tipada pode ser feita
assim, sem alterar o lake:
silver = (
lake / "silver" / "fundos" / "cda_fi"
/ f"competencia={competencia}" / "tabela=blc_7" / "data.parquet"
)
if not silver.exists():
raise SystemExit("Silver ainda não foi materializado para esta partição")
path_sql = silver.as_posix().replace("'", "''")
con = duckdb.connect()
try:
print(con.execute(
f"""
SELECT cnpj_fundo_classe, dt_comptc,
SUM(vl_merc_pos_final) AS vl_merc_pos_final
FROM read_parquet('{path_sql}')
WHERE dt_comptc IS NOT NULL
GROUP BY cnpj_fundo_classe, dt_comptc
ORDER BY dt_comptc, cnpj_fundo_classe
LIMIT 50
"""
).fetchdf())
finally:
con.close()
Limites e joins
- A competência é a data de posição (
DT_COMPTC), não a data de download; restatements podem substituir o conteúdo enquantobaixado=preserva a proveniência. - Não transforme
NULLem zero sem uma decisão de negócio. Em especial,DT_VENCausente pode significar um ativo sem vencimento fixo eDS_ATIVO_EXTERIORausente pode ser compensado pelos códigos de mercado e emissor. - Não faça join de posições com o bronze
cad_fiapenas porcnpj_fundo: esse bronze tem uma linha por fundo por atribuição de gestor e eventos de recadastramento. O join por fundo deve usar a visão silver corrente decad_fi, que reduz para uma linha porcnpj_fundo, e ainda assim a relação classe/fundo deve ser conferida. - Não há gold CVM correspondente documentado nesta receita. Sem uma página que marque serving como disponível, permaneça em Parquet/DuckDB; não presuma Postgres/Metabase.
3. Receita BACEN IF.DATA
O domínio BACEN é instituicoes, periodicidade trimestral (03, 06, 09,
12). As entradas do catálogo são ifdata_valores, ifdata_cadastro e o
snapshot ifdata_catalogo. Para consultas, os quatro produtos abaixo têm
contratos diferentes:
| Produto | Camada/caminho | Grain e IDs | Filtro temporal |
|---|---|---|---|
ifdata_valores |
silver silver/instituicoes/ifdata_valores/data.parquet |
Uma linha por (periodo, tipo_inst, cod_inst, report_type, col_name); saldo é tipado |
periodo trimestral, por exemplo 202603 |
ifdata_cadastro |
silver silver/instituicoes/ifdata_cadastro/data.parquet |
Uma linha por (periodo, tipo_inst, cod_inst); nome_instituicao, UF e grupo são atributos |
Mesmo periodo da observação |
Timeseries v_ts_* |
silver silver/instituicoes/v_ts_<tipo>_<relatorio>/data.parquet |
Uma linha por (cod_inst, periodo); cada col_name vira coluna |
periodo e tipo_inst já estão incorporados no nome da view |
fct_balance_sheet / fct_income_statement |
gold gold/instituicoes/<tabela>/data.parquet |
Fato longo: período, instituição, relatório e col_name, enriquecido pelo cadastro |
periodo; os relatórios são selecionados pelo fato |
v_institution_search |
gold gold/instituicoes/v_institution_search/data.parquet |
Cópia tipada do cadastro, uma linha por chave de cadastro | periodo ou atributos cadastrais |
cod_inst é o ID normalizado usado nas camadas tipadas; mantenha também
tipo_inst e periodo no join. Um join de valores com cadastro deve usar
(cod_inst, periodo, tipo_inst), que é a chave usada na construção dos fatos.
Consulta DuckDB sobre ifdata_valores
O bronze BACEN é all-VARCHAR; a consulta abaixo usa silver para obter
saldo numérico e filtra uma competência trimestral. IFDATA_PERIODO pode
ser substituído por outra competência realmente presente no lake.
import os
from pathlib import Path
import duckdb
lake = Path(os.environ.get("CIANO_LAKE_ROOT", "<CAMINHO_DO_LAKE>"))
periodo = int(os.environ.get("IFDATA_PERIODO", "202603"))
parquet = lake / "silver" / "instituicoes" / "ifdata_valores" / "data.parquet"
if not parquet.exists():
raise SystemExit(f"Silver não encontrado: {parquet}")
path_sql = parquet.as_posix().replace("'", "''")
con = duckdb.connect()
try:
print(con.execute(
f"""
SELECT periodo, tipo_inst, cod_inst, report_type, col_name, saldo
FROM read_parquet('{path_sql}')
WHERE periodo = ?
AND report_type = 'Resumo'
AND saldo IS NOT NULL
ORDER BY tipo_inst, cod_inst, col_name
LIMIT 50
""",
[periodo],
).fetchdf())
finally:
con.close()
O mesmo caminho em Python/DuckDB é uma leitura. Ele não baixa JSON, não cria
silver e não altera saldo. Para um cadastro nominal, troque o arquivo por
ifdata_cadastro/data.parquet e filtre nome_instituicao IS NOT NULL; para
um fato, troque por gold/instituicoes/fct_balance_sheet/data.parquet ou
fct_income_statement e mantenha o filtro de periodo.
NULL, sentinelas e duplicidade
- Sentinelas do BCB (
None, vazio,N/I,N/Ae marcadores???) são preservadas comoNULLemifdata_valores;NULLquer dizer informação não disponível, não saldo igual a zero. saldonão deve ser convertido comCOALESCE(saldo, 0)antes de uma decisão semântica. Para contar instituições, conte IDs; para somar valores, declare se linhas sem valor ficam fora da soma.ifdata_valorestemchave_status: verificadapara(periodo, tipo_inst, cod_inst, report_type, col_name);ifdata_cadastrotemchave_status: verificadapara(periodo, tipo_inst, cod_inst). Ainda assim, uma nova carga deve passar pelo gate: dados publicados pelo BCB podem ser restatados.gateé uma validação de cobertura contra os payloads esperados. É diagnóstico e escreve um manifesto de execução, mas não corrige nem deduplica Parquet. A camada bronze deve continuar refletindo a fonte.
Materializar timeseries e gold
Só execute estes comandos quando a intenção for escrever/atualizar artefatos. Eles exigem os silvers upstream e podem substituir o Parquet de saída por renomeação atômica; não são consultas ad hoc.
# Validação de cobertura; não materializa tabelas de dados.
uv run python -m ciano_lake gate --dataset ifdata_valores
uv run python -m ciano_lake gate --dataset ifdata_cadastro
# Lê silver ifdata_valores e materializa todos os silver v_ts_*.
uv run python -m ciano_lake timeseries
# Lê silver ifdata_valores + ifdata_cadastro e materializa os três gold.
uv run python -m ciano_lake gold-fcts
timeseries gera uma saída por par (tipo_inst, report_type) e falha se um
par catalogado não tiver linhas silver. gold-fcts gera
fct_balance_sheet, fct_income_statement e v_institution_search; se
algum silver upstream estiver ausente, não há gold válido para consultar.
Depois de materializar, leia os Parquets com a receita DuckDB acima e confira
o período máximo, a contagem de IDs e a presença de NULL antes de comparar
instituições.
Serving
Postgres/Metabase não é um atalho automático para estes exemplos. O gold Parquet continua sendo a fonte de verdade; a cópia em Postgres só é indicada quando a página do catálogo ou o runbook de publicação marcar o serving como disponível e validado. O fato de existir um comando de publicação não prova paridade, frescor, rollback ou agendamento operacional. Se a superfície não estiver marcada como disponível, permaneça no Parquet/DuckDB.
4. Checklist antes de compartilhar um resultado
- Confirmei
dataset_id,tabela_id, parte/era e competência no catálogo. - Li camada e grain; não comparei bronze VARCHAR com silver/gold tipado sem explicitar a conversão.
- Rodei
catalog-status,probeougateconforme o caso e li ochave_status, caps e eventuais_residuos.json. - Mantive
NULLdistinto de zero e filtrei uma competência/periodoclaro. - Evitei o fan-out do
cad_fibronze e não tratei serving como disponível sem evidência.