Pular para conteúdo

Selic / COPOM

Taxa Selic

selic — Dados e análises relacionados à taxa Selic e política monetária.

Submódulos

copom Calendário de reuniões do COPOM (BCB). compromissada Leilões de operações compromissadas do BCB. cpm Dados brutos de contratos CPM (opções digitais de COPOM) da B3. probabilities Probabilidades implícitas de mudança de meta nas reuniões do COPOM.

over(data)

Taxa SELIC Over para uma data específica.

Parameters:

Name Type Description Default
data DateLike

Data da consulta.

required

Returns:

Type Description
float

Taxa SELIC Over em decimal ou nan se não disponível.

Examples:

>>> import pyield as yd
>>> yd.selic.over("31-05-2024")  # decimal (0.104 = 10,4% a.a.)
0.104
Source code in pyield/bc/sgs.py
def selic_over(data: DateLike) -> float:
    """Taxa SELIC Over para uma data específica.

    Args:
        data: Data da consulta.

    Returns:
        Taxa SELIC Over em decimal ou ``nan`` se não disponível.

    Examples:
        >>> import pyield as yd
        >>> yd.selic.over("31-05-2024")  # decimal (0.104 = 10,4% a.a.)
        0.104
    """
    if any_is_empty(data):
        return float("nan")
    return _extrair_escalar(selic_over_serie(data, data), "taxa")

over_serie(inicio=None, fim=None, *, ultimos=None)

Taxa SELIC Over (série SGS 1178).

Taxa de juros média diária praticada no mercado interbancário, com títulos públicos como garantia.

Parameters:

Name Type Description Default
inicio DateLike | None

Data inicial.

None
fim DateLike | None

Data final. Se None, usa a data mais recente.

None
ultimos int | None

Número de registros mais recentes a retornar. Mutuamente exclusivo com inicio/fim.

None

Returns:

Type Description
DataFrame

DataFrame com colunas data e taxa (decimal), ou DataFrame vazio.

Examples:

>>> import pyield as yd
>>> # Copom de 29-01-2025: over subiu de 12,15% para 13,15% a.a.
>>> yd.selic.over_serie("28-01-2025", "31-01-2025")
shape: (4, 2)
┌────────────┬────────┐
│ data       ┆ taxa   │
│ ---        ┆ ---    │
│ date       ┆ f64    │
╞════════════╪════════╡
│ 2025-01-28 ┆ 0.1215 │
│ 2025-01-29 ┆ 0.1215 │
│ 2025-01-30 ┆ 0.1315 │
│ 2025-01-31 ┆ 0.1315 │
└────────────┴────────┘
Source code in pyield/bc/sgs.py
def selic_over_serie(
    inicio: DateLike | None = None,
    fim: DateLike | None = None,
    *,
    ultimos: int | None = None,
) -> pl.DataFrame:
    """Taxa SELIC Over (série SGS 1178).

    Taxa de juros média diária praticada no mercado interbancário,
    com títulos públicos como garantia.

    Args:
        inicio: Data inicial.
        fim: Data final. Se ``None``, usa a data mais recente.
        ultimos: Número de registros mais recentes a retornar.
            Mutuamente exclusivo com ``inicio``/``fim``.

    Returns:
        DataFrame com colunas data e taxa (decimal), ou DataFrame vazio.

    Examples:
        >>> import pyield as yd
        >>> # Copom de 29-01-2025: over subiu de 12,15% para 13,15% a.a.
        >>> yd.selic.over_serie("28-01-2025", "31-01-2025")
        shape: (4, 2)
        ┌────────────┬────────┐
        │ data       ┆ taxa   │
        │ ---        ┆ ---    │
        │ date       ┆ f64    │
        ╞════════════╪════════╡
        │ 2025-01-28 ┆ 0.1215 │
        │ 2025-01-29 ┆ 0.1215 │
        │ 2025-01-30 ┆ 0.1315 │
        │ 2025-01-31 ┆ 0.1315 │
        └────────────┴────────┘
    """
    return _converter_para_taxa(
        _buscar_serie(SerieSGS.SELIC_OVER, inicio, fim, ultimos)
    )

meta(data)

Taxa SELIC Meta para uma data específica.

Parameters:

Name Type Description Default
data DateLike

Data da consulta.

required

Returns:

Type Description
float

Taxa SELIC Meta em decimal ou nan se não disponível.

Examples:

>>> import pyield as yd
>>> yd.selic.meta("31-05-2024")  # decimal (0.105 = 10,5% a.a.)
0.105
Source code in pyield/bc/sgs.py
def selic_meta(data: DateLike) -> float:
    """Taxa SELIC Meta para uma data específica.

    Args:
        data: Data da consulta.

    Returns:
        Taxa SELIC Meta em decimal ou ``nan`` se não disponível.

    Examples:
        >>> import pyield as yd
        >>> yd.selic.meta("31-05-2024")  # decimal (0.105 = 10,5% a.a.)
        0.105
    """
    if any_is_empty(data):
        return float("nan")
    return _extrair_escalar(selic_meta_serie(data, data), "taxa")

