FernandezUNB's picture
Update app.py
e19d079 verified
import gradio as gr
import pandas as pd
import yfinance as yf
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px
from scipy.optimize import minimize
import warnings
import logging
from datetime import datetime, timedelta
# Configurar logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
warnings.filterwarnings("ignore")
# Taxa livre de risco (SELIC ~6% anual, 2025)
RISK_FREE_RATE = 0.06
# Lista de 30+ ativos comuns no Brasil (baseado em Ibovespa, liquidez e popularidade)
COMMON_ASSETS = [
"VALE3.SA", "PETR4.SA", "ITUB4.SA", "BBDC4.SA", "BBAS3.SA", "ABEV3.SA", "B3SA3.SA", "WEGE3.SA", "MGLU3.SA", "RENT3.SA",
"LREN3.SA", "RADL3.SA", "BRFS3.SA", "JBSS3.SA", "SUZB3.SA", "KLBN11.SA", "GGBR4.SA", "CSNA3.SA", "USIM5.SA", "EMBR3.SA",
"AZUL4.SA", "GOLL4.SA", "TAEE11.SA", "CPFE3.SA", "EQTL3.SA", "EGIE3.SA", "BPAC11.SA", "HAPV3.SA", "CIEL3.SA", "TOTS3.SA",
"^BVSP" # Benchmark sempre disponível
]
def validar_tickers(tickers):
"""Valida tickers usando intervalo recente (últimos 7 dias)"""
valid_tickers = []
today = datetime.now()
start_test = (today - timedelta(days=7)).strftime('%Y-%m-%d')
end_test = today.strftime('%Y-%m-%d')
for ticker in tickers:
try:
test_data = yf.download(ticker, start=start_test, end=end_test, progress=False)
if not test_data.empty and not test_data['Close'].isna().all():
valid_tickers.append(ticker)
else:
logger.warning(f"Ticker {ticker} sem dados recentes, ignorando")
except Exception as e:
logger.warning(f"Falha ao validar {ticker}: {e}")
return valid_tickers
def baixar_dados(tickers, start_date, end_date):
"""Baixa dados históricos com fallback para tickers válidos"""
try:
logger.info(f"Baixando dados para {tickers} de {start_date} a {end_date}")
dados_df = pd.DataFrame()
valid_tickers = validar_tickers(tickers)
if not valid_tickers:
raise ValueError("Nenhum ticker válido encontrado. Tente outros (ex.: VALE3.SA, PETR4.SA). Verifique conexão ou atualize yfinance.")
for ticker in valid_tickers:
data = yf.download(ticker, start=start_date, end=end_date, progress=False)['Close']
if data.empty or data.isna().all():
logger.warning(f"Dados inválidos para {ticker}, ignorando")
continue
dados_df[ticker] = data
dados_df = dados_df.dropna()
if dados_df.empty:
raise ValueError("Nenhum dado válido baixado. Verifique datas ou tickers (ex.: use intervalo recente como 2023-01-01 a 2024-07-31).")
logger.info(f"Dados baixados: {dados_df.shape}")
return dados_df
except Exception as e:
logger.error(f"Erro ao baixar dados: {e}")
raise
def calcular_retornos(dados):
"""Calcula retornos logarítmicos"""
retornos = np.log(dados / dados.shift(1)).dropna()
if retornos.empty:
raise ValueError("Nenhum retorno calculado. Dados insuficientes.")
return retornos
def calcular_metricas(retornos, precos, benchmark_retornos):
"""Calcula métricas avançadas"""
portfolio_retornos = retornos.drop(columns=['^BVSP'], errors='ignore').mean(axis=1)
mean_ret = portfolio_retornos.mean() * 252
vol = portfolio_retornos.std() * np.sqrt(252)
# Sharpe, Sortino, Treynor, Calmar
sharpe = (mean_ret - RISK_FREE_RATE) / vol if vol > 0 else 0
downside = portfolio_retornos[portfolio_retornos < 0].std()
sortino = (mean_ret - RISK_FREE_RATE) / (downside * np.sqrt(252)) if downside > 0 else 0
beta = np.cov(portfolio_retornos, benchmark_retornos)[0,1] / np.var(benchmark_retornos) if np.var(benchmark_retornos) > 0 else 0
treynor = (mean_ret - RISK_FREE_RATE) / beta if beta != 0 else 0
cumulative = (1 + portfolio_retornos).cumprod()
peak = cumulative.expanding(min_periods=1).max()
drawdown = (cumulative - peak) / peak
max_dd = drawdown.min()
calmar = mean_ret / abs(max_dd) if max_dd != 0 else 0
# Alpha
alpha = mean_ret - beta * benchmark_retornos.mean() * 252
# Stress test (-20% mercado)
stress_ret = portfolio_retornos * 0.8
stress_loss = (1 + stress_ret).cumprod()[-1] - 1
return {
'Retorno Anualizado (%)': f"{mean_ret:.2%}",
'Volatilidade Anual (%)': f"{vol:.2%}",
'Sharpe Ratio': f"{sharpe:.2f}",
'Sortino Ratio': f"{sortino:.2f}",
'Treynor Ratio': f"{treynor:.2f}",
'Calmar Ratio': f"{calmar:.2f}",
'Max Drawdown (%)': f"{max_dd:.2%}",
'Beta vs ^BVSP': f"{beta:.2f}",
'Alpha (%)': f"{alpha:.2%}",
'Perda em Stress Test (-20% mercado)': f"{stress_loss:.2%}"
}
def otimizar_pesos(retornos):
"""Otimiza pesos para maximizar Sharpe"""
n_assets = len(retornos.columns) - (1 if '^BVSP' in retornos.columns else 0)
if n_assets == 0:
return np.array([])
ret_medio = retornos.iloc[:, :-1].mean() if '^BVSP' in retornos.columns else retornos.mean()
cov = retornos.iloc[:, :-1].cov() if '^BVSP' in retornos.columns else retornos.cov()
def portfolio_performance(weights):
ret = np.dot(weights, ret_medio) * 252
vol = np.sqrt(np.dot(weights.T, np.dot(cov * 252, weights)))
return -ret / vol
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for _ in range(n_assets))
result = minimize(portfolio_performance, np.array([1/n_assets]*n_assets), method='SLSQP', bounds=bounds, constraints=constraints)
return result.x if result.success else np.array([1/n_assets]*n_assets)
def gerar_conclusoes(metricas, beta, alpha, max_dd, stress_loss, low_alpha_assets):
"""Gera insights profissionais"""
insights = []
sharpe = float(metricas['Sharpe Ratio'])
if sharpe > 1.5:
insights.append("Sharpe Ratio excepcional (>1.5): portfólio com ótimo risco-retorno. Mantenha alocação, mas monitore volatilidade semanalmente.")
elif sharpe < 0.5:
insights.append("Sharpe Ratio baixo (<0.5): alto risco para retorno. Considere adicionar renda fixa (ex.: Tesouro SELIC) ou derivativos para hedge.")
beta_val = float(beta)
if beta_val > 1.2:
insights.append(f"Beta alto ({beta}): portfólio sensível ao mercado. Reduza exposição a ações cíclicas como PETR4.SA e adicione defensivas como TAEE11.SA.")
elif beta_val < 0.8:
insights.append(f"Beta baixo ({beta}): defensivo, mas pode underperform em bull markets. Aumente exposição a ^BVSP ou crescimento como WEGE3.SA.")
alpha_val = float(alpha)
if alpha_val > 0.05:
insights.append(f"Alpha positivo ({metricas['Alpha (%)']}): excelente seleção de ativos. Foque em fundamentos fortes para sustentar outperform.")
elif alpha_val < -0.05:
insights.append(f"Alpha negativo ({metricas['Alpha (%)']}): reveja ativos com baixa performance. Substitua {', '.join(low_alpha_assets)} por líderes como VALE3.SA ou ITUB4.SA.")
if abs(float(max_dd.replace('%', ''))) > 20:
insights.append(f"Drawdown elevado ({metricas['Max Drawdown (%)']}): implemente stop-loss em 15% ou hedge com opções para proteção em crashes.")
if abs(float(stress_loss.replace('%', ''))) > 15:
insights.append(f"Stress test indica perda de {metricas['Perda em Stress Test (-20% mercado)']}: diversifique com ativos descorrelacionados (ex.: ouro via OURO11.SA ou bonds).")
insights.append("Recomendação Geral: Rebalanceie trimestralmente com base na fronteira eficiente. Se volatilidade >15%, aloque 20-30% em renda fixa. Monitore ^BVSP e notícias macro.")
return "\n".join(insights)
def analisar_portfolio(selected_assets, custom_tickers, data_inicio, data_fim, num_simulacoes, dias_projetados, confianca):
try:
# Combinar ativos selecionados + custom
custom_list = [t.strip().upper() for t in custom_tickers.split(',') if t.strip()]
custom_list = [t + '.SA' if not t.startswith('^') else t for t in custom_list]
tickers = list(set(selected_assets + custom_list))
# Validar datas
try:
start_date = datetime.strptime(data_inicio, '%Y-%m-%d')
end_date = datetime.strptime(data_fim, '%Y-%m-%d')
if start_date >= end_date:
raise ValueError("Data de início deve ser anterior à data de fim")
if end_date > datetime.now():
raise ValueError("Data de fim não pode ser futura")
except ValueError as e:
return None, {}, f"Erro nas datas: {str(e)}"
# Baixar dados
dados = baixar_dados(tickers, data_inicio, data_fim)
retornos = calcular_retornos(dados)
benchmark_ret = retornos['^BVSP'] if '^BVSP' in retornos.columns else retornos.mean(axis=1)
retornos_port = retornos.drop(columns=['^BVSP'], errors='ignore')
# Identificar low-alpha assets (placeholder: ativos com retorno < benchmark)
low_alpha_assets = retornos_port.mean()[retornos_port.mean() < benchmark_ret.mean()].index.tolist()
# Métricas
metricas = calcular_metricas(retornos, dados, benchmark_ret)
# Otimização
pesos_otim = otimizar_pesos(retornos)
# Simulação Monte Carlo
ret_medio = retornos_port.mean()
cov = retornos_port.cov()
sim_ret = np.random.multivariate_normal(ret_medio, cov, size=(dias_projetados, num_simulacoes))
precos_sim = np.cumprod(1 + sim_ret, axis=0) * 100
ret_final = np.log(precos_sim[-1] / precos_sim[0])
var = np.percentile(ret_final, (1 - confianca) * 100)
cvar = ret_final[ret_final <= var].mean()
# Drawdown simulado
sim_cumulative = np.cumprod(1 + sim_ret, axis=0)
sim_peak = np.maximum.accumulate(sim_cumulative, axis=0)
sim_drawdown = (sim_cumulative - sim_peak) / sim_peak
max_sim_dd = np.min(sim_drawdown, axis=0)
# Gráficos
fig = make_subplots(rows=2, cols=4, subplot_titles=(
"Preços Normalizados", "Retornos Cumulativos", "Correlação", "Risco-Retorno",
"Monte Carlo", "VaR/CVaR", "Drawdown Máximo", "Alocação Otimizada"
), horizontal_spacing=0.05, vertical_spacing=0.15)
# 1. Preços normalizados
precos_norm = dados / dados.iloc[0]
for col in precos_norm.columns:
fig.add_trace(go.Scatter(x=precos_norm.index, y=precos_norm[col], name=col, mode='lines'), row=1, col=1)
# 2. Retornos cumulativos
ret_cum = (1 + retornos_port).cumprod()
for col in ret_cum.columns:
fig.add_trace(go.Scatter(x=ret_cum.index, y=ret_cum[col], name=col, mode='lines'), row=1, col=2)
# 3. Heatmap correlação
corr = retornos_port.corr()
fig.add_trace(go.Heatmap(z=corr.values, x=corr.columns, y=corr.index, colorscale='RdBu'), row=1, col=3)
# 4. Risco-Retorno com fronteira
means = ret_medio * 252
vols = np.sqrt(np.diag(cov) * 252)
for i, asset in enumerate(ret_medio.index):
fig.add_trace(go.Scatter(x=[vols[i]], y=[means[i]], mode='markers+text', name=asset, text=[asset], textposition="top center"), row=1, col=4)
target_returns = np.linspace(min(means.min(), 0), means.max(), 20)
frontier_vols = []
for tr in target_returns:
def objective(w): return np.sqrt(np.dot(w.T, np.dot(cov*252, w)))
constraints = (
{'type': 'eq', 'fun': lambda x: np.sum(x) - 1},
{'type': 'eq', 'fun': lambda x: np.dot(x, ret_medio) * 252 - tr}
)
res = minimize(objective, pesos_otim, method='SLSQP', bounds=tuple((0,1) for _ in ret_medio), constraints=constraints)
if res.success:
frontier_vols.append(res.fun)
else:
frontier_vols.append(np.nan)
fig.add_trace(go.Scatter(x=frontier_vols, y=target_returns, mode='lines', name='Fronteira Eficiente', line=dict(color='green')), row=1, col=4)
# 5. Monte Carlo
for i in range(min(100, num_simulacoes)):
fig.add_trace(go.Scatter(x=range(dias_projetados), y=precos_sim[:, i], mode='lines', opacity=0.1, showlegend=False, line=dict(color='blue')), row=2, col=1)
fig.add_trace(go.Scatter(x=range(dias_projetados), y=np.median(precos_sim, axis=1), mode='lines', name='Mediana', line=dict(color='red', width=2)), row=2, col=1)
# 6. VaR/CVaR
fig.add_trace(go.Histogram(x=ret_final, nbinsx=50, name='Retornos Finais', opacity=0.7), row=2, col=2)
fig.add_vline(x=var, line_dash="dash", line_color="red", annotation_text=f"VaR {confianca*100}%: {var:.2%}", row=2, col=2)
fig.add_vline(x=cvar, line_dash="dash", line_color="orange", annotation_text=f"CVaR {confianca*100}%: {cvar:.2%}", row=2, col=2)
# 7. Drawdown simulado
fig.add_trace(go.Histogram(x=max_sim_dd, nbinsx=50, name='Drawdowns', opacity=0.7), row=2, col=3)
fig.add_vline(x=np.percentile(max_sim_dd, 5), line_dash="dash", line_color="purple", annotation_text="Drawdown 5%", row=2, col=3)
# 8. Alocação otimizada
fig.add_trace(go.Pie(labels=ret_medio.index, values=pesos_otim, name="Alocação"), row=2, col=4)
# Layout
fig.update_layout(
height=900, width=1400, title_text="Mestre do Mercado: Análise Quantitativa de Portfólio",
showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=-0.2, xanchor="center", x=0.5)
)
# Conclusões
conclusoes = gerar_conclusoes(metricas, metricas['Beta vs ^BVSP'], metricas['Alpha (%)'], metricas['Max Drawdown (%)'], metricas['Perda em Stress Test (-20% mercado)'], low_alpha_assets)
# Métricas JSON
metricas_json = metricas
metricas_json['VaR (%)'] = f"{var:.2%}"
metricas_json['CVaR (%)'] = f"{cvar:.2%}"
metricas_json['Pesos Otimizados'] = {ret_medio.index[i]: f"{w:.2%}" for i, w in enumerate(pesos_otim)}
return fig, metricas_json, conclusoes
except Exception as e:
logger.error(f"Erro na análise: {e}")
return None, {}, f"Erro: {str(e)}. Tente atualizar datas ou remover tickers inválidos como BBAS3.SA se o problema persistir."
# Interface Gradio
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🏆 Mestre do Mercado Financeiro: Análise Quantitativa Suprema")
gr.Markdown("Selecione ativos clicando nas checkboxes abaixo (múltiplos permitidos). Adicione outros via textbox. ^BVSP incluído como benchmark.")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Seleção de Ativos")
selected_assets = gr.CheckboxGroup(choices=COMMON_ASSETS, label="Ativos Comuns no Brasil (clique para selecionar)", value=["VALE3.SA", "PETR4.SA", "ITUB4.SA"])
custom_tickers = gr.Textbox(label="Outros Ativos (separar por vírgula, ex.: AAPL, TSLA)", value="")
data_inicio = gr.Textbox(label="Data Início (YYYY-MM-DD)", value="2022-01-01")
data_fim = gr.Textbox(label="Data Fim (YYYY-MM-DD)", value="2024-07-31")
num_simulacoes = gr.Slider(minimum=1000, maximum=50000, value=10000, step=1000, label="Simulações Monte Carlo")
dias_projetados = gr.Slider(minimum=30, maximum=500, value=252, step=1, label="Dias Projetados")
confianca = gr.Slider(minimum=0.90, maximum=0.99, value=0.95, step=0.01, label="Confiança VaR/CVaR")
analisar_btn = gr.Button("🚀 Analisar & Otimizar", variant="primary")
with gr.Column(scale=3):
gr.Markdown("### Resultados")
output_plot = gr.Plot(label="Gráficos Quantitativos")
with gr.Column(scale=1):
gr.Markdown("### Métricas & Insights")
output_stats = gr.JSON(label="Métricas do Portfólio")
output_conclusoes = gr.Markdown(label="Insights do Mestre")
gr.Examples(
examples=[
[["VALE3.SA", "PETR4.SA", "ITUB4.SA"], "", "2020-01-01", "2024-07-31", 10000, 252, 0.95],
[["BBAS3.SA", "ITSA4.SA", "TAEE11.SA"], "", "2018-01-01", "2024-07-31", 20000, 365, 0.99],
[["MGLU3.SA", "BBDC4.SA", "WEGE3.SA"], "", "2021-01-01", "2024-07-31", 15000, 180, 0.90]
],
inputs=[selected_assets, custom_tickers, data_inicio, data_fim, num_simulacoes, dias_projetados, confianca],
label="Exemplos Profissionais (clique para carregar)"
)
analisar_btn.click(
fn=analisar_portfolio,
inputs=[selected_assets, custom_tickers, data_inicio, data_fim, num_simulacoes, dias_projetados, confianca],
outputs=[output_plot, output_stats, output_conclusoes]
)
if __name__ == "__main__":
demo.launch(share=True)