Team Ai
Apppublic

diegobeyl/backtesting

sourceHugging Faceupdated 9mo agoView on Hugging Face
2likes
ANALISIS_PROFUNDO_V2.md1236 linesDownload Raw Back to docs
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

Showing the first 1,200 of 1236 lines. Download the file for the rest.