Jorge12344321 commited on
Commit
23cf9d5
·
0 Parent(s):

v4: BTC Forecast with Chronos — Streamlit dashboard + CLI backtest/forecast

Browse files
.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 &mdash; {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}%) &nbsp;|&nbsp; "
721
+ f"P10: ${p10[live_dias_slider - 1]:,.0f} &nbsp; "
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()