Skip to content

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_1 e cvm.fundos.cda_fi/blc_7.
  • BACEN IF.DATA: bacen.instituicoes.ifdata_valores/valores e bacen.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ínio fundos, 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=AAAAMM no caminho e DT_COMPTC no 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, EMISSOR e TP_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 enquanto baixado= preserva a proveniência.
  • Não transforme NULL em zero sem uma decisão de negócio. Em especial, DT_VENC ausente pode significar um ativo sem vencimento fixo e DS_ATIVO_EXTERIOR ausente pode ser compensado pelos códigos de mercado e emissor.
  • Não faça join de posições com o bronze cad_fi apenas por cnpj_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 de cad_fi, que reduz para uma linha por cnpj_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/A e marcadores ???) são preservadas como NULL em ifdata_valores; NULL quer dizer informação não disponível, não saldo igual a zero.
  • saldo não deve ser convertido com COALESCE(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_valores tem chave_status: verificada para (periodo, tipo_inst, cod_inst, report_type, col_name); ifdata_cadastro tem chave_status: verificada para (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

  1. Confirmei dataset_id, tabela_id, parte/era e competência no catálogo.
  2. Li camada e grain; não comparei bronze VARCHAR com silver/gold tipado sem explicitar a conversão.
  3. Rodei catalog-status, probe ou gate conforme o caso e li o chave_status, caps e eventuais _residuos.json.
  4. Mantive NULL distinto de zero e filtrei uma competência/periodo claro.
  5. Evitei o fan-out do cad_fi bronze e não tratei serving como disponível sem evidência.