# ============================================================ # app.py — Interfaz de usuario (Gradio) y visualizaciones (Plotly) # ------------------------------------------------------------ # - Capa de presentación: # * UI con Gradio (componentes y callbacks). # * Plot interactivo de la evolución de la correlación (Plotly). # * Iframes de bitcointreasuries. # * Botones de exportación (delegan en exports.py). # - La lógica y datos están en: # analytics.py, scraping.py, matching.py, data_sources.py, exports.py, config.py # - Sin estado persistente; la caché en memoria vive en data_sources.py. # ============================================================ from __future__ import annotations # --- Librerías UI & Visualización import gradio as gr import plotly.graph_objects as go # --- Núcleo de negocio y datos (módulos del proyecto) from analytics import evolucion_correlacion from matching import find_company_online from scraping import generate_tickers_from_bitcointreasuries from exports import exportar_correlacion, descargar_resumen_holding_excel from config import TICKERS_TO_IDS, GRADIO_SHARE from data_sources import get_data # precarga opcional de BTC-USD # ============================================================ # Helpers específicos de la UI # ============================================================ def get_iframe_stock_vs_btc(ticker: str) -> str: """ HTML del iframe 'Stock vs Bitcoin' para un ticker, usando el mapeo TICKERS_TO_IDS. """ try: entity_id = TICKERS_TO_IDS[ticker] return f""" https://bitcointreasuries.net/embed?component=StockVsBitcoinChart&entityId={entity_id} """ except KeyError: return f"No hay datos de holding de Bitcoin para {ticker}." def get_iframe_holdings(ticker: str) -> str: """ HTML del iframe 'Bitcoin Holdings' para un ticker, usando TICKERS_TO_IDS. """ try: entity_id = TICKERS_TO_IDS[ticker] return f""" https://bitcointreasuries.net/embed?component=BitcoinHoldingsChart&entityId={entity_id} """ except KeyError: return f"No hay datos de holding de Bitcoin para {ticker}." def plot_evolucion_correlacion_gradio( ticker_company: str, step: int, window_months: int ): """ Genera un gráfico Plotly con la evolución de la correlación entre `ticker_company` y BTC-USD. """ serie = evolucion_correlacion(ticker_company, step=step, window_months=window_months) if serie is None or serie.empty: return None, f"No hay datos de correlación disponibles para {ticker_company}." # --- Configuración del gráfico Plotly (estética y controles) fig = go.Figure() fig.add_trace( go.Scatter( x=serie.index, y=serie.values, mode="lines", name="Correlación", line=dict(color="#2263eb", width=2), fill="tozeroy", fillcolor="rgba(34,99,235,0.1)", hovertemplate="Fecha: %{x|%Y-%m-%d}
Correlación: %{y:.4f}", ) ) # Layout y controles de rango fig.update_layout( title_text=f"Evolución de la correlación entre {ticker_company} y BTC-USD", title_x=0, height=500, template="plotly_white", margin=dict(l=20, r=20, t=60, b=20), showlegend=False, xaxis=go.layout.XAxis( rangeslider=dict( visible=True, thickness=0.08, bgcolor="rgba(50, 0, 128, 0.1)", bordercolor="rgba(50, 0, 128, 0.2)", borderwidth=1, range=[serie.index.min(), serie.index.max()], ), rangeselector=dict( buttons=list( [ dict(count=1, label="1m", step="month", stepmode="backward"), dict(count=3, label="3m", step="month", stepmode="backward"), dict(count=6, label="6m", step="month", stepmode="backward"), dict(step="year", count=1, label="YTD", stepmode="todate"), dict(count=1, label="1y", step="year", stepmode="backward"), dict(step="all", label="All"), ] ), bgcolor="rgba(247,247,247,255)", bordercolor="rgba(0,0,0,0)", borderwidth=0, activecolor="rgba(230,233,255,255)", font=dict(color="#000000", size=10), x=0.9, y=1.1, xanchor="right", yanchor="top", ), type="date", domain=[0, 1], rangeslider_yaxis_rangemode="auto", showgrid=False, gridcolor="#e0e0e0", zeroline=False, ), yaxis=go.layout.YAxis( title_text="Correlación", side="left", showgrid=True, gridcolor="#e0e0e0", zeroline=False, tickformat=".2f", range=[-1.0, 1.0], ), hovermode="x unified", paper_bgcolor="rgba(255,255,255,1)", plot_bgcolor="rgba(255,255,255,1)", ) # Anotación en el máximo max_value = float(serie.max()) max_index = serie.idxmax() date_str = max_index.date() if hasattr(max_index, "date") else str(max_index) fig.add_annotation( x=max_index, y=max_value, text=f"Corr_max: {max_value:.4f}
Fecha: {date_str}", showarrow=True, arrowhead=2, ax=0, ay=-40, bgcolor="rgba(255, 255, 255, 0.8)", bordercolor="#c7c7c7", borderwidth=1, borderpad=4, font=dict(size=10, color="#333"), ) return fig, "Gráfico de correlación generado." # ============================================================ # Datos iniciales para la UI # ============================================================ # Lista inicial del Dropdown desde bitcointreasuries (equivale a tu generar_tickers()). tickers_dropdown = generate_tickers_from_bitcointreasuries() # Precarga rápida de BTC-USD en caché con yfinance (opcional, sin clave). get_data("BTC-USD", silent=True) # ============================================================ # Construcción de la interfaz (Gradio Blocks) # ============================================================ with gr.Blocks(title="Análisis de Correlación") as demo: # --- Cabecera y branding gr.Image( "https://cdn.prod.website-files.com/67174dcb1d43b06a0eae317e/672bbbc7a2b88045e5661994_logo-afi.svg", show_label=False, ) gr.Markdown("# Analizador de Correlación de Bitcoin") gr.Markdown("Explora la correlación de diferentes activos con Bitcoin y sus holdings.") # --- Buscador de empresa/ticker with gr.Row(): input_company = gr.Dropdown( label="Buscar por Nombre (se usarán los primeros 6 caracteres)", choices=tickers_dropdown, value="MicroStrategy", allow_custom_value=True, interactive=True, ) texto_info1 = gr.Textbox(label="Nombre Completo:", interactive=False) # Estado del TICKER resuelto (lo usamos en los callbacks) texto_info2 = gr.Textbox(label="Ticker:", interactive=False) # Resolver (Nombre, Ticker) al cambiar la selección input_company.change( fn=lambda texto: find_company_online(texto[:6]), inputs=input_company, outputs=[texto_info1, texto_info2], ) # Resolver también al cargar la app para mostrar valores iniciales demo.load( fn=lambda texto: find_company_online(texto[:6]), inputs=input_company, outputs=[texto_info1, texto_info2], ) # --- Tabs de la aplicación with gr.Tab("Evolución de Correlación"): with gr.Row(): input_step = gr.Slider( minimum=1, maximum=30, value=1, step=1, label="Indica cada cuántos días se calcula la correlación", ) input_window = gr.Slider( minimum=1, maximum=60, value=12, step=1, label="Ventana rolling (meses)", ) btn_plot = gr.Button("Mostrar Gráfico") output_plot = gr.Plot(label="Gráfico de Evolución") btn_excel = gr.Button("Descargar en Excel") output_file = gr.File(label="Archivo Excel") output_msg = gr.Label() # Graficar (usa el ticker resuelto en texto_info2) btn_plot.click( fn=plot_evolucion_correlacion_gradio, inputs=[texto_info2, input_step, input_window], outputs=[output_plot, output_msg], ) # Exportar a Excel (serie de correlación) btn_excel.click( fn=exportar_correlacion, inputs=[texto_info2, input_step, input_window], outputs=[output_file, output_msg], ) with gr.Tab("Stock vs BTC"): btn_stock_btc = gr.Button("Cargar Gráfico") output_iframe_stock = gr.HTML(label="Stock vs Bitcoin Chart") btn_stock_btc.click( fn=get_iframe_stock_vs_btc, inputs=[texto_info2], outputs=[output_iframe_stock] ) with gr.Tab("Holding de Bitcoin"): btn_holdings = gr.Button("Cargar Gráfico") output_iframe_holdings = gr.HTML(label="Bitcoin Holdings Chart") btn_holdings.click( fn=get_iframe_holdings, inputs=[texto_info2], outputs=[output_iframe_holdings] ) with gr.Tab("Exportar Resumen Holding"): input_template_url = gr.Textbox( label="URL de la plantilla de Excel (Google Drive)", value=( "https://docs.google.com/spreadsheets/d/1Ad3bbYQAAZO6ljkaCGTGSw5aG_FQF7Lg/" "edit?usp=sharing&ouid=109222615757657219494&rtpof=true&sd=true" ), placeholder="Ej: https://docs.google.com/spreadsheets/d/ID_DEL_ARCHIVO/...", ) btn_descargar_resumen = gr.Button("Descargar Resumen a Excel") output_resumen_file = gr.File(label="Archivo de Resumen de Holdings") output_resumen_msg = gr.Label() btn_descargar_resumen.click( fn=descargar_resumen_holding_excel, inputs=[input_template_url], outputs=[output_resumen_file, output_resumen_msg], ) # ============================================================ # Lanzamiento de la app # ============================================================ if __name__ == "__main__": # GRADIO_SHARE controla si se expone URL pública (útil para demos/HF Spaces) demo.launch(share=GRADIO_SHARE)