---
title: "Lab 3: Séries Financeiras e Modelos GARCH"
subtitle: "Modelando volatilidade com dados reais da B3"
execute:
eval: false
---
::: {.objetivos}
#### Objetivos do Lab
- [Aplicar]{.bloom-badge .bloom-aplicar} download e tratamento de dados financeiros com yfinance
- [Analisar]{.bloom-badge .bloom-analisar} fatos estilizados em ativos brasileiros
- [Criar]{.bloom-badge .bloom-criar} modelos GARCH e prever volatilidade
:::
## Setup
```{python}
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from arch import arch_model
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
plt.rcParams.update({
'figure.figsize': (12, 5),
'axes.spines.top': False,
'axes.spines.right': False,
'font.size': 12
})
INSPER_RED = '#E50505'
INSPER_TURQUESA = '#3ACC9F'
INSPER_AMARELO = '#FFCC00'
INSPER_GRAY = '#5B5B5B'
```
## Parte 1: Baixando Dados do Yahoo Finance
```{python}
# Definir ativos e período
ativos = ['PETR4.SA', 'VALE3.SA', 'ITUB4.SA', 'WEGE3.SA', '^BVSP']
nomes = {
'PETR4.SA': 'Petrobras', 'VALE3.SA': 'Vale',
'ITUB4.SA': 'Itaú', 'WEGE3.SA': 'WEG', '^BVSP': 'Ibovespa'
}
start_date = '2015-01-01'
# Download
data = yf.download(ativos, start=start_date, auto_adjust=False)['Adj Close']
data = data.dropna()
print(f"Período: {data.index.min().date()} a {data.index.max().date()}")
print(f"Observações: {len(data)}")
data.head()
```
```{python}
# Preços ajustados normalizados (base 100)
precos_norm = (data / data.iloc[0]) * 100
fig = px.line(
precos_norm, y=precos_norm.columns,
title='Preços Normalizados (Base 100)',
labels={'value': 'Índice', 'variable': 'Ativo'},
template='plotly_white'
)
fig.update_layout(font_family='Inter', hovermode='x unified')
fig.show()
```
## Parte 2: Log-Retornos
```{python}
# Calcular log-retornos
retornos = np.log(data / data.shift(1)).dropna()
# Estatísticas descritivas
desc = retornos.describe().T
desc['skewness'] = retornos.skew()
desc['kurtosis'] = retornos.kurtosis() # excesso de curtose
desc['jarque_bera_p'] = [stats.jarque_bera(retornos[col])[1] for col in retornos.columns]
desc[['mean', 'std', 'skewness', 'kurtosis', 'jarque_bera_p']].round(4)
```
::: {.callout-note}
## Interprete a Tabela
- **Curtose > 0**: caudas mais pesadas que a normal (esperado!)
- **Assimetria < 0**: cauda esquerda mais pesada (quedas mais extremas)
- **Jarque-Bera p < 0.05**: rejeita normalidade
:::
```{python}
# Retornos ao longo do tempo
fig, axes = plt.subplots(len(ativos), 1, figsize=(14, 3*len(ativos)), sharex=True)
for i, ativo in enumerate(ativos):
axes[i].plot(retornos[ativo], color=INSPER_GRAY, linewidth=0.3, alpha=0.7)
axes[i].set_ylabel(nomes.get(ativo, ativo))
axes[i].axhline(y=0, color=INSPER_RED, linestyle='-', linewidth=0.5)
# Destacar períodos de alta volatilidade
vol_rolling = retornos[ativo].rolling(21).std() * np.sqrt(252)
high_vol = vol_rolling > vol_rolling.quantile(0.9)
axes[i].fill_between(retornos.index, retornos[ativo].min(), retornos[ativo].max(),
where=high_vol, alpha=0.15, color=INSPER_RED)
axes[0].set_title('Log-Retornos Diários (áreas vermelhas = alta volatilidade)', fontweight='bold')
plt.tight_layout()
plt.show()
```
## Parte 3: Verificando os Fatos Estilizados
### Caudas Pesadas
```{python}
# QQ-Plot: Normal vs. t de Student
ativo_foco = 'PETR4.SA'
ret = retornos[ativo_foco].values
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# QQ-Plot Normal
stats.probplot(ret, dist="norm", plot=axes[0])
axes[0].set_title(f'{nomes[ativo_foco]} — QQ-Plot Normal', fontweight='bold')
axes[0].get_lines()[0].set_color(INSPER_RED)
# QQ-Plot t-Student
stats.probplot(ret, dist="t", sparams=(4,), plot=axes[1])
axes[1].set_title(f'{nomes[ativo_foco]} — QQ-Plot t-Student (df=4)', fontweight='bold')
axes[1].get_lines()[0].set_color(INSPER_TURQUESA)
plt.tight_layout()
plt.show()
```
### Clusters de Volatilidade
```{python}
# ACF dos retornos vs. retornos ao quadrado
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
plot_acf(retornos[ativo_foco].dropna(), ax=axes[0,0], lags=40, zero=False,
title=f'ACF — Retornos {nomes[ativo_foco]}')
plot_acf(retornos[ativo_foco].dropna()**2, ax=axes[0,1], lags=40, zero=False,
title=f'ACF — Retornos ao Quadrado {nomes[ativo_foco]}')
plot_pacf(retornos[ativo_foco].dropna(), ax=axes[1,0], lags=40, zero=False,
title=f'PACF — Retornos')
plot_pacf(retornos[ativo_foco].dropna()**2, ax=axes[1,1], lags=40, zero=False,
title=f'PACF — Retornos ao Quadrado')
plt.tight_layout()
plt.show()
```
::: {.callout-caution collapse="true"}
## Exercício: Interpretação
1. **Os retornos têm autocorrelação significativa? E os retornos ao quadrado?** Os retornos simples geralmente não têm autocorrelação significativa (mercado eficiente). Mas os retornos ao quadrado têm autocorrelação forte e persistente — isso indica clusters de volatilidade.
2. **O que a ACF dos retornos ao quadrado está nos dizendo?** Que a **variância** (volatilidade) muda ao longo do tempo de forma previsível. Períodos de alta volatilidade tendem a ser seguidos por mais alta volatilidade — exatamente o que os modelos GARCH capturam.
3. **O QQ-Plot com distribuição t se ajusta melhor?** Sim — a distribuição t de Student com graus de liberdade baixos (4–8) se ajusta melhor às caudas dos retornos financeiros. Isso implica que devemos usar distribuição t (não normal) ao estimar modelos GARCH.
:::
## Parte 4: Ajustando Modelos GARCH
```{python}
# GARCH(1,1) com distribuição t de Student
ret_pct = retornos[ativo_foco] * 100 # arch trabalha com retornos percentuais
# Modelo base: GARCH(1,1)
modelo_garch = arch_model(ret_pct, vol='Garch', p=1, q=1, dist='t')
resultado_garch = modelo_garch.fit(disp='off')
print(resultado_garch.summary())
```
```{python}
# Extrair volatilidade condicional
vol_condicional = resultado_garch.conditional_volatility / 100 # voltar para decimal
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
subplot_titles=[f'Retornos — {nomes[ativo_foco]}',
'Volatilidade Condicional (GARCH)'])
fig.add_trace(go.Scatter(x=retornos.index, y=retornos[ativo_foco],
mode='lines', line=dict(color=INSPER_GRAY, width=0.5),
name='Retornos'), row=1, col=1)
fig.add_trace(go.Scatter(x=retornos.index, y=vol_condicional,
mode='lines', line=dict(color=INSPER_RED, width=1),
name='Volatilidade GARCH'), row=2, col=1)
fig.update_layout(height=600, template='plotly_white', font_family='Inter',
showlegend=False)
fig.show()
```
### Comparando Variantes
```{python}
# Grid de modelos
modelos = {
'GARCH(1,1)': {'vol': 'Garch', 'p': 1, 'q': 1},
'GARCH(2,1)': {'vol': 'Garch', 'p': 2, 'q': 1},
'EGARCH(1,1)': {'vol': 'EGARCH', 'p': 1, 'q': 1},
'GJR-GARCH(1,1)': {'vol': 'Garch', 'p': 1, 'o': 1, 'q': 1},
}
resultados = {}
for nome, params in modelos.items():
try:
modelo = arch_model(ret_pct, dist='t', **params)
fit = modelo.fit(disp='off')
resultados[nome] = {
'AIC': fit.aic,
'BIC': fit.bic,
'Log-Lik': fit.loglikelihood,
}
except Exception as e:
print(f"Erro em {nome}: {e}")
pd.DataFrame(resultados).T.sort_values('AIC')
```
::: {.callout-tip}
## Escolhendo o Melhor Modelo
Compare pelo **AIC** ou **BIC**. Se a diferença for pequena (< 2), prefira o modelo mais simples. Verifique também se o **efeito alavancagem** é significativo (EGARCH/GJR).
:::
## Parte 5: Previsão de Volatilidade
```{python}
# Previsão h passos à frente
h = 10 # dias
forecast = resultado_garch.forecast(horizon=h, reindex=False)
vol_futura = np.sqrt(forecast.variance.values[-1, :]) / 100
# Visualizar
fig, ax = plt.subplots(figsize=(10, 4))
# Histórico recente
hist_vol = vol_condicional[-60:]
ax.plot(range(len(hist_vol)), hist_vol.values, color=INSPER_GRAY, label='Histórico')
# Previsão
ax.plot(range(len(hist_vol), len(hist_vol) + h), vol_futura,
color=INSPER_RED, linewidth=2, marker='o', label='Previsão GARCH')
ax.axhline(y=vol_condicional.mean(), color=INSPER_TURQUESA,
linestyle='--', label='Média histórica', alpha=0.7)
ax.set_title(f'Previsão de Volatilidade — {nomes[ativo_foco]}', fontweight='bold')
ax.set_xlabel('Dias')
ax.set_ylabel('Volatilidade')
ax.legend()
plt.tight_layout()
plt.show()
```
## Exercício Final
::: {.callout-important}
## Desafio
Escolha **3 ativos** da B3 que sejam relevantes para sua área:
1. Baixe os dados e calcule log-retornos
2. Verifique os fatos estilizados (normalidade, ACF dos retornos ao quadrado)
3. Ajuste GARCH(1,1) com distribuição t para cada ativo
4. Compare pelo menos 2 variantes (GARCH vs. EGARCH ou GJR)
5. Faça previsão de volatilidade para os próximos 5 dias
6. **Interprete**: qual ativo apresenta maior persistência de volatilidade? Qual tem efeito alavancagem mais forte?
:::