meta_serie(inicio=None, fim=None, *, ultimos=None)

Taxa SELIC Meta (série SGS 432).

Taxa de juros oficial definida pelo COPOM.

Parameters:

Name Type Description Default
inicio DateLike | None

Data inicial.

None
fim DateLike | None

Data final. Se None, usa a data mais recente.

None
ultimos int | None

Número de registros mais recentes a retornar. Mutuamente exclusivo com inicio/fim.

None

Returns:

Type Description
DataFrame

DataFrame com colunas data e taxa (decimal), ou DataFrame vazio.

Examples:

>>> import pyield as yd
>>> # Copom de 29-01-2025: meta subiu de 12,25% para 13,25% a.a.
>>> yd.selic.meta_serie("28-01-2025", "31-01-2025")
shape: (4, 2)
┌────────────┬────────┐
│ data       ┆ taxa   │
│ ---        ┆ ---    │
│ date       ┆ f64    │
╞════════════╪════════╡
│ 2025-01-28 ┆ 0.1225 │
│ 2025-01-29 ┆ 0.1225 │
│ 2025-01-30 ┆ 0.1325 │
│ 2025-01-31 ┆ 0.1325 │
└────────────┴────────┘
Source code in pyield/bc/sgs.py
def selic_meta_serie(
    inicio: DateLike | None = None,
    fim: DateLike | None = None,
    *,
    ultimos: int | None = None,
) -> pl.DataFrame:
    """Taxa SELIC Meta (série SGS 432).

    Taxa de juros oficial definida pelo COPOM.

    Args:
        inicio: Data inicial.
        fim: Data final. Se ``None``, usa a data mais recente.
        ultimos: Número de registros mais recentes a retornar.
            Mutuamente exclusivo com ``inicio``/``fim``.

    Returns:
        DataFrame com colunas data e taxa (decimal), ou DataFrame vazio.

    Examples:
        >>> import pyield as yd
        >>> # Copom de 29-01-2025: meta subiu de 12,25% para 13,25% a.a.
        >>> yd.selic.meta_serie("28-01-2025", "31-01-2025")
        shape: (4, 2)
        ┌────────────┬────────┐
        │ data       ┆ taxa   │
        │ ---        ┆ ---    │
        │ date       ┆ f64    │
        ╞════════════╪════════╡
        │ 2025-01-28 ┆ 0.1225 │
        │ 2025-01-29 ┆ 0.1225 │
        │ 2025-01-30 ┆ 0.1325 │
        │ 2025-01-31 ┆ 0.1325 │
        └────────────┴────────┘
    """
    return _converter_para_taxa(
        _buscar_serie(SerieSGS.SELIC_META, inicio, fim, ultimos)
    )

Calendário COPOM

copom — COPOM meeting calendar.

Past meetings sourced from the BCB public API (atas endpoint). Future meetings for the current cycle are hardcoded from the official BCB public note and must be updated each January.

BCB API field mapping

nroReuniao     → MeetingNumber  (sequential BCB number)
dataReferencia → EndDate        (last day of the 2-day meeting)
StartDate      derived as EndDate − 1 calendar day (always 2-day meetings)

calendar(start=None, end=None)

Return the full COPOM meeting calendar (past + future).

Past meetings are fetched live from the BCB API. Future meetings come from the hardcoded annual constant. Duplicates between the two sources are removed by deduplication on EndDate, so there is no need to manually keep the lists in sync.

Parameters

start, end : DateLike | None Optional inclusive date range filter applied to EndDate. None means no bound.

Returns

pl.DataFrame Columns: MeetingNumber : Int32 BCB sequential number (null for future) StartDate : Date first day of the 2-day meeting EndDate : Date last day of the 2-day meeting ExpiryDate : Date next Brazilian business day after EndDate (= B3 CPM contract settlement/expiry date) Rows are sorted by EndDate ascending.

Notes

ExpiryDate is computed with du.deslocar_expr("EndDate", 1), using the Brazilian holiday calendar already embedded in pyield.du.

Examples:

