Lab 4: Otimização de Portfólios

Da teoria à prática: montando e avaliando carteiras

Objetivos do Lab

  • Aplicar cálculo de retorno esperado e matriz de covariância
  • Criar a fronteira eficiente com otimização numérica
  • Avaliar performance de portfólios contra benchmarks

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 scipy.optimize import minimize
from scipy import stats

import warnings
warnings.filterwarnings('ignore')

INSPER_RED = '#E50505'
INSPER_TURQUESA = '#3ACC9F'
INSPER_AMARELO = '#FFCC00'
INSPER_ROXO = '#730D9F'
INSPER_GRAY = '#5B5B5B'

Parte 1: Dados e Retornos

Código
# Ativos para o portfólio
ativos = ['PETR4.SA', 'VALE3.SA', 'ITUB4.SA', 'WEGE3.SA',
          'ABEV3.SA', 'RENT3.SA', 'BBAS3.SA', 'SUZB3.SA']
benchmark = '^BVSP'

start_date = '2018-01-01'

# Download
todos = ativos + [benchmark]
precos = yf.download(todos, start=start_date, auto_adjust=False)['Adj Close'].dropna()

# Separar benchmark
bench_precos = precos[benchmark]
precos = precos[ativos]

# Log-retornos diários
retornos = np.log(precos / precos.shift(1)).dropna()
ret_bench = np.log(bench_precos / bench_precos.shift(1)).dropna()

# Retorno e risco anualizados
ret_anual = retornos.mean() * 252
risco_anual = retornos.std() * np.sqrt(252)

# Tabela resumo
resumo = pd.DataFrame({
    'Retorno Anual (%)': ret_anual * 100,
    'Risco Anual (%)': risco_anual * 100,
    'Sharpe': ret_anual / risco_anual
}).round(3)

resumo.sort_values('Sharpe', ascending=False)
Código
# Scatter retorno vs. risco
fig = px.scatter(
    resumo, x='Risco Anual (%)', y='Retorno Anual (%)',
    text=resumo.index.str.replace('.SA', ''),
    title='Retorno vs. Risco — Ativos Individuais',
    template='plotly_white'
)
fig.update_traces(textposition='top center', marker=dict(size=12, color=INSPER_RED))
fig.update_layout(font_family='Inter')
fig.show()

Parte 2: Matriz de Correlação e Covariância

Código
# Correlação
corr = retornos.corr()

fig = px.imshow(
    corr,
    text_auto='.2f',
    color_continuous_scale=['#3ACC9F', '#FFFFFF', '#E50505'],
    title='Matriz de Correlação',
    template='plotly_white'
)
fig.update_layout(font_family='Inter')
fig.show()
Código
# Covariância anualizada
cov_anual = retornos.cov() * 252

Parte 3: Fronteira Eficiente

Código
n_ativos = len(ativos)
mu = ret_anual.values
sigma = cov_anual.values

def portfolio_stats(weights, mu, sigma):
    """Retorna retorno e risco de um portfólio"""
    ret = weights @ mu
    risk = np.sqrt(weights @ sigma @ weights)
    return ret, risk

def neg_sharpe(weights, mu, sigma, rf=0.10):
    """Sharpe negativo (para minimização)"""
    ret, risk = portfolio_stats(weights, mu, sigma)
    return -(ret - rf) / risk

def portfolio_variance(weights, sigma):
    """Variância do portfólio (para minimização)"""
    return weights @ sigma @ weights

# Restrições
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]  # soma = 1
bounds = [(0, 0.30)] * n_ativos  # máx 30% por ativo

# Portfólio de mínima variância
w0 = np.ones(n_ativos) / n_ativos
res_minvar = minimize(portfolio_variance, w0, args=(sigma,),
                       method='SLSQP', bounds=bounds, constraints=constraints)

# Portfólio de máximo Sharpe
res_sharpe = minimize(neg_sharpe, w0, args=(mu, sigma, 0.10),
                       method='SLSQP', bounds=bounds, constraints=constraints)

print("Portfólio Mínima Variância:")
for ativo, peso in zip(ativos, res_minvar.x):
    if peso > 0.01:
        print(f"  {ativo}: {peso:.1%}")

print(f"\nPortfólio Máximo Sharpe:")
for ativo, peso in zip(ativos, res_sharpe.x):
    if peso > 0.01:
        print(f"  {ativo}: {peso:.1%}")
Código
# Gerar fronteira eficiente
n_portfolios = 100
retornos_alvo = np.linspace(mu.min(), mu.max(), n_portfolios)
riscos_fronteira = []

for ret_alvo in retornos_alvo:
    constraints_ef = [
        {'type': 'eq', 'fun': lambda w: np.sum(w) - 1},
        {'type': 'eq', 'fun': lambda w, r=ret_alvo: w @ mu - r}
    ]
    res = minimize(portfolio_variance, w0, args=(sigma,),
                    method='SLSQP', bounds=bounds, constraints=constraints_ef)
    if res.success:
        riscos_fronteira.append(np.sqrt(res.fun))
    else:
        riscos_fronteira.append(np.nan)

# Portfólios aleatórios (para contexto visual)
n_random = 5000
ret_random, risk_random = [], []
for _ in range(n_random):
    w = np.random.dirichlet(np.ones(n_ativos))
    ret, risk = portfolio_stats(w, mu, sigma)
    ret_random.append(ret)
    risk_random.append(risk)

