Team Ai
Apppublic

diegobeyl/backtesting

sourceHugging Faceupdated 9mo agoView on Hugging Face
2likes
timeout_limiter.py276 linesDownload Raw Back to core
1"""2Módulo de Timeouts y Límites de Ejecución - Fase 33Previene que backtests larguísimos bloqueen la aplicación4"""5 6import time7import threading8from typing import Optional, Callable, Any, Dict9from datetime import datetime, timedelta10import logging11 12logger = logging.getLogger(__name__)13 14 15class TimeoutException(Exception):16    """Excepción cuando se excede el timeout"""17    pass18 19 20class ExecutionLimiter:21    """22    Limita el tiempo de ejecución de operaciones23    Soporta:24    - Timeout global25    - Timeouts por operación26    - Estimación de tiempo restante27    - Cancelación de operaciones28    """29    30    # Timeouts por defecto (en segundos)31    DEFAULT_TIMEOUTS = {32        'download_data': 60,      # 1 minuto para descargar33        'backtest': 300,          # 5 minutos para backtest34        'analysis': 120,          # 2 minutos para análisis35        'optimization': 600,      # 10 minutos para optimización36    }37    38    # Límites de duración de ejecución39    EXECUTION_LIMITS = {40        'data_download': {41            'min_seconds': 1,42            'max_seconds': 120,43            'warning_seconds': 60,44            'description': 'Descarga de datos'45        },46        'backtest_single': {47            'min_seconds': 1,48            'max_seconds': 600,49            'warning_seconds': 300,50            'description': 'Backtest de un activo'51        },52        'backtest_multi': {53            'min_seconds': 1,54            'max_seconds': 1200,55            'warning_seconds': 600,56            'description': 'Backtest multi-activo'57        },58    }59    60    def __init__(self, operation: str = 'backtest'):61        """62        Inicializa el limitador63        64        Args:65            operation: Tipo de operación ('download_data', 'backtest', 'analysis', etc.)66        """67        self.operation = operation68        self.start_time = None69        self.timeout = self.DEFAULT_TIMEOUTS.get(operation, 300)70        self.elapsed = 071        self.is_running = False72        self.was_cancelled = False73        self._timer_thread = None74    75    def start(self) -> None:76        """Inicia el cronómetro"""77        self.start_time = time.time()78        self.is_running = True79        self.elapsed = 080        self.was_cancelled = False81        logger.info(f"⏱️  Iniciando {self.operation} con timeout de {self.timeout}s")82    83    def stop(self) -> float:84        """Detiene el cronómetro y retorna tiempo transcurrido"""85        if not self.is_running:86            return self.elapsed87        88        self.elapsed = time.time() - self.start_time89        self.is_running = False90        logger.info(f"⏱️  {self.operation} completado en {self.elapsed:.2f}s")91        return self.elapsed92    93    def get_elapsed(self) -> float:94        """Retorna tiempo transcurrido hasta ahora"""95        if not self.is_running or self.start_time is None:96            return self.elapsed97        return time.time() - self.start_time98    99    def check_timeout(self) -> None:100        """Verifica si se excedió el timeout"""101        if not self.is_running:102            return103        104        elapsed = self.get_elapsed()105        if elapsed > self.timeout:106            self.was_cancelled = True107            raise TimeoutException(108                f"Timeout excedido en {self.operation}\n"109                f"Límite: {self.timeout}s\n"110                f"Transcurrido: {elapsed:.2f}s"111            )112    113    def get_remaining_time(self) -> float:114        """Retorna segundos restantes"""115        elapsed = self.get_elapsed()116        remaining = max(0, self.timeout - elapsed)117        return remaining118    119    def get_progress_percent(self) -> float:120        """Retorna progreso como porcentaje"""121        elapsed = self.get_elapsed()122        percent = min(100, (elapsed / self.timeout) * 100)123        return percent124    125    def format_remaining(self) -> str:126        """Retorna tiempo restante formateado"""127        remaining = self.get_remaining_time()128        minutes = int(remaining // 60)129        seconds = int(remaining % 60)130        return f"{minutes}m {seconds}s"131    132    def format_elapsed(self) -> str:133        """Retorna tiempo transcurrido formateado"""134        elapsed = self.get_elapsed()135        minutes = int(elapsed // 60)136        seconds = int(elapsed % 60)137        return f"{minutes}m {seconds}s"138    139    def get_status_message(self) -> str:140        """Retorna mensaje de estado"""141        elapsed = self.get_elapsed()142        remaining = self.get_remaining_time()143        percent = self.get_progress_percent()144        145        return (146            f"⏱️  {self.operation}\n"147            f"   Transcurrido: {self.format_elapsed()}\n"148            f"   Restante: {self.format_remaining()}\n"149            f"   Progreso: {percent:.0f}%"150        )151 152 153class OperationTracker:154    """155    Rastreia múltiples operaciones simultáneamente156    Útil para backtests multi-activo157    """158    159    def __init__(self):160        self.operations: Dict[str, ExecutionLimiter] = {}161        self.results: Dict[str, Any] = {}162    163    def start_operation(self, operation_id: str, operation_type: str = 'backtest') -> ExecutionLimiter:164        """Inicia una nueva operación"""165        limiter = ExecutionLimiter(operation_type)166        limiter.start()167        self.operations[operation_id] = limiter168        return limiter169    170    def complete_operation(self, operation_id: str, result: Any = None) -> float:171        """Completa una operación"""172        if operation_id not in self.operations:173            return 0174        175        elapsed = self.operations[operation_id].stop()176        if result is not None:177            self.results[operation_id] = result178        return elapsed179    180    def get_total_elapsed(self) -> float:181        """Retorna tiempo total de todas las operaciones"""182        total = 0183        for limiter in self.operations.values():184            total += limiter.get_elapsed()185        return total186    187    def get_status_summary(self) -> str:188        """Retorna resumen de estado"""189        summary = "📊 Estado de Operaciones:\n"190        for op_id, limiter in self.operations.items():191            status = "✅" if limiter.elapsed > 0 else "⏱️"192            summary += f"  {status} {op_id}: {limiter.format_elapsed()}\n"193        return summary194 195 196class ProgressTracker:197    """198    Rastreia el progreso de operaciones largas199    Proporciona ETAs y estimaciones200    """201    202    def __init__(self, total_items: int, operation_name: str = 'Procesamiento'):203        self.total_items = total_items204        self.operation_name = operation_name205        self.processed = 0206        self.start_time = time.time()207    208    def update(self, items_processed: int = 1) -> None:209        """Actualiza contador"""210        self.processed += items_processed211    212    def get_progress_percent(self) -> float:213        """Retorna progreso como %"""214        if self.total_items == 0:215            return 100216        return (self.processed / self.total_items) * 100217    218    def get_elapsed_time(self) -> float:219        """Retorna tiempo transcurrido"""220        return time.time() - self.start_time221    222    def get_estimated_total_time(self) -> float:223        """Estima tiempo total basado en progreso"""224        elapsed = self.get_elapsed_time()225        if self.processed == 0:226            return 0227        228        rate = self.processed / elapsed229        if rate == 0:230            return 0231        232        return self.total_items / rate233    234    def get_eta(self) -> float:235        """Retorna ETA en segundos"""236        total = self.get_estimated_total_time()237        elapsed = self.get_elapsed_time()238        return max(0, total - elapsed)239    240    def format_eta(self) -> str:241        """Retorna ETA formateado"""242        eta_seconds = self.get_eta()243        if eta_seconds < 60:244            return f"{int(eta_seconds)}s"245        246        minutes = int(eta_seconds // 60)247        seconds = int(eta_seconds % 60)248        return f"{minutes}m {seconds}s"249    250    def format_progress_bar(self, width: int = 20) -> str:251        """Retorna barra de progreso ASCII"""252        percent = self.get_progress_percent()253        filled = int(width * percent / 100)254        bar = "█" * filled + "░" * (width - filled)255        return f"[{bar}] {percent:.0f}%"256    257    def get_status(self) -> str:258        """Retorna mensaje de estado completo"""259        return (260            f"📊 {self.operation_name}\n"261            f"   Progreso: {self.processed}/{self.total_items} ({self.get_progress_percent():.0f}%)\n"262            f"   {self.format_progress_bar()}\n"263            f"   Transcurrido: {self.format_elapsed()}\n"264            f"   ETA: {self.format_eta()}"265        )266    267    def format_elapsed(self) -> str:268        """Retorna tiempo transcurrido formateado"""269        elapsed = self.get_elapsed_time()270        if elapsed < 60:271            return f"{int(elapsed)}s"272        273        minutes = int(elapsed // 60)274        seconds = int(elapsed % 60)275        return f"{minutes}m {seconds}s"276