Operar opções na B3 sem ferramenta decente é quase um ritual de sofrimento: home broker engasga, sites de terceiros pedem assinatura cara, e você ainda fica refém de telas pré-fabricadas. Neste tutorial você vai construir, em Python e em ~30 minutos, sua própria visualização de cadeia de opções da B3 — calls e puts, com bid/ask, volume e moneyness — usando a API brapi.
O Que é uma Cadeia de Opções?
Uma cadeia de opções (ou option chain) é a tabela que mostra todas as séries de opções negociadas para um determinado ativo subjacente em um determinado vencimento. Em outras palavras: para PETR4, com vencimento em maio de 2026, qual a lista de calls e puts disponíveis, com seus respectivos strikes, prêmios, volumes e bids/asks.
Visualmente, uma cadeia tem este formato (calls à esquerda, puts à direita, strike no meio):
CALLS PUTS
Strike Bid Ask Vol || Strike Bid Ask Vol
30.00 2.10 2.13 3.2k || 30.00 0.18 0.21 1.4k
32.00 1.05 1.07 5.1k || 32.00 0.78 0.82 2.6k
34.00 0.31 0.34 2.8k || 34.00 2.85 2.92 0.9k
36.00 0.08 0.10 1.1k || 36.00 5.10 5.20 0.4kA cadeia é a base de qualquer análise de opções — escolha de strikes, montagem de spreads, identificação de assimetrias.
Plano mínimo: Pro. No sandbox sem token, os endpoints de opções aceitam
apenas underlying=PETR4. Perfeito para acompanhar este tutorial — todos
os exemplos funcionam com PETR4.
O Fluxo da API
A brapi expõe 4 endpoints de opções, desenhados para um fluxo natural:
/api/v2/options/expirations— descobre os vencimentos disponíveis/api/v2/options/strikes— lista os strikes em um vencimento (opcional)/api/v2/options/chain— retorna a cadeia completa com OHLCV/api/v2/options/historical— histórico de uma série específica
Para construir uma cadeia visual, vamos usar 1 + 3.
O Que Vamos Construir
options-chain/
├── brapi_options.py # Cliente HTTP
├── chain_view.py # Renderização tabular
├── filters.py # ITM/ATM/OTM, liquidez
└── run.py # Pipeline principalSaída: tabela formatada com calls e puts lado a lado, com indicadores visuais de moneyness e liquidez.
Pré-requisitos
mkdir options-chain && cd options-chain
python -m venv .venv && source .venv/bin/activate
pip install requests pandas tabulate openpyxl
# Para o sandbox PETR4 não é necessário token
# Para outros ativos: gerar token Pro em brapi.dev/dashboard
export BRAPI_TOKEN="seu_token_pro" # opcionalPasso 1: Cliente HTTP
import os
import requests
from typing import Literal
BASE = "https://brapi.dev/api/v2/options"
TOKEN = os.getenv("BRAPI_TOKEN", "")
HEADERS = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {}
def get_expirations(underlying: str, include_expired: bool = False) -> list[str]:
"""Descobre os vencimentos negociados para um ativo."""
params = {"underlying": underlying}
if include_expired:
params["includeExpired"] = "true"
resp = requests.get(f"{BASE}/expirations", params=params, headers=HEADERS, timeout=30)
resp.raise_for_status()
return resp.json().get("expirations", [])
def get_chain(
underlying: str,
expiration_date: str,
side: Literal["call", "put"] | None = None,
min_strike: float | None = None,
max_strike: float | None = None,
date: str | None = None,
) -> dict:
"""Retorna a cadeia de opções para um vencimento específico."""
params: dict = {
"underlying": underlying,
"expirationDate": expiration_date,
}
if side:
params["side"] = side
if min_strike is not None:
params["minStrike"] = min_strike
if max_strike is not None:
params["maxStrike"] = max_strike
if date:
params["date"] = date
resp = requests.get(f"{BASE}/chain", params=params, headers=HEADERS, timeout=30)
resp.raise_for_status()
return resp.json()Pontos:
expirationsaceitaincludeExpiredpara análises históricas (default: só futuros).chainaceita filtros por strike e side que reduzem o payload — útil para apps que renderizam apenas uma janela de strikes ATM.
Passo 2: Cadeia Bruta
Vamos buscar a cadeia completa para PETR4 no próximo vencimento:
from brapi_options import get_chain, get_expirations
def main() -> None:
underlying = "PETR4"
# 1) Próximo vencimento
expirations = get_expirations(underlying)
next_exp = expirations[0]
print(f"Próximo vencimento: {next_exp}")
# 2) Cadeia completa
chain = get_chain(underlying, next_exp)
series = chain["series"]
print(f"Total de séries negociadas: {len(series)}")
print(f"Data de referência: {chain['date']}")
if __name__ == "__main__":
main()Saída:
Próximo vencimento: 2026-05-15
Total de séries negociadas: 47
Data de referência: 2026-05-08A resposta de cada série tem o seguinte shape:
{
"symbol": "PETRE300", // Código da série
"underlyingSymbol": "PETR4",
"side": "call", // call ou put
"market": "equity",
"strike": 30.12, // Strike em R$
"expirationDate": "2026-05-15",
"firstTradeDate": "2025-11-19",
"lastTradeDate": "2026-05-08",
"date": 1746662400, // Timestamp UNIX do fechamento
"open": 1.70,
"high": 2.13,
"low": 1.52,
"average": 1.77,
"close": 2.06,
"bid": 2.03, // Melhor oferta de compra
"ask": 2.09, // Melhor oferta de venda
"trades": 1542,
"volume": 2841600, // Quantidade negociada
"financialVolume": 5029632.0 // Volume em R$
}Passo 3: Cálculo de Moneyness
Para qualquer análise séria, você precisa do preço atual do ativo subjacente para classificar opções em ITM (in-the-money), ATM (at-the-money) ou OTM (out-of-the-money).
import pandas as pd
import requests
def get_underlying_price(underlying: str) -> float:
"""Pega o último preço do subjacente via /api/quote."""
token = __import__("os").getenv("BRAPI_TOKEN", "")
headers = {"Authorization": f"Bearer {token}"} if token else {}
resp = requests.get(
f"https://brapi.dev/api/quote/{underlying}",
headers=headers,
timeout=30,
)
resp.raise_for_status()
return float(resp.json()["results"][0]["regularMarketPrice"])
def classify_moneyness(strike: float, spot: float, side: str) -> str:
"""Classifica a opção em ITM, ATM ou OTM."""
diff_pct = (strike - spot) / spot
# Janela de ATM: ±1% do strike
if abs(diff_pct) <= 0.01:
return "ATM"
if side == "call":
return "ITM" if strike < spot else "OTM"
else: # put
return "ITM" if strike > spot else "OTM"
def liquidity_tier(trades: int) -> str:
"""Classifica liquidez baseado em número de negócios."""
if trades >= 500:
return "alta"
if trades >= 100:
return "média"
if trades >= 20:
return "baixa"
return "ilíquida"
def chain_to_dataframe(series_list: list[dict], spot: float) -> pd.DataFrame:
"""Converte a lista de séries em DataFrame pronto para análise."""
df = pd.DataFrame(series_list)
df["moneyness"] = df.apply(
lambda row: classify_moneyness(row["strike"], spot, row["side"]),
axis=1,
)
df["liquidity"] = df["trades"].apply(liquidity_tier)
# Spread bid-ask em % do mid (proxy de liquidez)
df["mid"] = (df["bid"] + df["ask"]) / 2
df["spread_pct"] = ((df["ask"] - df["bid"]) / df["mid"]) * 100
return dfPasso 4: Visualização Lado a Lado
A representação clássica é calls à esquerda, puts à direita, strikes no meio:
import pandas as pd
from tabulate import tabulate
def render_side_by_side(df: pd.DataFrame, spot: float) -> str:
"""Renderiza a cadeia com calls e puts lado a lado, ordenadas por strike."""
calls = df[df["side"] == "call"].set_index("strike")
puts = df[df["side"] == "put"].set_index("strike")
all_strikes = sorted(set(calls.index) | set(puts.index))
rows = []
for strike in all_strikes:
atm_marker = "←" if abs(strike - spot) <= 0.01 * spot else ""
call_data = calls.loc[strike] if strike in calls.index else None
put_data = puts.loc[strike] if strike in puts.index else None
row = [
f"{call_data['bid']:.2f}" if call_data is not None and pd.notna(call_data['bid']) else "—",
f"{call_data['ask']:.2f}" if call_data is not None and pd.notna(call_data['ask']) else "—",
f"{int(call_data['trades']):,}" if call_data is not None and pd.notna(call_data['trades']) else "—",
f"{strike:.2f} {atm_marker}",
f"{put_data['bid']:.2f}" if put_data is not None and pd.notna(put_data['bid']) else "—",
f"{put_data['ask']:.2f}" if put_data is not None and pd.notna(put_data['ask']) else "—",
f"{int(put_data['trades']):,}" if put_data is not None and pd.notna(put_data['trades']) else "—",
]
rows.append(row)
headers = [
"Call Bid", "Call Ask", "Call Trades",
"Strike",
"Put Bid", "Put Ask", "Put Trades",
]
return tabulate(rows, headers=headers, tablefmt="github", colalign=("right", "right", "right", "center", "right", "right", "right"))Pipeline final:
from brapi_options import get_chain, get_expirations
from filters import chain_to_dataframe, get_underlying_price
from chain_view import render_side_by_side
def main() -> None:
underlying = "PETR4"
# 1) Próximo vencimento
expirations = get_expirations(underlying)
next_exp = expirations[0]
print(f"Próximo vencimento: {next_exp}\n")
# 2) Cadeia + spot
chain = get_chain(underlying, next_exp)
spot = get_underlying_price(underlying)
print(f"Spot {underlying}: R$ {spot:.2f}\n")
# 3) DataFrame com moneyness e liquidez
df = chain_to_dataframe(chain["series"], spot)
# 4) Filtra strikes próximos do dinheiro (±15%)
band = 0.15
df_filtered = df[
(df["strike"] >= spot * (1 - band))
& (df["strike"] <= spot * (1 + band))
]
# 5) Renderiza lado a lado
print(render_side_by_side(df_filtered, spot))
if __name__ == "__main__":
main()Saída esperada:
Próximo vencimento: 2026-05-15
Spot PETR4: R$ 33.18
| Call Bid | Call Ask | Call Trades | Strike | Put Bid | Put Ask | Put Trades |
|---------:|---------:|------------:|:--------:|--------:|--------:|-----------:|
| 4.20 | 4.32 | 1,892 | 29.00 | 0.10 | 0.12 | 425 |
| 3.10 | 3.18 | 2,210 | 30.00 | 0.21 | 0.24 | 802 |
| 2.10 | 2.16 | 3,456 | 31.00 | 0.45 | 0.49 | 1,124 |
| 1.20 | 1.24 | 4,891 | 32.00 | 0.94 | 0.98 | 2,345 |
| 0.50 | 0.52 | 5,612 | 33.00 ← | 1.81 | 1.85 | 3,108 |
| 0.16 | 0.18 | 3,201 | 34.00 | 3.10 | 3.18 | 1,890 |
| 0.04 | 0.06 | 1,055 | 35.00 | 4.50 | 4.62 | 920 |
| 0.01 | 0.03 | 421 | 36.00 | 5.85 | 5.95 | 310 |A seta ← marca o strike ATM. Em ~50 linhas de código, você reproduziu o que apps profissionais cobram para ver.
Passo 5: Filtros Estratégicos
Agora que temos o DataFrame, podemos extrair insights práticos.
A) Calls OTM Líquidas para Covered Call
def covered_call_candidates(df: pd.DataFrame, spot: float) -> pd.DataFrame:
"""Calls 5%-15% OTM com liquidez decente."""
calls = df[df["side"] == "call"]
return calls[
(calls["strike"] >= spot * 1.05)
& (calls["strike"] <= spot * 1.15)
& (calls["trades"] >= 100) # Líquidas
& (calls["spread_pct"] <= 5) # Spread bid-ask < 5% do mid
].sort_values("strike")B) Puts ATM para Hedge
def hedge_put_candidates(df: pd.DataFrame, spot: float) -> pd.DataFrame:
"""Puts próximas do dinheiro (±5%) com volume."""
puts = df[df["side"] == "put"]
return puts[
(puts["strike"] >= spot * 0.95)
& (puts["strike"] <= spot * 1.05)
& (puts["trades"] >= 200)
].sort_values("strike")C) Detecção de Arbitragem Bid-Ask
Spreads bid-ask muito largos podem revelar oportunidades pontuais — ou apenas indicar série ilíquida. Vale conferir manualmente:
def wide_spreads(df: pd.DataFrame, threshold_pct: float = 10.0) -> pd.DataFrame:
"""Séries com spread bid-ask acima de threshold_pct."""
return df[df["spread_pct"] >= threshold_pct].sort_values("spread_pct", ascending=False)D) Resumo por Moneyness
def moneyness_summary(df: pd.DataFrame) -> pd.DataFrame:
return df.groupby(["side", "moneyness"]).agg(
total_series=("symbol", "count"),
avg_trades=("trades", "mean"),
avg_volume=("volume", "mean"),
avg_spread_pct=("spread_pct", "mean"),
).round(2)Saída exemplo:
total_series avg_trades avg_volume avg_spread_pct
side moneyness
call ATM 2 4_120 2_890_000 1.8
ITM 6 1_580 1_450_000 3.1
OTM 12 2_340 1_850_000 4.5
put ATM 2 3_210 1_910_000 2.0
ITM 6 890 720_000 6.2
OTM 11 1_650 1_240_000 5.8ITM puts têm spread pior do que ATM — informação útil para escolher onde alocar.
Passo 6: Exportando para Excel
Para análise visual ou compartilhamento com o time:
def export_chain_excel(df: pd.DataFrame, spot: float, output_path: str) -> None:
calls = df[df["side"] == "call"].set_index("strike")
puts = df[df["side"] == "put"].set_index("strike")
cols = ["bid", "ask", "close", "trades", "volume", "moneyness", "liquidity", "spread_pct"]
calls_view = calls[cols].add_prefix("call_")
puts_view = puts[cols].add_prefix("put_")
combined = calls_view.join(puts_view, how="outer").sort_index()
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
combined.to_excel(writer, sheet_name=f"Chain (spot {spot:.2f})")
print(f"Exportado para {output_path}")Passo 7: Acompanhando Múltiplos Vencimentos
Quer comparar o mesmo strike em vencimentos diferentes? (Útil para análise de estrutura a termo da volatilidade.)
def compare_expirations(
underlying: str,
strike: float,
side: str = "call",
) -> pd.DataFrame:
"""Pega o mesmo strike em todos os vencimentos disponíveis."""
expirations = get_expirations(underlying)
rows = []
for exp in expirations[:6]: # próximos 6 vencimentos
chain = get_chain(
underlying=underlying,
expiration_date=exp,
side=side,
min_strike=strike - 0.01,
max_strike=strike + 0.01,
)
for s in chain["series"]:
rows.append({
"expirationDate": exp,
"symbol": s["symbol"],
"strike": s["strike"],
"close": s["close"],
"bid": s["bid"],
"ask": s["ask"],
"trades": s["trades"],
})
return pd.DataFrame(rows)Útil para identificar:
- Vencimentos com prêmios "esquecidos" (mispriced)
- Curvas de prêmio decrescentes (theta acelerado)
- Liquidez relativa entre vencimentos curtos vs longos
Cuidados Importantes
Dados são EOD
A API entrega dados de fim de pregão (após ~19h BRT). Não use bid/ask como proxy de mercado em tempo real durante o pregão — só servem para decisões pós-fechamento ou planejamento da próxima abertura.
Sem gregas (delta, gamma, vega)
A API entrega OHLCV, bid/ask, trades e volume. Não há cálculo de gregas
ou volatilidade implícita. Para essas métricas, calcule você mesmo via
Black-Scholes-Merton com mibian, QuantLib ou py_vollib e o histórico
da série.
Símbolo da série
O ticker (ex.: PETRE370) tem padrão da bolsa: {ATIVO}{LETRA_MÊS}{ID_STRIKE}.
A letra do mês codifica o tipo (A-L = call, M-X = put). O ID_STRIKE não
é o valor em reais — sempre use o campo strike da resposta para o valor
monetário. Mais detalhes em /docs/opcoes.
Indo Além
A partir desse esqueleto, você pode evoluir para:
- Calculadora de gregas com Black-Scholes (delta, gamma, vega, theta)
- Volatilidade implícita invertendo o BSM com Brent ou Newton-Raphson
- Volatility smile plotando IV vs strike para um mesmo vencimento
- Backtest de estratégias (covered call, cash-secured put, spreads) — veja o tutorial dedicado de Backtesting de Opções com Python
Conclusão
A cadeia de opções é a lente pela qual qualquer trader de derivativos olha o mercado. Tendo a cadeia em Python, você passa a ter:
- Visualização customizável (sem depender de home broker)
- Filtros automáticos por moneyness, liquidez e spread
- Histórico estruturado para análises e backtests
- Comparação entre vencimentos sem dor
Em 200 linhas de Python e sem custos adicionais, você reproduz funcionalidades de plataformas que cobram caro pelo mesmo dado público.
Próximos passos: se você está começando, leia primeiro o guia de opções para iniciantes. Se já entende a mecânica, pule direto para o tutorial de Backtesting de Opções. E se quer gerar renda extra com o que já tem em carteira, veja o guia da Covered Call.
Disclaimer: opções são derivativos com risco de perda total ou superior ao capital investido. Estude antes de operar.
