File size: 8,121 Bytes
5aa014a fb1db80 5aa014a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | # ============================================================
# analytics.py — Cálculo de correlaciones entre activos y BTC
# ------------------------------------------------------------
# NOTA PARA AUDITORÍA:
# - Este módulo expone la función `evolucion_correlacion`, encargada de
# calcular la correlación rolling entre el precio de cierre de un
# activo (empresa/ETF) y BTC-USD.
# - La función:
# 1) Obtiene (o descarga si falta) los históricos con yfinance.
# 2) Alinea por fecha y calcula rendimientos diarios (% change).
# 3) Aplica correlación móvil (rolling window) de tamaño
# `window_months * 30` días.
# 4) Devuelve una Serie con la evolución o, si step=0, el último
# valor de correlación como serie de un solo punto.
# - Dependencias externas: yfinance, pandas, datetime.
# - Caché en memoria: `all_historical_data` (dict[str, DataFrame]).
# Este diccionario debe existir en tiempo de ejecución. Por defecto,
# se intenta importar desde `data_sources` (propuesta de modularización)
# o, si no existe, desde `processes` (compatibilidad con tu estructura
# actual). Si no se encuentra, se crea un dict vacío.
# - Recomendación futura: sustituir `print` por `logging`.
# ============================================================
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Optional
import pandas as pd
import yfinance as yf
from config import (
SCRAPERAPI_KEY, CRYPTOCOMPARE_API_KEY, TIMEZONE_TARGET, GRADIO_SHARE,
CRYPTOCOMPARE_BASE_URL, BITCOINTREASURIES_STOCKS_URL, BITCOINTREASURIES_ETFS_URL,
YF_SEARCH_ENDPOINT, TICKERS_TO_IDS
)
# ------------------------------------------------------------
# Importación del caché global de históricos
# - Primero intenta el módulo recomendado `data_sources`
# - Si no existe, recurre al módulo histórico `processes`
# - Como último recurso, define un dict vacío (seguirá funcionando,
# pero descargará siempre con yfinance).
# ------------------------------------------------------------
try:
# Proyecto modularizado (recomendado)
from data_sources import all_historical_data # type: ignore
except Exception:
try:
# Compatibilidad con el proyecto actual
from processes import all_historical_data # type: ignore
except Exception:
# Fallback mínimo
all_historical_data = {} # type: ignore
print(
"[analytics] Advertencia: no se encontró 'all_historical_data' en "
"'data_sources' ni en 'processes'. Se usará un diccionario vacío."
)
__all__ = ["evolucion_correlacion"]
def evolucion_correlacion(
ticker_company: str,
step: int,
ticker_btc: str = "BTC-USD",
window_months: int = 12,
) -> Optional[pd.Series]:
"""
Calcula la evolución de la correlación (rolling) entre un activo (empresa/ETF)
y BTC-USD, usando precios de CIERRE diarios.
Parámetros
----------
ticker_company : str
Ticker del activo a analizar (p. ej., 'MSTR', 'TSLA', 'COIN').
step : int
Salto en días para muestrear la serie de correlación resultante.
- Si `step == 0`: devuelve SOLO el último valor de correlación
(una Serie con un único punto indexado en `end_date`).
- Si `step > 0`: devuelve la serie completa de correlación rolling
muestreada cada `step` filas (conservando el offset original).
ticker_btc : str, opcional
Ticker de BTC (por defecto: "BTC-USD").
Debe existir o poder descargarse con yfinance.
window_months : int, opcional
Tamaño de la ventana móvil en meses (por defecto: 12).
Internamente se convierte a días: `window_months * 30`.
Devolución
----------
pandas.Series | None
Serie temporal con la evolución de la correlación rolling.
Devuelve `None` si faltan datos o no se pudieron descargar.
Notas de implementación
-----------------------
- Lógica conservada tal cual respecto a tu implementación original:
* Descarga condicional con yfinance si el ticker no está en `all_historical_data`.
* Cálculo de rendimientos con `pct_change()` y `dropna()`.
* Ventana rolling expresada como texto con sufijo 'D' (días).
* Cuando `step > 0`, se devuelve `dynamic_corr.iloc[9::step]`, manteniendo
el mismo offset de tu código.
- Recomendación: sustituir prints por `logging` y añadir control de reintentos.
"""
# Rango temporal de trabajo: ~10 años atrás hasta hoy (misma lógica)
end_date = datetime.now()
start_date = end_date - timedelta(days=10 * 365)
# ------------------------------------------------------------
# Descarga condicional para el ticker de la empresa
# ------------------------------------------------------------
if ticker_company not in all_historical_data:
print(f"Descargando datos para {ticker_company}...")
data_company = yf.download(ticker_company, start=start_date, end=end_date)
if not data_company.empty:
all_historical_data[ticker_company] = data_company
print(f"Datos de {ticker_company} descargados con éxito.")
else:
print(f"Error al descargar datos para {ticker_company}.")
return None
# ------------------------------------------------------------
# Descarga condicional para BTC-USD (o el ticker_btc especificado)
# ------------------------------------------------------------
if ticker_btc not in all_historical_data:
print(f"Descargando datos para {ticker_btc}...")
data_btc = yf.download(ticker_btc, start=start_date, end=end_date)
if not data_btc.empty:
all_historical_data[ticker_btc] = data_btc
print(f"Datos de {ticker_btc} descargados con éxito.")
else:
print(f"Error al descargar datos para {ticker_btc}.")
return None
# ------------------------------------------------------------
# Extracción de cierres y alineación por fechas
# ------------------------------------------------------------
company_data: pd.DataFrame = all_historical_data.get(ticker_company, pd.DataFrame())
btc_data: pd.DataFrame = all_historical_data.get(ticker_btc, pd.DataFrame())
if company_data.empty or btc_data.empty:
print(
f"No podemos calcular la correlación dinámica: "
f"No hay datos disponibles para {ticker_company} o {ticker_btc}."
)
return None
company_prices = company_data["Close"]
btc_prices = btc_data["Close"]
# Alinear por índice (fecha) y eliminar faltantes
combined_prices = pd.concat([company_prices, btc_prices], axis=1).dropna()
combined_prices.columns = [ticker_company, ticker_btc]
if combined_prices.empty:
print(
f"No hay datos históricos en común entre {ticker_company} y {ticker_btc}."
)
return None
# ------------------------------------------------------------
# Cálculo de rendimientos y correlación rolling
# ------------------------------------------------------------
company_returns = combined_prices[ticker_company].pct_change().dropna()
btc_returns = combined_prices[ticker_btc].pct_change().dropna()
window_days = window_months * 30 # Mantener equivalencia con el código original
if step == 0:
# Correlación en la última fecha disponible (Serie 1x1)
dynamic_corr_last = (
company_returns.rolling(window=f"{window_days}D").corr(btc_returns).iloc[-1]
)
# Devolvemos una Serie con índice en la fecha actual (misma firma de retorno)
return pd.Series([dynamic_corr_last], index=[end_date])
# Serie completa de correlación rolling, muestreada cada `step` elementos
dynamic_corr = company_returns.rolling(window=f"{window_days}D").corr(btc_returns).dropna()
# Conserva el muestreo con desplazamiento como en tu implementación (iloc[9::step])
return dynamic_corr.iloc[9::step]
|