O Yahoo entrega a cadeia de hoje e nada do que veio antes. Estas quatro fontes dão o histórico, de graça, cada uma com um limite real que você precisa conhecer antes de construir em cima dela. Passos de cadastro, Python executável e o resultado que obtive ao rodar cada bloco em 7 de agosto de 2026.
O motivo pelo qual precisei desses dados, para começo de conversa. Trading de volatilidade e as gregas que vão além de delta e gamma, trabalhadas numa cadeia de opções real de NVDA, e não num exemplo de brinquedo. Cada número na tela saiu de uma cadeia real, que é exatamente o problema que as quatro fontes abaixo resolvem.
Noventa e quatro características das empresas alimentam uma rede neural com três camadas ocultas, treinada em janelas expansíveis para que a validação nunca veja o futuro. As previsões ranqueiam todas as ações, então o decil superior é comprado e o decil inferior vendido. A seleção do modelo usa o coeficiente de informação em vez do erro quadrático médio, que se sustenta melhor com poucos dados. O método vem de Gu, Kelly e Xiu (2020), Empirical Asset Pricing via Machine Learning, refeito aqui com dados gratuitos em vez das 30.000 ações e 920 variáveis do original. O último passo conecta a carteira à API da Interactive Brokers por meio de um script em Python, para que a estratégia envie suas próprias ordens long short em vez de apenas imprimir um backtest. Código e notebook estão no GitHub.
Dados de ações são um problema resolvido. Quer dez anos de fechamentos do SPY? Uma linha de yfinance e pronto. Opções não são. No momento em que você quer a cadeia como ela estava numa data passada, as ferramentas gratuitas que todo mundo usa simplesmente param de funcionar, e a maioria descobre isso no meio da construção de um backtest.
Este é o formato exato do problema. Isto roda sem erro e não devolve nada que sirva como histórico.
import yfinance as yf
spy = yf.Ticker("SPY")
print(len(spy.options), spy.options[:3])
# 29 ('2026-08-10', '2026-08-11', '2026-08-12')
chain = spy.option_chain(spy.options[2])
print(chain.calls.columns.tolist())
# ['contractSymbol', 'lastTradeDate', 'strike', 'lastPrice', 'bid', 'ask', 'change',
# 'percentChange', 'volume', 'openInterest', 'impliedVolatility', 'inTheMoney',
# 'contractSize', 'currency']
Vinte e nove vencimentos, sem gregas, com volatilidade implícita, e nenhum argumento em lugar algum que aceite uma data de cotação. O objeto é um retrato ao vivo do book neste instante. Pergunte de novo amanhã e o dia de hoje sumiu, para sempre, porque nada fica armazenado. É por isso que quem trabalha sério com opções ou paga um feed de fornecedor ou roda um cron job que fotografa a cadeia toda noite e espera um ano por uma amostra utilizável.
As quatro fontes abaixo pulam essa espera. Ordenei pelo atrito entre você e os dados no disco: a primeira não exige conta nenhuma e a última exige um checkout.
| Fonte | Cobertura | Gregas e VI | Conta | A pegadinha |
|---|---|---|---|---|
| DoltHub | 2.321 ativos dos EUA, de 2019 até ontem | sim | nenhuma | extração em massa exige o CLI do dolt |
| Cboe | VIX diário, de 1990 até ontem | não se aplica | nenhuma | só o índice, as cadeias ficam no DataShop |
| Kaggle | SPY EOD, de 2010 a 2023 | sim, pré-calculadas | gratuita, token de API | um ticker, para em 2023 |
| OptionsDX | 10 tickers, de 2010 a 2023, de EOD a minuto a minuto | sim | gratuita, depois checkout | um ano por pedido, algumas variantes pagas |
Dolt é um banco de dados SQL com a semântica do git. Branches, commits, diffs, tudo isso, em tabelas em vez de arquivos. O repo post-no-preference/options é um espelho do mercado de opções dos EUA mantido pela comunidade nesse motor, e é público. Há uma consequência importante escondida nessa frase. Como um repo Dolt fala SQL via HTTP, você consegue consultar o mercado de opções inteiro de um notebook sem instalar nada além de requests.
option_chain e volatility_history.master, não main. Aponte a API para main e toda consulta falha com branch not found, que é o motivo mais comum para as pessoas desistirem dessa fonte.https://www.dolthub.com/api/v1alpha1/post-no-preference/options/master?q=<your SQL>. Sem chave, sem header, sem conta.dolt e rode dolt clone post-no-preference/options. Depois disso, um dolt pull traz cada novo dia. Conte com dezenas de gigabytes.Este é o script que eu realmente guardaria. Ele encapsula a API numa função, puxa o vencimento de setembro do SPY como estava em 7 de agosto de 2026 e lê o skew de 25 delta direto das gregas retornadas. Cada coluna abaixo vem da fonte, nada aqui está sendo estimado.
import requests
import pandas as pd
API = "https://www.dolthub.com/api/v1alpha1/post-no-preference/options/master"
NUM = ["strike", "bid", "ask", "vol", "delta", "gamma", "vega"]
def dolt(sql: str) -> pd.DataFrame:
"""Run SQL against the public DoltHub repo. No auth, no rate limit key."""
r = requests.get(API, params={"q": sql}, timeout=60)
r.raise_for_status()
j = r.json()
if j["query_execution_status"] != "Success":
raise RuntimeError(j["query_execution_message"])
return pd.DataFrame(j["rows"])
def chain(symbol: str, date: str, expiry: str) -> pd.DataFrame:
df = dolt(f"""
SELECT date, expiration, strike, call_put, bid, ask, vol, delta, gamma, vega
FROM option_chain
WHERE act_symbol = '{symbol}' AND date = '{date}' AND expiration = '{expiry}'
ORDER BY strike, call_put
""")
df[NUM] = df[NUM].astype(float)
return df
df = chain("SPY", "2026-08-07", "2026-09-18")
calls, puts = df[df.call_put == "Call"], df[df.call_put == "Put"]
def nearest(frame, col, target):
return frame.loc[(frame[col] - target).abs().idxmin()]
atm = nearest(calls, "delta", 0.50)
c25 = nearest(calls, "delta", 0.25)
p25 = nearest(puts, "delta", -0.25)
print(f"{len(df)} quotes, {len(calls)} calls and {len(puts)} puts")
print(f"ATM strike {atm.strike:.0f} IV {atm.vol:.2%}")
print(f"25d call strike {c25.strike:.0f} IV {c25.vol:.2%}")
print(f"25d put strike {p25.strike:.0f} IV {p25.vol:.2%}")
print(f"25 delta skew: {(p25.vol - c25.vol) * 100:.2f} vol points")
| Ponto | Strike | Delta | Volatilidade implícita |
|---|---|---|---|
| Put 25 delta | 750 | -0.2626 | 15.22% |
| Call no dinheiro | 773 | 0.5254 | 13.38% |
| Call 25 delta | 796 | 0.2599 | 12.14% |
Três números e o smile já aparece. O strike de baixa carrega 3,08 pontos de vol a mais que o strike equivalente de alta, que é o skew de ações fazendo o que sempre faz: proteção via put demandada e alta via call ofertada. Você não instalou nada, não assinou nada, e pode rodar essa função num intervalo de datas para obter o skew como série temporal.
Agregações pesadas sobre a tabela inteira estouram o tempo no endpoint público. Um COUNT(DISTINCT act_symbol) sobre option_chain retorna context deadline exceeded, e isso é a API se protegendo, não um erro no seu SQL. Trabalhe por ativo e por data, do jeito que a chave primária está organizada, e fica rápido. Se precisar varrer o mercado inteiro, é para isso que serve o clone. A tabela mais leve volatility_history responde a essas perguntas, e é dela que vêm os números de cobertura desta página: 2.321 ativos distintos, data mais recente 7 de agosto de 2026.
A Cboe publica a série completa do VIX no próprio CDN como um CSV simples. É a bolsa que calcula o índice, então esse arquivo é a fonte primária, não uma redistribuição. Ele começa em 2 de janeiro de 1990 e é atualizado diariamente. Uma URL, sem conta, sem chave.
https://cdn.cboe.com/api/global/us_indices/daily_prices/VIX_History.csv. Abra uma vez no navegador para confirmar que são quatro colunas, DATE, OPEN, HIGH, LOW, CLOSE.VIX por outro ticker de índice nesse caminho para obter o histórico dele. O mesmo padrão serve para os outros índices de volatilidade da Cboe.A pergunta útil quase nunca é o nível do VIX. É o percentil, porque um VIX em 15 significa uma coisa em 2017 e outra depois de um choque. Este bloco responde isso em umas dez linhas, direto da URL, sem arquivo local.
import pandas as pd
URL = "https://cdn.cboe.com/api/global/us_indices/daily_prices/VIX_History.csv"
vix = pd.read_csv(URL, parse_dates=["DATE"]).set_index("DATE").sort_index()
close = vix["CLOSE"]
last = close.iloc[-1]
five = close[close.index >= close.index[-1] - pd.Timedelta(days=1825)]
print(f"{len(close)} sessions, {close.index[0]:%Y-%m-%d} to {close.index[-1]:%Y-%m-%d}")
print(f"last close {last:.2f}")
print(f"percentile, 1990+ {(close < last).mean():.1%}")
print(f"percentile, 5y {(five < last).mean():.1%}")
print(f"mean {close.mean():.2f}, median {close.median():.2f}")
print(f"sessions above 30 {(close > 30).mean():.2%}")
print(close.nlargest(5).round(2))
| Medida | Valor |
|---|---|
| Último fechamento | 14.90 |
| Percentil desde 1990 | 31.3% |
| Percentil, últimos 5 anos | 20.2% |
| Média, mediana | 19.44, 17.61 |
| Pregões fechando acima de 30 | 7.95% |
| Maior fechamento da história | 82,69 em 16 mar 2020 |
Dois percentis que discordam, e é exatamente por isso que vale calcular os dois. Contra o histórico completo de 36 anos, 14,90 fica no 31º percentil, nada de especial. Contra os últimos cinco anos, fica no 20º, discretamente baixo. As caudas de 2008 e 2020 puxam a distribuição da amostra longa para a direita, então a janela longa favorece o presente. E 7,95% de todos os pregões fecharam acima de 30, então um regime que a maioria considera raro é, na verdade, um pregão a cada treze.
Vale ter isso em mente quando você dimensiona uma operação vendida em vol com base em um backtest que por acaso começa em 2012.
O dataset é dudesurfin/spy-options-eod-volatility-surface-2010-2023, e todo o crédito a quem o montou, porque reunir catorze anos limpos de cadeias não é trabalho de fim de semana. SPY de fim de dia, um arquivo parquet por ano, cerca de 600 MB no total, com gregas e volatilidade implícita já calculadas por quem publicou. Foi com esse conjunto que treinei a rede de superfície de IV, e é o caminho mais rápido para uma superfície de volatilidade real que existe de graça.
O layout é largo, não longo. Uma linha por data de cotação, vencimento e strike, trazendo os dois lados ao mesmo tempo, com nomes de coluna entre colchetes que chegam como [C_IV], [P_IV], [UNDERLYING_LAST] e assim por diante. Tire os colchetes no carregamento, senão toda referência a coluna no seu código fica feia.
kaggle.json.~/.kaggle/kaggle.json no Linux ou macOS, ou em C:\Users\<you>\.kaggle\kaggle.json no Windows.pip install kagglehub pandas pyarrow. O pyarrow não é opcional, o pandas não lê parquet sem um engine.import kagglehub
path = kagglehub.dataset_download(
"dudesurfin/spy-options-eod-volatility-surface-2010-2023"
)
print(path) # local cache dir holding spy_eod_2010.parquet ... spy_eod_2023.parquet
# CLI equivalent, if you prefer:
# kaggle datasets download -d dudesurfin/spy-options-eod-volatility-surface-2010-2023
Você nem precisa mexer na API se preferir. Abra a página do dataset, clique em Download e recebe tudo em um único zip. Ou abra o Data Explorer à direita e baixe arquivos individuais, que é o que eu faria primeiro. São 14, de spy_eod_2010.parquet a spy_eod_2023.parquet, um por ano. Pegue um único ano, confira se as colunas são as que você espera e depois decida se quer os outros 13. Não faz sentido baixar 600 MB para descobrir que o schema não serve para você.
Qualquer gráfico de superfície vai impressionar aqui, então prefiro usar os dados para algo que você simplesmente não consegue fazer com um feed ao vivo. Fevereiro de 2018, a semana em que a venda de volatilidade explodiu e o XIV foi encerrado. Isto extrai a volatilidade implícita at the money nos quatro pregões em torno do evento, uma pergunta que só cadeias históricas respondem.
import pandas as pd
from pathlib import Path
df = pd.read_parquet(Path(path) / "spy_eod_2018.parquet")
df.columns = [c.strip().strip("[]") for c in df.columns] # the bracket tax
for c in ["UNDERLYING_LAST", "STRIKE", "DTE", "C_IV", "P_IV"]:
df[c] = pd.to_numeric(df[c], errors="coerce")
def atm_iv(day: str, lo: int, hi: int) -> pd.Series:
"""At the money implied vol for the expiry closest to the middle of [lo, hi] days."""
d = df[df.QUOTE_DATE.astype(str).str.startswith(day)].copy()
w = d[d.DTE.between(lo, hi)]
w = w[w.DTE == w.DTE.iloc[(w.DTE - (lo + hi) / 2).abs().argsort().iloc[0]]]
return w.loc[(w.STRIKE - w.UNDERLYING_LAST).abs().idxmin()]
for day in ["2018-02-02", "2018-02-05", "2018-02-06", "2018-02-09"]:
r30, r60 = atm_iv(day, 20, 40), atm_iv(day, 50, 70)
print(f"{day} spot {r30.UNDERLYING_LAST:7.2f} "
f"30d call IV {r30.C_IV:6.2%} 60d call IV {r60.C_IV:6.2%}")
| Pregão | Spot | IV call 30d | IV call 60d | IV put 30d |
|---|---|---|---|---|
| Sex 2 fev | 275.52 | 14.99% | 14.61% | 13.18% |
| Seg 5 fev | 263.93 | 33.94% | 30.23% | 18.31% |
| Ter 6 fev | 269.17 | 21.53% | 18.80% | 20.53% |
| Sex 9 fev | 261.70 | 22.25% | 19.66% | 21.84% |
A vol at the money de trinta dias foi de 15 para 34 em um pregão e devolveu a maior parte no dia seguinte, enquanto o ponto de sessenta dias se mexeu bem menos. É a estrutura a termo invertendo sob estresse e depois se recompondo, e é a imagem mais clara de por que volatilidade de prazo curto é um ativo diferente de volatilidade de prazo longo.
Agora olhe a última coluna na segunda-feira. A IV da call diz 33,94% e a IV da put diz 18,31% praticamente no mesmo strike. Isso não é uma visão de mercado, é um artefato dos dados. Num dia de tendência violenta, os dois lados de um snapshot de fim de dia são registrados contra um ativo-objeto que já se moveu, e a paridade put call quebra no arquivo. Tome isso como alerta permanente para qualquer fonte EOD gratuita: use o lado fora do dinheiro de cada strike, descarte cotações com spread relativo largo e nunca confie em uma única linha num dia em que o mercado abriu com gap.
A OptionsDX vende cadeias históricas e distribui uma parte de graça. São dez tickers listados, SPY, SPX, VIX, QQQ, TSLA, AAPL, NVDA, UVXY, SLV e BTC na Deribit, cada um cobrindo de 2010 a 2023, em cinco frequências de cotação, do fim de dia até minuto a minuto. A descrição do produto é específica sobre o conteúdo: todos os vencimentos e strikes, gregas, volatilidade implícita, bid, ask, último preço e o preço do ativo-objeto.
Duas coisas para deixar claro. Todo produto da loja tem preço em faixa começando em zero, então algumas combinações de ano e frequência são gratuitas e outras não. Confira o preço depois de escolher, não antes. E o pedido é um ano por vez, entregue em CSVs mensais, então catorze anos de SPY são catorze passagens separadas pelo checkout.
.txt, separados por vírgula, com cabeçalhos entre colchetes.Não fixe nomes de coluna no código contra um arquivo de fornecedor que você baixa de novo todo ano. Este loader normaliza qualquer cabeçalho que chegar, encontra as colunas de IV e gregas por padrão e transforma o formato largo em uma linha por cotação de opção, já com os filtros de liquidez aplicados. É o mesmo formato que uso no parquet do Kaggle, porque os dois chegam largos, com um bloco de call e um de put por strike.
import glob
import numpy as np
import pandas as pd
DTE_MIN, DTE_MAX = 7, 365 # skip 0 DTE noise and the sparse LEAPS tail
IV_MIN, IV_MAX = 0.03, 2.00 # kill negatives and the 40.0 blow-ups
MAX_REL_SPREAD = 0.50 # drop a quote if (ask - bid) / mid exceeds this
def load_optionsdx(pattern: str) -> pd.DataFrame:
"""Read every monthly file matching `pattern` into one long, filtered frame."""
frames = [pd.read_csv(f, skipinitialspace=True) for f in sorted(glob.glob(pattern))]
df = pd.concat(frames, ignore_index=True)
df.columns = [c.strip().strip("[]").upper() for c in df.columns]
base = pd.DataFrame({
"date": pd.to_datetime(df["QUOTE_DATE"]),
"expiry": pd.to_datetime(df["EXPIRE_DATE"]),
"dte": pd.to_numeric(df["DTE"], errors="coerce"),
"spot": pd.to_numeric(df["UNDERLYING_LAST"], errors="coerce"),
"strike": pd.to_numeric(df["STRIKE"], errors="coerce"),
})
out = []
for prefix, kind in (("C", "call"), ("P", "put")):
bid = pd.to_numeric(df[f"{prefix}_BID"], errors="coerce")
ask = pd.to_numeric(df[f"{prefix}_ASK"], errors="coerce")
mid = (bid + ask) / 2.0
side = base.assign(
type=kind,
mid=mid,
iv=pd.to_numeric(df[f"{prefix}_IV"], errors="coerce"),
delta=pd.to_numeric(df.get(f"{prefix}_DELTA"), errors="coerce"),
rel_spread=(ask - bid) / mid.replace(0, np.nan),
)
# keep only the out of the money side, where the IV is cleanest
otm = side.strike > side.spot if kind == "call" else side.strike < side.spot
out.append(side[otm])
q = pd.concat(out, ignore_index=True)
q = q[q.dte.between(DTE_MIN, DTE_MAX)
& q.iv.between(IV_MIN, IV_MAX)
& q.rel_spread.le(MAX_REL_SPREAD)
& q.mid.gt(0)]
q["log_m"] = np.log(q.strike / q.spot)
q["tau"] = q.dte / 365.0
return q.reset_index(drop=True)
quotes = load_optionsdx("spy_eod_2023*.txt")
print(quotes.shape)
print(quotes.groupby("type").iv.describe()[["count", "mean", "50%"]])
Para ver se ele se sustenta, rodei-o em um ano inteiro de SPY de fim de dia no mesmo layout largo, trocando read_csv por read_parquet e sem mudar mais nada. 548.099 cotações passam pelos filtros, e a divisão é o motivo para manter os dois lados em vez de tirar a média.
| Lado | Cotações | IV média | IV mediana |
|---|---|---|---|
| Calls, acima do spot | 181,880 | 13.89% | 13.58% |
| Puts, abaixo do spot | 366,219 | 26.79% | 24.00% |
Sobrevivem duas vezes mais cotações de puts do que de calls, e elas carregam cerca do dobro da volatilidade implícita. É o skew de novo, agora como população e não em três strikes. É por isso que um ajuste de superfície que ignora o tipo de opção vai subajustar o lado de baixa, onde está todo o risco interessante.
Quatro filtros concentram quase todo o valor aqui. Só fora do dinheiro, porque uma cotação muito dentro do dinheiro é quase toda valor intrínseco e sua volatilidade implícita é numericamente instável. Spread relativo limitado, porque uma cotação que ninguém negociaria não é um preço. Volatilidade implícita limitada, porque arquivos gratuitos têm linhas em que o solver falhou e deixou um 40,0 na coluna. Dias até o vencimento limitados, porque as linhas de zero DTE se comportam como outro instrumento.
Pule esses quatro e seu ajuste de superfície vai gastar a capacidade aprendendo o lixo, e não o smile. Já cometi esse erro, e o sinal é um modelo cujos piores erros ficam todos nas asas extremas.
| Se você quer | Use | Porque |
|---|---|---|
| Acompanhar o skew de um ativo ao longo do tempo | DoltHub | gregas na tabela, consulta por símbolo e data, sem instalação |
| Ajustar uma superfície de volatilidade hoje | Kaggle | catorze anos limpos, IV pré-calculada, um único download |
| Fazer screening do mercado inteiro | DoltHub clonado | 2.321 símbolos localmente, a API pública vai dar timeout |
| Estudar um ativo específico além do SPY | OptionsDX | dez tickers, e intradiário se a variante for gratuita |
| Contextualizar um nível de vol | Cboe | 36 anos de VIX direto da bolsa que o calcula |
| Pegar a cadeia de hoje | yfinance | ainda o snapshot mais rápido, só guarde você mesmo |
O resumo honesto é que nada disso substitui um feed pago. A cobertura para em 2023 em duas das quatro, o book de cripto é um mercado diferente das opções de ações, e ninguém vai te dar dados tick a tick de graça de um ativo à sua escolha. O que você ganha é o suficiente para montar uma superfície, medir um skew, testar uma ideia de prêmio de variância e descobrir se a estratégia vale a pena pagar por dados antes de pagar por dados.
Um hábito para copiar, seja qual for a fonte. Comece hoje um snapshot noturno das cadeias que importam para você, mesmo que nunca olhe para ele. Histórico de opções é o único dataset que você não consegue comprar de volta barato, e daqui a um ano o cron job que você configurar hoje à noite será a única amostra que ninguém mais tem.
A rede de superfície de volatilidade treinada no dataset do Kaggle: duas redes neurais aprendem a superfície de IV do SPY
As gregas que vivem nessas cadeias: vanna, volga, color, zomma e speed
Publico os modelos enquanto os construo, as mudanças que faço neles e os resultados que não funcionaram. Mais curtos que estes textos, e são muitos mais.
Siga-me no X