Spaces:
Runtime error
Runtime error
| 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) |