>>> import pyield as yd
>>> cal = yd.selic.copom.calendar()
>>> "ExpiryDate" in cal.columns
True
>>> cal["EndDate"].is_sorted()
True
Source code in pyield/selic/copom.py
def calendar(
    start: DateLike | None = None,
    end: DateLike | None = None,
) -> pl.DataFrame:
    """
    Return the full COPOM meeting calendar (past + future).

    Past meetings are fetched live from the BCB API.
    Future meetings come from the hardcoded annual constant.
    Duplicates between the two sources are removed by deduplication
    on EndDate, so there is no need to manually keep the lists in sync.

    Parameters
    ----------
    start, end : DateLike | None
        Optional inclusive date range filter applied to EndDate.
        None means no bound.

    Returns
    -------
    pl.DataFrame
        Columns:
            MeetingNumber : Int32   BCB sequential number (null for future)
            StartDate     : Date    first day of the 2-day meeting
            EndDate       : Date    last day of the 2-day meeting
            ExpiryDate    : Date    next Brazilian business day after EndDate
                                    (= B3 CPM contract settlement/expiry date)
        Rows are sorted by EndDate ascending.

    Notes
    -----
    ExpiryDate is computed with ``du.deslocar_expr("EndDate", 1)``, using
    the Brazilian holiday calendar already embedded in ``pyield.du``.

    Examples:
        >>> import pyield as yd
        >>> cal = yd.selic.copom.calendar()
        >>> "ExpiryDate" in cal.columns
        True
        >>> cal["EndDate"].is_sorted()
        True
    """
    past = _fetch_past_meetings()
    future = _build_future_meetings()

    df = (
        pl.concat([past, future], how="diagonal")
        .unique(subset=["EndDate"])
        .sort("EndDate")
    )

    # ExpiryDate: next business day after the meeting ends.
    # Vectorized via deslocar_expr — consistent with the rest of the codebase.
    df = df.with_columns(ExpiryDate=du.deslocar_expr("EndDate", 1))

    # Optional date-range filter on EndDate
    if start is not None:
        start_date = converter_datas(start)
        if start_date is not None:
            df = df.filter(pl.col("EndDate") >= start_date)
    if end is not None:
        end_date = converter_datas(end)
        if end_date is not None:
            df = df.filter(pl.col("EndDate") <= end_date)

    return df

next_meeting(reference=None)

Return the single next COPOM meeting on or after reference.

If reference is None, today's date (Brazil timezone) is used. Returns a one-row DataFrame with the same schema as :func:calendar.

Examples:

>>> import pyield as yd
>>> row = yd.selic.copom.next_meeting()
>>> len(row)
1
Source code in pyield/selic/copom.py
def next_meeting(reference: DateLike | None = None) -> pl.DataFrame:
    """
    Return the single next COPOM meeting on or after ``reference``.

    If ``reference`` is None, today's date (Brazil timezone) is used.
    Returns a one-row DataFrame with the same schema as :func:`calendar`.

    Examples:
        >>> import pyield as yd
        >>> row = yd.selic.copom.next_meeting()
        >>> len(row)
        1
    """
    ref = relogio.hoje() if reference is None else converter_datas(reference)
    cal = calendar()
    return cal.filter(pl.col("EndDate") >= ref).head(1)

Compromissadas BCB

Módulo para consulta dos leilões de operações compromissadas (repos) realizados pelo BCB.

Fonte oficial (API OData): https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/aplicacao#!/recursos/leiloes_compromissadas

Exemplo de chamada bruta (CSV): https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/odata/leiloes_compromissadas(dataLancamentoInicio=@dataLancamentoInicio,dataLancamentoFim=@dataLancamentoFim,horaInicio=@horaInicio,dataLiquidacao=@dataLiquidacao,dataRetorno=@dataRetorno,publicoPermitidoLeilao=@publicoPermitidoLeilao,nomeTipoOferta=@nomeTipoOferta)?@dataLancamentoInicio='2025-08-21'&@dataLancamentoFim='2025-08-21'&$format=text/csv

Exemplo (CSV original): id , dataMovimento, horaInicio, publicoPermitidoLeilao, numeroComunicado, nomeTipoOferta , ofertante , prazoDiasCorridos, dataLiquidacao, dataRetorno, volumeAceito, taxaCorte, percentualCorte ac1b013d13d6fb1d9d9e251b800010ee, 2025-08-21 , 09:00 , SomenteDealer , null , Tomador , Banco Central, 1, 2025-08-21 , 2025-08-22 , 647707406, "14,9" , 0 ac1b013d13d6fb1d9d9e251b8000121e, 2025-08-21 , 12:00 , TodoMercado , 43716 , Compromissada 1047, Banco Central, 91, 2025-08-22 , 2025-11-21 , 5000000, "99,78" , "64,13"

compromissadas(inicio=None, fim=None)

Consulta e retorna leilões de operações compromissadas do BCB.

Semântica dos parâmetros de período (API OData): - inicio somente: dados de inicio até o fim da série. - fim somente: dados do início da série até fim. - ambos omitidos: série histórica completa.

Parameters:

Name Type Description Default
inicio DateLike | None

Data inicial (inclusive) ou None.

None
fim DateLike | None

Data final (inclusive) ou None.

None

Returns:

Type Description
DataFrame

DataFrame com colunas normalizadas em português e tipos

DataFrame

enriquecidos (frações decimais, inteiros, datas).

