Team Ai
Apppublic

diegobeyl/backtesting

sourceHugging Faceupdated 9mo agoView on Hugging Face
2likes
validator.py264 linesDownload Raw Back to core
1"""2Módulo de validación y control de límites3Previene bloqueos y solicitudes excesivas4"""5 6from datetime import datetime, timedelta7from typing import Dict, Tuple, Optional8import logging9 10logger = logging.getLogger(__name__)11 12# Límites de seguridad por timeframe13# Formato: 'timeframe': (max_candles, max_years, description)14# Basado en límites reales de Yahoo Finance15SAFETY_LIMITS = {16    '1m': (10080, 0.02, "máx 7 días"),          # 7 días * 1440 min/día = 10,080 velas17    '5m': (17280, 0.16, "máx 60 días"),         # 60 días * 288 velas/día = 17,280 velas18    '15m': (5760, 0.16, "máx 60 días"),         # 60 días * 96 velas/día = 5,760 velas19    '30m': (2880, 0.16, "máx 60 días"),         # 60 días * 48 velas/día = 2,880 velas20    '1h': (17520, 2, "máx 730 días"),           # 730 días * 24 horas/día = 17,520 velas21    '4h': (4380, 2, "máx 730 días"),            # 730 días * 6 velas/día = 4,380 velas22    '1d': (7300, 20, "máx 20 años"),            # 20 años * 365 días = 7,300 velas23    '1wk': (5200, 100, "máx ~100 años"),        # 100 años * 52 semanas = 5,200 velas24    '1mo': (1200, 100, "máx ~100 años"),        # 100 años * 12 meses = 1,200 velas25}26 27# Límites extendidos cuando se usa Twelve Data API28# Twelve Data permite hasta 5000 velas por request29SAFETY_LIMITS_WITH_API = {30    '1m': (10080, 0.02, "máx 7 días"),          # Sin cambio (limitado por data source)31    '5m': (17280, 0.16, "máx 60 días"),         # Sin cambio32    '15m': (5760, 0.16, "máx 60 días"),         # Sin cambio33    '30m': (2880, 0.16, "máx 60 días"),         # Sin cambio34    '1h': (87600, 10, "máx 10 años"),           # 3650 días * 24 horas = 87,600 velas (Twelve Data)35    '4h': (21900, 10, "máx 10 años"),           # 3650 días * 6 velas/día = 21,900 velas36    '1d': (7300, 20, "máx 20 años"),            # Sin cambio37    '1wk': (5200, 100, "máx ~100 años"),        # Sin cambio38    '1mo': (1200, 100, "máx ~100 años"),        # Sin cambio39}40 41# Descripción de límites para el usuario42LIMIT_DESCRIPTIONS = {43    '1m': {44        'friendly_name': '1 minuto',45        'max_days': 7,46        'reason': 'Máximo 7 días. Límite de Yahoo Finance para datos intradiarios.'47    },48    '5m': {49        'friendly_name': '5 minutos',50        'max_days': 60,51        'reason': 'Máximo 60 días. Límite de Yahoo Finance para datos intradiarios.'52    },53    '15m': {54        'friendly_name': '15 minutos',55        'max_days': 60,56        'reason': 'Máximo 60 días. Límite de Yahoo Finance para datos intradiarios.'57    },58    '30m': {59        'friendly_name': '30 minutos',60        'max_days': 60,61        'reason': 'Máximo 60 días. Límite de Yahoo Finance para datos intradiarios.'62    },63    '1h': {64        'friendly_name': '1 hora',65        'max_days': 730,66        'max_days_with_api': 3650,67        'reason': 'Máximo 730 días (~2 años) con Yahoo Finance. Hasta 10 años con Twelve Data API.'68    },69    '4h': {70        'friendly_name': '4 horas',71        'max_days': 730,72        'max_days_with_api': 3650,73        'reason': 'Máximo 730 días (~2 años) con Yahoo Finance. Hasta 10 años con Twelve Data API.'74    },75    '1d': {76        'friendly_name': '1 día',77        'max_days': 7300,78        'reason': 'Máximo 20 años. Historial completo disponible.'79    },80    '1wk': {81        'friendly_name': '1 semana',82        'max_days': 36500,83        'reason': 'Máximo ~100 años. Historial completo disponible.'84    },85    '1mo': {86        'friendly_name': '1 mes',87        'max_days': 36500,88        'reason': 'Máximo ~100 años. Historial completo disponible.'89    },90}91 92 93class ValidationError(Exception):94    """Excepción personalizada para errores de validación"""95    pass96 97 98class DataValidator:99    """Validador de parámetros de descarga de datos"""100    101    @staticmethod102    def validate_timeframe(timeframe: str) -> None:103        """Valida que el timeframe sea válido"""104        valid_timeframes = list(SAFETY_LIMITS.keys())105        if timeframe not in valid_timeframes:106            raise ValidationError(107                f"Timeframe inválido: '{timeframe}'. "108                f"Opciones válidas: {', '.join(valid_timeframes)}"109            )110    111    @staticmethod112    def validate_symbol(symbol: str) -> None:113        """Valida que el símbolo tenga formato correcto"""114        if not symbol:115            raise ValidationError(116                f"Símbolo inválido: '{symbol}'. "117                f"El símbolo no puede estar vacío."118            )119        # Aceptar múltiples formatos: BTC-USD (crypto), AAPL (acciones), EURUSD=X (forex), ANA.MC (internacional)120        # Permitir alfanumérico, guiones, iguales, puntos y circunflejo (para índices como ^GSPC)121        if not all(c.isalnum() or c in '-=.^' for c in symbol):122            raise ValidationError(123                f"Símbolo inválido: '{symbol}'. "124                f"Debe contener solo letras, números, guiones, puntos e iguales."125            )126    127    @staticmethod128    def validate_date_range(start_date: datetime, end_date: datetime) -> None:129        """Valida que el rango de fechas sea válido"""130        if start_date >= end_date:131            raise ValidationError(132                f"Rango de fechas inválido: inicio ({start_date.date()}) "133                f"debe ser antes que fin ({end_date.date()})"134            )135    136    @staticmethod137    def calculate_candles(start_date: datetime, end_date: datetime, timeframe: str) -> int:138        """Calcula cuántas velas se descargarán"""139        days = (end_date - start_date).days140        141        candles_per_day = {142            '1m': 1440,  # 24 * 60143            '5m': 288,   # 1440 / 5144            '15m': 96,   # 1440 / 15145            '30m': 48,   # 1440 / 30146            '1h': 24,    # 24147            '4h': 6,     # 24 / 4148            '1d': 1,149            '1wk': 1 / 7,150            '1mo': 1 / 30,151        }152        153        return int(days * candles_per_day.get(timeframe, 1))154    155    @staticmethod156    def validate_request(157        symbol: str,158        timeframe: str,159        start_date: datetime,160        end_date: datetime,161        source: str = 'yahoo',162        has_api_key: bool = False163    ) -> Tuple[bool, Optional[str]]:164        """165        Valida una solicitud completa de descarga.166 167        Args:168            symbol: Símbolo del activo169            timeframe: Timeframe de las velas170            start_date: Fecha de inicio171            end_date: Fecha de fin172            source: Fuente de datos (yahoo, etc.)173            has_api_key: Si el usuario proporcionó API key de Twelve Data174 175        Retorna: (is_valid, error_message)176        """177        try:178            # Validar parámetros básicos179            DataValidator.validate_symbol(symbol)180            DataValidator.validate_timeframe(timeframe)181            DataValidator.validate_date_range(start_date, end_date)182 183            # Calcular velas necesarias184            candles = DataValidator.calculate_candles(start_date, end_date, timeframe)185            days = (end_date - start_date).days186            years = days / 365.25187 188            # Obtener límites para este timeframe189            # Si tiene API key de Twelve Data, usar límites extendidos190            if has_api_key and timeframe in ['1h', '4h', '1d']:191                max_candles, max_years, description = SAFETY_LIMITS_WITH_API[timeframe]192            else:193                max_candles, max_years, description = SAFETY_LIMITS[timeframe]194 195            # Validar contra límites196            if candles > max_candles:197                friendly = LIMIT_DESCRIPTIONS.get(timeframe, {}).get('friendly_name', timeframe)198                reason = LIMIT_DESCRIPTIONS.get(timeframe, {}).get('reason', 'Demasiados datos')199 200                # Mensaje personalizado dependiendo si tiene API key201                if not has_api_key and timeframe in ['1h', '4h']:202                    api_suggestion = (203                        f"\n  4️⃣ Usar Twelve Data API (hasta 10 años en {friendly})\n"204                        f"     👉 Ingresa tu API key en el sidebar\n"205                        f"     👉 Gratis en https://twelvedata.com/pricing"206                    )207                else:208                    api_suggestion = ""209 210                return False, (211                    f"⚠️ PERÍODO DE TIEMPO DEMASIADO LARGO\n\n"212                    f"📊 Solicitud Actual:\n"213                    f"  • Símbolo: {symbol}\n"214                    f"  • Timeframe: {friendly}\n"215                    f"  • Período: {days} días (~{years:.1f} años)\n"216                    f"  • Velas resultantes: {candles:,}\n\n"217                    f"❌ Límite Excedido:\n"218                    f"  • Máximo permitido: {max_candles:,} velas\n"219                    f"  • Máximo período: {max_years} años\n"220                    f"  • Razón: {reason}\n\n"221                    f"✅ Soluciones:\n"222                    f"  1️⃣ Reducir el período temporal (máx {max_years} años)\n"223                    f"  2️⃣ Usar timeframe más grande (ej: 4h, 1d, 1wk)\n"224                    f"  3️⃣ Usar el selector de mes para datos específicos"225                    f"{api_suggestion}\n\n"226                    f"💡 Ejemplo: Para {symbol} en {friendly}, puedes obtener hasta {max_years} años"227                )228 229            return True, None230 231        except ValidationError as e:232            return False, str(e)233        except Exception as e:234            return False, f"Error de validación: {str(e)}"235    236    @staticmethod237    def get_download_info(238        symbol: str,239        timeframe: str,240        start_date: datetime,241        end_date: datetime242    ) -> Dict:243        """Retorna información sobre la descarga propuesta"""244        candles = DataValidator.calculate_candles(start_date, end_date, timeframe)245        days = (end_date - start_date).days246        years = days / 365.25247        248        friendly_name = LIMIT_DESCRIPTIONS.get(timeframe, {}).get('friendly_name', timeframe)249        max_candles, _, _ = SAFETY_LIMITS.get(timeframe, (0, 0, ''))250        251        return {252            'symbol': symbol,253            'timeframe': timeframe,254            'friendly_timeframe': friendly_name,255            'start_date': start_date.strftime('%Y-%m-%d'),256            'end_date': end_date.strftime('%Y-%m-%d'),257            'days': days,258            'years': round(years, 2),259            'candles': candles,260            'max_candles': max_candles,261            'usage_percent': round((candles / max_candles) * 100, 1),262            'estimated_mb': round(candles * 0.005, 2),  # ~5KB por vela263        }264