Selic (CPM/COPOM)
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 futures 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
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | |
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:
- Their contribution to the normalized distribution is undefined.
- Polars
group_by().agg(sum())returns 0.0 (not null) for all-null groups, which would break the invariantProb.sum() == 1.0per 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
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | |
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