Output Columns
  • data_leilao (Date): data de ocorrência do leilão.
  • data_liquidacao (Date): data de liquidação (início da operação).
  • data_retorno (Date): data de recompra / término da operação.
  • hora_inicio (Time): horário de início do leilão.
  • prazo_dc (Int64): dias corridos até a data de retorno.
  • prazo_du (Int64): dias úteis entre liquidação e retorno.
  • comunicado (Int64): número do comunicado/aviso do BC (pode ser nulo).
  • tipo_oferta (String): classif. do tipo de oferta (ex: Tomador, Compromissada 1047).
  • publico (String): público permitido no leilão (SomenteDealer, TodoMercado).
  • financeiro_aceito (Float64): financeiro aceito no leilão em reais (convertido de milhares).
  • taxa_corte (Float64): taxa de corte (ex. 0.1490 = 14,90%). Nula se financeiro_aceito = 0.
  • pct_aceito (Float64): percentual do volume ofertado efetivamente aceito (0-100). 100 = nenhuma rejeição. 0 indica nada aceito (volume_aceito = 0).
Notes
  • Dados ordenados por: data_leilao, hora_inicio, tipo_oferta.

Examples:

>>> import polars as pl
>>> _ = pl.Config.set_tbl_width_chars(210)
>>> _ = pl.Config.set_tbl_cols(-1)
>>> import pyield as yd
>>> yd.selic.compromissadas(inicio="21-08-2025", fim="21-08-2025")
shape: (2, 12)
┌─────────────┬─────────────────┬──────────────┬─────────────┬──────────┬──────────┬────────────┬────────────────────┬───────────────┬───────────────────┬────────────┬────────────┐
│ data_leilao ┆ data_liquidacao ┆ data_retorno ┆ hora_inicio ┆ prazo_dc ┆ prazo_du ┆ comunicado ┆ tipo_oferta        ┆ publico       ┆ financeiro_aceito ┆ taxa_corte ┆ pct_aceito │
│ ---         ┆ ---             ┆ ---          ┆ ---         ┆ ---      ┆ ---      ┆ ---        ┆ ---                ┆ ---           ┆ ---               ┆ ---        ┆ ---        │
│ date        ┆ date            ┆ date         ┆ time        ┆ i64      ┆ i64      ┆ i64        ┆ str                ┆ str           ┆ f64               ┆ f64        ┆ f64        │
╞═════════════╪═════════════════╪══════════════╪═════════════╪══════════╪══════════╪════════════╪════════════════════╪═══════════════╪═══════════════════╪════════════╪════════════╡
│ 2025-08-21  ┆ 2025-08-21      ┆ 2025-08-22   ┆ 09:00:00    ┆ 1        ┆ 1        ┆ null       ┆ Tomador            ┆ SomenteDealer ┆ 6.4771e11         ┆ 0.149      ┆ 100.0      │
│ 2025-08-21  ┆ 2025-08-22      ┆ 2025-11-21   ┆ 12:00:00    ┆ 91       ┆ 64       ┆ 43716      ┆ Compromissada 1047 ┆ TodoMercado   ┆ 5.0000e9          ┆ 0.9978     ┆ 35.87      │
└─────────────┴─────────────────┴──────────────┴─────────────┴──────────┴──────────┴────────────┴────────────────────┴───────────────┴───────────────────┴────────────┴────────────┘
>>> _ = pl.Config.restore_defaults()
>>> _ = pl.Config.set_tbl_width_chars(150)
Source code in pyield/selic/compromissada.py
def compromissadas(
    inicio: DateLike | None = None,
    fim: DateLike | None = None,
) -> pl.DataFrame:
    """Consulta e retorna leilões de operações compromissadas do BCB.

    Semântica dos parâmetros de período (API OData):
        - inicio somente: dados de inicio até o fim da série.
        - fim somente: dados do início da série até fim.
        - ambos omitidos: série histórica completa.

    Args:
        inicio: Data inicial (inclusive) ou None.
        fim: Data final (inclusive) ou None.

    Returns:
        DataFrame com colunas normalizadas em português e tipos
        enriquecidos (frações decimais, inteiros, datas).

    Output Columns:
        - data_leilao (Date): data de ocorrência do leilão.
        - data_liquidacao (Date): data de liquidação (início da operação).
        - data_retorno (Date): data de recompra / término da operação.
        - hora_inicio (Time): horário de início do leilão.
        - prazo_dc (Int64): dias corridos até a data de retorno.
        - prazo_du (Int64): dias úteis entre liquidação e retorno.
        - comunicado (Int64): número do comunicado/aviso do BC (pode ser nulo).
        - tipo_oferta (String): classif. do tipo de oferta (ex: Tomador, Compromissada 1047).
        - publico (String): público permitido no leilão (SomenteDealer, TodoMercado).
        - financeiro_aceito (Float64): financeiro aceito no leilão em reais (convertido de milhares).
        - taxa_corte (Float64): taxa de corte (ex. 0.1490 = 14,90%). Nula se financeiro_aceito = 0.
        - pct_aceito (Float64): percentual do volume ofertado efetivamente aceito (0-100).
          100 = nenhuma rejeição. 0 indica nada aceito (volume_aceito = 0).

    Notes:
        - Dados ordenados por: data_leilao, hora_inicio, tipo_oferta.

    Examples:
        >>> import polars as pl
        >>> _ = pl.Config.set_tbl_width_chars(210)
        >>> _ = pl.Config.set_tbl_cols(-1)
        >>> import pyield as yd
        >>> yd.selic.compromissadas(inicio="21-08-2025", fim="21-08-2025")
        shape: (2, 12)
        ┌─────────────┬─────────────────┬──────────────┬─────────────┬──────────┬──────────┬────────────┬────────────────────┬───────────────┬───────────────────┬────────────┬────────────┐
        │ data_leilao ┆ data_liquidacao ┆ data_retorno ┆ hora_inicio ┆ prazo_dc ┆ prazo_du ┆ comunicado ┆ tipo_oferta        ┆ publico       ┆ financeiro_aceito ┆ taxa_corte ┆ pct_aceito │
        │ ---         ┆ ---             ┆ ---          ┆ ---         ┆ ---      ┆ ---      ┆ ---        ┆ ---                ┆ ---           ┆ ---               ┆ ---        ┆ ---        │
        │ date        ┆ date            ┆ date         ┆ time        ┆ i64      ┆ i64      ┆ i64        ┆ str                ┆ str           ┆ f64               ┆ f64        ┆ f64        │
        ╞═════════════╪═════════════════╪══════════════╪═════════════╪══════════╪══════════╪════════════╪════════════════════╪═══════════════╪═══════════════════╪════════════╪════════════╡
        │ 2025-08-21  ┆ 2025-08-21      ┆ 2025-08-22   ┆ 09:00:00    ┆ 1        ┆ 1        ┆ null       ┆ Tomador            ┆ SomenteDealer ┆ 6.4771e11         ┆ 0.149      ┆ 100.0      │
        │ 2025-08-21  ┆ 2025-08-22      ┆ 2025-11-21   ┆ 12:00:00    ┆ 91       ┆ 64       ┆ 43716      ┆ Compromissada 1047 ┆ TodoMercado   ┆ 5.0000e9          ┆ 0.9978     ┆ 35.87      │
        └─────────────┴─────────────────┴──────────────┴─────────────┴──────────┴──────────┴────────────┴────────────────────┴───────────────┴───────────────────┴────────────┴────────────┘
        >>> _ = pl.Config.restore_defaults()
        >>> _ = pl.Config.set_tbl_width_chars(150)
    """
    params = _montar_parametros(inicio, fim)
    url = montar_url(URL_BASE_API, params)
    dados = buscar_csv(url)
    df = parsear_csv(dados)
    if df.is_empty():
        return pl.DataFrame()
    return _processar_df(df)

