AFI / app.py
Quant0's picture
Create app.py
b876318 verified
Raw History Blame Contribute Delete
10.9 kB
# ============================================================
# 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}<br>Correlación: %{y:.4f}<extra></extra>",
)
)
# Layout y controles de rango
fig.update_layout(
title_text=f"<b>Evolución de la correlación entre {ticker_company} y BTC-USD<b>",
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}<br>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)