- Deleted the `okx_config.json` file as part of the configuration refactor. - Updated `BaseDataCollector` to include an optional `timeframes` parameter for more flexible data collection. - Modified `DataCollectionService` and `OKXCollector` to pass and utilize the new `timeframes` parameter. - Enhanced `ExchangeCollectorConfig` to validate timeframes, ensuring they are provided and correctly formatted. - Updated documentation to reflect the new configurable timeframes feature, improving clarity for users. These changes streamline the configuration process and improve the flexibility of data collection, aligning with project standards for maintainability and usability.
252 lines
9.2 KiB
Python
252 lines
9.2 KiB
Python
"""
|
|
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) |