Contratos CPM

CPM — B3 COPOM Digital Option contract data.

Ticker format: CPM{month_code}{year2}{C|P}{strike_6digits} Example: CPMZ25C099500 ^^^ → prefix "CPM" ^ → month code "Z" = December ^^ → year "25" = 2025 ^ → option type "C" = call ^^^^^^ → strike "099500" → 99.500

Strike interpretation (Selic Meta context): strike_float = int(strike_6digits) / 1000 # e.g. 99.500 change_bps = round((strike_float - 100) * 100) # e.g. -50 bps

Month codes (B3 standard future convention): F=1, G=2, H=3, J=4, K=5, M=6, N=7, Q=8, U=9, V=10, X=11, Z=12

Implementation note

CPM options expire on the first business day AFTER the COPOM meeting ends (ExpiryDate), not on the first business day of the meeting month. This module bypasses the generic B3 pipeline (which applies a 6-char ticker filter and a DaysToExp > 0 filter, both incorrect for CPM) and calls the lower-level price-report helpers directly.

ExpiryDate and MeetingEndDate are resolved by joining against the COPOM calendar (bc.copom.calendar()) on meeting month + year extracted from the ticker. The join is a left join so CPM rows are never dropped even if the calendar has gaps for very recently announced future meetings.

data(date)

Fetch B3 end-of-day CPM contract data for a given trade date.

Parameters

date : DateLike Trade date. Accepts DD-MM-YYYY, YYYY-MM-DD, datetime.date, etc.

Returns

pl.DataFrame Columns: TradeDate : date TickerSymbol : str MeetingEndDate : date actual COPOM meeting end date (from BCB calendar) ExpiryDate : date next business day after MeetingEndDate (= B3 CPM contract settlement date) OptionType : str "call" or "put" StrikeChangeBps: int change in bps vs 100.000 strike SettlementPrice: float B3 official "Preço de Referência" from the CSV endpoint, in points (0–100). This is the price shown on the B3 dashboard ("Probabilidades da Taxa Selic Meta"). Null for older dates (> ~1 month) where the CSV endpoint is unavailable. BDaysToExp : int business days from TradeDate to ExpiryDate

Returns empty DataFrame with correct schema if no CPM data
exists for the requested date (weekend, holiday, etc.).

Examples:

