Ajasra 90cb450640 Remove OKX configuration file and enhance data collector with timeframes support
- 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.
2025-06-07 15:46:24 +08:00

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)