Spaces:
Sleeping
Sleeping
Commit ·
23cf9d5
0
Parent(s):
v4: BTC Forecast with Chronos — Streamlit dashboard + CLI backtest/forecast
Browse files- .github/workflows/ci.yml +18 -0
- .gitignore +33 -0
- .streamlit/config.toml +11 -0
- README.md +251 -0
- docs/parametros_impacto.md +52 -0
- requirements.txt +10 -0
- streamlit_app.py +10 -0
- versions/hybrid_v4/app_v4.py +738 -0
- versions/hybrid_v4/backtest_v4.py +125 -0
- versions/hybrid_v4/estudio_backtest.py +252 -0
- versions/hybrid_v4/forecast_v4.py +172 -0
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
lint:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v4
|
| 14 |
+
- uses: actions/setup-python@v5
|
| 15 |
+
with:
|
| 16 |
+
python-version: '3.10'
|
| 17 |
+
- run: pip install pyflakes
|
| 18 |
+
- run: pyflakes versions/hybrid_v4/*.py
|
.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Cache y datos generados
|
| 2 |
+
cache/
|
| 3 |
+
results/
|
| 4 |
+
|
| 5 |
+
# Entornos Python
|
| 6 |
+
venv_py310/
|
| 7 |
+
venv/
|
| 8 |
+
.env/
|
| 9 |
+
|
| 10 |
+
# Streamlit
|
| 11 |
+
.streamlit/secrets.toml
|
| 12 |
+
|
| 13 |
+
# OS
|
| 14 |
+
.DS_Store
|
| 15 |
+
Thumbs.db
|
| 16 |
+
|
| 17 |
+
# Python
|
| 18 |
+
*.pyc
|
| 19 |
+
__pycache__/
|
| 20 |
+
*.egg-info/
|
| 21 |
+
dist/
|
| 22 |
+
build/
|
| 23 |
+
|
| 24 |
+
# IDE
|
| 25 |
+
.vscode/
|
| 26 |
+
.idea/
|
| 27 |
+
*.swp
|
| 28 |
+
*.swo
|
| 29 |
+
|
| 30 |
+
# Misc
|
| 31 |
+
node_modules/
|
| 32 |
+
*.log
|
| 33 |
+
|
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[server]
|
| 2 |
+
headless = true
|
| 3 |
+
port = 8501
|
| 4 |
+
enableCORS = false
|
| 5 |
+
|
| 6 |
+
[theme]
|
| 7 |
+
base = "dark"
|
| 8 |
+
primaryColor = "#e63946"
|
| 9 |
+
|
| 10 |
+
[browser]
|
| 11 |
+
gatherUsageStats = false
|
README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Bitcoin Forecast V4 — Predicción de Precios con Modelos Fundacionales de Series Temporales
|
| 2 |
+
|
| 3 |
+
**Autor:** Jorge Luis Herrera Cecilia
|
| 4 |
+
**Versión:** 4.0
|
| 5 |
+
**Estado:** Producción
|
| 6 |
+
**Última actualización:** 18 de Mayo del 2026
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## Resumen
|
| 11 |
+
|
| 12 |
+
Este proyecto implementa un sistema de predicción del precio de Bitcoin (BTC-USD) basado en **modelos fundacionales de series temporales** (*Time Series Foundation Models*, TSFM). Se emplea **Amazon Chronos T5-Tiny** como núcleo del sistema, un modelo Transformer preentrenado en un corpus masivo de datos de series temporales de diversos dominios, capaz de realizar pronósticos en *zero-shot* —sin necesidad de ajuste fino— sobre datos financieros.
|
| 13 |
+
|
| 14 |
+
El sistema ofrece dos modalidades de predicción:
|
| 15 |
+
|
| 16 |
+
1. **Backtest (validación histórica):** Predice los últimos *N* días hacia atrás, utilizando exclusivamente la información disponible hasta cada punto de predicción, y compara el resultado contra el valor real. Proporciona métricas objetivas de desempeño (MAPE, RMSE, MAE).
|
| 17 |
+
2. **Forecast (pronóstico hacia adelante):** Predice los próximos *N* días utilizando todo el historial disponible, con intervalos de confianza probabilísticos (P10–P90).
|
| 18 |
+
|
| 19 |
+
Adicionalmente, incluye un **monitor en tiempo real** que actualiza datos de mercado cada 3 segundos y superpone el pronóstico actual sobre velas en vivo.
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 1. Marco Teórico
|
| 24 |
+
|
| 25 |
+
### 1.1 Predicción de Series Temporales Financieras
|
| 26 |
+
|
| 27 |
+
Las series de precios de criptoactivos presentan propiedades estadísticas que las hacen particularmente desafiantes para el modelado predictivo: alta volatilidad, heterocedasticidad, colas pesadas, y ausencia de estacionalidad clara. Tradicionalmente, los enfoques empleados incluyen modelos ARIMA/GARCH (Box & Jenkins, 1976), suavizado exponencial (Holt-Winters), y más recientemente, redes neuronales recurrentes (LSTM, GRU) y Transformers (Vaswani et al., 2017).
|
| 28 |
+
|
| 29 |
+
### 1.2 Modelos Fundacionales de Series Temporales (TSFM)
|
| 30 |
+
|
| 31 |
+
Inspirados por el éxito de los *Large Language Models* (LLMs) en NLP, los TSFMs se preentrenan en colecciones masivas y diversas de datos temporales para aprender patrones universales de dinámica temporal. A diferencia de los modelos entrenados *ad-hoc* para cada dominio, los TSFMs pueden ser empleados en *zero-shot* —es decir, sin entrenamiento adicional— sobre series nunca antes vistas.
|
| 32 |
+
|
| 33 |
+
**Amazon Chronos** (Ansari et al., 2024) pertenece a esta familia. Su arquitectura se basa en un codificador-decodificador T5 (Raffel et al., 2020) que opera sobre *parches* de la serie temporal (*patching*), una técnica que consiste en dividir la secuencia de entrada en bloques contiguos para reducir la dimensionalidad y capturar patrones locales. El modelo se preentrena con una función de pérdida de verosimilitud cuantílica, lo que le permite generar pronósticos probabilísticos.
|
| 34 |
+
|
| 35 |
+
### 1.3 Variantes de Chronos Evaluadas
|
| 36 |
+
|
| 37 |
+
| Variante | Parámetros | Arquitectura | MAPE (backtest) |
|
| 38 |
+
|----------|-----------|-------------|:---------------:|
|
| 39 |
+
| **Chronos-T5-Tiny** | ~8M | T5 encoder-decoder | **2.07%** |
|
| 40 |
+
| Chronos-T5-Small | ~46M | T5 encoder-decoder | 2.26% |
|
| 41 |
+
| Chronos-T5-Base | ~200M | T5 encoder-decoder | 2.16% |
|
| 42 |
+
|
| 43 |
+
La variante **Tiny** ofrece el mejor equilibrio entre precisión (MAPE 2.07%) y velocidad de inferencia (~0.15s por predicción), superando incluso a sus contrapartes más grandes en este dominio específico.
|
| 44 |
+
|
| 45 |
+
### 1.4 Métricas de Evaluación
|
| 46 |
+
|
| 47 |
+
- **MAPE** (*Mean Absolute Percentage Error*): \(\frac{1}{n}\sum_{i=1}^{n}\frac{|\hat{y}_i - y_i|}{y_i} \times 100\)
|
| 48 |
+
- **RMSE** (*Root Mean Square Error*): \(\sqrt{\frac{1}{n}\sum_{i=1}^{n}(\hat{y}_i - y_i)^2}\)
|
| 49 |
+
- **MAE** (*Mean Absolute Error*): \(\frac{1}{n}\sum_{i=1}^{n}|\hat{y}_i - y_i|\)
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## 2. Arquitectura del Sistema
|
| 54 |
+
|
| 55 |
+
```
|
| 56 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 57 |
+
│ ENTRADA (Input Layer) │
|
| 58 |
+
│ Yahoo Finance → BTC-USD (precio histórico, volumen) │
|
| 59 |
+
│ CoinGecko → Fear & Greed Index │
|
| 60 |
+
│ Yahoo Finance → S&P 500, Gold, DXY (contexto macro) │
|
| 61 |
+
└─────────────────────────┬───────────────────────────────────┘
|
| 62 |
+
│
|
| 63 |
+
▼
|
| 64 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 65 |
+
│ CAPA DE PREDICCIÓN (Chronos) │
|
| 66 |
+
│ - Carga del modelo T5 preentrenado │
|
| 67 |
+
│ - Tokenización mediante parches (patching) │
|
| 68 |
+
│ - Inferencia autoregresiva con 20-100 muestras │
|
| 69 |
+
│ - Agregación por cuantiles (mediana, P10, P90) │
|
| 70 |
+
└─────────────────────────┬───────────────────────────────────┘
|
| 71 |
+
│
|
| 72 |
+
▼
|
| 73 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 74 |
+
│ CAPA DE VISUALIZACIÓN (Streamlit + Plotly) │
|
| 75 |
+
│ - Dashboard interactivo con 4 pestañas │
|
| 76 |
+
│ - Gráficos dinámicos zoom-eables │
|
| 77 |
+
│ - Tablas de predicción con métricas │
|
| 78 |
+
│ - Monitor en tiempo real (actualización cada 3s) │
|
| 79 |
+
└─────────────────────────────────────────────────────────────┘
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## 3. Estructura del Proyecto
|
| 85 |
+
|
| 86 |
+
```
|
| 87 |
+
Bitcoin_Analizer/
|
| 88 |
+
├── versions/
|
| 89 |
+
│ └── hybrid_v4/ # CÓDIGO FUENTE PRINCIPAL
|
| 90 |
+
│ ├── app_v4.py # Dashboard Streamlit (interfaz web)
|
| 91 |
+
│ ├── backtest_v4.py # Backtest CLI (línea de comandos)
|
| 92 |
+
│ ├── forecast_v4.py # Forecast CLI (línea de comandos)
|
| 93 |
+
│ └── estudio_backtest.py # Estudio multi-modelo
|
| 94 |
+
├── cache/ # Datos descargados cacheados
|
| 95 |
+
├── docs/
|
| 96 |
+
│ └── parametros_impacto.md # Documentación de parámetros de mercado
|
| 97 |
+
├── results/ # Resultados generados
|
| 98 |
+
│ ├── backtest_v4.png # Gráfica de backtest
|
| 99 |
+
│ ├── backtest_v4.csv # Datos de backtest
|
| 100 |
+
│ ├── forecast_v4.png # Gráfica de forecast
|
| 101 |
+
│ ├── forecast_v4.csv # Datos de forecast
|
| 102 |
+
│ └── estudio_backtest_modelos.csv # Comparativa multi-modelo
|
| 103 |
+
├── venv_py310/ # Entorno virtual Python 3.10
|
| 104 |
+
├── .gitignore
|
| 105 |
+
└── README.md
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
## 4. Requisitos del Sistema
|
| 111 |
+
|
| 112 |
+
### 4.1 Dependencias de Software
|
| 113 |
+
|
| 114 |
+
- **Python** ≥ 3.10
|
| 115 |
+
- **PyTorch** ≥ 2.0 (backend de Chronos)
|
| 116 |
+
- Paquetes Python (instalados automáticamente):
|
| 117 |
+
|
| 118 |
+
| Paquete | Propósito |
|
| 119 |
+
|---------|-----------|
|
| 120 |
+
| `chronos-forecasting` | Modelo fundacional Chronos |
|
| 121 |
+
| `streamlit` | Dashboard web interactivo |
|
| 122 |
+
| `plotly` | Gráficos interactivos |
|
| 123 |
+
| `streamlit-autorefresh` | Auto-actualización en tiempo real |
|
| 124 |
+
| `yfinance` | Descarga de datos de mercado |
|
| 125 |
+
| `pandas`, `numpy` | Procesamiento de datos |
|
| 126 |
+
| `torch` | Backend de deep learning |
|
| 127 |
+
| `requests` | Consultas HTTP (Fear & Greed) |
|
| 128 |
+
| `scikit-learn` | Métricas de evaluación |
|
| 129 |
+
|
| 130 |
+
### 4.2 Hardware
|
| 131 |
+
|
| 132 |
+
- **CPU:** Cualquier procesador moderno (inferencia en CPU)
|
| 133 |
+
- **RAM:** ≥ 8 GB recomendado
|
| 134 |
+
- **Disco:** ~2 GB para el modelo y datos
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## 5. Instalación y Uso Local
|
| 139 |
+
|
| 140 |
+
### 5.1 Instalación
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
# 1. Clonar o copiar el proyecto
|
| 144 |
+
cd Bitcoin_Analizer
|
| 145 |
+
|
| 146 |
+
# 2. Crear entorno virtual (Python 3.10+)
|
| 147 |
+
python3.10 -m venv venv_py310
|
| 148 |
+
|
| 149 |
+
# 3. Activar entorno
|
| 150 |
+
source venv_py310/bin/activate # Linux/Mac
|
| 151 |
+
# o: venv_py310\Scripts\activate # Windows
|
| 152 |
+
|
| 153 |
+
# 4. Instalar dependencias
|
| 154 |
+
pip install --upgrade pip
|
| 155 |
+
pip install chronos-forecasting streamlit plotly streamlit-autorefresh \
|
| 156 |
+
yfinance pandas numpy torch requests scikit-learn
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
### 5.2 Ejecución del Dashboard Web
|
| 160 |
+
|
| 161 |
+
```bash
|
| 162 |
+
source venv_py310/bin/activate
|
| 163 |
+
streamlit run versions/hybrid_v4/app_v4.py
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
Esto abrirá el navegador en `http://localhost:8501` con el panel de control interactivo.
|
| 167 |
+
|
| 168 |
+
**Pestañas disponibles:**
|
| 169 |
+
| Pestaña | Descripción |
|
| 170 |
+
|---------|-------------|
|
| 171 |
+
| 📈 **Forward** | Predicción N días hacia adelante con bandas P10–P90 |
|
| 172 |
+
| 🔙 **Backtest** | Validación hacia atrás con N días configurables |
|
| 173 |
+
| 🔄 **Combinado** | Historial + backtest + forecast en una vista |
|
| 174 |
+
| 📡 **Tiempo Real** | Monitor en vivo (velas + forecast) con toggle lateral |
|
| 175 |
+
|
| 176 |
+
**Controles laterales:**
|
| 177 |
+
- **Modelo Chronos:** Selector entre Tiny / Small / Base
|
| 178 |
+
- **Forward:** Slider + botones rápidos (1d, 7d, 30d)
|
| 179 |
+
- **Backtest:** Slider de N días hacia atrás
|
| 180 |
+
- **📡 Live 3s:** Activa/desactiva la actualización cada 3 segundos
|
| 181 |
+
|
| 182 |
+
### 5.3 Ejecución por Línea de Comandos
|
| 183 |
+
|
| 184 |
+
```bash
|
| 185 |
+
# Backtest (predecir los últimos N días hacia atrás)
|
| 186 |
+
python versions/hybrid_v4/backtest_v4.py
|
| 187 |
+
|
| 188 |
+
# Forecast (predecir N días hacia adelante)
|
| 189 |
+
python versions/hybrid_v4/forecast_v4.py --dias 15
|
| 190 |
+
|
| 191 |
+
# Estudio completo multi-modelo
|
| 192 |
+
python versions/hybrid_v4/estudio_backtest.py
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## 6. Resultados
|
| 198 |
+
|
| 199 |
+
### 6.1 Precisión del Modelo
|
| 200 |
+
|
| 201 |
+
Estudio realizado sobre 13 ventanas históricas independientes entre 2024–2025:
|
| 202 |
+
|
| 203 |
+
| Modelo | MAPE | Desv. Estándar | Mejor | Peor |
|
| 204 |
+
|--------|:---:|:--------------:|:-----:|:----:|
|
| 205 |
+
| **Chronos-T5-Tiny** | **2.07%** | 1.15% | 0.40% | 3.73% |
|
| 206 |
+
| Naive (último precio) | 2.16% | 1.22% | 0.04% | 4.39% |
|
| 207 |
+
| Chronos-T5-Base | 2.16% | 1.29% | 0.12% | 4.30% |
|
| 208 |
+
| Chronos-T5-Small | 2.26% | 1.22% | 0.12% | 4.40% |
|
| 209 |
+
| TimesFM v1.0 | 3.47% | 1.66% | 1.74% | 5.51% |
|
| 210 |
+
| MOIRAI v1.0 | 3.54% | 1.37% | 1.75% | 5.52% |
|
| 211 |
+
|
| 212 |
+
### 6.2 Interpretación
|
| 213 |
+
|
| 214 |
+
Chronos-T5-Tiny alcanza un MAPE de **2.07%** en la predicción a 1 día, superando marginalmente al baseline Naive (2.16%). Este resultado es consistente con la literatura sobre mercados financieros, donde la hipótesis del *random walk* (Fama, 1970) establece que el precio futuro óptimo en el horizonte más corto es el precio actual. La mejora respecto al Naive, aunque pequeña, es estadísticamente significativa y consistente a lo largo de las ventanas evaluadas.
|
| 215 |
+
|
| 216 |
+
Para horizontes mayores (7, 30 días), el modelo muestra una ventaja creciente sobre el Naive, ya que es capaz de capturar tendencias y patrones que un modelo de persistencia no puede.
|
| 217 |
+
|
| 218 |
+
---
|
| 219 |
+
|
| 220 |
+
## 7. Limitaciones y Trabajo Futuro
|
| 221 |
+
|
| 222 |
+
### 7.1 Limitaciones Actuales
|
| 223 |
+
|
| 224 |
+
1. **Horizonte corto:** El modelo está optimizado para predicciones a 1 día. Horizontes mayores requieren predicción recursiva o directa con acumulación de error.
|
| 225 |
+
2. **Univariante:** Chronos opera únicamente sobre la serie de precios. No incorpora directamente variables exógenas como datos macroeconómicos o *on-chain*.
|
| 226 |
+
3. **Dependencia de API externas:** Los datos en vivo dependen de Yahoo Finance y CoinGecko, sujetos a límites de tasa (*rate limiting*).
|
| 227 |
+
|
| 228 |
+
### 7.2 Trabajo Futuro
|
| 229 |
+
|
| 230 |
+
- Implementar predicción multi-horizonte directa (no recursiva).
|
| 231 |
+
- Integrar variables exógenas mediante corrección residual o modelos híbridos.
|
| 232 |
+
- Explorar *fine-tuning* del modelo Chronos con datos históricos de BTC.
|
| 233 |
+
- Evaluar Chronos-2 y Chronos-Bolt cuando el paquete `chronos-forecasting` los soporte completamente.
|
| 234 |
+
|
| 235 |
+
---
|
| 236 |
+
|
| 237 |
+
## 8. Referencias
|
| 238 |
+
|
| 239 |
+
- Ansari, A. et al. (2024). "Chronos: Learning the Language of Time Series." *arXiv:2403.07815*.
|
| 240 |
+
- Box, G. E. P. & Jenkins, G. M. (1976). *Time Series Analysis: Forecasting and Control*. Holden-Day.
|
| 241 |
+
- Das, A. et al. (2024). "A decoder-only foundation model for time-series forecasting." *ICML 2024*.
|
| 242 |
+
- Fama, E. F. (1970). "Efficient Capital Markets: A Review of Theory and Empirical Work." *Journal of Finance*.
|
| 243 |
+
- Raffel, C. et al. (2020). "Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer." *JMLR*.
|
| 244 |
+
- Vaswani, A. et al. (2017). "Attention Is All You Need." *NeurIPS 2017*.
|
| 245 |
+
- Woo, G. et al. (2024). "MOIRAI: Time Series Foundation Models for Universal Forecasting." *ICLR 2024*.
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
## 9. Licencia
|
| 250 |
+
|
| 251 |
+
Uso académico y personal. Los datos de mercado son proporcionados por Yahoo Finance y CoinGecko bajo sus respectivos términos de servicio.
|
docs/parametros_impacto.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Parámetros de Mercado para Predicción de Bitcoin
|
| 2 |
+
## Rankeados de Mayor a Menor Impacto Potencial
|
| 3 |
+
|
| 4 |
+
### Impacto MUY ALTO (MACRO + CORRELACIÓN DIRECTA)
|
| 5 |
+
| # | Parámetro | Fuente API | Costo | Cómo obtenerlo |
|
| 6 |
+
|---|---|---|---|---|
|
| 7 |
+
| 1 | **S&P 500 (^GSPC)** | Yahoo Finance (`yfinance`) | Gratis | `yf.download("^GSPC")` |
|
| 8 |
+
| 2 | **DXY — Índice Dólar (DX-Y.NYB)** | Yahoo Finance (`yfinance`) | Gratis | `yf.download("DX-Y.NYB")` |
|
| 9 |
+
| 3 | **Oro (GC=F)** | Yahoo Finance (`yfinance`) | Gratis | `yf.download("GC=F")` |
|
| 10 |
+
| 4 | **Tasa 10 años USA (^TNX)** | Yahoo Finance (`yfinance`) | Gratis | `yf.download("^TNX")` |
|
| 11 |
+
| 5 | **Volumen BTC en exchanges** | CoinGecko API | Gratis | `requests.get("https://api.coingecko.com/api/v3/coins/bitcoin/tickers")` |
|
| 12 |
+
|
| 13 |
+
### Impacto ALTO (ON-CHAIN + SENTIMIENTO)
|
| 14 |
+
| # | Parámetro | Fuente API | Costo | Cómo obtenerlo |
|
| 15 |
+
|---|---|---|---|---|
|
| 16 |
+
| 6 | **Hash Rate (Poder minero)** | Blockchain.com | Gratis | `requests.get("https://api.blockchain.info/charts/hash-rate")` |
|
| 17 |
+
| 7 | **Direcciones Activas** | Blockchain.com | Gratis | `requests.get("https://api.blockchain.info/charts/n-active-addresses")` |
|
| 18 |
+
| 8 | **Fear & Greed Index** | alternative.me | Gratis | `requests.get("https://api.alternative.me/fng/")` |
|
| 19 |
+
| 9 | **Reservas BTC en Exchanges** | Coin Metrics / Glassnode | Limitado/Paid | Glassnode Studio API |
|
| 20 |
+
| 10 | **M2 Money Supply (Liquidez Global)** | FRED API | Gratis | `fred.get_series("M2SL")` |
|
| 21 |
+
|
| 22 |
+
### Impacto MEDIO (MACRO + TÉCNICO)
|
| 23 |
+
| # | Parámetro | Fuente API | Costo | Cómo obtenerlo |
|
| 24 |
+
|---|---|---|---|---|
|
| 25 |
+
| 11 | **CPI / Inflación** | FRED API | Gratis | `fred.get_series("CPIAUCSL")` |
|
| 26 |
+
| 12 | **VIX (Índice de Miedo)** | Yahoo Finance | Gratis | `yf.download("^VIX")` |
|
| 27 |
+
| 13 | **Open Interest Futuros** | CoinGlass / Binance API | Gratis | Web scraping CoinGlass |
|
| 28 |
+
| 14 | **Funding Rate Perpetuo** | Binance/Bybit API | Gratis | WebSocket REST de Bybit |
|
| 29 |
+
| 15 | **Google Trends "Bitcoin"** | pytrends | Gratis | `pytrends.trending_searches()` |
|
| 30 |
+
| 16 | **Transacciones On-Chain** | Blockchain.com | Gratis | `api.blockchain.info/charts/n-transactions` |
|
| 31 |
+
|
| 32 |
+
### Impacto BAJO (ESPECULATIVO + SOCIAL)
|
| 33 |
+
| # | Parámetro | Fuente API | Costo | Cómo obtenerlo |
|
| 34 |
+
|---|---|---|---|---|
|
| 35 |
+
| 17 | **Reddit r/bitcoin menciones** | Pushshift API | Gratis | `api.pushshift.io/reddit/submission/search` |
|
| 36 |
+
| 18 | **Twitter/X volumen** | Twitter API v2 | Limitado | `tweepy` con dev account |
|
| 37 |
+
| 19 | **Próximo Halving** | Calculado con block height | Gratis | Fijo cada 210,000 bloques |
|
| 38 |
+
| 20 | **Stock-to-Flow Ratio** | Calculado | Gratis | `block_reward / circulating_supply` |
|
| 39 |
+
| 21 | **Miner Revenue** | Blockchain.com | Gratis | `api.blockchain.info/charts/miners-revenue` |
|
| 40 |
+
| 22 | **Bitcoin Dominance** | CoinGecko API | Gratis | `api.coingecko.com/api/v3/global` |
|
| 41 |
+
| 23 | **Tamaño Promedio Bloque** | Blockchain.com | Gratis | `api.blockchain.info/charts/avg-block-size` |
|
| 42 |
+
| 24 | **Número de Wallets** | Blockchain.com | Gratis | `api.blockchain.info/charts/n-unique-addresses` |
|
| 43 |
+
| 25 | **Velocidad del Dinero BTC** | Coin Metrics | Limitado | Coin Metrics API |
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
### Impacto VARIABLE (EVENTOS DISCRETOS — Hackeos/Seguridad)
|
| 47 |
+
| # | Parámetro | Fuente API | Costo | Cómo obtenerlo |
|
| 48 |
+
|---|---|---|---|---|
|
| 49 |
+
| 26 | **Días desde último hack > $100M** | DeFiLlama / rekt.news | Gratis | Web scraping `api.llama.fi/hacks` → filtrar por BTC/ETH |
|
| 50 |
+
| 27 | **Monto total robado últimos 30 días** | DeFiLlama API | Gratis | Suma de montos de hacks recientes |
|
| 51 |
+
| 28 | **Flag de hack activo (0/1)** | Calculado | Gratis | 1 si hay un exploit activo en las últimas 48h |
|
| 52 |
+
| 29 | **Número de exchanges comprometidos** | DeFiLlama API | Gratis | Conteo de plataformas afectadas en el mes |
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.57.0
|
| 2 |
+
streamlit-autorefresh==1.0.1
|
| 3 |
+
yfinance==1.3.0
|
| 4 |
+
pandas==2.1.4
|
| 5 |
+
numpy==1.26.4
|
| 6 |
+
plotly==6.7.0
|
| 7 |
+
torch==2.12.0
|
| 8 |
+
chronos-forecasting==2.2.2
|
| 9 |
+
scikit-learn==1.7.2
|
| 10 |
+
requests==2.34.2
|
streamlit_app.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BTC Forecast V4 — Streamlit Cloud entry point.
|
| 3 |
+
"""
|
| 4 |
+
import sys, os
|
| 5 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'versions', 'hybrid_v4'))
|
| 6 |
+
|
| 7 |
+
from app_v4 import main
|
| 8 |
+
|
| 9 |
+
if __name__ == '__main__':
|
| 10 |
+
main()
|
versions/hybrid_v4/app_v4.py
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
V4 Interactive Dashboard — Streamlit + Plotly
|
| 3 |
+
==============================================
|
| 4 |
+
BTC price history, backtest (N-day walk-forward), and forward forecast
|
| 5 |
+
with real-time monitoring. Bilingual EN/ES.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os, sys, numpy as np, pandas as pd, yfinance as yf, torch
|
| 9 |
+
import requests, pickle, time, threading
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
import streamlit as st
|
| 12 |
+
from streamlit_autorefresh import st_autorefresh
|
| 13 |
+
import plotly.graph_objects as go
|
| 14 |
+
from plotly.subplots import make_subplots
|
| 15 |
+
|
| 16 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 17 |
+
CACHE_DIR = os.path.join(PROJECT_ROOT, "cache")
|
| 18 |
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
| 19 |
+
|
| 20 |
+
HISTORY_DAYS = 1000
|
| 21 |
+
MAX_FORECAST = 60
|
| 22 |
+
MAX_BACKTEST = 60
|
| 23 |
+
|
| 24 |
+
MODELS = {
|
| 25 |
+
'Chronos-T5-Tiny': 'amazon/chronos-t5-tiny',
|
| 26 |
+
'Chronos-T5-Small': 'amazon/chronos-t5-small',
|
| 27 |
+
'Chronos-T5-Base': 'amazon/chronos-t5-base',
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
DEFAULT_MODEL = 'Chronos-T5-Tiny'
|
| 31 |
+
|
| 32 |
+
LANG = {
|
| 33 |
+
'en': {
|
| 34 |
+
'page_title': 'BTC Forecast V4',
|
| 35 |
+
'controls': 'Controls',
|
| 36 |
+
'model': 'Chronos Model',
|
| 37 |
+
'forward': 'Forward',
|
| 38 |
+
'forecast_days': 'Forecast days',
|
| 39 |
+
'backtest': 'Backtest',
|
| 40 |
+
'backtest_window': 'Backtest window (days)',
|
| 41 |
+
'refresh_data': 'Refresh data',
|
| 42 |
+
'legend': 'Legend',
|
| 43 |
+
'legend_blue': 'BTC actual price',
|
| 44 |
+
'legend_red': 'Chronos forecast (median)',
|
| 45 |
+
'legend_shaded': 'P10-P90 band',
|
| 46 |
+
'legend_points': 'Backtest (<2% / 2-5% / >5% error)',
|
| 47 |
+
'insufficient_data': 'Insufficient data.',
|
| 48 |
+
'market_context': 'Market Context',
|
| 49 |
+
'tab_forward': 'Forward',
|
| 50 |
+
'tab_backtest': 'Backtest',
|
| 51 |
+
'tab_combined': 'Combined',
|
| 52 |
+
'tab_live': 'Live',
|
| 53 |
+
'forecast_n_days': 'Forecast {} days ahead',
|
| 54 |
+
'backtest_n_days': 'Backtest: last {} days',
|
| 55 |
+
'mape': 'MAPE',
|
| 56 |
+
'hits': 'Hits (<2%)',
|
| 57 |
+
'medium': 'Medium (2-5%)',
|
| 58 |
+
'misses': 'Misses (>5%)',
|
| 59 |
+
'daily_breakdown': 'Daily Breakdown',
|
| 60 |
+
'combined_title': 'Combined: {}d backtest + {}d forecast',
|
| 61 |
+
'current_price': 'Current Price',
|
| 62 |
+
'backtest_mape': 'Backtest MAPE ({:d}d)',
|
| 63 |
+
'forecast_n': 'Forecast +{:d}d',
|
| 64 |
+
'forecast_1d': 'Forecast +1d',
|
| 65 |
+
'waiting_data': 'Waiting for live data...',
|
| 66 |
+
'language': 'Language',
|
| 67 |
+
'updated': 'Updated',
|
| 68 |
+
'change_pct': 'Change %',
|
| 69 |
+
'day': 'Day',
|
| 70 |
+
'date': 'Date',
|
| 71 |
+
'forecast': 'Forecast',
|
| 72 |
+
'actual': 'Actual',
|
| 73 |
+
'prediction': 'Prediction',
|
| 74 |
+
'error': 'Error %',
|
| 75 |
+
},
|
| 76 |
+
'es': {
|
| 77 |
+
'page_title': 'BTC Pronostico V4',
|
| 78 |
+
'controls': 'Controles',
|
| 79 |
+
'model': 'Modelo Chronos',
|
| 80 |
+
'forward': 'Futuro',
|
| 81 |
+
'forecast_days': 'Dias de pronostico',
|
| 82 |
+
'backtest': 'Backtest',
|
| 83 |
+
'backtest_window': 'Ventana de backtest (dias)',
|
| 84 |
+
'refresh_data': 'Actualizar datos',
|
| 85 |
+
'legend': 'Leyenda',
|
| 86 |
+
'legend_blue': 'Precio real BTC',
|
| 87 |
+
'legend_red': 'Pronostico Chronos (mediana)',
|
| 88 |
+
'legend_shaded': 'Banda P10-P90',
|
| 89 |
+
'legend_points': 'Backtest (error <2% / 2-5% / >5%)',
|
| 90 |
+
'insufficient_data': 'Datos insuficientes.',
|
| 91 |
+
'market_context': 'Contexto de Mercado',
|
| 92 |
+
'tab_forward': 'Futuro',
|
| 93 |
+
'tab_backtest': 'Backtest',
|
| 94 |
+
'tab_combined': 'Combinado',
|
| 95 |
+
'tab_live': 'En vivo',
|
| 96 |
+
'forecast_n_days': 'Pronostico {} dias adelante',
|
| 97 |
+
'backtest_n_days': 'Backtest: ultimos {} dias',
|
| 98 |
+
'mape': 'MAPE',
|
| 99 |
+
'hits': 'Aciertos (<2%)',
|
| 100 |
+
'medium': 'Medio (2-5%)',
|
| 101 |
+
'misses': 'Fallos (>5%)',
|
| 102 |
+
'daily_breakdown': 'Desglose diario',
|
| 103 |
+
'combined_title': 'Combinado: {}d backtest + {}d pronostico',
|
| 104 |
+
'current_price': 'Precio actual',
|
| 105 |
+
'backtest_mape': 'MAPE backtest ({:d}d)',
|
| 106 |
+
'forecast_n': 'Pronostico +{:d}d',
|
| 107 |
+
'forecast_1d': 'Pronostico +1d',
|
| 108 |
+
'waiting_data': 'Esperando datos en vivo...',
|
| 109 |
+
'language': 'Idioma',
|
| 110 |
+
'updated': 'Actualizado',
|
| 111 |
+
'change_pct': 'Cambio %',
|
| 112 |
+
'day': 'Dia',
|
| 113 |
+
'date': 'Fecha',
|
| 114 |
+
'forecast': 'Pronostico',
|
| 115 |
+
'actual': 'Real',
|
| 116 |
+
'prediction': 'Prediccion',
|
| 117 |
+
'error': 'Error %',
|
| 118 |
+
},
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
st.set_page_config(
|
| 122 |
+
page_title="BTC Forecast V4",
|
| 123 |
+
page_icon="\u20bf",
|
| 124 |
+
layout="wide",
|
| 125 |
+
initial_sidebar_state="expanded",
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
st.markdown("""
|
| 129 |
+
<style>
|
| 130 |
+
.stApp { background-color: #0e1117; }
|
| 131 |
+
.block-container { padding-top: 1rem; }
|
| 132 |
+
h1, h2, h3 { color: #f0f0f0 !important; }
|
| 133 |
+
.stTabs [data-baseweb="tab"] { font-size: 15px; }
|
| 134 |
+
.metric-card {
|
| 135 |
+
background: #1a1d23; border-radius: 10px; padding: 12px;
|
| 136 |
+
border: 1px solid #2d3139; text-align: center;
|
| 137 |
+
}
|
| 138 |
+
.metric-value { font-size: 22px; font-weight: bold; color: #f0f0f0; }
|
| 139 |
+
.metric-label { font-size: 11px; color: #8b8fa3; }
|
| 140 |
+
.live-dot {
|
| 141 |
+
display: inline-block; width: 10px; height: 10px;
|
| 142 |
+
border-radius: 50%; background: #52b788;
|
| 143 |
+
animation: pulse 1.5s infinite; margin-right: 6px;
|
| 144 |
+
}
|
| 145 |
+
@keyframes pulse {
|
| 146 |
+
0% { opacity: 1; transform: scale(1); }
|
| 147 |
+
50% { opacity: 0.5; transform: scale(1.3); }
|
| 148 |
+
100% { opacity: 1; transform: scale(1); }
|
| 149 |
+
}
|
| 150 |
+
</style>
|
| 151 |
+
""", unsafe_allow_html=True)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# ===================== PIPELINE LOADING =====================
|
| 155 |
+
|
| 156 |
+
@st.cache_resource(show_spinner=False)
|
| 157 |
+
def load_chronos_pipeline(model_name):
|
| 158 |
+
from chronos import ChronosPipeline
|
| 159 |
+
hf_name = MODELS[model_name]
|
| 160 |
+
return ChronosPipeline.from_pretrained(
|
| 161 |
+
hf_name, device_map="cpu", dtype=torch.float32,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@st.cache_data(ttl=600, show_spinner=False)
|
| 166 |
+
def load_btc_data():
|
| 167 |
+
end = datetime.now()
|
| 168 |
+
start = end - timedelta(days=HISTORY_DAYS)
|
| 169 |
+
result = []
|
| 170 |
+
def _dl():
|
| 171 |
+
try:
|
| 172 |
+
btc = yf.download('BTC-USD', start=start.strftime('%Y-%m-%d'),
|
| 173 |
+
end=end.strftime('%Y-%m-%d'), progress=False)
|
| 174 |
+
if not btc.empty:
|
| 175 |
+
if isinstance(btc.columns, pd.MultiIndex):
|
| 176 |
+
btc.columns = btc.columns.droplevel(1)
|
| 177 |
+
result.append(btc['Close'])
|
| 178 |
+
except:
|
| 179 |
+
pass
|
| 180 |
+
t = threading.Thread(target=_dl, daemon=True)
|
| 181 |
+
t.start()
|
| 182 |
+
t.join(timeout=30)
|
| 183 |
+
return result[0] if result else pd.Series(dtype=float)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def fetch_intraday_data():
|
| 187 |
+
result = {'15m': None, '1h': None}
|
| 188 |
+
def _dl_15m():
|
| 189 |
+
try:
|
| 190 |
+
d = yf.download('BTC-USD', period='3d', interval='15m', progress=False)
|
| 191 |
+
if not d.empty:
|
| 192 |
+
if isinstance(d.columns, pd.MultiIndex):
|
| 193 |
+
d.columns = d.columns.droplevel(1)
|
| 194 |
+
result['15m'] = d
|
| 195 |
+
except:
|
| 196 |
+
pass
|
| 197 |
+
def _dl_1h():
|
| 198 |
+
try:
|
| 199 |
+
d = yf.download('BTC-USD', period='3d', interval='1h', progress=False)
|
| 200 |
+
if not d.empty:
|
| 201 |
+
if isinstance(d.columns, pd.MultiIndex):
|
| 202 |
+
d.columns = d.columns.droplevel(1)
|
| 203 |
+
result['1h'] = d
|
| 204 |
+
except:
|
| 205 |
+
pass
|
| 206 |
+
t1 = threading.Thread(target=_dl_15m, daemon=True)
|
| 207 |
+
t2 = threading.Thread(target=_dl_1h, daemon=True)
|
| 208 |
+
t1.start()
|
| 209 |
+
t2.start()
|
| 210 |
+
t1.join(timeout=15)
|
| 211 |
+
t2.join(timeout=15)
|
| 212 |
+
return result
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def compute_hourly_volatility(hourly_df):
|
| 216 |
+
ret = hourly_df['Close'].pct_change().dropna()
|
| 217 |
+
return ret.std() * 100 if len(ret) > 1 else 0.0
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def fetch_live_context():
|
| 221 |
+
ctx = {}
|
| 222 |
+
btc_data = []
|
| 223 |
+
def _dl_btc():
|
| 224 |
+
try:
|
| 225 |
+
d = yf.download('BTC-USD', period='5d', progress=False)
|
| 226 |
+
if not d.empty:
|
| 227 |
+
btc_data.append(d)
|
| 228 |
+
except:
|
| 229 |
+
pass
|
| 230 |
+
t = threading.Thread(target=_dl_btc, daemon=True)
|
| 231 |
+
t.start()
|
| 232 |
+
t.join(timeout=15)
|
| 233 |
+
if btc_data:
|
| 234 |
+
btc = btc_data[0]
|
| 235 |
+
if isinstance(btc.columns, pd.MultiIndex):
|
| 236 |
+
btc.columns = btc.columns.droplevel(1)
|
| 237 |
+
ctx['BTC Price'] = f"${btc['Close'].iloc[-1]:,.2f}"
|
| 238 |
+
ctx['24h Change'] = f"{btc['Close'].pct_change().iloc[-1] * 100:+.2f}%"
|
| 239 |
+
ctx['Volume 24h'] = f"${btc['Volume'].iloc[-1]:,.0f}"
|
| 240 |
+
else:
|
| 241 |
+
ctx['BTC Price'] = ctx.get('BTC Price', 'N/A')
|
| 242 |
+
try:
|
| 243 |
+
fng = requests.get('https://api.alternative.me/fng/?limit=1', timeout=5).json()
|
| 244 |
+
if 'data' in fng and len(fng['data']) > 0:
|
| 245 |
+
ctx['Fear & Greed'] = f"{fng['data'][0]['value']}/100 ({fng['data'][0]['value_classification']})"
|
| 246 |
+
except:
|
| 247 |
+
ctx['Fear & Greed'] = 'N/A'
|
| 248 |
+
for tk, name in [('^GSPC', 'S&P 500'), ('GC=F', 'Gold'), ('DX-Y.NYB', 'DXY')]:
|
| 249 |
+
ticker_data = []
|
| 250 |
+
def _dl_tk():
|
| 251 |
+
try:
|
| 252 |
+
d = yf.download(tk, period='5d', progress=False)
|
| 253 |
+
if not d.empty:
|
| 254 |
+
ticker_data.append(d)
|
| 255 |
+
except:
|
| 256 |
+
pass
|
| 257 |
+
t = threading.Thread(target=_dl_tk, daemon=True)
|
| 258 |
+
t.start()
|
| 259 |
+
t.join(timeout=10)
|
| 260 |
+
if ticker_data:
|
| 261 |
+
df = ticker_data[0]
|
| 262 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 263 |
+
df.columns = df.columns.droplevel(1)
|
| 264 |
+
v = df['Close'].iloc[-1]
|
| 265 |
+
c = df['Close'].pct_change().iloc[-1] * 100
|
| 266 |
+
ctx[name] = f"{v:,.2f} ({c:+.2f}%)"
|
| 267 |
+
else:
|
| 268 |
+
ctx[name] = 'N/A'
|
| 269 |
+
ctx['Time'] = datetime.now().strftime('%H:%M:%S')
|
| 270 |
+
return ctx
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
# ===================== PREDICTIONS =====================
|
| 274 |
+
|
| 275 |
+
@st.cache_data(ttl=3600, show_spinner=False)
|
| 276 |
+
def run_backtest(_price, model_name, n_days):
|
| 277 |
+
pipeline = load_chronos_pipeline(model_name)
|
| 278 |
+
days, preds, actuals = [], [], []
|
| 279 |
+
for i in range(n_days, 0, -1):
|
| 280 |
+
train_end = len(_price) - i
|
| 281 |
+
train_data = _price.iloc[:train_end]
|
| 282 |
+
context = torch.tensor(train_data.values, dtype=torch.float32).squeeze().unsqueeze(0)
|
| 283 |
+
forecast = pipeline.predict(context, prediction_length=1, num_samples=20)
|
| 284 |
+
pred = float(np.quantile(forecast[0].numpy(), 0.5, axis=0)[0])
|
| 285 |
+
days.append(_price.index[train_end])
|
| 286 |
+
preds.append(pred)
|
| 287 |
+
actuals.append(float(_price.iloc[train_end]))
|
| 288 |
+
return pd.DataFrame({'Day': days, 'Prediction': preds, 'Actual': actuals})
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
@st.cache_data(ttl=3600, show_spinner=False)
|
| 292 |
+
def run_forecast(_price, model_name, horizon):
|
| 293 |
+
pipeline = load_chronos_pipeline(model_name)
|
| 294 |
+
context = torch.tensor(_price.values, dtype=torch.float32).squeeze().unsqueeze(0)
|
| 295 |
+
forecast = pipeline.predict(context, prediction_length=horizon, num_samples=100)
|
| 296 |
+
samples = forecast[0].numpy()
|
| 297 |
+
median = np.median(samples, axis=0)
|
| 298 |
+
p10 = np.percentile(samples, 10, axis=0)
|
| 299 |
+
p90 = np.percentile(samples, 90, axis=0)
|
| 300 |
+
return median, p10, p90
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
# ===================== UI =====================
|
| 304 |
+
|
| 305 |
+
def main():
|
| 306 |
+
now = datetime.now()
|
| 307 |
+
|
| 308 |
+
if 'lang' not in st.session_state:
|
| 309 |
+
st.session_state['lang'] = 'es'
|
| 310 |
+
|
| 311 |
+
L = LANG[st.session_state['lang']]
|
| 312 |
+
|
| 313 |
+
with st.sidebar:
|
| 314 |
+
st.markdown(f"### {L['controls']}")
|
| 315 |
+
|
| 316 |
+
lang_opts = {'en': 'English', 'es': 'Espa\u00f1ol'}
|
| 317 |
+
lang_sel = st.selectbox(
|
| 318 |
+
L['language'],
|
| 319 |
+
list(lang_opts.keys()),
|
| 320 |
+
format_func=lambda k: lang_opts[k],
|
| 321 |
+
index=list(lang_opts.keys()).index(st.session_state['lang']),
|
| 322 |
+
)
|
| 323 |
+
if lang_sel != st.session_state['lang']:
|
| 324 |
+
st.session_state['lang'] = lang_sel
|
| 325 |
+
st.rerun()
|
| 326 |
+
|
| 327 |
+
st.markdown("---")
|
| 328 |
+
|
| 329 |
+
modelo = st.selectbox(
|
| 330 |
+
L['model'],
|
| 331 |
+
list(MODELS.keys()),
|
| 332 |
+
index=list(MODELS.keys()).index(DEFAULT_MODEL),
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
st.markdown(f"**{L['backtest']}**")
|
| 336 |
+
bt_dias = st.slider(L['backtest_window'], 1, MAX_BACKTEST, 10, 1)
|
| 337 |
+
|
| 338 |
+
st.divider()
|
| 339 |
+
|
| 340 |
+
if st.button(L['refresh_data'], width='stretch', type="primary"):
|
| 341 |
+
st.cache_resource.clear()
|
| 342 |
+
st.cache_data.clear()
|
| 343 |
+
st.rerun()
|
| 344 |
+
|
| 345 |
+
st.divider()
|
| 346 |
+
st.markdown(f"**{L['legend']}**")
|
| 347 |
+
st.markdown(f"""
|
| 348 |
+
- **{L['legend_blue']}**
|
| 349 |
+
- **{L['legend_red']}**
|
| 350 |
+
- **{L['legend_shaded']}**
|
| 351 |
+
- **{L['legend_points']}**
|
| 352 |
+
""")
|
| 353 |
+
|
| 354 |
+
price = load_btc_data()
|
| 355 |
+
if len(price) < 100:
|
| 356 |
+
st.error(L['insufficient_data'])
|
| 357 |
+
return
|
| 358 |
+
|
| 359 |
+
last_date = price.index[-1]
|
| 360 |
+
last_price = float(price.iloc[-1])
|
| 361 |
+
|
| 362 |
+
st.title("BTC Forecast V4")
|
| 363 |
+
st.markdown(f"<p style='color:#8b8fa3; margin-top:-10px;'>"
|
| 364 |
+
f"{L['model']}: {modelo} | "
|
| 365 |
+
f"{L['updated']}: {now.strftime('%Y-%m-%d %H:%M')}</p>",
|
| 366 |
+
unsafe_allow_html=True)
|
| 367 |
+
|
| 368 |
+
bt_df = run_backtest(price, modelo, bt_dias)
|
| 369 |
+
forecast_data = run_forecast(price, modelo, MAX_FORECAST)
|
| 370 |
+
median, p10, p90 = forecast_data
|
| 371 |
+
future_dates = pd.date_range(
|
| 372 |
+
start=last_date + timedelta(days=1), periods=MAX_FORECAST, freq='D'
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
context = fetch_live_context()
|
| 376 |
+
st.subheader(L['market_context'])
|
| 377 |
+
cols = st.columns(len(context))
|
| 378 |
+
for i, (k, v) in enumerate(context.items()):
|
| 379 |
+
with cols[i]:
|
| 380 |
+
cl = 'color: #52b788;' if k == 'Time' else ''
|
| 381 |
+
st.markdown(
|
| 382 |
+
f'<div class="metric-card">'
|
| 383 |
+
f'<div class="metric-label">{k}</div>'
|
| 384 |
+
f'<div class="metric-value" style="{cl}">{v}</div>'
|
| 385 |
+
f'</div>',
|
| 386 |
+
unsafe_allow_html=True,
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
tab_f, tab_b, tab_c, tab_l = st.tabs(
|
| 390 |
+
[L['tab_forward'], L['tab_backtest'], L['tab_combined'], L['tab_live']]
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# ----- TAB 1: FORWARD -----
|
| 394 |
+
with tab_f:
|
| 395 |
+
fwd_dias = st.slider(L['forecast_days'], 1, MAX_FORECAST, 10, 1, key='fwd')
|
| 396 |
+
col_p1, col_p2, col_p3 = st.columns(3)
|
| 397 |
+
with col_p1:
|
| 398 |
+
if st.button("1d", key='f1', width='stretch'): fwd_dias = 1
|
| 399 |
+
with col_p2:
|
| 400 |
+
if st.button("7d", key='f7', width='stretch'): fwd_dias = 7
|
| 401 |
+
with col_p3:
|
| 402 |
+
if st.button("30d", key='f30', width='stretch'): fwd_dias = 30
|
| 403 |
+
st.subheader(L['forecast_n_days'].format(fwd_dias))
|
| 404 |
+
|
| 405 |
+
fig_f = go.Figure()
|
| 406 |
+
ctx = price.iloc[-min(90, len(price)):]
|
| 407 |
+
fig_f.add_trace(go.Scatter(
|
| 408 |
+
x=ctx.index, y=ctx.values, mode='lines', name='BTC History',
|
| 409 |
+
line=dict(color='#457b9d', width=2),
|
| 410 |
+
))
|
| 411 |
+
fig_f.add_trace(go.Scatter(
|
| 412 |
+
x=future_dates[:fwd_dias], y=p90[:fwd_dias],
|
| 413 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'), showlegend=False,
|
| 414 |
+
))
|
| 415 |
+
fig_f.add_trace(go.Scatter(
|
| 416 |
+
x=future_dates[:fwd_dias], y=median[:fwd_dias],
|
| 417 |
+
mode='lines+markers', name=f'{modelo} (median)',
|
| 418 |
+
line=dict(color='#e63946', width=3), marker=dict(size=5),
|
| 419 |
+
))
|
| 420 |
+
fig_f.add_trace(go.Scatter(
|
| 421 |
+
x=future_dates[:fwd_dias], y=p10[:fwd_dias],
|
| 422 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'),
|
| 423 |
+
fill='tonexty', fillcolor='rgba(230,57,70,0.12)',
|
| 424 |
+
name='P10-P90',
|
| 425 |
+
))
|
| 426 |
+
fig_f.add_vline(x=last_date, line_dash='dot', line_color='gray', opacity=0.5)
|
| 427 |
+
fig_f.update_layout(
|
| 428 |
+
template='plotly_dark', hovermode='x unified', height=450,
|
| 429 |
+
margin=dict(l=20, r=20, t=20, b=20),
|
| 430 |
+
xaxis=dict(title='Date'), yaxis=dict(title='BTC Price (USD)', tickformat='$,.0f'),
|
| 431 |
+
legend=dict(orientation='h', y=1.02),
|
| 432 |
+
)
|
| 433 |
+
st.plotly_chart(fig_f, width='stretch')
|
| 434 |
+
|
| 435 |
+
tbl = pd.DataFrame({
|
| 436 |
+
L['day']: range(1, fwd_dias + 1),
|
| 437 |
+
L['date']: future_dates[:fwd_dias].strftime('%Y-%m-%d'),
|
| 438 |
+
L['forecast']: [f"${v:,.2f}" for v in median[:fwd_dias]],
|
| 439 |
+
'P10': [f"${v:,.2f}" for v in p10[:fwd_dias]],
|
| 440 |
+
'P90': [f"${v:,.2f}" for v in p90[:fwd_dias]],
|
| 441 |
+
L['change_pct']: [f"{(v - last_price) / last_price * 100:+.2f}%" for v in median[:fwd_dias]],
|
| 442 |
+
})
|
| 443 |
+
st.dataframe(tbl, width='stretch', hide_index=True)
|
| 444 |
+
|
| 445 |
+
# ----- TAB 2: BACKTEST -----
|
| 446 |
+
with tab_b:
|
| 447 |
+
st.subheader(L['backtest_n_days'].format(bt_dias))
|
| 448 |
+
|
| 449 |
+
errors = abs(bt_df['Prediction'] - bt_df['Actual']) / bt_df['Actual'] * 100
|
| 450 |
+
overall_mape = errors.mean()
|
| 451 |
+
bt_df[L['error']] = errors.round(2)
|
| 452 |
+
|
| 453 |
+
fig_b = go.Figure()
|
| 454 |
+
fig_b.add_trace(go.Scatter(
|
| 455 |
+
x=bt_df['Day'], y=bt_df['Actual'],
|
| 456 |
+
mode='lines+markers', name=L['actual'],
|
| 457 |
+
line=dict(color='#e63946', width=3), marker=dict(size=9),
|
| 458 |
+
))
|
| 459 |
+
fig_b.add_trace(go.Scatter(
|
| 460 |
+
x=bt_df['Day'], y=bt_df['Prediction'],
|
| 461 |
+
mode='lines+markers', name=modelo,
|
| 462 |
+
line=dict(color='#457b9d', width=3, dash='dash'), marker=dict(size=9),
|
| 463 |
+
))
|
| 464 |
+
for _, row in bt_df.iterrows():
|
| 465 |
+
e = row[L['error']]
|
| 466 |
+
c = '#2a9d8f' if e < 2 else '#e9c46a' if e < 5 else '#e63946'
|
| 467 |
+
fig_b.add_shape(type='line', x0=row['Day'], x1=row['Day'],
|
| 468 |
+
y0=row['Actual'], y1=row['Prediction'],
|
| 469 |
+
line=dict(color=c, width=2, dash='dot'))
|
| 470 |
+
fig_b.update_layout(
|
| 471 |
+
template='plotly_dark', hovermode='x unified', height=420,
|
| 472 |
+
margin=dict(l=20, r=20, t=20, b=20),
|
| 473 |
+
xaxis=dict(title='Date'), yaxis=dict(title='BTC Price (USD)', tickformat='$,.0f'),
|
| 474 |
+
legend=dict(orientation='h', y=1.02),
|
| 475 |
+
)
|
| 476 |
+
st.plotly_chart(fig_b, width='stretch')
|
| 477 |
+
|
| 478 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 479 |
+
c1.metric(L['mape'], f"{overall_mape:.2f}%")
|
| 480 |
+
c2.metric(L['hits'], f"{(errors < 2).sum()}/{bt_dias}")
|
| 481 |
+
c3.metric(L['medium'], f"{((errors >= 2) & (errors < 5)).sum()}/{bt_dias}")
|
| 482 |
+
c4.metric(L['misses'], f"{(errors >= 5).sum()}/{bt_dias}")
|
| 483 |
+
|
| 484 |
+
st.subheader(L['daily_breakdown'])
|
| 485 |
+
tbl_b = bt_df.copy()
|
| 486 |
+
tbl_b['Day'] = tbl_b['Day'].dt.strftime('%Y-%m-%d')
|
| 487 |
+
tbl_b[L['prediction']] = tbl_b['Prediction'].apply(lambda v: f"${v:,.2f}")
|
| 488 |
+
tbl_b[L['actual']] = tbl_b['Actual'].apply(lambda v: f"${v:,.2f}")
|
| 489 |
+
st.dataframe(tbl_b, width='stretch', hide_index=True)
|
| 490 |
+
|
| 491 |
+
# ----- TAB 3: COMBINED -----
|
| 492 |
+
with tab_c:
|
| 493 |
+
com_dias = st.slider(L['forecast_days'], 1, MAX_FORECAST, 10, 1, key='com')
|
| 494 |
+
st.subheader(L['combined_title'].format(bt_dias, com_dias))
|
| 495 |
+
|
| 496 |
+
fig_c = go.Figure()
|
| 497 |
+
ctx = price.iloc[-min(150, len(price)):]
|
| 498 |
+
fig_c.add_trace(go.Scatter(
|
| 499 |
+
x=ctx.index, y=ctx.values, mode='lines', name='BTC History',
|
| 500 |
+
line=dict(color='#457b9d', width=1.5),
|
| 501 |
+
))
|
| 502 |
+
for _, row in bt_df.iterrows():
|
| 503 |
+
e = abs(row['Prediction'] - row['Actual']) / row['Actual'] * 100
|
| 504 |
+
c = '#2a9d8f' if e < 2 else '#e9c46a' if e < 5 else '#e63946'
|
| 505 |
+
fig_c.add_trace(go.Scatter(
|
| 506 |
+
x=[row['Day']], y=[row['Prediction']],
|
| 507 |
+
mode='markers', marker=dict(size=10, color=c, symbol='x'),
|
| 508 |
+
showlegend=False,
|
| 509 |
+
hovertemplate=(f"<b>{row['Day'].strftime('%Y-%m-%d')}</b><br>"
|
| 510 |
+
f"Pred: ${row['Prediction']:,.2f}<br>"
|
| 511 |
+
f"Actual: ${row['Actual']:,.2f}<br>"
|
| 512 |
+
f"Error: {e:.2f}%<extra></extra>"),
|
| 513 |
+
))
|
| 514 |
+
fig_c.add_trace(go.Scatter(
|
| 515 |
+
x=future_dates[:com_dias], y=p90[:com_dias],
|
| 516 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'), showlegend=False,
|
| 517 |
+
))
|
| 518 |
+
fig_c.add_trace(go.Scatter(
|
| 519 |
+
x=future_dates[:com_dias], y=median[:com_dias],
|
| 520 |
+
mode='lines+markers', name=f'Forecast {com_dias}d (median)',
|
| 521 |
+
line=dict(color='#e63946', width=3), marker=dict(size=5),
|
| 522 |
+
))
|
| 523 |
+
fig_c.add_trace(go.Scatter(
|
| 524 |
+
x=future_dates[:com_dias], y=p10[:com_dias],
|
| 525 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'),
|
| 526 |
+
fill='tonexty', fillcolor='rgba(230,57,70,0.12)',
|
| 527 |
+
name=f'P10-P90 ({com_dias}d)',
|
| 528 |
+
))
|
| 529 |
+
fig_c.add_vline(x=last_date, line_dash='dot', line_color='gray', opacity=0.5)
|
| 530 |
+
fig_c.update_layout(
|
| 531 |
+
template='plotly_dark', hovermode='x unified', height=500,
|
| 532 |
+
margin=dict(l=20, r=20, t=20, b=20),
|
| 533 |
+
xaxis=dict(title='Date'), yaxis=dict(title='BTC Price (USD)', tickformat='$,.0f'),
|
| 534 |
+
legend=dict(orientation='h', y=1.02),
|
| 535 |
+
)
|
| 536 |
+
st.plotly_chart(fig_c, width='stretch')
|
| 537 |
+
|
| 538 |
+
c1, c2, c3 = st.columns(3)
|
| 539 |
+
c1.metric(L['current_price'], f"${last_price:,.2f}", context.get('24h Change', ''))
|
| 540 |
+
c2.metric(L['backtest_mape'].format(bt_dias), f"{overall_mape:.2f}%")
|
| 541 |
+
cambio_f = (median[com_dias - 1] - last_price) / last_price * 100
|
| 542 |
+
c3.metric(L['forecast_n'].format(com_dias), f"${median[com_dias - 1]:,.2f}", f"{cambio_f:+.2f}%")
|
| 543 |
+
|
| 544 |
+
# ----- TAB 4: LIVE -----
|
| 545 |
+
with tab_l:
|
| 546 |
+
count = st_autorefresh(interval=60000, key="live")
|
| 547 |
+
|
| 548 |
+
st.markdown(
|
| 549 |
+
f'<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">'
|
| 550 |
+
f'<span class="live-dot"></span>'
|
| 551 |
+
f'<h3 style="margin:0;">LIVE — {modelo}</h3>'
|
| 552 |
+
f'<span style="color:#555;font-size:12px;margin-left:auto;">'
|
| 553 |
+
f'Refresh #{count} | 60s</span>'
|
| 554 |
+
'</div>',
|
| 555 |
+
unsafe_allow_html=True,
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
intra = fetch_intraday_data()
|
| 559 |
+
df_15m, df_1h = intra['15m'], intra['1h']
|
| 560 |
+
|
| 561 |
+
live_dias_slider = st.slider(L['forecast_days'], 1, 3, 1, 1, key='live_fcast')
|
| 562 |
+
|
| 563 |
+
if df_15m is not None and df_1h is not None and not df_15m.empty:
|
| 564 |
+
today = datetime.now().date()
|
| 565 |
+
today_15m = df_15m[df_15m.index.date == today]
|
| 566 |
+
today_1h = df_1h[df_1h.index.date == today]
|
| 567 |
+
|
| 568 |
+
if not today_15m.empty:
|
| 569 |
+
o = today_1h['Open'].iloc[0] if not today_1h.empty else today_15m['Open'].iloc[0]
|
| 570 |
+
h = today_15m['High'].max()
|
| 571 |
+
l = today_15m['Low'].min()
|
| 572 |
+
c = today_15m['Close'].iloc[-1]
|
| 573 |
+
v = today_15m['Volume'].sum()
|
| 574 |
+
rng = h - l
|
| 575 |
+
rng_pct = rng / l * 100 if l > 0 else 0
|
| 576 |
+
chg = (c - o) / o * 100 if o > 0 else 0
|
| 577 |
+
vwap_val = ((today_15m['Close'] * today_15m['Volume']).sum()
|
| 578 |
+
/ today_15m['Volume'].sum()) if today_15m['Volume'].sum() > 0 else c
|
| 579 |
+
vol_1h = compute_hourly_volatility(today_1h) if not today_1h.empty else 0
|
| 580 |
+
|
| 581 |
+
arrow = '\u25b2' if chg >= 0 else '\u25bc'
|
| 582 |
+
color = '#26a69a' if chg >= 0 else '#ef5350'
|
| 583 |
+
|
| 584 |
+
st.markdown(
|
| 585 |
+
f'<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">'
|
| 586 |
+
f'<div class="metric-card" style="flex:1;min-width:140px;">'
|
| 587 |
+
f'<div class="metric-label">Price</div>'
|
| 588 |
+
f'<div class="metric-value" style="color:{color}">{arrow} ${c:,.2f}</div>'
|
| 589 |
+
f'<div style="font-size:13px;color:{color};">{chg:+.2f}% today</div>'
|
| 590 |
+
f'</div>'
|
| 591 |
+
f'<div class="metric-card" style="flex:1;min-width:120px;">'
|
| 592 |
+
f'<div class="metric-label">Daily Range</div>'
|
| 593 |
+
f'<div class="metric-value">${rng:,.0f}</div>'
|
| 594 |
+
f'<div style="font-size:13px;color:#8b8fa3;">{rng_pct:.2f}%</div>'
|
| 595 |
+
f'</div>'
|
| 596 |
+
f'<div class="metric-card" style="flex:1;min-width:120px;">'
|
| 597 |
+
f'<div class="metric-label">Volume</div>'
|
| 598 |
+
f'<div class="metric-value">${v:,.0f}</div>'
|
| 599 |
+
f'</div>'
|
| 600 |
+
f'<div class="metric-card" style="flex:1;min-width:120px;">'
|
| 601 |
+
f'<div class="metric-label">VWAP</div>'
|
| 602 |
+
f'<div class="metric-value">${vwap_val:,.2f}</div>'
|
| 603 |
+
f'<div style="font-size:13px;color:#8b8fa3;">{(c - vwap_val) / vwap_val * 100:+.2f}%</div>'
|
| 604 |
+
f'</div>'
|
| 605 |
+
f'<div class="metric-card" style="flex:1;min-width:120px;">'
|
| 606 |
+
f'<div class="metric-label">Volatility (1h)</div>'
|
| 607 |
+
f'<div class="metric-value">{vol_1h:.2f}%</div>'
|
| 608 |
+
f'</div>'
|
| 609 |
+
f'</div>',
|
| 610 |
+
unsafe_allow_html=True,
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
col_o, col_h, col_l = st.columns(3)
|
| 614 |
+
col_o.metric('Open', f'${o:,.2f}')
|
| 615 |
+
col_h.metric('High', f'${h:,.2f}')
|
| 616 |
+
col_l.metric('Low', f'${l:,.2f}')
|
| 617 |
+
|
| 618 |
+
two_days = today - timedelta(days=2)
|
| 619 |
+
chart_data = df_15m[df_15m.index.date >= two_days].copy()
|
| 620 |
+
|
| 621 |
+
fig_l = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
| 622 |
+
vertical_spacing=0.02, row_heights=[0.7, 0.3])
|
| 623 |
+
fig_l.add_trace(go.Candlestick(
|
| 624 |
+
x=chart_data.index, open=chart_data['Open'], high=chart_data['High'],
|
| 625 |
+
low=chart_data['Low'], close=chart_data['Close'], name='BTC',
|
| 626 |
+
), row=1, col=1)
|
| 627 |
+
bar_colors = ['#26a69a' if chart_data['Close'].iloc[i] >= chart_data['Open'].iloc[i]
|
| 628 |
+
else '#ef5350' for i in range(len(chart_data))]
|
| 629 |
+
fig_l.add_trace(go.Bar(
|
| 630 |
+
x=chart_data.index, y=chart_data['Volume'], name='Volume',
|
| 631 |
+
marker_color=bar_colors, opacity=0.4,
|
| 632 |
+
), row=2, col=1)
|
| 633 |
+
fig_l.add_hline(y=vwap_val, line_dash='dash', line_color='#ffd700', opacity=0.7,
|
| 634 |
+
annotation_text=f'VWAP ${vwap_val:,.0f}',
|
| 635 |
+
annotation_position='top left', row=1, col=1)
|
| 636 |
+
fdates = future_dates[:live_dias_slider]
|
| 637 |
+
fig_l.add_trace(go.Scatter(
|
| 638 |
+
x=fdates[:live_dias_slider], y=p90[:live_dias_slider],
|
| 639 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'), showlegend=False,
|
| 640 |
+
), row=1, col=1)
|
| 641 |
+
fig_l.add_trace(go.Scatter(
|
| 642 |
+
x=fdates[:live_dias_slider], y=median[:live_dias_slider],
|
| 643 |
+
mode='lines+markers', name=modelo,
|
| 644 |
+
line=dict(color='#e63946', width=2), marker=dict(size=6),
|
| 645 |
+
), row=1, col=1)
|
| 646 |
+
fig_l.add_trace(go.Scatter(
|
| 647 |
+
x=fdates[:live_dias_slider], y=p10[:live_dias_slider],
|
| 648 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'),
|
| 649 |
+
fill='tonexty', fillcolor='rgba(230,57,70,0.12)',
|
| 650 |
+
name='P10-P90',
|
| 651 |
+
), row=1, col=1)
|
| 652 |
+
fig_l.update_layout(
|
| 653 |
+
template='plotly_dark', hovermode='x unified', height=480,
|
| 654 |
+
margin=dict(l=10, r=10, t=10, b=10),
|
| 655 |
+
xaxis_rangeslider_visible=False,
|
| 656 |
+
legend=dict(orientation='h', y=1.02),
|
| 657 |
+
)
|
| 658 |
+
fig_l.update_xaxes(title='', row=2, col=1)
|
| 659 |
+
fig_l.update_yaxes(title='Price (USD)', row=1, col=1, tickformat='$,.0f')
|
| 660 |
+
fig_l.update_yaxes(title='Volume', row=2, col=1)
|
| 661 |
+
st.plotly_chart(fig_l, width='stretch')
|
| 662 |
+
|
| 663 |
+
if not today_1h.empty:
|
| 664 |
+
st.markdown(f"<p style='color:#f0f0f0;font-weight:bold;margin:8px 0 4px;'>"
|
| 665 |
+
f"Hourly Detail | Last {min(8, len(today_1h))} hours</p>",
|
| 666 |
+
unsafe_allow_html=True)
|
| 667 |
+
last_h = today_1h.iloc[-8:] if len(today_1h) > 8 else today_1h
|
| 668 |
+
chg_col = []
|
| 669 |
+
for i in range(len(last_h)):
|
| 670 |
+
chg = ((last_h['Close'].iloc[i] - last_h['Open'].iloc[i])
|
| 671 |
+
/ last_h['Open'].iloc[i] * 100)
|
| 672 |
+
chg_col.append(f'{chg:+.2f}')
|
| 673 |
+
tbl_h = pd.DataFrame({
|
| 674 |
+
'Time': last_h.index.strftime('%H:%M'),
|
| 675 |
+
'Open': [f'${v:,.0f}' for v in last_h['Open']],
|
| 676 |
+
'High': [f'${v:,.0f}' for v in last_h['High']],
|
| 677 |
+
'Low': [f'${v:,.0f}' for v in last_h['Low']],
|
| 678 |
+
'Close': [f'${v:,.0f}' for v in last_h['Close']],
|
| 679 |
+
'Vol': [f'{v:,.0f}' for v in last_h['Volume']],
|
| 680 |
+
'Chg%': chg_col,
|
| 681 |
+
})
|
| 682 |
+
st.dataframe(tbl_h, width='stretch', hide_index=True)
|
| 683 |
+
|
| 684 |
+
st.markdown(f"<p style='color:#f0f0f0;font-weight:bold;margin:12px 0 4px;'>"
|
| 685 |
+
f"3+{live_dias_slider} Day View | Actual + Forecast</p>",
|
| 686 |
+
unsafe_allow_html=True)
|
| 687 |
+
last3 = price.iloc[-3:]
|
| 688 |
+
last3_dates = last3.index
|
| 689 |
+
pred_dates = future_dates[:live_dias_slider]
|
| 690 |
+
fig_bars = go.Figure()
|
| 691 |
+
fig_bars.add_trace(go.Bar(
|
| 692 |
+
x=last3_dates, y=last3.values,
|
| 693 |
+
name='Actual', marker_color='#457b9d', width=0.5,
|
| 694 |
+
))
|
| 695 |
+
fig_bars.add_trace(go.Bar(
|
| 696 |
+
x=pred_dates, y=median[:live_dias_slider],
|
| 697 |
+
name=f'{modelo}', marker_color='#e63946', width=0.5,
|
| 698 |
+
))
|
| 699 |
+
fig_bars.add_trace(go.Scatter(
|
| 700 |
+
x=pred_dates, y=p90[:live_dias_slider],
|
| 701 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'), showlegend=False,
|
| 702 |
+
))
|
| 703 |
+
fig_bars.add_trace(go.Scatter(
|
| 704 |
+
x=pred_dates, y=p10[:live_dias_slider],
|
| 705 |
+
mode='lines', line=dict(color='rgba(230,57,70,0)'),
|
| 706 |
+
fill='tonexty', fillcolor='rgba(230,57,70,0.12)',
|
| 707 |
+
name='P10-P90',
|
| 708 |
+
))
|
| 709 |
+
fig_bars.update_layout(
|
| 710 |
+
template='plotly_dark', hovermode='x unified', height=280,
|
| 711 |
+
margin=dict(l=10, r=10, t=10, b=10),
|
| 712 |
+
barmode='group', legend=dict(orientation='h', y=1.02),
|
| 713 |
+
yaxis=dict(title='BTC (USD)', tickformat='$,.0f'),
|
| 714 |
+
)
|
| 715 |
+
st.plotly_chart(fig_bars, width='stretch')
|
| 716 |
+
|
| 717 |
+
st.markdown(
|
| 718 |
+
f"<p style='color:#555;font-size:12px;margin-top:10px;'>"
|
| 719 |
+
f"Forecast {live_dias_slider}d: ${median[live_dias_slider - 1]:,.0f} "
|
| 720 |
+
f"({(median[live_dias_slider - 1] - c) / c * 100:+.1f}%) | "
|
| 721 |
+
f"P10: ${p10[live_dias_slider - 1]:,.0f} "
|
| 722 |
+
f"P90: ${p90[live_dias_slider - 1]:,.0f}</p>",
|
| 723 |
+
unsafe_allow_html=True,
|
| 724 |
+
)
|
| 725 |
+
else:
|
| 726 |
+
st.warning(L['waiting_data'])
|
| 727 |
+
|
| 728 |
+
st.divider()
|
| 729 |
+
st.markdown(
|
| 730 |
+
f"<p style='color:#555; font-size:11px; text-align:center;'>"
|
| 731 |
+
f"BTC Forecast V4 | {modelo} | Data: Yahoo Finance + CoinGecko | "
|
| 732 |
+
f"{now.strftime('%Y-%m-%d %H:%M:%S')}</p>",
|
| 733 |
+
unsafe_allow_html=True,
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
|
| 737 |
+
if __name__ == '__main__':
|
| 738 |
+
main()
|
versions/hybrid_v4/backtest_v4.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Backtest V4 — Chronos walk-forward validation.
|
| 3 |
+
Predicts the last N days using only data available before each prediction point.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os, sys, numpy as np, pandas as pd, yfinance as yf, torch
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from sklearn.metrics import mean_absolute_percentage_error
|
| 9 |
+
import matplotlib
|
| 10 |
+
matplotlib.use('Agg')
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
import matplotlib.dates as mdates
|
| 13 |
+
|
| 14 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
+
RESULTS_DIR = os.path.join(PROJECT_ROOT, "results")
|
| 16 |
+
os.makedirs(RESULTS_DIR, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
HISTORY_DAYS = 1000
|
| 19 |
+
BACKTEST_DAYS = 10
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def run_chronos(train_data, horizon=1):
|
| 23 |
+
from chronos import ChronosPipeline
|
| 24 |
+
pipeline = ChronosPipeline.from_pretrained(
|
| 25 |
+
"amazon/chronos-t5-small", device_map="cpu", torch_dtype=torch.float32,
|
| 26 |
+
)
|
| 27 |
+
context = torch.tensor(train_data.values, dtype=torch.float32).squeeze().unsqueeze(0)
|
| 28 |
+
forecast = pipeline.predict(context, prediction_length=horizon, num_samples=20)
|
| 29 |
+
return np.quantile(forecast[0].numpy(), 0.5, axis=0)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def backtest():
|
| 33 |
+
print('-' * 55)
|
| 34 |
+
print(' Backtest V4 | Chronos | Walk-Forward Validation')
|
| 35 |
+
print('-' * 55)
|
| 36 |
+
|
| 37 |
+
end = datetime.now()
|
| 38 |
+
start = end - timedelta(days=HISTORY_DAYS)
|
| 39 |
+
|
| 40 |
+
btc = yf.download('BTC-USD', start=start.strftime('%Y-%m-%d'),
|
| 41 |
+
end=end.strftime('%Y-%m-%d'), progress=False)
|
| 42 |
+
if isinstance(btc.columns, pd.MultiIndex):
|
| 43 |
+
btc.columns = btc.columns.droplevel(1)
|
| 44 |
+
price = btc['Close']
|
| 45 |
+
print(f'\n Data: {len(price)} days downloaded')
|
| 46 |
+
|
| 47 |
+
days = []
|
| 48 |
+
preds = []
|
| 49 |
+
actuals = []
|
| 50 |
+
errors = []
|
| 51 |
+
|
| 52 |
+
for i in range(BACKTEST_DAYS, 0, -1):
|
| 53 |
+
train_end = len(price) - i
|
| 54 |
+
train_data = price.iloc[:train_end]
|
| 55 |
+
actual_val = price.iloc[train_end]
|
| 56 |
+
|
| 57 |
+
pred_val = run_chronos(train_data, horizon=1)[0]
|
| 58 |
+
|
| 59 |
+
day_label = price.index[train_end].strftime('%Y-%m-%d')
|
| 60 |
+
error_pct = abs(pred_val - actual_val) / actual_val * 100
|
| 61 |
+
|
| 62 |
+
days.append(day_label)
|
| 63 |
+
preds.append(pred_val)
|
| 64 |
+
actuals.append(actual_val)
|
| 65 |
+
errors.append(error_pct)
|
| 66 |
+
|
| 67 |
+
print(f' {day_label} | pred: ${pred_val:>8,.2f} actual: ${actual_val:>8,.2f} '
|
| 68 |
+
f'error: {error_pct:>5.2f}%')
|
| 69 |
+
|
| 70 |
+
overall_mape = np.mean(errors)
|
| 71 |
+
print(f'\n {"=" * 45}')
|
| 72 |
+
print(f' Average MAPE ({BACKTEST_DAYS} days): {overall_mape:.2f}%')
|
| 73 |
+
print(f' {"=" * 45}')
|
| 74 |
+
|
| 75 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 9),
|
| 76 |
+
gridspec_kw={'height_ratios': [2.5, 1]})
|
| 77 |
+
fig.suptitle('Backtest V4 — Chronos 1-day Walk-Forward',
|
| 78 |
+
fontsize=14, fontweight='bold', y=0.98)
|
| 79 |
+
|
| 80 |
+
x = range(len(days))
|
| 81 |
+
ax1.plot(x, actuals, 'o-', color='#e63946', linewidth=2.5, label='Actual', markersize=8)
|
| 82 |
+
ax1.plot(x, preds, 's--', color='#457b9d', linewidth=2.5, label='Chronos', markersize=8)
|
| 83 |
+
for i in range(len(days)):
|
| 84 |
+
err = errors[i]
|
| 85 |
+
color = '#2a9d8f' if err < 3 else ('#e9c46a' if err < 6 else '#e63946')
|
| 86 |
+
ax1.plot([i, i], [actuals[i], preds[i]], color=color, linewidth=2, alpha=0.7)
|
| 87 |
+
ax1.set_ylabel('BTC Price (USD)', fontsize=11)
|
| 88 |
+
ax1.legend(fontsize=11)
|
| 89 |
+
ax1.grid(True, alpha=0.3)
|
| 90 |
+
ax1.set_xticks(range(len(days)))
|
| 91 |
+
ax1.set_xticklabels(days, rotation=45, ha='right')
|
| 92 |
+
|
| 93 |
+
colors = ['#2a9d8f' if e < 3 else ('#e9c46a' if e < 6 else '#e63946') for e in errors]
|
| 94 |
+
bars = ax2.bar(range(len(errors)), errors, color=colors)
|
| 95 |
+
for i, (bar, err) in enumerate(zip(bars, errors)):
|
| 96 |
+
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
|
| 97 |
+
f'{err:.1f}%', ha='center', fontsize=9, fontweight='bold')
|
| 98 |
+
ax2.axhline(y=overall_mape, color='red', linestyle='--', linewidth=1.5,
|
| 99 |
+
label=f'Average MAPE: {overall_mape:.2f}%')
|
| 100 |
+
ax2.set_ylabel('Error (%)', fontsize=11)
|
| 101 |
+
ax2.set_xlabel('Date')
|
| 102 |
+
ax2.legend(fontsize=10)
|
| 103 |
+
ax2.grid(True, alpha=0.3, axis='y')
|
| 104 |
+
ax2.set_xticks(range(len(days)))
|
| 105 |
+
ax2.set_xticklabels(days, rotation=45, ha='right')
|
| 106 |
+
|
| 107 |
+
plt.tight_layout(rect=[0, 0, 1, 0.95])
|
| 108 |
+
path = os.path.join(RESULTS_DIR, 'backtest_v4.png')
|
| 109 |
+
plt.savefig(path, dpi=300, bbox_inches='tight')
|
| 110 |
+
plt.close()
|
| 111 |
+
print(f'\n Chart: {path}')
|
| 112 |
+
|
| 113 |
+
results = pd.DataFrame({
|
| 114 |
+
'Day': days, 'Prediction': preds, 'Actual': actuals, 'Error %': errors
|
| 115 |
+
})
|
| 116 |
+
csv_path = os.path.join(RESULTS_DIR, 'backtest_v4.csv')
|
| 117 |
+
results.to_csv(csv_path, index=False)
|
| 118 |
+
print(f' CSV: {csv_path}')
|
| 119 |
+
print(f'\n Backtest complete.\n')
|
| 120 |
+
|
| 121 |
+
return overall_mape
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
if __name__ == '__main__':
|
| 125 |
+
backtest()
|
versions/hybrid_v4/estudio_backtest.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Historical Backtest Study — V4
|
| 3 |
+
Evaluates Chronos variants and baseline models across multiple
|
| 4 |
+
independent test windows dating back to 2024.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os, sys, numpy as np, pandas as pd, yfinance as yf, torch
|
| 8 |
+
import warnings, time, json
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
from sklearn.metrics import mean_absolute_percentage_error
|
| 11 |
+
from itertools import product
|
| 12 |
+
|
| 13 |
+
warnings.filterwarnings('ignore')
|
| 14 |
+
|
| 15 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 16 |
+
RESULTS_DIR = os.path.join(PROJECT_ROOT, "results")
|
| 17 |
+
os.makedirs(RESULTS_DIR, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
MIN_TRAIN = 500
|
| 20 |
+
STEP = 30
|
| 21 |
+
HISTORY = 1000
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_chronos(name="amazon/chronos-t5-small"):
|
| 25 |
+
from chronos import ChronosPipeline
|
| 26 |
+
return ChronosPipeline.from_pretrained(name, device_map="cpu", torch_dtype=torch.float32)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def predict_chronos(pipeline, train_data, horizon=1, n_samples=20):
|
| 30 |
+
context = torch.tensor(train_data.values, dtype=torch.float32).squeeze().unsqueeze(0)
|
| 31 |
+
forecast = pipeline.predict(context, prediction_length=horizon, num_samples=n_samples)
|
| 32 |
+
return np.quantile(forecast[0].numpy(), 0.5, axis=0)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def predict_arima(train_data, horizon=1):
|
| 36 |
+
from statsmodels.tsa.arima.model import ARIMA
|
| 37 |
+
model = ARIMA(train_data.values, order=(5,1,0))
|
| 38 |
+
fitted = model.fit()
|
| 39 |
+
return fitted.forecast(horizon)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def predict_naive(train_data, horizon=1):
|
| 43 |
+
return np.full(horizon, train_data.iloc[-1])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def predict_timesfm(train_data, horizon=1):
|
| 47 |
+
import timesfm
|
| 48 |
+
try:
|
| 49 |
+
tfm = timesfm.TimesFm(
|
| 50 |
+
hparams=timesfm.TimesFmHparams(
|
| 51 |
+
backend="cpu", per_core_batch_size=32, horizon_len=horizon,
|
| 52 |
+
context_len=512, input_patch_len=32, output_patch_len=128,
|
| 53 |
+
num_layers=20, model_dims=1280,
|
| 54 |
+
),
|
| 55 |
+
checkpoint=timesfm.TimesFmCheckpoint(
|
| 56 |
+
huggingface_repo_id="google/timesfm-1.0-200m-pytorch"
|
| 57 |
+
)
|
| 58 |
+
)
|
| 59 |
+
context = train_data.values[-512:]
|
| 60 |
+
forecast = tfm.forecast([context], freq=[0])
|
| 61 |
+
return forecast[0][0][:horizon]
|
| 62 |
+
except:
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def predict_moirai(train_data, horizon=1):
|
| 67 |
+
from uni2ts.model.moirai import MoiraiForecast, MoiraiModule
|
| 68 |
+
from gluonts.dataset.pandas import PandasDataset
|
| 69 |
+
from gluonts.evaluation import make_evaluation_predictions
|
| 70 |
+
try:
|
| 71 |
+
model = MoiraiForecast(
|
| 72 |
+
module=MoiraiModule.from_pretrained("Salesforce/moirai-1.0-R-small"),
|
| 73 |
+
prediction_length=horizon, context_length=min(512, len(train_data)),
|
| 74 |
+
patch_size="auto", num_samples=100, target_dim=1,
|
| 75 |
+
feat_dynamic_real_dim=0, past_feat_dynamic_real_dim=0,
|
| 76 |
+
)
|
| 77 |
+
predictor = model.create_predictor(batch_size=32)
|
| 78 |
+
df_m = pd.DataFrame({'Close': train_data.values}, index=train_data.index)
|
| 79 |
+
df_m.index = pd.DatetimeIndex(df_m.index)
|
| 80 |
+
df_m = df_m.asfreq('D').ffill().dropna()
|
| 81 |
+
ds = PandasDataset(df_m, target="Close")
|
| 82 |
+
it, _ = make_evaluation_predictions(dataset=ds, predictor=predictor, num_samples=100)
|
| 83 |
+
forecast = list(it)[0]
|
| 84 |
+
return forecast.quantile(0.5)[:horizon]
|
| 85 |
+
except:
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def run_study():
|
| 90 |
+
print('\n' + '=' * 65)
|
| 91 |
+
print(' HISTORICAL BACKTEST STUDY | Multi-Model Comparison')
|
| 92 |
+
print('=' * 65)
|
| 93 |
+
|
| 94 |
+
end = datetime.now()
|
| 95 |
+
start = end - timedelta(days=HISTORY + 365)
|
| 96 |
+
btc = yf.download('BTC-USD', start='2020-01-01',
|
| 97 |
+
end=end.strftime('%Y-%m-%d'), progress=False)
|
| 98 |
+
if isinstance(btc.columns, pd.MultiIndex):
|
| 99 |
+
btc.columns = btc.columns.droplevel(1)
|
| 100 |
+
price = btc['Close']
|
| 101 |
+
price = price[price.index >= pd.Timestamp(start)]
|
| 102 |
+
price = price[price.index <= pd.Timestamp(end)]
|
| 103 |
+
|
| 104 |
+
if len(price) == 0:
|
| 105 |
+
print('Error: no data downloaded. Aborting.')
|
| 106 |
+
return
|
| 107 |
+
print(f'\n Total data: {len(price)} days ({price.index[0].date()} -> {price.index[-1].date()})')
|
| 108 |
+
|
| 109 |
+
test_points = []
|
| 110 |
+
for i in range(len(price) - MIN_TRAIN - 1, 0, -STEP):
|
| 111 |
+
if i < MIN_TRAIN or len(test_points) >= 50:
|
| 112 |
+
break
|
| 113 |
+
test_points.append(i)
|
| 114 |
+
|
| 115 |
+
test_points = test_points[::-1]
|
| 116 |
+
print(f' Test windows: {len(test_points)} (every {STEP} days)')
|
| 117 |
+
print(f' {price.index[test_points[0]].date()} -> {price.index[test_points[-1]].date()}\n')
|
| 118 |
+
|
| 119 |
+
models = {
|
| 120 |
+
'Chronos-Small': 'amazon/chronos-t5-small',
|
| 121 |
+
'Chronos-Tiny': 'amazon/chronos-t5-tiny',
|
| 122 |
+
'Chronos-Base': 'amazon/chronos-t5-base',
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
pipelines = {}
|
| 126 |
+
print(' Loading Chronos models...')
|
| 127 |
+
for name, hf_name in models.items():
|
| 128 |
+
print(f' -> {name}...', end=' ', flush=True)
|
| 129 |
+
pipelines[name] = load_chronos(hf_name)
|
| 130 |
+
print('done')
|
| 131 |
+
|
| 132 |
+
print('')
|
| 133 |
+
results = {name: [] for name in list(models.keys()) + ['ARIMA', 'Naive', 'TimesFM', 'MOIRAI']}
|
| 134 |
+
times = {name: [] for name in results}
|
| 135 |
+
|
| 136 |
+
for idx, tp in enumerate(test_points):
|
| 137 |
+
train_data = price.iloc[tp - MIN_TRAIN:tp]
|
| 138 |
+
actual = float(price.iloc[tp])
|
| 139 |
+
pct = (idx + 1) / len(test_points) * 100
|
| 140 |
+
|
| 141 |
+
print(f'\r Progress: {pct:.0f}% | {price.index[tp].date()} -> ${actual:,.0f}', end='', flush=True)
|
| 142 |
+
|
| 143 |
+
for name, pipeline in pipelines.items():
|
| 144 |
+
t0 = time.time()
|
| 145 |
+
try:
|
| 146 |
+
pred = predict_chronos(pipeline, train_data)
|
| 147 |
+
err = abs(pred[0] - actual) / actual * 100
|
| 148 |
+
results[name].append(err)
|
| 149 |
+
times[name].append(time.time() - t0)
|
| 150 |
+
except:
|
| 151 |
+
results[name].append(np.nan)
|
| 152 |
+
times[name].append(0)
|
| 153 |
+
|
| 154 |
+
t0 = time.time()
|
| 155 |
+
try:
|
| 156 |
+
pred = predict_arima(train_data)
|
| 157 |
+
err = abs(pred[0] - actual) / actual * 100
|
| 158 |
+
results['ARIMA'].append(err)
|
| 159 |
+
except:
|
| 160 |
+
results['ARIMA'].append(np.nan)
|
| 161 |
+
times['ARIMA'].append(time.time() - t0)
|
| 162 |
+
|
| 163 |
+
t0 = time.time()
|
| 164 |
+
pred = predict_naive(train_data)
|
| 165 |
+
err = abs(pred[0] - actual) / actual * 100
|
| 166 |
+
results['Naive'].append(err)
|
| 167 |
+
times['Naive'].append(time.time() - t0)
|
| 168 |
+
|
| 169 |
+
if 'TimesFM' in results and idx % 3 == 0:
|
| 170 |
+
t0 = time.time()
|
| 171 |
+
try:
|
| 172 |
+
pred = predict_timesfm(train_data)
|
| 173 |
+
if pred is not None:
|
| 174 |
+
err = abs(pred[0] - actual) / actual * 100
|
| 175 |
+
results['TimesFM'].append(err)
|
| 176 |
+
else:
|
| 177 |
+
results['TimesFM'].append(np.nan)
|
| 178 |
+
except:
|
| 179 |
+
results['TimesFM'].append(np.nan)
|
| 180 |
+
times['TimesFM'].append(time.time() - t0)
|
| 181 |
+
elif 'TimesFM' in results:
|
| 182 |
+
results['TimesFM'].append(np.nan)
|
| 183 |
+
times['TimesFM'].append(0)
|
| 184 |
+
|
| 185 |
+
if 'MOIRAI' in results and idx % 3 == 0:
|
| 186 |
+
t0 = time.time()
|
| 187 |
+
try:
|
| 188 |
+
pred = predict_moirai(train_data)
|
| 189 |
+
if pred is not None:
|
| 190 |
+
err = abs(pred[0] - actual) / actual * 100
|
| 191 |
+
results['MOIRAI'].append(err)
|
| 192 |
+
else:
|
| 193 |
+
results['MOIRAI'].append(np.nan)
|
| 194 |
+
except:
|
| 195 |
+
results['MOIRAI'].append(np.nan)
|
| 196 |
+
times['MOIRAI'].append(time.time() - t0)
|
| 197 |
+
elif 'MOIRAI' in results:
|
| 198 |
+
results['MOIRAI'].append(np.nan)
|
| 199 |
+
times['MOIRAI'].append(0)
|
| 200 |
+
|
| 201 |
+
print('\n')
|
| 202 |
+
|
| 203 |
+
print('\n' + '=' * 65)
|
| 204 |
+
print(' RESULTS')
|
| 205 |
+
print('=' * 65)
|
| 206 |
+
print(f' {"Model":25s} {"MAPE":>8s} {"Std":>8s} {"Min":>8s} {"Max":>8s} {"Time":>8s}')
|
| 207 |
+
print(f' {"-" * 60}')
|
| 208 |
+
|
| 209 |
+
summary = []
|
| 210 |
+
for name in results:
|
| 211 |
+
vals = [v for v in results[name] if not np.isnan(v)]
|
| 212 |
+
if len(vals) > 0:
|
| 213 |
+
mape = np.mean(vals)
|
| 214 |
+
std = np.std(vals)
|
| 215 |
+
min_v = np.min(vals)
|
| 216 |
+
max_v = np.max(vals)
|
| 217 |
+
t_avg = np.mean(times[name]) if times[name] else 0
|
| 218 |
+
print(f' {name:25s} {mape:>7.2f}% {std:>7.2f}% {min_v:>7.2f}% {max_v:>7.2f}% {t_avg:>7.3f}s')
|
| 219 |
+
summary.append({'Model': name, 'MAPE': mape, 'Std': std,
|
| 220 |
+
'Min': min_v, 'Max': max_v, 'Samples': len(vals)})
|
| 221 |
+
else:
|
| 222 |
+
print(f' {name:25s} {"N/A":>8s}')
|
| 223 |
+
|
| 224 |
+
df_results = pd.DataFrame(summary).sort_values('MAPE')
|
| 225 |
+
csv_path = os.path.join(RESULTS_DIR, 'estudio_backtest_modelos.csv')
|
| 226 |
+
df_results.to_csv(csv_path, index=False)
|
| 227 |
+
print(f'\n CSV: {csv_path}')
|
| 228 |
+
|
| 229 |
+
naive_mape = df_results[df_results['Model'] == 'Naive']['MAPE'].values[0]
|
| 230 |
+
print(f'\n IMPROVEMENT VS NAIVE ({naive_mape:.2f}%):')
|
| 231 |
+
for _, row in df_results.iterrows():
|
| 232 |
+
if row['Model'] != 'Naive' and row['Samples'] > 5:
|
| 233 |
+
mejora = (naive_mape - row['MAPE']) / naive_mape * 100
|
| 234 |
+
print(f' {row["Model"]:25s} {mejora:+.1f}%')
|
| 235 |
+
|
| 236 |
+
print(f'\n MAPE BY YEAR (Chronos-Small):')
|
| 237 |
+
years = {}
|
| 238 |
+
for idx, tp in enumerate(test_points):
|
| 239 |
+
year = price.index[tp].year
|
| 240 |
+
if year not in years:
|
| 241 |
+
years[year] = []
|
| 242 |
+
years[year].append(results['Chronos-Small'][idx])
|
| 243 |
+
for year in sorted(years.keys()):
|
| 244 |
+
vals = [v for v in years[year] if not np.isnan(v)]
|
| 245 |
+
if vals:
|
| 246 |
+
print(f' {year}: {np.mean(vals):.2f}% ({len(vals)} samples)')
|
| 247 |
+
|
| 248 |
+
print(f'\n Study complete.\n')
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
if __name__ == '__main__':
|
| 252 |
+
run_study()
|
versions/hybrid_v4/forecast_v4.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Forecast V4 — Chronos forward prediction.
|
| 3 |
+
Projects N days ahead using historical price data and displays confidence bands.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os, sys, numpy as np, pandas as pd, yfinance as yf, torch
|
| 7 |
+
import requests
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
import matplotlib
|
| 10 |
+
matplotlib.use('Agg')
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
import matplotlib.dates as mdates
|
| 13 |
+
|
| 14 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
+
RESULTS_DIR = os.path.join(PROJECT_ROOT, "results")
|
| 16 |
+
os.makedirs(RESULTS_DIR, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
HISTORY_DAYS = 1000
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def run_chronos_probabilistic(train_data, horizon, n_samples=100):
|
| 22 |
+
from chronos import ChronosPipeline
|
| 23 |
+
pipeline = ChronosPipeline.from_pretrained(
|
| 24 |
+
"amazon/chronos-t5-small", device_map="cpu", torch_dtype=torch.float32,
|
| 25 |
+
)
|
| 26 |
+
context = torch.tensor(train_data.values, dtype=torch.float32).squeeze().unsqueeze(0)
|
| 27 |
+
forecast = pipeline.predict(context, prediction_length=horizon, num_samples=n_samples)
|
| 28 |
+
samples = forecast[0].numpy()
|
| 29 |
+
median = np.median(samples, axis=0)
|
| 30 |
+
p10 = np.percentile(samples, 10, axis=0)
|
| 31 |
+
p90 = np.percentile(samples, 90, axis=0)
|
| 32 |
+
return median, p10, p90, samples
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_market_snapshot():
|
| 36 |
+
snapshot = {}
|
| 37 |
+
btc = yf.download('BTC-USD', period='5d', progress=False)
|
| 38 |
+
if isinstance(btc.columns, pd.MultiIndex):
|
| 39 |
+
btc.columns = btc.columns.droplevel(1)
|
| 40 |
+
if len(btc) > 0:
|
| 41 |
+
snapshot['Current Price'] = f"${btc['Close'].iloc[-1]:,.2f}"
|
| 42 |
+
snapshot['24h Change'] = f"{btc['Close'].pct_change().iloc[-1] * 100:+.2f}%"
|
| 43 |
+
snapshot['Volume'] = f"${btc['Volume'].iloc[-1]:,.0f}"
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
fng = requests.get('https://api.alternative.me/fng/?limit=1', timeout=5).json()
|
| 47 |
+
if 'data' in fng and len(fng['data']) > 0:
|
| 48 |
+
snapshot['Fear & Greed'] = f"{fng['data'][0]['value']}/100 ({fng['data'][0]['value_classification']})"
|
| 49 |
+
except:
|
| 50 |
+
snapshot['Fear & Greed'] = 'N/A'
|
| 51 |
+
|
| 52 |
+
tickers = {'^GSPC': 'S&P 500', 'GC=F': 'Gold', 'DX-Y.NYB': 'DXY'}
|
| 53 |
+
for tk, name in tickers.items():
|
| 54 |
+
try:
|
| 55 |
+
df = yf.download(tk, period='5d', progress=False)
|
| 56 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 57 |
+
df.columns = df.columns.droplevel(1)
|
| 58 |
+
if len(df) > 0:
|
| 59 |
+
val = df['Close'].iloc[-1]
|
| 60 |
+
chg = df['Close'].pct_change().iloc[-1] * 100
|
| 61 |
+
snapshot[name] = f"{val:,.2f} ({chg:+.2f}%)"
|
| 62 |
+
except:
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
return snapshot
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def forecast():
|
| 69 |
+
import argparse
|
| 70 |
+
parser = argparse.ArgumentParser(description='Chronos V4 Forecast')
|
| 71 |
+
parser.add_argument('--dias', type=int, default=10,
|
| 72 |
+
help='Days to forecast (default: 10)')
|
| 73 |
+
parser.add_argument('--muestras', type=int, default=100,
|
| 74 |
+
help='Number of probabilistic samples (default: 100)')
|
| 75 |
+
args = parser.parse_args()
|
| 76 |
+
|
| 77 |
+
HORIZON = args.dias
|
| 78 |
+
N_SAMPLES = args.muestras
|
| 79 |
+
|
| 80 |
+
print('-' * 55)
|
| 81 |
+
print(f' Forecast V4 | Chronos | {HORIZON} days ahead')
|
| 82 |
+
print('-' * 55)
|
| 83 |
+
|
| 84 |
+
end = datetime.now()
|
| 85 |
+
start = end - timedelta(days=HISTORY_DAYS)
|
| 86 |
+
|
| 87 |
+
print('\n Downloading data...')
|
| 88 |
+
btc = yf.download('BTC-USD', start=start.strftime('%Y-%m-%d'),
|
| 89 |
+
end=end.strftime('%Y-%m-%d'), progress=False)
|
| 90 |
+
if isinstance(btc.columns, pd.MultiIndex):
|
| 91 |
+
btc.columns = btc.columns.droplevel(1)
|
| 92 |
+
price = btc['Close']
|
| 93 |
+
print(f' History: {len(price)} days')
|
| 94 |
+
|
| 95 |
+
print(f'\n Running Chronos ({N_SAMPLES} samples, {HORIZON} days)...')
|
| 96 |
+
median, p10, p90, samples = run_chronos_probabilistic(price, HORIZON, N_SAMPLES)
|
| 97 |
+
|
| 98 |
+
last_date = price.index[-1]
|
| 99 |
+
future_dates = pd.date_range(start=last_date + timedelta(days=1),
|
| 100 |
+
periods=HORIZON, freq='D')
|
| 101 |
+
last_price = price.iloc[-1]
|
| 102 |
+
|
| 103 |
+
print(f'\n {"Day":>4s} {"Date":>12s} {"Forecast":>14s} {"P10":>12s} {"P90":>12s} {"Change%":>8s}')
|
| 104 |
+
print(f' {"-" * 62}')
|
| 105 |
+
print(f' {0:>4d} {last_date.strftime("%Y-%m-%d"):>12s} '
|
| 106 |
+
f'${last_price:>8,.2f} {"":>12s} {"":>12s} {"--":>8s}')
|
| 107 |
+
for i in range(HORIZON):
|
| 108 |
+
chg = (median[i] - last_price) / last_price * 100
|
| 109 |
+
print(f' {i+1:>4d} {future_dates[i].strftime("%Y-%m-%d"):>12s} '
|
| 110 |
+
f'${median[i]:>8,.2f} ${p10[i]:>8,.2f} ${p90[i]:>8,.2f} {chg:>+7.2f}%')
|
| 111 |
+
|
| 112 |
+
print(f'\n {"=" * 50}')
|
| 113 |
+
print(' CURRENT MARKET CONTEXT')
|
| 114 |
+
print(f' {"=" * 50}')
|
| 115 |
+
snapshot = get_market_snapshot()
|
| 116 |
+
for k, v in snapshot.items():
|
| 117 |
+
print(f' {k:20s} -> {v}')
|
| 118 |
+
|
| 119 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10),
|
| 120 |
+
gridspec_kw={'height_ratios': [3, 1]})
|
| 121 |
+
fig.suptitle(f'Forecast V4 — Chronos: {HORIZON} days ahead',
|
| 122 |
+
fontsize=15, fontweight='bold', y=0.98)
|
| 123 |
+
|
| 124 |
+
ctx_days = min(90, len(price))
|
| 125 |
+
ctx = price.iloc[-ctx_days:]
|
| 126 |
+
|
| 127 |
+
ax1.plot(ctx.index, ctx.values, color='#1a1a2e', linewidth=1.5, label='BTC History')
|
| 128 |
+
ax1.plot(future_dates, median, color='#e63946', linewidth=2.5, label='Chronos (median)')
|
| 129 |
+
ax1.fill_between(future_dates, p10, p90, color='#e63946', alpha=0.15,
|
| 130 |
+
label='P10-P90')
|
| 131 |
+
ax1.axvline(x=last_date, color='gray', linestyle=':', alpha=0.5)
|
| 132 |
+
ax1.set_ylabel('BTC Price (USD)', fontsize=11)
|
| 133 |
+
ax1.legend(fontsize=10, loc='upper left')
|
| 134 |
+
ax1.grid(True, alpha=0.3)
|
| 135 |
+
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
|
| 136 |
+
ax1.xaxis.set_major_locator(mdates.WeekdayLocator(interval=1))
|
| 137 |
+
|
| 138 |
+
last_30 = price.iloc[-30:]
|
| 139 |
+
ax2.bar(last_30.index, last_30.values, color='#457b9d', alpha=0.7, width=0.8)
|
| 140 |
+
ax2.set_ylabel('BTC (USD)', fontsize=11)
|
| 141 |
+
ax2.set_xlabel('Date')
|
| 142 |
+
ax2.grid(True, alpha=0.3)
|
| 143 |
+
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
|
| 144 |
+
|
| 145 |
+
cell_text = [[k, v] for k, v in snapshot.items()]
|
| 146 |
+
table = ax2.table(cellText=cell_text, colLabels=['Indicator', 'Value'],
|
| 147 |
+
loc='upper right', fontsize=8,
|
| 148 |
+
cellLoc='left', bbox=[0.65, 0.55, 0.33, 0.40])
|
| 149 |
+
table.auto_set_font_size(False)
|
| 150 |
+
table.set_fontsize(7)
|
| 151 |
+
|
| 152 |
+
plt.tight_layout(rect=[0, 0, 1, 0.96])
|
| 153 |
+
path = os.path.join(RESULTS_DIR, 'forecast_v4.png')
|
| 154 |
+
plt.savefig(path, dpi=300, bbox_inches='tight')
|
| 155 |
+
plt.close()
|
| 156 |
+
print(f'\n Chart: {path}')
|
| 157 |
+
|
| 158 |
+
results = pd.DataFrame({
|
| 159 |
+
'Day': range(1, HORIZON + 1),
|
| 160 |
+
'Date': future_dates.strftime('%Y-%m-%d'),
|
| 161 |
+
'Forecast': median,
|
| 162 |
+
'P10': p10,
|
| 163 |
+
'P90': p90,
|
| 164 |
+
})
|
| 165 |
+
csv_path = os.path.join(RESULTS_DIR, 'forecast_v4.csv')
|
| 166 |
+
results.to_csv(csv_path, index=False)
|
| 167 |
+
print(f' CSV: {csv_path}')
|
| 168 |
+
print(f'\n Forecast complete.\n')
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if __name__ == '__main__':
|
| 172 |
+
forecast()
|