>>> import pyield as yd
>>> df = yd.selic.cpm.data("29-01-2025")
>>> df.is_empty() or set(df.schema.keys()) >= {
...     "data_referencia",
...     "codigo_negociacao",
...     "data_fim_reuniao",
...     "data_expiracao",
...     "tipo_opcao",
...     "variacao_strike_bps",
...     "preco_ajuste",
... }
True
Source code in pyield/selic/cpm.py
def data(date: DateLike) -> pl.DataFrame:
    """
    Fetch B3 end-of-day CPM contract data for a given trade date.

    Parameters
    ----------
    date : DateLike
        Trade date. Accepts DD-MM-YYYY, YYYY-MM-DD, datetime.date, etc.

    Returns
    -------
    pl.DataFrame
        Columns:
            TradeDate      : date
            TickerSymbol   : str
            MeetingEndDate : date   actual COPOM meeting end date (from BCB calendar)
            ExpiryDate     : date   next business day after MeetingEndDate
                                    (= B3 CPM contract settlement date)
            OptionType     : str    "call" or "put"
            StrikeChangeBps: int    change in bps vs 100.000 strike
            SettlementPrice: float  B3 official "Preço de Referência" from the
                                    CSV endpoint, in points (0–100).  This is
                                    the price shown on the B3 dashboard
                                    ("Probabilidades da Taxa Selic Meta").
                                    Null for older dates (> ~1 month) where
                                    the CSV endpoint is unavailable.
            BDaysToExp     : int    business days from TradeDate to ExpiryDate

        Returns empty DataFrame with correct schema if no CPM data
        exists for the requested date (weekend, holiday, etc.).

    Examples:
        >>> import pyield as yd
        >>> df = yd.selic.cpm.data("29-01-2025")
        >>> df.is_empty() or set(df.schema.keys()) >= {
        ...     "data_referencia",
        ...     "codigo_negociacao",
        ...     "data_fim_reuniao",
        ...     "data_expiracao",
        ...     "tipo_opcao",
        ...     "variacao_strike_bps",
        ...     "preco_ajuste",
        ... }
        True
    """
    trade_date = cv.converter_datas(date) if date is not None else None
    if trade_date is None:
        return _empty_schema()

    try:
        df = boletim.buscar(trade_date, prefixo_ticker="CPM")
    except Exception:
        logger.exception("CPM: falha ao baixar SPR para %s.", trade_date)
        return _empty_schema()

    if df.is_empty():
        return _empty_schema()
    df = df.rename(_RENOMEAR_COLUNAS_CPM, strict=False)
    df = df.with_columns(data_referencia=trade_date)

    # Extrai tipo de opção (codigo_negociacao[6]) e variação de strike (codigo_negociacao[7:13])
    # inteiramente com expressões de string Polars — sem loops Python.
    df = df.with_columns(
        tipo_opcao=pl.col("codigo_negociacao")
        .str.slice(6, 1)
        .replace({"C": "call", "P": "put"}),
        variacao_strike_bps=(
            pl.col("codigo_negociacao")
            .str.slice(7, 6)
            .cast(pl.Int64, strict=False)
            .floordiv(10)
            .sub(10_000)
            .cast(pl.Int32)
        ),
        # Mês e ano da reunião extraídos das posições 3 e 4-5 do código de negociação.
        # Usados como chaves de junção com o calendário COPOM.
        _mes_reuniao=(
            pl.col("codigo_negociacao")
            .str.slice(3, 1)
            .replace(_MONTH_CODE_STR)
            .cast(pl.Int32, strict=False)
        ),
        _ano_reuniao=(
            pl.col("codigo_negociacao")
            .str.slice(4, 2)
            .cast(pl.Int32, strict=False)
            .add(2000)
        ),
    )

    # Join with COPOM calendar to get MeetingEndDate and the correct ExpiryDate.
    # Import is deferred to avoid a module-level circular dependency risk.
    from pyield.selic import copom  # noqa: PLC0415

    cal = copom.calendar().select(
        _mes_reuniao=pl.col("EndDate").dt.month().cast(pl.Int32),
        _ano_reuniao=pl.col("EndDate").dt.year().cast(pl.Int32),
        data_fim_reuniao=pl.col("EndDate"),
        data_expiracao=pl.col("ExpiryDate"),
    )

    df = df.join(cal, on=["_mes_reuniao", "_ano_reuniao"], how="left").drop(
        "_mes_reuniao", "_ano_reuniao"
    )

    # dias_uteis: dias úteis de data_referencia até data_expiracao.
    df = df.with_columns(
        dias_uteis=du.contar_expr("data_referencia", "data_expiracao").cast(pl.Int32)
    )

    # preco_ajuste: "Preço de Referência" da B3 via endpoint CSV.
    # O XML SPR para contratos de opções não contém este campo;
    # o XML é usado acima apenas para obter a lista de contratos (códigos de negociação).
    precos_ajuste = _fetch_settlement_prices(trade_date)
    if not precos_ajuste.is_empty():
        df = df.join(precos_ajuste, on="codigo_negociacao", how="left")
    else:
        df = df.with_columns(preco_ajuste=pl.lit(None, dtype=pl.Float64))

    return df.select(
        pl.col("data_referencia"),
        pl.col("codigo_negociacao"),
        pl.col("data_fim_reuniao"),
        pl.col("data_expiracao"),
        pl.col("tipo_opcao"),
        pl.col("variacao_strike_bps"),
        pl.col("preco_ajuste"),
        pl.col("dias_uteis"),
    ).sort("data_expiracao", "variacao_strike_bps")