# Plot
fig = go.Figure()

# Random portfolios
fig.add_trace(go.Scatter(x=np.array(risk_random)*100, y=np.array(ret_random)*100,
                          mode='markers', marker=dict(size=2, color=INSPER_GRAY, opacity=0.3),
                          name='Portfólios Aleatórios'))

# Fronteira eficiente
fig.add_trace(go.Scatter(x=np.array(riscos_fronteira)*100, y=retornos_alvo*100,
                          mode='lines', line=dict(color=INSPER_RED, width=3),
                          name='Fronteira Eficiente'))

# Portfólios especiais
r_mv, s_mv = portfolio_stats(res_minvar.x, mu, sigma)
r_ms, s_ms = portfolio_stats(res_sharpe.x, mu, sigma)

fig.add_trace(go.Scatter(x=[s_mv*100], y=[r_mv*100], mode='markers',
                          marker=dict(size=15, color=INSPER_TURQUESA, symbol='star'),
                          name='Mín. Variância'))
fig.add_trace(go.Scatter(x=[s_ms*100], y=[r_ms*100], mode='markers',
                          marker=dict(size=15, color=INSPER_AMARELO, symbol='star'),
                          name='Máx. Sharpe'))

# Ativos individuais
fig.add_trace(go.Scatter(x=risco_anual.values*100, y=ret_anual.values*100,
                          mode='markers+text', text=[a.replace('.SA','') for a in ativos],
                          textposition='top center',
                          marker=dict(size=10, color=INSPER_ROXO),
                          name='Ativos Individuais'))

fig.update_layout(
    title='Fronteira Eficiente de Markowitz',
    xaxis_title='Risco Anual (%)', yaxis_title='Retorno Anual (%)',
    template='plotly_white', font_family='Inter',
    legend=dict(yanchor='top', y=0.99, xanchor='left', x=0.01)
)
fig.show()

Parte 4: Backtest

Código
# Split temporal: treino até 2023, teste 2024+
split_date = '2024-01-01'
ret_treino = retornos.loc[:split_date]
ret_teste = retornos.loc[split_date:]

# Otimizar no treino
mu_train = ret_treino.mean().values * 252
sigma_train = ret_treino.cov().values * 252

res_bt = minimize(neg_sharpe, w0, args=(mu_train, sigma_train, 0.10),
                   method='SLSQP', bounds=bounds, constraints=constraints)
pesos_otimos = res_bt.x

# Performance no teste
ret_portfolio_teste = (ret_teste * pesos_otimos).sum(axis=1)
ret_bench_teste = ret_bench.loc[split_date:]

# Alinhamento
idx_comum = ret_portfolio_teste.index.intersection(ret_bench_teste.index)
ret_portfolio_teste = ret_portfolio_teste.loc[idx_comum]
ret_bench_teste = ret_bench_teste.loc[idx_comum]

# Retorno acumulado
cum_portfolio = (1 + ret_portfolio_teste).cumprod()
cum_bench = (1 + ret_bench_teste).cumprod()

fig = go.Figure()
fig.add_trace(go.Scatter(x=cum_portfolio.index, y=cum_portfolio.values,
                          mode='lines', name='Portfólio Ótimo',
                          line=dict(color=INSPER_RED, width=2)))
fig.add_trace(go.Scatter(x=cum_bench.index, y=cum_bench.values,
                          mode='lines', name='Ibovespa',
                          line=dict(color=INSPER_GRAY, width=2)))
fig.update_layout(
    title='Backtest: Portfólio Otimizado vs. Ibovespa',
    template='plotly_white', font_family='Inter',
    yaxis_title='Retorno Acumulado', hovermode='x unified'
)
fig.show()
Código
# Métricas de performance
def performance_metrics(returns, rf_daily=0.10/252):
    """Calcula métricas de performance"""
    ret_anual = returns.mean() * 252
    vol_anual = returns.std() * np.sqrt(252)
    sharpe = (ret_anual - 0.10) / vol_anual

    # Max drawdown
    cum = (1 + returns).cumprod()
    peak = cum.cummax()
    drawdown = (cum - peak) / peak
    max_dd = drawdown.min()

    return {
        'Retorno Anual (%)': f"{ret_anual*100:.2f}",
        'Volatilidade (%)': f"{vol_anual*100:.2f}",
        'Sharpe': f"{sharpe:.3f}",
        'Max Drawdown (%)': f"{max_dd*100:.2f}",
    }

metricas = pd.DataFrame({
    'Portfólio': performance_metrics(ret_portfolio_teste),
    'Ibovespa': performance_metrics(ret_bench_teste)
})
metricas

Competição de Portfólios

ImportanteDinâmica: Competição
  1. Forme duplas ou trios
  2. Escolha 5 a 10 ativos da B3
  3. Otimize o portfólio usando os dados até hoje
  4. Simule o retorno nos últimos 6 meses (backtest)
  5. Apresente em 5 min: quais ativos, por quê, qual performance

Critérios de avaliação: - Sharpe ratio (40%) - Justificativa das escolhas (40%) - Qualidade do diagnóstico (20%)

De volta ao topo