""" Exchange Factory for creating data collectors. This module provides a factory pattern for creating data collectors from different exchanges based on configuration. """ import importlib from typing import Dict, List, Optional, Any, Type, Tuple from dataclasses import dataclass, field from utils.logger import get_logger from ..base_collector import BaseDataCollector, DataType from ..common import CandleProcessingConfig from .registry import EXCHANGE_REGISTRY, get_supported_exchanges, get_exchange_info from .exceptions import ( ExchangeError, ExchangeNotSupportedError, InvalidConfigurationError, CollectorCreationError, ValidationError ) # Initialize logger logger = get_logger('exchanges') @dataclass class ExchangeCollectorConfig: """Configuration for creating an exchange collector.""" exchange: str symbol: str data_types: List[DataType] timeframes: List[str] = field(default_factory=lambda: ['1m', '5m']) # Default timeframes auto_restart: bool = True health_check_interval: float = 30.0 store_raw_data: bool = True custom_params: Optional[Dict[str, Any]] = None def __post_init__(self): """Validate configuration after initialization.""" if not self.exchange: raise InvalidConfigurationError("Exchange name cannot be empty") if not self.symbol: raise InvalidConfigurationError("Symbol cannot be empty") if not self.data_types: raise InvalidConfigurationError("At least one data type must be specified") if not self.timeframes: raise InvalidConfigurationError("At least one timeframe must be specified") logger.debug(f"Created collector config for {self.exchange} {self.symbol}") class ExchangeFactory: """Factory for creating exchange-specific data collectors.""" @staticmethod def create_collector(config: ExchangeCollectorConfig) -> BaseDataCollector: """ Create a data collector for the specified exchange. Args: config: Configuration for the collector Returns: Instance of the appropriate collector class Raises: ExchangeNotSupportedError: If exchange is not supported CollectorCreationError: If collector creation fails """ exchange_name = config.exchange.lower() logger.info(f"Creating collector for {exchange_name} {config.symbol}") if exchange_name not in EXCHANGE_REGISTRY: supported = get_supported_exchanges() error_msg = f"Exchange '{config.exchange}' not supported. Supported exchanges: {supported}" logger.error(error_msg) raise ExchangeNotSupportedError(error_msg) exchange_info = get_exchange_info(exchange_name) collector_class_path = exchange_info['collector'] # Parse module and class name module_path, class_name = collector_class_path.rsplit('.', 1) try: # Import the module logger.debug(f"Importing collector module {module_path}") module = importlib.import_module(module_path) # Get the collector class collector_class = getattr(module, class_name) # Prepare collector arguments collector_args = { 'symbol': config.symbol, 'data_types': config.data_types, 'auto_restart': config.auto_restart, 'health_check_interval': config.health_check_interval, 'store_raw_data': config.store_raw_data, 'timeframes': config.timeframes # Pass timeframes to collector } # Add any custom parameters if config.custom_params: # If custom_params contains a candle_config key, use it, otherwise create one if 'candle_config' not in config.custom_params: config.custom_params['candle_config'] = CandleProcessingConfig( timeframes=config.timeframes ) collector_args.update(config.custom_params) else: # Create default candle config if no custom params collector_args['candle_config'] = CandleProcessingConfig( timeframes=config.timeframes ) # Create and return the collector instance logger.info(f"Successfully created collector for {exchange_name} {config.symbol}") return collector_class(**collector_args) except ImportError as e: error_msg = f"Failed to import collector class '{collector_class_path}': {e}" logger.error(error_msg) raise CollectorCreationError(error_msg) from e except Exception as e: error_msg = f"Failed to create collector for '{config.exchange}': {e}" logger.error(error_msg) raise CollectorCreationError(error_msg) from e @staticmethod def create_multiple_collectors(configs: List[ExchangeCollectorConfig]) -> List[BaseDataCollector]: """ Create multiple collectors from a list of configurations. Args: configs: List of collector configurations Returns: List of collector instances """ collectors = [] logger.info(f"Creating {len(configs)} collectors") for config in configs: try: collector = ExchangeFactory.create_collector(config) collectors.append(collector) logger.debug(f"Successfully created collector for {config.exchange} {config.symbol}") except ExchangeError as e: logger.error(f"Failed to create collector for {config.exchange} {config.symbol}: {e}") logger.info(f"Successfully created {len(collectors)} out of {len(configs)} collectors") return collectors @staticmethod def get_supported_pairs(exchange: str) -> List[str]: """ Get supported trading pairs for an exchange. Args: exchange: Exchange name Returns: List of supported trading pairs """ exchange_info = get_exchange_info(exchange) if exchange_info: return exchange_info.get('supported_pairs', []) return [] @staticmethod def get_supported_data_types(exchange: str) -> List[str]: """ Get supported data types for an exchange. Args: exchange: Exchange name Returns: List of supported data types """ exchange_info = get_exchange_info(exchange) if exchange_info: return exchange_info.get('supported_data_types', []) return [] @staticmethod def validate_config(config: ExchangeCollectorConfig) -> Tuple[bool, List[str]]: """ Validate collector configuration. Args: config: Configuration to validate Returns: Tuple of (is_valid, list_of_errors) """ logger.debug(f"Validating configuration for {config.exchange} {config.symbol}") errors = [] # Check if exchange is supported if config.exchange.lower() not in EXCHANGE_REGISTRY: errors.append(f"Exchange '{config.exchange}' not supported") # Check if symbol is supported supported_pairs = ExchangeFactory.get_supported_pairs(config.exchange) if supported_pairs and config.symbol not in supported_pairs: errors.append(f"Symbol '{config.symbol}' not supported for {config.exchange}") # Check if data types are supported supported_data_types = ExchangeFactory.get_supported_data_types(config.exchange) if supported_data_types: for data_type in config.data_types: if data_type.value not in supported_data_types: errors.append(f"Data type '{data_type.value}' not supported for {config.exchange}") is_valid = len(errors) == 0 if not is_valid: logger.warning(f"Configuration validation failed for {config.exchange}: {errors}") else: logger.debug(f"Configuration validation passed for {config.exchange}") return is_valid, errors def create_okx_collector(symbol: str, data_types: Optional[List[DataType]] = None, **kwargs) -> BaseDataCollector: """ Convenience function to create an OKX collector. Args: symbol: Trading pair symbol (e.g., 'BTC-USDT') data_types: List of data types to collect **kwargs: Additional collector parameters Returns: OKX collector instance """ if data_types is None: data_types = [DataType.TRADE, DataType.ORDERBOOK] logger.debug(f"Creating OKX collector for {symbol}") config = ExchangeCollectorConfig( exchange='okx', symbol=symbol, data_types=data_types, **kwargs ) return ExchangeFactory.create_collector(config)