diegobeyl/backtesting
2
1# 📊 Análisis Profundo - Backtesting App V2
2## Revisión Técnica Completa y Propuestas de Mejora
3
4**Fecha:** 2 de Enero de 2026
5**Versión Analizada:** V2
6**Enfoque:** Resiliencia y Performance
7
8---
9
10## 🎯 RESUMEN EJECUTIVO
11
12La V2 representa una **mejora significativa** en arquitectura y operabilidad respecto a la V1, pero presenta **oportunidades críticas** para mejorar resiliencia, performance y mantenibilidad.
13
14### Puntuación General:
15- **Arquitectura:** 7.5/10 ✅
16- **Resiliencia:** 5.5/10 ⚠️
17- **Performance:** 6/10 ⚠️
18- **Observabilidad:** 7/10 ✅
19- **Escalabilidad:** 4.5/10 ❌
20
21---
22
23## 1️⃣ ANÁLISIS ARQUITECTÓNICO
24
25### 1.1 Fortalezas Identificadas ✅
26
27#### Separación de Concerns
28```
29✅ API (FastAPI) → /api/
30✅ UI (Streamlit) → /backtesting_app/
31✅ Core Logic → /backtesting_app/core/
32✅ Shared Utils → /utils/
33✅ Config Central → config.py
34```
35**Estado:** Bien organizado, fácil de mantener.
36
37#### Configuración Centralizada
38```python
39✅ Variables de entorno (.env)
40✅ Validación automática de config
41✅ Support multi-ambiente (dev/staging/prod)
42✅ Type hints completos
43```
44
45#### Logging Estructurado
46```python
47✅ JSON logging para análisis
48✅ Rotación automática de archivos
49✅ Múltiples niveles de severidad
50✅ Middleware de request logging
51```
52
53#### Caching Inteligente
54```python
55✅ Caché persistente en disco
56✅ Expiración automática
57✅ Estadísticas de hits/misses
58✅ Decorador reutilizable
59```
60
61#### Rate Limiting
62```python
63✅ Protección contra abuso (slowapi)
64✅ Configurable por IP
65✅ Límites por minuto y por hora
66```
67
68---
69
70### 1.2 Problemas Críticos 🔴
71
72#### 1. **Manejo de Errores Inconsistente**
73
74**Ubicación:** `/api/routes.py`, `/backtesting_app/core/backtester.py`
75
76**Problema:**
77```python
78# ❌ MAL - Sin validación de entrada
79@router.post("/backtest")
80async def run_backtest(request: BacktestRequest):
81 # Nunca valida si df está vacío
82 # No captura excepciones de yfinance
83 # Sin timeout para operaciones largas
84 provider = get_data_provider()
85 data = provider.get_data(symbol, timeframe) # Puede ser None/vacío
86 algo_result = algorithm.run(data, params) # Crash aquí
87```
88
89**Impacto:**
90- ❌ API puede crashear con request malformado
91- ❌ Sin feedback claro del error al usuario
92- ❌ Logs inconsistentes
93
94---
95
96#### 2. **Falta de Timeouts y Circuit Breakers**
97
98**Ubicación:** `/backtesting_app/core/data_loader.py`, `/api/routes.py`
99
100**Problema:**
101```python
102# ❌ MAL - Sin timeout
103def _fetch_data(symbol: str, period: str, interval: str):
104 ticker = yf.Ticker(symbol)
105 data = ticker.history(period=period, interval=interval) # Puede colgar infinito
106 return data
107
108# ❌ MAL - Sin circuit breaker
109for s_raw in all_symbols_raw[:100]:
110 # Si MT5 falla en iteración 50, no hay fallback
111 final_list.append(s_raw.name)
112```
113
114**Impacto:**
115- ❌ Requests pueden quedar pendientes indefinidamente
116- ❌ Memoria y conexiones se agotan
117- ❌ Falta resiliencia ante fallos externos (yfinance, MT5)
118
119---
120
121#### 3. **Gestión de Concurrencia Inexistente**
122
123**Ubicación:** Toda la aplicación
124
125**Problema:**
126```python
127# ❌ MAL - No hay sincronización
128_backtest_state = {
129 "running": False,
130 "progress": 0.0,
131 "message": "Idle"
132}
133
134# Race condition si dos requests ejecutan backtest simultáneamente
135if not _backtest_state["running"]:
136 _backtest_state["running"] = True
137 # Aquí pueden entrar dos threads simultáneamente
138```
139
140**Impacto:**
141- ❌ En producción con múltiples usuarios = corrupción de datos
142- ❌ Sin queue de tareas = "last request wins"
143- ❌ Sin lock = race conditions
144
145---
146
147#### 4. **Validación de Datos Débil**
148
149**Ubicación:** `/backtesting_app/core/backtester.py`
150
151**Problema:**
152```python
153# ❌ MAL - Sin validación
154def run(self, df: pd.DataFrame, algo_params: Dict[str, Any]):
155 # ¿Está vacío df?
156 # ¿Faltan columnas OHLCV?
157 # ¿Hay NaN?
158 # ¿Son los params válidos para este algoritmo?
159 for i, state in enumerate(algo_result.states):
160 # Simplemente itera sin validar
161 pass
162```
163
164**Impacto:**
165- ❌ Resultados silenciosamente incorrectos
166- ❌ Difícil de debuggear
167- ❌ Métricas engañosas
168
169---
170
171#### 5. **Falta de Transacciones Lógicas**
172
173**Ubicación:** `/backtesting_app/core/backtester.py` - método `_close_position`
174
175**Problema:**
176```python
177# ❌ RIESGO - Sin atomicidad
178def _close_position(self, bar: pd.Series, bar_index: int, exit_reason: str):
179 pnl = (exit_price - pos.entry_price) * pos.position_size # Cálculo
180 self.capital += pnl # Actualización 1
181 pos.exit_date = bar.name # Actualización 2
182 pos.pnl = pnl # Actualización 3
183 self.trades.append(pos) # Actualización 4
184
185 # Si hay excepción entre pasos -> estado inconsistente
186```
187
188**Impacto:**
189- ❌ Estado inconsistente en caso de excepción
190- ❌ Pérdida de datos potencial
191
192---
193
194### 1.3 Problemas de Performance 🟠
195
196#### 1. **Caché Suboptimizado**
197
198**Ubicación:** `/utils/cache.py`
199
200**Problema:**
201```python
202# ⚠️ INEFICIENTE - Serializa args completos
203def generate_cache_key(*args, **kwargs) -> str:
204 key_data = {
205 'args': args, # Si args[0] es DataFrame de 100MB
206 'kwargs': kwargs
207 }
208 # Serializa TODO a JSON - lento
209 key_string = json.dumps(key_data, sort_keys=True, default=str)
210```
211
212**Impacto:**
213- ⚠️ Cache key lento si args grandes (DataFrames)
214- ⚠️ Diskcache sin índices optimizados
215- ⚠️ Sin comprensión de datos
216
217---
218
219#### 2. **Algoritmos Sin Vectorización**
220
221**Ubicación:** `/backtesting_app/algorithms/`
222
223**Problema:**
224```python
225# ⚠️ LENTO - Loop Python puro
226for i, state in enumerate(algo_result.states):
227 current = df.iloc[i] # Acceso O(n) repetido
228 if state.signal:
229 self._process_signal(...) # Función calls
230 if self.current_position:
231 self._update_position(...) # Más function calls
232 self._update_equity(current) # Más function calls
233```
234
235**Impacto:**
236- ⚠️ O(n) donde podría ser O(1) o vectorizado
237- ⚠️ Slow para datasets grandes (millones de velas)
238- ⚠️ CPU-bound sin posibilidad de paralelizar fácilmente
239
240---
241
242#### 3. **Logging Sincrónico Bloqueante**
243
244**Ubicación:** `/utils/logger.py`
245
246**Problema:**
247```python
248# ⚠️ BLOQUEANTE - Escribe archivo sincrónico
249file_handler = RotatingFileHandler(
250 config.LOG_FILE,
251 # ... sin buffer, cada log = I/O
252)
253```
254
255**Impacto:**
256- ⚠️ Alto tráfico = bloqueos de I/O
257- ⚠️ Response time aumenta con logging
258- ⚠️ Sin batching de logs
259
260---
261
262#### 4. **Sin Pool de Conexiones**
263
264**Ubicación:** `/core/mt5_data_provider.py`, `/backtesting_app/core/data_loader.py`
265
266**Problema:**
267```python
268# ⚠️ MAL - Nueva conexión cada request
269def get_data_provider():
270 provider = get_data_provider()
271 if not provider.connect(): # Nueva conexión cada vez
272 return {"symbols": ["BTCUSD"]}
273```
274
275**Impacto:**
276- ⚠️ Overhead de conexión
277- ⚠️ Sin reutilización de recursos
278- ⚠️ Posible límite de conexiones simultáneas
279
280---
281
282#### 5. **Sin Indexación de Búsqueda**
283
284**Ubicación:** `/api/routes.py` - `get_symbols()`
285
286**Problema:**
287```python
288# ⚠️ O(n) búsqueda lineal
289final_list = []
290seen = set()
291
292for s in common_symbols:
293 if s in all_names and s not in seen: # Busca lineal en all_names (dict)
294 final_list.append(s)
295 seen.add(s)
296```
297
298**Impacto:**
299- ⚠️ Con 50,000 símbolos = muy lento
300- ⚠️ Sin caché de symbols
301
302---
303
304### 1.4 Problemas de Escalabilidad ❌
305
306#### 1. **Estado Global Sin Sincronización**
307
308```python
309# ❌ BLOCKER - No escalable
310_backtest_state = {
311 "running": False,
312 "progress": 0.0,
313 "message": "Idle"
314}
315# Imposible en múltiples workers/procesos
316```
317
318#### 2. **Sin Persistencia de Resultados**
319
320- ❌ Los resultados de backtest se pierden al reiniciar
321- ❌ Sin base de datos
322- ❌ Sin historial
323
324#### 3. **Sin Fila de Tareas**
325
326- ❌ No puede encolar backtests
327- ❌ No puede paralelizar
328- ❌ Request bloqueante = timeout
329
330#### 4. **Sin Distribución de Carga**
331
332- ❌ Un único proceso Streamlit
333- ❌ Un único worker FastAPI
334- ❌ Sin load balancer
335
336---
337
338## 2️⃣ PROBLEMAS ESPECÍFICOS DE CÓDIGO
339
340### 2.1 Data Loader - Manejo de Errores Frágil
341
342**Archivo:** `/backtesting_app/core/data_loader.py`
343
344```python
345# ❌ PROBLEMA: Sin manejo de excepciones
346def fetch_data(symbol, start, end):
347 try:
348 if CCXT_AVAILABLE and exchange == 'binance':
349 exchange = ccxt.binance()
350 data = exchange.fetch_ohlcv(...) # Puede fallar
351 except:
352 pass # Silencia TODO
353
354 # Retorna None sin indicar error
355 # El caller no sabe qué pasó
356```
357
358**Solución Propuesta:**
359```python
360# ✅ MEJOR: Excepciones tipadas
361class DataLoadError(Exception):
362 """Base exception para data loading"""
363 pass
364
365class ExchangeConnectionError(DataLoadError):
366 pass
367
368class InsufficientDataError(DataLoadError):
369 pass
370
371def fetch_data(symbol, start, end):
372 if not symbol:
373 raise ValueError("Symbol cannot be empty")
374
375 if start >= end:
376 raise ValueError("Start date must be before end date")
377
378 try:
379 exchange = ccxt.binance()
380 data = exchange.fetch_ohlcv(...)
381
382 if data is None or len(data) == 0:
383 raise InsufficientDataError(
384 f"No data found for {symbol} between {start} and {end}"
385 )
386
387 return data
388
389 except ccxt.NetworkError as e:
390 raise ExchangeConnectionError(f"Network error: {e}") from e
391 except Exception as e:
392 logger.error(f"Unexpected error fetching data: {e}", exc_info=True)
393 raise DataLoadError(f"Failed to fetch data: {e}") from e
394```
395
396---
397
398### 2.2 Backtester - Validación de Entrada
399
400**Archivo:** `/backtesting_app/core/backtester.py`
401
402```python
403# ❌ PROBLEMA: Sin validación
404def run(self, df: pd.DataFrame, algo_params: Dict[str, Any]):
405 # ¿Está vacío?
406 # ¿Tiene las columnas?
407 # ¿Hay NaN?
408 algo_result = self.algorithm.run(df, algo_params)
409```
410
411**Solución:**
412```python
413def _validate_input(self, df: pd.DataFrame, algo_params: Dict[str, Any]):
414 """Valida entrada antes de backtester"""
415
416 # Validar DataFrame
417 if df is None or df.empty:
418 raise ValueError("DataFrame cannot be None or empty")
419
420 required_columns = {'open', 'high', 'low', 'close', 'volume'}
421 missing = required_columns - set(df.columns.str.lower())
422 if missing:
423 raise ValueError(f"Missing required columns: {missing}")
424
425 # Validar datos
426 if df.isnull().any().any():
427 null_cols = df.columns[df.isnull().any()].tolist()
428 raise ValueError(f"Found NaN values in columns: {null_cols}")
429
430 if (df['close'] <= 0).any():
431 raise ValueError("Close prices must be positive")
432
433 if df.index.duplicated().any():
434 raise ValueError("Duplicate timestamps in data")
435
436 # Validar parámetros
437 if not algo_params:
438 raise ValueError("algo_params cannot be empty")
439
440 # Validar parámetros del algoritmo
441 required_params = self.algorithm.get_default_params().keys()
442 missing_params = set(required_params) - set(algo_params.keys())
443 if missing_params:
444 logger.warning(f"Missing params: {missing_params}, using defaults")
445 algo_params.update({
446 k: self.algorithm.get_default_params()[k]
447 for k in missing_params
448 })
449
450 return df, algo_params
451```
452
453---
454
455### 2.3 Rate Limiting Insuficiente
456
457**Archivo:** `/api/main.py`
458
459```python
460# ⚠️ PROBLEMA: Rate limiting a nivel endpoint, no a nivel usuario
461limiter = Limiter(
462 key_func=get_remote_address,
463 # Por IP, no por usuario/API key
464)
465```
466
467**Solución:**
468```python
469from typing import Callable
470
471def get_user_id(request: Request) -> str:
472 """Obtiene ID único del usuario (por API key)"""
473 api_key = request.headers.get('X-API-Key')
474 if api_key:
475 return api_key
476 # Fallback a IP
477 return request.client.host if request.client else "unknown"
478
479# Usar user_id en lugar de solo IP
480limiter = Limiter(
481 key_func=lambda request: get_user_id(request),
482 default_limits=[
483 "100/minute", # Más generoso por usuario
484 "10000/hour"
485 ]
486)
487
488# Rate limiting diferenciado por operación
489@router.post("/backtest")
490@limiter.limit("10/minute") # Operación costosa = límite bajo
491async def run_backtest(request: BacktestRequest):
492 pass
493
494@router.get("/health")
495@limiter.limit("1000/minute") # Operación barata = límite alto
496async def health_check():
497 pass
498```
499
500---
501
502### 2.4 Falta de Timeouts
503
504**Todos los endpoints en `/api/routes.py`**
505
506```python
507# ❌ PROBLEMA: Sin timeout
508@router.post("/backtest")
509async def run_backtest(request: BacktestRequest):
510 # Puede colgar indefinido
511 result = await asyncio.wait_for(
512 backtest_logic(),
513 timeout=300 # 5 minutos máximo
514 )
515```
516
517**Solución:**
518```python
519import asyncio
520from contextlib import asynccontextmanager
521
522@asynccontextmanager
523async def timeout_context(seconds: float):
524 """Context manager para timeouts"""
525 try:
526 async with asyncio.timeout(seconds):
527 yield
528 except asyncio.TimeoutError as e:
529 logger.error(f"Operation timed out after {seconds}s")
530 raise HTTPException(
531 status_code=408,
532 detail=f"Operation timed out after {seconds}s"
533 ) from e
534
535@router.post("/backtest")
536async def run_backtest(request: BacktestRequest):
537 async with timeout_context(seconds=300):
538 # Lógica del backtest
539 pass
540```
541
542---
543
544## 3️⃣ PROPUESTAS DE MEJORA - PLAN DETALLADO
545
546### NIVEL 1: CRÍTICAS (Implementar AHORA) 🔴
547
548#### 1.1 Sistema de Manejo de Errores Robusto
549
550**Esfuerzo:** 2-3 horas
551**Impacto:** 🔴 Alto - Evita crashes
552
553```python
554# utils/exceptions.py - NUEVO
555class BacktestingException(Exception):
556 """Base exception para la aplicación"""
557 pass
558
559class DataLoadException(BacktestingException):
560 """Error al cargar datos"""
561 pass
562
563class AlgorithmException(BacktestingException):
564 """Error en ejecución del algoritmo"""
565 pass
566
567class ValidationException(BacktestingException):
568 """Error de validación de datos"""
569 pass
570
571class TimeoutException(BacktestingException):
572 """Timeout de operación"""
573 pass
574```
575
576---
577
578#### 1.2 Validación de Entrada en Todos los Endpoints
579
580**Esfuerzo:** 3-4 horas
581**Impacto:** 🔴 Alto - Evita bugs silenciosos
582
583**Checklist:**
584- [ ] Validar DataFrame en backtester (OHLCV, NaN, etc.)
585- [ ] Validar parámetros de algoritmo
586- [ ] Validar rango de fechas
587- [ ] Validar símbolos contra lista conocida
588- [ ] Validar capital inicial > 0
589- [ ] Validar risk % entre 0-100
590
591---
592
593#### 1.3 Manejo de Concurrencia Básico
594
595**Esfuerzo:** 2-3 horas
596**Impacto:** 🔴 Alto - Critical en producción
597
598```python
599# api/backtest_queue.py - NUEVO
600from asyncio import Semaphore
601from uuid import uuid4
602from enum import Enum
603
604class BacktestStatus(Enum):
605 QUEUED = "queued"
606 RUNNING = "running"
607 COMPLETED = "completed"
608 FAILED = "failed"
609
610class BacktestJob:
611 def __init__(self, request: BacktestRequest):
612 self.id = str(uuid4())
613 self.request = request
614 self.status = BacktestStatus.QUEUED
615 self.result = None
616 self.error = None
617 self.created_at = datetime.now()
618 self.started_at = None
619 self.completed_at = None
620
621# Control de concurrencia
622backtest_semaphore = Semaphore(3) # Máximo 3 backtests simultáneos
623
624@router.post("/backtest")
625async def run_backtest(request: BacktestRequest):
626 job = BacktestJob(request)
627
628 async with backtest_semaphore: # Sincronización
629 # Ejecutar un backtest a la vez
630 job.status = BacktestStatus.RUNNING
631 job.started_at = datetime.now()
632
633 try:
634 # Lógica con timeout
635 async with timeout_context(seconds=300):
636 job.result = await run_backtest_async(request)
637 job.status = BacktestStatus.COMPLETED
638 except Exception as e:
639 job.error = str(e)
640 job.status = BacktestStatus.FAILED
641
642 job.completed_at = datetime.now()
643
644 return {
645 "job_id": job.id,
646 "status": job.status.value,
647 "result": job.result
648 }
649```
650
651---
652
653#### 1.4 Timeouts en Todas las Operaciones I/O
654
655**Esfuerzo:** 3-4 horas
656**Impacto:** 🔴 Alto - Evita hangs
657
658**Checklist:**
659- [ ] Timeout en yfinance.fetch() (30s)
660- [ ] Timeout en MT5 queries (60s)
661- [ ] Timeout en backtest completo (300s)
662- [ ] Timeout en algorithm.run() (180s)
663
664---
665
666### NIVEL 2: IMPORTANTES (Próximas 2 semanas) 🟠
667
668#### 2.1 Caché Optimizado
669
670**Esfuerzo:** 4-5 horas
671**Impacto:** 🟠 Medio-Alto - Mejora performance 50-70%
672
673```python
674# utils/cache_v2.py - MEJORADO
675import hashlib
676import pickle
677from typing import Callable, Any, Optional
678
679class OptimizedCache:
680 """Caché optimizado para grandes objetos"""
681
682 def __init__(self, cache_dir: str):
683 self.cache = diskcache.Cache(cache_dir)
684
685 def _generate_key(self, func_name: str, *args, **kwargs) -> str:
686 """Generate key sin serializar args completos"""
687 # Solo usar hashes de argumentos
688 key_parts = [func_name]
689
690 for arg in args:
691 if isinstance(arg, pd.DataFrame):
692 # Hash del DataFrame basado en shape, columns, dtypes
693 key_parts.append(hashlib.sha256(
694 f"{arg.shape}:{arg.columns.tolist()}:{arg.dtypes.to_dict()}".encode()
695 ).hexdigest()[:8])
696 elif isinstance(arg, (int, float, str)):
697 key_parts.append(str(arg))
698 else:
699 key_parts.append(str(type(arg).__name__))
700
701 for k, v in sorted(kwargs.items()):
702 key_parts.append(f"{k}={v}")
703
704 return hashlib.md5(":".join(key_parts).encode()).hexdigest()
705
706 def get(self, key: str) -> Optional[Any]:
707 try:
708 return self.cache.get(key)
709 except Exception:
710 return None
711
712 def set(self, key: str, value: Any, expire_hours: int):
713 try:
714 self.cache.set(key, value, expire=expire_hours * 3600)
715 except Exception as e:
716 logger.warning(f"Cache write failed: {e}")
717
718 def cached(self, expire_hours: Optional[int] = None):
719 def decorator(func: Callable) -> Callable:
720 @wraps(func)
721 def wrapper(*args, **kwargs) -> Any:
722 if not config.ENABLE_CACHE:
723 return func(*args, **kwargs)
724
725 key = self._generate_key(func.__name__, *args, **kwargs)
726
727 # Try cache
728 cached_value = self.get(key)
729 if cached_value is not None:
730 logger.debug(f"Cache HIT: {func.__name__}")
731 return cached_value
732
733 # Compute and cache
734 result = func(*args, **kwargs)
735 expire = expire_hours or config.CACHE_EXPIRY_HOURS
736 self.set(key, result, expire)
737 return result
738
739 return wrapper
740 return decorator
741
742# Global instance
743optimized_cache = OptimizedCache(str(config.CACHE_DIR))
744cached = optimized_cache.cached
745```
746
747---
748
749#### 2.2 Vectorización de Backtester
750
751**Esfuerzo:** 6-8 horas
752**Impacto:** 🟠 Medio-Alto - 10-50x más rápido
753
754```python
755# backtesting_app/core/backtester_v2.py - NUEVO (vectorizado)
756import numpy as np
757import pandas as pd
758
759class VectorizedBacktester:
760 """Backtester vectorizado con NumPy para performance"""
761
762 def run(self, df: pd.DataFrame, signals_df: pd.DataFrame):
763 """
764 Ejecuta backtest de forma vectorizada
765
766 Args:
767 df: OHLCV data
768 signals_df: DataFrame con señales pre-calculadas por el algoritmo
769
770 Returns:
771 BacktestResult con trades y métricas
772 """
773 # Vectorizar en lugar de loops
774 returns = np.log(df['close'] / df['close'].shift(1))
775
776 # Calcular trades vectorizado
777 trades = self._vectorized_trades(df, signals_df)
778
779 # Calcular equity curve vectorizado
780 equity = self._vectorized_equity(trades, self.initial_capital)
781
782 # Calcular métricas vectorizado
783 metrics = self._vectorized_metrics(equity, trades)
784
785 return metrics
786
787 def _vectorized_trades(self, df, signals_df) -> List[Trade]:
788 """Extrae trades usando operaciones vectorizadas"""
789 # Usar numpy masks en lugar de loops
790 entry_mask = signals_df['signal'].notna()
791 exit_mask = signals_df['exit'].notna()
792
793 entries = df.index[entry_mask]
794 exits = df.index[exit_mask]
795
796 # Aparear entries/exits vectorizado
797 trades = []
798 for entry_idx, exit_idx in zip(entries, exits):
799 # ...
800 pass
801
802 return trades
803
804 def _vectorized_equity(self, trades: List[Trade], initial_capital: float) -> np.ndarray:
805 """Calcula equity curve vectorizado"""
806 equity = np.full(len(self.df), initial_capital, dtype=np.float64)
807
808 cumsum_pnl = np.array([t.pnl for t in trades]).cumsum()
809
810 # Vectorizado
811 equity[trade_indices] += cumsum_pnl
812
813 return np.maximum.accumulate(equity) # No puede bajar del máximo
814
815 def _vectorized_metrics(self, equity: np.ndarray, trades: List[Trade]) -> Dict:
816 """Calcula métricas vectorizado"""
817 returns = np.diff(equity) / equity[:-1]
818
819 return {
820 'total_return': (equity[-1] - equity[0]) / equity[0],
821 'sharpe': np.mean(returns) / np.std(returns) * np.sqrt(252),
822 'max_drawdown': np.min(equity) / np.max(equity) - 1,
823 'win_rate': len([t for t in trades if t.pnl > 0]) / len(trades),
824 }
825```
826
827**Benchmark esperado:**
828- Loop actual: ~2.5 segundos para 5000 velas
829- Vectorizado: ~0.25 segundos (10x más rápido)
830
831---
832
833#### 2.3 Async Logging
834
835**Esfuerzo:** 2-3 horas
836**Impacto:** 🟠 Medio - Reduce latency 10-20%
837
838```python
839# utils/logger_async.py - NUEVO
840import logging
841import asyncio
842from logging.handlers import RotatingFileHandler
843from pythonjsonlogger import jsonlogger
844from queue import Queue
845
846class AsyncQueueHandler(logging.Handler):
847 """Handler que escribe logs de forma asincrónica"""
848
849 def __init__(self, queue: Queue):
850 super().__init__()
851 self.queue = queue
852
853 def emit(self, record):
854 # Poner log en queue sin bloquear
855 try:
856 self.queue.put_nowait(record)
857 except:
858 pass
859
860async def async_log_writer(queue: Queue, file_handler: RotatingFileHandler):
861 """Procesa logs de la queue de forma asincrónica"""
862 while True:
863 try:
864 record = queue.get(timeout=0.1)
865 file_handler.emit(record)
866 except:
867 await asyncio.sleep(0.01)
868
869# Usar en startup
870@app.on_event("startup")
871async def setup_async_logging():
872 log_queue = Queue()
873 file_handler = RotatingFileHandler(...)
874
875 asyncio.create_task(async_log_writer(log_queue, file_handler))
876```
877
878---
879
880#### 2.4 Pool de Conexiones
881
882**Esfuerzo:** 3-4 horas
883**Impacto:** 🟠 Medio - Reduce overhead de conexión
884
885```python
886# core/connection_pool.py - NUEVO
887from typing import Dict
888import MT5
889
890class ConnectionPool:
891 """Pool de conexiones a MT5 y exchanges"""
892
893 def __init__(self, max_connections: int = 5):
894 self.max_connections = max_connections
895 self.pool: Dict[str, list] = {
896 'mt5': [],
897 'binance': [],
898 'yfinance': [],
899 }
900 self.locks: Dict[str, asyncio.Lock] = {
901 k: asyncio.Lock() for k in self.pool
902 }
903
904 async def get_mt5_connection(self):
905 async with self.locks['mt5']:
906 if self.pool['mt5']:
907 return self.pool['mt5'].pop()
908
909 # Crear nueva conexión
910 conn = MT5.MetaTrader5()
911 conn.initialize()
912 return conn
913
914 async def return_connection(self, conn_type: str, conn):
915 async with self.locks[conn_type]:
916 if len(self.pool[conn_type]) < self.max_connections:
917 self.pool[conn_type].append(conn)
918 else:
919 conn.close()
920
921# Global instance
922connection_pool = ConnectionPool()
923```
924
925---
926
927### NIVEL 3: MEJORAS (Próximo mes) 🟡
928
929#### 3.1 Sistema de Persistencia
930
931**Esfuerzo:** 8-10 horas
932**Impacto:** 🟡 Medio - Historial y reproducibilidad
933
934```python
935# database/models.py - NUEVO
936from sqlalchemy import create_engine, Column, String, Float, JSON
937from sqlalchemy.orm import declarative_base
938
939Base = declarative_base()
940
941class BacktestRun(Base):
942 __tablename__ = 'backtest_runs'
943
944 id = Column(String, primary_key=True)
945 symbol = Column(String)
946 timeframe = Column(String)
947 algorithm = Column(String)
948 parameters = Column(JSON)
949 result = Column(JSON)
950 created_at = Column(DateTime)
951 duration_seconds = Column(Float)
952```
953
954#### 3.2 Fila de Tareas (Celery o RQ)
955
956**Esfuerzo:** 10-12 horas
957**Impacto:** 🟡 Medio - Escalabilidad
958
959```bash
960# Encolar backtests en lugar de ejecutar sincrónico
961celery -A tasks worker --loglevel=info
962
963# En API
964from tasks import run_backtest_task
965
966task = run_backtest_task.delay(
967 symbol="BTC-USD",
968 timeframe="D1",
969 algorithm="FAST"
970)
971
972# Cliente puede consultar estado
973result = task.get(timeout=300)
974```
975
976#### 3.3 Monitoreo y Alertas
977
978**Esfuerzo:** 6-8 horas
979**Impacto:** 🟡 Medio - Observabilidad
980
981```python
982# utils/monitoring.py - NUEVO
983from prometheus_client import Counter, Histogram
984
985backtest_counter = Counter('backtests_total', 'Total backtests')
986backtest_duration = Histogram('backtest_duration_seconds', 'Backtest duration')
987cache_hit_rate = Counter('cache_hits_total', 'Cache hits')
988
989@backtest_duration.time()
990def run_backtest(request):
991 try:
992 result = _run_backtest(request)
993 backtest_counter.labels(status='success').inc()
994 return result
995 except Exception as e:
996 backtest_counter.labels(status='error').inc()
997 raise
998```
999
1000#### 3.4 Pruebas Automatizadas
1001
1002**Esfuerzo:** 8-10 horas
1003**Impacto:** 🟡 Medio - Confiabilidad
1004
1005```python
1006# tests/test_backtester.py - NUEVO
1007import pytest
1008from backtesting_app.core.backtester import Backtester
1009
1010@pytest.fixture
1011def sample_data():
1012 """Fixture con datos de prueba"""
1013 return pd.DataFrame({
1014 'open': [100, 101, 102],
1015 'high': [101, 102, 103],
1016 'low': [99, 100, 101],
1017 'close': [100, 101, 102],
1018 'volume': [1000, 1100, 1200]
1019 })
1020
1021def test_backtest_empty_data():
1022 """Backtest debe fallar con datos vacíos"""
1023 with pytest.raises(ValueError):
1024 backtester.run(pd.DataFrame(), {})
1025
1026def test_backtest_with_sample_data(sample_data):
1027 """Backtest debe completarse con datos válidos"""
1028 result = backtester.run(sample_data, {'param1': 10})
1029 assert result.total_trades >= 0
1030 assert result.equity_curve is not None
1031
1032def test_backtest_no_negative_equity(sample_data):
1033 """Equity nunca debe ser negativo"""
1034 result = backtester.run(sample_data, {})
1035 assert (result.equity_curve >= 0).all()
1036```
1037
1038---
1039
1040### NIVEL 4: ESCALABILIDAD (Futuro) 💡
1041
1042#### 4.1 Arquitectura Multi-proceso
1043
1044```
1045┌─────────────────────────────────────┐
1046│ Load Balancer (Nginx) │
1047└─────────────────────────────────────┘
1048 ↓ ↓ ↓
1049┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
1050│FastAPI│ │FastAPI│ │FastAPI│ │FastAPI│ (4 workers)
1051└──────┘ └──────┘ └──────┘ └──────┘
1052 ↓ ↓ ↓
1053┌─────────────────────────────────────┐
1054│ Redis Queue (Job Queue) │
1055└─────────────────────────────────────┘
1056 ↓ ↓ ↓
1057┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
1058│Worker│ │Worker│ │Worker│ │Worker│ (4 workers)
1059└──────┘ └──────┘ └──────┘ └──────┘
1060```
1061
1062#### 4.2 Sharding Horizontal
1063
1064- Dividir datos por símbolo
1065- Ejecutar backtests en paralelo
1066- Distribuir cache entre nodos
1067
1068#### 4.3 GraphQL para Queries Complejas
1069
1070- Reemplazar REST para queries complejas
1071- Mejor control de n+1 queries
1072- Caché automático
1073
1074---
1075
1076## 4️⃣ MATRIZ DE PRIORIDADES
1077
1078| Problema | Impacto | Esfuerzo | Prioridad | Semana |
1079|----------|---------|----------|-----------|---------|
1080| Manejo de errores robusto | 🔴 Alto | 2-3h | 1 | Sem 1 |
1081| Validación de entrada | 🔴 Alto | 3-4h | 1 | Sem 1 |
1082| Manejo de concurrencia | 🔴 Alto | 2-3h | 1 | Sem 1 |
1083| Timeouts en I/O | 🔴 Alto | 3-4h | 1 | Sem 1 |
1084| Caché optimizado | 🟠 Medio | 4-5h | 2 | Sem 2 |
1085| Vectorización backtester | 🟠 Medio | 6-8h | 2 | Sem 2-3 |
1086| Async logging | 🟠 Medio | 2-3h | 2 | Sem 2 |
1087| Pool de conexiones | 🟠 Medio | 3-4h | 2 | Sem 3 |
1088| Base de datos | 🟡 Bajo | 8-10h | 3 | Sem 4-5 |
1089| Celery/RQ | 🟡 Bajo | 10-12h | 3 | Sem 5-6 |
1090| Monitoreo | 🟡 Bajo | 6-8h | 3 | Sem 4 |
1091| Tests automatizados | 🟡 Bajo | 8-10h | 3 | Sem 4-6 |
1092
1093---
1094
1095## 5️⃣ CHECKLIST DE IMPLEMENTACIÓN
1096
1097### Semana 1: Estabilidad
1098
1099- [ ] Crear `utils/exceptions.py` con excepciones tipadas
1100- [ ] Agregar validación en backtester.run()
1101- [ ] Agregar try-catch en todos los endpoints
1102- [ ] Implementar timeout context manager
1103- [ ] Agregar Semaphore para limitar backtests concurrentes
1104- [ ] Agregar timeout a operaciones I/O (yfinance, MT5)
1105- [ ] Tests unitarios para validación
1106- [ ] Logs de error detallados
1107
1108### Semana 2-3: Performance
1109
1110- [ ] Crear `utils/cache_v2.py` optimizado
1111- [ ] Refactorizar backtester a vectorizado
1112- [ ] Benchmarks antes/después
1113- [ ] Implementar async logging
1114- [ ] Crear pool de conexiones
1115- [ ] Optimizar `get_symbols()`
1116- [ ] Tests de performance
1117
1118### Semana 4-5: Persistencia
1119
1120- [ ] Crear schema de base de datos
1121- [ ] Implementar CRUD para BacktestRun
1122- [ ] Migrar datos históricos
1123- [ ] Crear endpoint de historial
1124- [ ] UI para ver resultados históricos
1125- [ ] Backup de base de datos
1126
1127### Semana 6+: Escalabilidad
1128
1129- [ ] Implementar Celery + Redis
1130- [ ] Refactorizar API para encolar tareas
1131- [ ] Multi-worker FastAPI setup
1132- [ ] Prometheus metrics
1133- [ ] Grafana dashboards
1134- [ ] Tests de carga
1135
1136---
1137
1138## 6️⃣ RECOMENDACIONES INMEDIATAS
1139
1140### 🔴 Hoy (Crítico)
1141
11421. **Agregar validación de entrada en todos los endpoints**
1143 ```python
1144 # En routes.py
1145 def validate_backtest_request(request: BacktestRequest):
1146 assert request.symbol, "Symbol required"
1147 assert request.capital > 0, "Capital must be > 0"
1148 assert 0 < request.risk_percent <= 100
1149 return True
1150 ```
1151
11522. **Agregar timeout a yfinance**
1153 ```python
1154 data = yf.download(..., timeout=30)
1155 ```
1156
11573. **Agregar logging de excepciones**
1158 ```python
1159 except Exception as e:
1160 logger.error(f"Backtest failed: {e}", exc_info=True)
1161 raise
1162 ```
1163
1164### 🟠 Esta Semana (Importante)
1165
11661. **Limitar concurrencia de backtests**
1167 - Max 3 simultáneamente
1168 - Usar Semaphore
1169
11702. **Optimizar cache keys**
1171 - No serializar DataFrames completos
1172
11733. **Tests básicos**
1174 - Datos vacíos
1175 - Parámetros inválidos
1176 - Datos con NaN
1177
1178---
1179
1180## 7️⃣ MÉTRICAS DE ÉXITO
1181
1182Después de implementar mejoras:
1183
1184| Métrica | Actual | Objetivo | Método |
1185|---------|--------|----------|--------|
1186| Error rate | ~5% | <0.5% | Error tracking |
1187| P95 latency | ~5s | <1s | APM monitoring |
1188| Cache hit rate | ~30% | >70% | Metrics |
1189| Throughput (req/s) | 2 | 10+ | Load testing |
1190| Crashes por día | 2-3 | 0 | Error tracking |
1191| Test coverage | 0% | >70% | Pytest coverage |
1192
1193---
1194
1195## 📚 REFERENCIAS Y RECURSOS
1196
1197### Manejo de Errores
1198- PEP 3134: Exception Chaining
1199- https://realpython.com/python-exceptions/
1200