Probabilidades Implícitas

probabilities — Implied COPOM meeting probabilities from CPM option prices.

The CPM contract is a cash-or-nothing European option. Under risk-neutral pricing, the B3 settlement price in points (0–100) encodes the market-implied probability of each Selic change scenario, discounted by the DI rate to expiry (B3 Pricing Manual §3.5).

Probability conventions

RawProb = SettlementPrice * DiscountExp / 100

  Direct risk-neutral probability per B3 Manual §3.5 (inverted):

      p_n(K) = PR_n * exp(+n * r_n) / N

  where
      PR_n  = SettlementPrice  (B3 "Preço de Referência", points 0–100)
      N     = 100              (fixed notional)
      n     = BDaysToExp / 252 (time in years, business-day convention)
      r_n   = ln(1 + DI1Rate)  (continuously-compounded DI1 rate to expiry)
      DI1Rate = flat-forward interpolated DI1 rate from TradeDate to ExpiryDate

  SettlementPrice in cpm.data() is the B3 official "Preço de Referência"
  from the B3 CSV endpoint — the price shown on the B3 dashboard
  ("Probabilidades da Taxa Selic Meta") and the output of B3's
  P1/P2/P3/P4 methodology.  It may be null for dates older than ~1 month
  where the CSV endpoint is unavailable.

  May not sum to 1.0 per meeting due to bid-ask spreads or B3 P1/P2 pricing.
  When BDaysToExp == 0 (meeting-day itself), DiscountExp == 1.0 exactly and
  RawProb reduces to SettlementPrice / 100.

Prob = RawProb / sum(RawProb) within ExpiryDate group Normalized so each meeting sums to exactly 1.0. This is the B3 P3 pricing adjustment. Use this for scenario analysis and charts.

CumProb = cumulative sum of Prob, sorted by StrikeChangeBps ascending.

Notes on null SettlementPrice

CPM contracts are sometimes listed without a settlement price (no B3 official pricing for that strike on that date, or CSV endpoint unavailable for older dates). Strikes with null SettlementPrice are excluded from the probability output because:

  1. Their contribution to the normalized distribution is undefined.
  2. Polars group_by().agg(sum()) returns 0.0 (not null) for all-null groups, which would break the invariant Prob.sum() == 1.0 per meeting.

As a consequence, a meeting where ALL listed strikes have null prices (e.g. CPMH25 on the January 2025 COPOM day) will not appear in the output. MeetingRank is therefore assigned over the priced meetings only and is always a consecutive sequence [1, 2, …, n].

Notes on DI1 fallback

If DI1 data is unavailable for the trade date (network error, holiday, etc.), DI1Rate falls back to 0.0 and DiscountExp to 1.0, so RawProb reduces to SettlementPrice / 100 — equivalent to the old (incorrect) formula. This degradation is logged as a warning but never raises an exception.

all_meetings(date, option_type='call')

Implied COPOM probabilities for every meeting with CPM contracts trading on date.

Only strikes with a non-null SettlementPrice are included. Meetings where all listed strikes have null prices are excluded entirely (see module-level notes).

Parameters

date : DateLike Trade date. option_type : {"call", "put"} Which side to use. Default "call" (the liquid side in practice).

Returns

pl.DataFrame Columns: TradeDate : Date MeetingEndDate : Date ExpiryDate : Date MeetingRank : Int32 1 = nearest meeting with priced contracts StrikeChangeBps: Int32 sorted ascending within each meeting BDaysToExp : Int32 business days from TradeDate to ExpiryDate SettlementPrice: Float64 B3 "Preço de Referência" in points (0–100) DI1Rate : Float64 flat-forward DI1 rate to ExpiryDate DiscountExp : Float64 exp(BDaysToExp/252 * ln(1+DI1Rate)) RawProb : Float64 SettlementPrice * DiscountExp / 100 Prob : Float64 normalized, sums to 1.0 per meeting CumProb : Float64 cumulative Prob ascending by strike

Sorted by (MeetingRank, StrikeChangeBps).
Returns empty DataFrame with correct schema on missing data.

Examples:

