Lab 3: Séries Financeiras e Modelos GARCH

Modelando volatilidade com dados reais da B3

Objetivos do Lab

  • Aplicar download e tratamento de dados financeiros com yfinance
  • Analisar fatos estilizados em ativos brasileiros
  • Criar modelos GARCH e prever volatilidade

Setup

Código
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

Código
# 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()
Código
# 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

Código
# 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)
NotaInterprete 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
Código
# 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

Código
# 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

Código
# 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()
  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

Código
# 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())
Código
# 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

Código
# 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')
DicaEscolhendo 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

Código
# 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

ImportanteDesafio

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?
De volta ao topo