>>> import pyield as yd
>>> import polars as pl
>>> df = yd.selic.probabilities.all_meetings("29-01-2025")
>>> df.is_empty() or df["ranking_reuniao"].min() == 1
True
>>> sums = df.group_by("data_expiracao").agg(pl.col("prob").sum())
>>> df.is_empty() or (sums["prob"] - 1.0).abs().max() < 1e-9
True
Source code in pyield/selic/probabilities.py
def all_meetings(
    date: DateLike,
    option_type: str = "call",
) -> pl.DataFrame:
    """
    Implied COPOM probabilities for every meeting with CPM contracts
    trading on `date`.

    Only strikes with a non-null SettlementPrice are included.  Meetings
    where all listed strikes have null prices are excluded entirely (see
    module-level notes).

    Parameters
    ----------
    date : DateLike
        Trade date.
    option_type : {"call", "put"}
        Which side to use. Default "call" (the liquid side in practice).

    Returns
    -------
    pl.DataFrame
        Columns:
            TradeDate      : Date
            MeetingEndDate : Date
            ExpiryDate     : Date
            MeetingRank    : Int32   1 = nearest meeting with priced contracts
            StrikeChangeBps: Int32   sorted ascending within each meeting
            BDaysToExp     : Int32   business days from TradeDate to ExpiryDate
            SettlementPrice: Float64 B3 "Preço de Referência" in points (0–100)
            DI1Rate        : Float64 flat-forward DI1 rate to ExpiryDate
            DiscountExp    : Float64 exp(BDaysToExp/252 * ln(1+DI1Rate))
            RawProb        : Float64 SettlementPrice * DiscountExp / 100
            Prob           : Float64 normalized, sums to 1.0 per meeting
            CumProb        : Float64 cumulative Prob ascending by strike

        Sorted by (MeetingRank, StrikeChangeBps).
        Returns empty DataFrame with correct schema on missing data.

    Examples:
        >>> import pyield as yd
        >>> import polars as pl
        >>> df = yd.selic.probabilities.all_meetings("29-01-2025")
        >>> df.is_empty() or df["ranking_reuniao"].min() == 1
        True
        >>> sums = df.group_by("data_expiracao").agg(pl.col("prob").sum())
        >>> df.is_empty() or (sums["prob"] - 1.0).abs().max() < 1e-9
        True
    """
    raw = cpm.data(date)
    if raw.is_empty():
        return _empty_schema()

    df = (
        raw.filter(pl.col("tipo_opcao") == option_type)
        # Excluir strikes sem preço de ajuste — ver docstring do módulo.
        .filter(pl.col("preco_ajuste").is_not_null())
        .pipe(_add_meeting_rank)
        .pipe(_add_probabilities)
        .select(
            "data_referencia",
            "data_fim_reuniao",
            "data_expiracao",
            "ranking_reuniao",
            "variacao_strike_bps",
            "dias_uteis",
            "preco_ajuste",
            "taxa_di1",
            "fator_desconto",
            "prob_bruta",
            "prob",
            "prob_acumulada",
        )
        .sort(["ranking_reuniao", "variacao_strike_bps"])
    )

    return df if not df.is_empty() else _empty_schema()

meeting(date, expiration=None, option_type='call')

Implied COPOM probabilities for a single meeting.

Parameters

date : DateLike Trade date. expiration : DateLike | None ExpiryDate of the target meeting (B3 contract expiry date, i.e. next business day after the meeting end). If None, the nearest meeting with priced contracts is used. option_type : {"call", "put"} Which side to use. Default "call".

Returns

pl.DataFrame Same schema as all_meetings(), filtered to the single meeting. MeetingRank is always 1 in this output (relative to the selected meeting — do not confuse with rank across all meetings).

Examples:

>>> import pyield as yd
>>> df = yd.selic.probabilities.meeting("29-01-2025")
>>> df.is_empty() or abs(df["prob"].sum() - 1.0) < 1e-9
True
>>> df.is_empty() or df["prob_acumulada"].tail(1).item() == 1.0
True
Source code in pyield/selic/probabilities.py
def meeting(
    date: DateLike,
    expiration: DateLike | None = None,
    option_type: str = "call",
) -> pl.DataFrame:
    """
    Implied COPOM probabilities for a single meeting.

    Parameters
    ----------
    date : DateLike
        Trade date.
    expiration : DateLike | None
        ExpiryDate of the target meeting (B3 contract expiry date,
        i.e. next business day after the meeting end).
        If None, the nearest meeting with priced contracts is used.
    option_type : {"call", "put"}
        Which side to use. Default "call".

    Returns
    -------
    pl.DataFrame
        Same schema as all_meetings(), filtered to the single meeting.
        MeetingRank is always 1 in this output (relative to the
        selected meeting — do not confuse with rank across all meetings).

    Examples:
        >>> import pyield as yd
        >>> df = yd.selic.probabilities.meeting("29-01-2025")
        >>> df.is_empty() or abs(df["prob"].sum() - 1.0) < 1e-9
        True
        >>> df.is_empty() or df["prob_acumulada"].tail(1).item() == 1.0
        True
    """
    df = all_meetings(date, option_type=option_type)
    if df.is_empty():
        return _empty_schema()

    if expiration is None:
        target_expiry = df.filter(pl.col("ranking_reuniao") == 1)["data_expiracao"][0]
    else:
        target_expiry = converter_datas(expiration)

    return df.filter(pl.col("data_expiracao") == target_expiry).with_columns(
        ranking_reuniao=pl.lit(1, dtype=pl.Int32)
    )