TCPDashboard/data/exchanges/okx/data_processor.py
Vasily.onl bc13cfcbe0 Enhance logging capabilities across data collection components
- Added optional logger parameter to various classes including `BaseDataCollector`, `CollectorManager`, `RealTimeCandleProcessor`, and `BatchCandleProcessor` to support conditional logging.
- Implemented error-only logging mode, allowing components to log only error and critical messages when specified.
- Updated logging calls to utilize new helper methods for improved readability and maintainability.
- Enhanced documentation to include details on the new logging system and its usage across components.
- Ensured that child components inherit the logger from their parent components for consistent logging behavior.
2025-06-01 14:42:29 +08:00

739 lines
31 KiB
Python

"""
OKX-specific data processing utilities.
This module provides OKX-specific data validation, transformation, and processing
utilities that extend the common data processing framework.
"""
import re
from datetime import datetime, timezone
from decimal import Decimal
from typing import Dict, List, Optional, Any, Union, Tuple
from enum import Enum
from ...base_collector import DataType, MarketDataPoint
from ...common import (
DataValidationResult,
StandardizedTrade,
OHLCVCandle,
CandleProcessingConfig,
RealTimeCandleProcessor,
BaseDataValidator,
ValidationResult,
BaseDataTransformer,
UnifiedDataTransformer,
create_standardized_trade
)
class OKXMessageType(Enum):
"""OKX WebSocket message types."""
DATA = "data"
SUBSCRIPTION_SUCCESS = "subscribe"
UNSUBSCRIPTION_SUCCESS = "unsubscribe"
ERROR = "error"
PING = "ping"
PONG = "pong"
class OKXTradeField(Enum):
"""OKX trade data field names."""
INST_ID = "instId"
TRADE_ID = "tradeId"
PRICE = "px"
SIZE = "sz"
SIDE = "side"
TIMESTAMP = "ts"
class OKXOrderbookField(Enum):
"""OKX orderbook data field names."""
INST_ID = "instId"
ASKS = "asks"
BIDS = "bids"
TIMESTAMP = "ts"
SEQID = "seqId"
class OKXTickerField(Enum):
"""OKX ticker data field names."""
INST_ID = "instId"
LAST = "last"
LAST_SZ = "lastSz"
ASK_PX = "askPx"
ASK_SZ = "askSz"
BID_PX = "bidPx"
BID_SZ = "bidSz"
OPEN_24H = "open24h"
HIGH_24H = "high24h"
LOW_24H = "low24h"
VOL_24H = "vol24h"
VOL_CNY_24H = "volCcy24h"
TIMESTAMP = "ts"
class OKXDataValidator(BaseDataValidator):
"""
OKX-specific data validator extending the common base validator.
This class provides OKX-specific validation for message formats,
symbol patterns, and data structures.
"""
def __init__(self, component_name: str = "okx_data_validator", logger = None):
"""Initialize OKX data validator."""
super().__init__("okx", component_name, logger)
# OKX-specific patterns
self._symbol_pattern = re.compile(r'^[A-Z0-9]+-[A-Z0-9]+$') # BTC-USDT, ETH-USDC
self._trade_id_pattern = re.compile(r'^\d+$') # OKX uses numeric trade IDs
# OKX-specific valid channels
self._valid_channels = {
'trades', 'books5', 'books50', 'books-l2-tbt', 'tickers',
'candle1m', 'candle5m', 'candle15m', 'candle1H', 'candle4H', 'candle1D'
}
if self.logger:
self.logger.debug("Initialized OKX data validator")
def validate_symbol_format(self, symbol: str) -> ValidationResult:
"""Validate OKX symbol format (e.g., BTC-USDT)."""
errors = []
warnings = []
if not isinstance(symbol, str):
errors.append(f"Symbol must be string, got {type(symbol)}")
return ValidationResult(False, errors, warnings)
if not self._symbol_pattern.match(symbol):
errors.append(f"Invalid OKX symbol format: {symbol}. Expected format: BASE-QUOTE (e.g., BTC-USDT)")
return ValidationResult(len(errors) == 0, errors, warnings)
def validate_websocket_message(self, message: Dict[str, Any]) -> DataValidationResult:
"""Validate OKX WebSocket message structure."""
errors = []
warnings = []
try:
# Check basic message structure
if not isinstance(message, dict):
errors.append(f"Message must be a dictionary, got {type(message)}")
return DataValidationResult(False, errors, warnings)
# Identify message type
message_type = self._identify_message_type(message)
if message_type == OKXMessageType.DATA:
return self._validate_data_message(message)
elif message_type in [OKXMessageType.SUBSCRIPTION_SUCCESS, OKXMessageType.UNSUBSCRIPTION_SUCCESS]:
return self._validate_subscription_message(message)
elif message_type == OKXMessageType.ERROR:
return self._validate_error_message(message)
elif message_type in [OKXMessageType.PING, OKXMessageType.PONG]:
return DataValidationResult(True, [], []) # Ping/pong are always valid
else:
warnings.append("Unknown message type, basic validation only")
return DataValidationResult(True, [], warnings)
except Exception as e:
errors.append(f"Exception during message validation: {str(e)}")
return DataValidationResult(False, errors, warnings)
def validate_trade_data(self, data: Dict[str, Any], symbol: Optional[str] = None) -> DataValidationResult:
"""Validate OKX trade data structure and values."""
errors = []
warnings = []
sanitized_data = data.copy()
try:
# Check required fields
required_fields = [field.value for field in OKXTradeField]
missing_fields = []
for field in required_fields:
if field not in data:
missing_fields.append(field)
if missing_fields:
errors.extend([f"Missing required trade field: {field}" for field in missing_fields])
return DataValidationResult(False, errors, warnings)
# Validate individual fields using base validator methods
symbol_result = self.validate_symbol_format(data[OKXTradeField.INST_ID.value])
if not symbol_result.is_valid:
errors.extend(symbol_result.errors)
if symbol:
match_result = self.validate_symbol_match(data[OKXTradeField.INST_ID.value], symbol)
warnings.extend(match_result.warnings)
trade_id_result = self.validate_trade_id(data[OKXTradeField.TRADE_ID.value])
if not trade_id_result.is_valid:
errors.extend(trade_id_result.errors)
warnings.extend(trade_id_result.warnings)
price_result = self.validate_price(data[OKXTradeField.PRICE.value])
if not price_result.is_valid:
errors.extend(price_result.errors)
else:
sanitized_data[OKXTradeField.PRICE.value] = str(price_result.sanitized_data)
warnings.extend(price_result.warnings)
size_result = self.validate_size(data[OKXTradeField.SIZE.value])
if not size_result.is_valid:
errors.extend(size_result.errors)
else:
sanitized_data[OKXTradeField.SIZE.value] = str(size_result.sanitized_data)
warnings.extend(size_result.warnings)
side_result = self.validate_trade_side(data[OKXTradeField.SIDE.value])
if not side_result.is_valid:
errors.extend(side_result.errors)
timestamp_result = self.validate_timestamp(data[OKXTradeField.TIMESTAMP.value])
if not timestamp_result.is_valid:
errors.extend(timestamp_result.errors)
warnings.extend(timestamp_result.warnings)
return DataValidationResult(len(errors) == 0, errors, warnings, sanitized_data)
except Exception as e:
errors.append(f"Exception during trade validation: {str(e)}")
return DataValidationResult(False, errors, warnings)
def validate_orderbook_data(self, data: Dict[str, Any], symbol: Optional[str] = None) -> DataValidationResult:
"""Validate OKX orderbook data structure and values."""
errors = []
warnings = []
sanitized_data = data.copy()
try:
# Check required fields
required_fields = [OKXOrderbookField.INST_ID.value, OKXOrderbookField.ASKS.value,
OKXOrderbookField.BIDS.value, OKXOrderbookField.TIMESTAMP.value]
missing_fields = []
for field in required_fields:
if field not in data:
missing_fields.append(field)
if missing_fields:
errors.extend([f"Missing required orderbook field: {field}" for field in missing_fields])
return DataValidationResult(False, errors, warnings)
# Validate symbol
symbol_result = self.validate_symbol_format(data[OKXOrderbookField.INST_ID.value])
if not symbol_result.is_valid:
errors.extend(symbol_result.errors)
if symbol:
match_result = self.validate_symbol_match(data[OKXOrderbookField.INST_ID.value], symbol)
warnings.extend(match_result.warnings)
# Validate timestamp
timestamp_result = self.validate_timestamp(data[OKXOrderbookField.TIMESTAMP.value])
if not timestamp_result.is_valid:
errors.extend(timestamp_result.errors)
warnings.extend(timestamp_result.warnings)
# Validate asks and bids using base validator
asks_result = self.validate_orderbook_side(data[OKXOrderbookField.ASKS.value], "asks")
if not asks_result.is_valid:
errors.extend(asks_result.errors)
else:
sanitized_data[OKXOrderbookField.ASKS.value] = asks_result.sanitized_data
warnings.extend(asks_result.warnings)
bids_result = self.validate_orderbook_side(data[OKXOrderbookField.BIDS.value], "bids")
if not bids_result.is_valid:
errors.extend(bids_result.errors)
else:
sanitized_data[OKXOrderbookField.BIDS.value] = bids_result.sanitized_data
warnings.extend(bids_result.warnings)
# Validate sequence ID if present
if OKXOrderbookField.SEQID.value in data:
seq_id = data[OKXOrderbookField.SEQID.value]
if not isinstance(seq_id, (int, str)) or (isinstance(seq_id, str) and not seq_id.isdigit()):
errors.append("Invalid sequence ID format")
return DataValidationResult(len(errors) == 0, errors, warnings, sanitized_data)
except Exception as e:
errors.append(f"Exception during orderbook validation: {str(e)}")
return DataValidationResult(False, errors, warnings)
def validate_ticker_data(self, data: Dict[str, Any], symbol: Optional[str] = None) -> DataValidationResult:
"""Validate OKX ticker data structure and values."""
errors = []
warnings = []
sanitized_data = data.copy()
try:
# Check required fields
required_fields = [OKXTickerField.INST_ID.value, OKXTickerField.LAST.value, OKXTickerField.TIMESTAMP.value]
missing_fields = []
for field in required_fields:
if field not in data:
missing_fields.append(field)
if missing_fields:
errors.extend([f"Missing required ticker field: {field}" for field in missing_fields])
return DataValidationResult(False, errors, warnings)
# Validate symbol
symbol_result = self.validate_symbol_format(data[OKXTickerField.INST_ID.value])
if not symbol_result.is_valid:
errors.extend(symbol_result.errors)
if symbol:
match_result = self.validate_symbol_match(data[OKXTickerField.INST_ID.value], symbol)
warnings.extend(match_result.warnings)
# Validate timestamp
timestamp_result = self.validate_timestamp(data[OKXTickerField.TIMESTAMP.value])
if not timestamp_result.is_valid:
errors.extend(timestamp_result.errors)
warnings.extend(timestamp_result.warnings)
# Validate price fields (optional fields)
price_fields = [OKXTickerField.LAST, OKXTickerField.ASK_PX, OKXTickerField.BID_PX,
OKXTickerField.OPEN_24H, OKXTickerField.HIGH_24H, OKXTickerField.LOW_24H]
for field in price_fields:
if field.value in data and data[field.value] not in [None, ""]:
price_result = self.validate_price(data[field.value])
if not price_result.is_valid:
errors.extend([f"{field.value}: {error}" for error in price_result.errors])
else:
sanitized_data[field.value] = str(price_result.sanitized_data)
warnings.extend([f"{field.value}: {warning}" for warning in price_result.warnings])
# Validate size fields (optional fields)
size_fields = [OKXTickerField.LAST_SZ, OKXTickerField.ASK_SZ, OKXTickerField.BID_SZ]
for field in size_fields:
if field.value in data and data[field.value] not in [None, ""]:
size_result = self.validate_size(data[field.value])
if not size_result.is_valid:
errors.extend([f"{field.value}: {error}" for error in size_result.errors])
else:
sanitized_data[field.value] = str(size_result.sanitized_data)
warnings.extend([f"{field.value}: {warning}" for warning in size_result.warnings])
# Validate volume fields (optional fields)
volume_fields = [OKXTickerField.VOL_24H, OKXTickerField.VOL_CNY_24H]
for field in volume_fields:
if field.value in data and data[field.value] not in [None, ""]:
volume_result = self.validate_volume(data[field.value])
if not volume_result.is_valid:
errors.extend([f"{field.value}: {error}" for error in volume_result.errors])
warnings.extend([f"{field.value}: {warning}" for warning in volume_result.warnings])
return DataValidationResult(len(errors) == 0, errors, warnings, sanitized_data)
except Exception as e:
errors.append(f"Exception during ticker validation: {str(e)}")
return DataValidationResult(False, errors, warnings)
# Private helper methods for OKX-specific validation
def _identify_message_type(self, message: Dict[str, Any]) -> OKXMessageType:
"""Identify the type of OKX WebSocket message."""
if 'event' in message:
event = message['event']
if event == 'subscribe':
return OKXMessageType.SUBSCRIPTION_SUCCESS
elif event == 'unsubscribe':
return OKXMessageType.UNSUBSCRIPTION_SUCCESS
elif event == 'error':
return OKXMessageType.ERROR
if 'data' in message and 'arg' in message:
return OKXMessageType.DATA
# Default to data type for unknown messages
return OKXMessageType.DATA
def _validate_data_message(self, message: Dict[str, Any]) -> DataValidationResult:
"""Validate OKX data message structure."""
errors = []
warnings = []
# Check required fields
if 'arg' not in message:
errors.append("Missing 'arg' field in data message")
if 'data' not in message:
errors.append("Missing 'data' field in data message")
if errors:
return DataValidationResult(False, errors, warnings)
# Validate arg structure
arg = message['arg']
if not isinstance(arg, dict):
errors.append("'arg' field must be a dictionary")
else:
if 'channel' not in arg:
errors.append("Missing 'channel' in arg")
elif arg['channel'] not in self._valid_channels:
warnings.append(f"Unknown channel: {arg['channel']}")
if 'instId' not in arg:
errors.append("Missing 'instId' in arg")
# Validate data structure
data = message['data']
if not isinstance(data, list):
errors.append("'data' field must be a list")
elif len(data) == 0:
warnings.append("Empty data array")
return DataValidationResult(len(errors) == 0, errors, warnings)
def _validate_subscription_message(self, message: Dict[str, Any]) -> DataValidationResult:
"""Validate subscription/unsubscription message."""
errors = []
warnings = []
if 'event' not in message:
errors.append("Missing 'event' field")
if 'arg' not in message:
errors.append("Missing 'arg' field")
return DataValidationResult(len(errors) == 0, errors, warnings)
def _validate_error_message(self, message: Dict[str, Any]) -> DataValidationResult:
"""Validate error message."""
errors = []
warnings = []
if 'event' not in message or message['event'] != 'error':
errors.append("Invalid error message structure")
if 'msg' in message:
warnings.append(f"OKX error: {message['msg']}")
return DataValidationResult(len(errors) == 0, errors, warnings)
class OKXDataTransformer(BaseDataTransformer):
"""
OKX-specific data transformer extending the common base transformer.
This class handles transformation of OKX data formats to standardized formats.
"""
def __init__(self, component_name: str = "okx_data_transformer", logger = None):
"""Initialize OKX data transformer."""
super().__init__("okx", component_name, logger)
def transform_trade_data(self, raw_data: Dict[str, Any], symbol: str) -> Optional[StandardizedTrade]:
"""Transform OKX trade data to standardized format."""
try:
return create_standardized_trade(
symbol=raw_data[OKXTradeField.INST_ID.value],
trade_id=raw_data[OKXTradeField.TRADE_ID.value],
price=raw_data[OKXTradeField.PRICE.value],
size=raw_data[OKXTradeField.SIZE.value],
side=raw_data[OKXTradeField.SIDE.value],
timestamp=raw_data[OKXTradeField.TIMESTAMP.value],
exchange="okx",
raw_data=raw_data,
is_milliseconds=True
)
except Exception as e:
if self.logger:
self.logger.error(f"{self.component_name}: Error transforming OKX trade data: {e}")
return None
def transform_orderbook_data(self, raw_data: Dict[str, Any], symbol: str) -> Optional[Dict[str, Any]]:
"""Transform OKX orderbook data to standardized format."""
try:
# Basic transformation - can be enhanced as needed
return {
'symbol': raw_data[OKXOrderbookField.INST_ID.value],
'asks': raw_data[OKXOrderbookField.ASKS.value],
'bids': raw_data[OKXOrderbookField.BIDS.value],
'timestamp': self.timestamp_to_datetime(raw_data[OKXOrderbookField.TIMESTAMP.value]),
'exchange': 'okx',
'raw_data': raw_data
}
except Exception as e:
if self.logger:
self.logger.error(f"{self.component_name}: Error transforming OKX orderbook data: {e}")
return None
def transform_ticker_data(self, raw_data: Dict[str, Any], symbol: str) -> Optional[Dict[str, Any]]:
"""Transform OKX ticker data to standardized format."""
try:
# Transform ticker data to standardized format
ticker_data = {
'symbol': raw_data[OKXTickerField.INST_ID.value],
'timestamp': self.timestamp_to_datetime(raw_data[OKXTickerField.TIMESTAMP.value]),
'exchange': 'okx',
'raw_data': raw_data
}
# Add available price fields
price_fields = {
'last': OKXTickerField.LAST.value,
'bid': OKXTickerField.BID_PX.value,
'ask': OKXTickerField.ASK_PX.value,
'open_24h': OKXTickerField.OPEN_24H.value,
'high_24h': OKXTickerField.HIGH_24H.value,
'low_24h': OKXTickerField.LOW_24H.value
}
for std_field, okx_field in price_fields.items():
if okx_field in raw_data and raw_data[okx_field] not in [None, ""]:
decimal_price = self.safe_decimal_conversion(raw_data[okx_field], std_field)
if decimal_price:
ticker_data[std_field] = decimal_price
# Add volume fields
if OKXTickerField.VOL_24H.value in raw_data:
volume = self.safe_decimal_conversion(raw_data[OKXTickerField.VOL_24H.value], 'volume_24h')
if volume:
ticker_data['volume_24h'] = volume
return ticker_data
except Exception as e:
if self.logger:
self.logger.error(f"{self.component_name}: Error transforming OKX ticker data: {e}")
return None
class OKXDataProcessor:
"""
Main OKX data processor using common utilities.
This class provides a simplified interface for OKX data processing,
leveraging the common validation, transformation, and aggregation utilities.
"""
def __init__(self,
symbol: str,
config: Optional[CandleProcessingConfig] = None,
component_name: str = "okx_data_processor",
logger = None):
"""
Initialize OKX data processor.
Args:
symbol: Trading symbol to process
config: Candle processing configuration
component_name: Name for logging
"""
self.symbol = symbol
self.component_name = component_name
self.logger = logger
# Core components using common utilities
self.validator = OKXDataValidator(f"{component_name}_validator", self.logger)
self.transformer = OKXDataTransformer(f"{component_name}_transformer", self.logger)
self.unified_transformer = UnifiedDataTransformer(self.transformer, f"{component_name}_unified", self.logger)
# Real-time candle processing using common utilities
self.config = config or CandleProcessingConfig()
self.candle_processor = RealTimeCandleProcessor(
symbol, "okx", self.config, f"{component_name}_candles", self.logger
)
# Callbacks
self.trade_callbacks: List[callable] = []
self.candle_callbacks: List[callable] = []
# Connect candle processor callbacks
self.candle_processor.add_candle_callback(self._emit_candle_to_callbacks)
if self.logger:
self.logger.info(f"{self.component_name}: Initialized OKX data processor for {symbol} with real-time candle processing")
def add_trade_callback(self, callback: callable) -> None:
"""Add callback for processed trades."""
self.trade_callbacks.append(callback)
def add_candle_callback(self, callback: callable) -> None:
"""Add callback for completed candles."""
self.candle_callbacks.append(callback)
def validate_and_process_message(self, message: Dict[str, Any], expected_symbol: Optional[str] = None) -> Tuple[bool, List[MarketDataPoint], List[str]]:
"""
Validate and process complete OKX WebSocket message.
This is the main entry point for real-time WebSocket data.
Args:
message: Complete WebSocket message from OKX
expected_symbol: Expected trading symbol for validation
Returns:
Tuple of (success, list of market data points, list of errors)
"""
try:
# First validate the message structure
validation_result = self.validator.validate_websocket_message(message)
if not validation_result.is_valid:
if self.logger:
self.logger.error(f"{self.component_name}: Message validation failed: {validation_result.errors}")
return False, [], validation_result.errors
# Log warnings if any
if validation_result.warnings:
if self.logger:
self.logger.warning(f"{self.component_name}: Message validation warnings: {validation_result.warnings}")
# Process data if it's a data message
if 'data' in message and 'arg' in message:
return self._process_data_message(message, expected_symbol)
# Non-data messages are considered successfully processed but return no data points
return True, [], []
except Exception as e:
error_msg = f"{self.component_name}: Exception during message validation and processing: {str(e)}"
if self.logger:
self.logger.error(error_msg)
return False, [], [error_msg]
def _process_data_message(self, message: Dict[str, Any], expected_symbol: Optional[str] = None) -> Tuple[bool, List[MarketDataPoint], List[str]]:
"""Process OKX data message and return market data points."""
errors = []
market_data_points = []
try:
arg = message['arg']
channel = arg['channel']
inst_id = arg['instId']
data_list = message['data']
# Determine data type from channel
data_type = self._channel_to_data_type(channel)
if not data_type:
errors.append(f"Unsupported channel: {channel}")
return False, [], errors
# Process each data item
for data_item in data_list:
try:
# Validate and transform based on channel type
if channel == 'trades':
validation_result = self.validator.validate_trade_data(data_item, expected_symbol)
elif channel in ['books5', 'books50', 'books-l2-tbt']:
validation_result = self.validator.validate_orderbook_data(data_item, expected_symbol)
elif channel == 'tickers':
validation_result = self.validator.validate_ticker_data(data_item, expected_symbol)
else:
errors.append(f"Unsupported channel for validation: {channel}")
continue
if not validation_result.is_valid:
errors.extend(validation_result.errors)
continue
if validation_result.warnings:
if self.logger:
self.logger.warning(f"{self.component_name}: Data validation warnings: {validation_result.warnings}")
# Create MarketDataPoint using sanitized data
sanitized_data = validation_result.sanitized_data or data_item
timestamp_ms = sanitized_data.get('ts')
if timestamp_ms:
timestamp = datetime.fromtimestamp(int(timestamp_ms) / 1000, tz=timezone.utc)
else:
timestamp = datetime.now(timezone.utc)
market_data_point = MarketDataPoint(
exchange="okx",
symbol=inst_id,
timestamp=timestamp,
data_type=data_type,
data=sanitized_data
)
market_data_points.append(market_data_point)
# Real-time processing for trades
if channel == 'trades' and inst_id == self.symbol:
self._process_real_time_trade(sanitized_data)
except Exception as e:
if self.logger:
self.logger.error(f"{self.component_name}: Error processing data item: {e}")
errors.append(f"{self.component_name}: Error processing data item: {str(e)}")
return len(errors) == 0, market_data_points, errors
except Exception as e:
error_msg = f"{self.component_name}: Exception during data message processing: {str(e)}"
errors.append(error_msg)
return False, [], errors
def _process_real_time_trade(self, trade_data: Dict[str, Any]) -> None:
"""Process real-time trade for candle generation."""
try:
# Transform to standardized format using the unified transformer
standardized_trade = self.unified_transformer.transform_trade_data(trade_data, self.symbol)
if standardized_trade:
# Process for real-time candles using common utilities
completed_candles = self.candle_processor.process_trade(standardized_trade)
# Emit trade to callbacks
for callback in self.trade_callbacks:
try:
callback(standardized_trade)
except Exception as e:
if self.logger:
self.logger.error(f"{self.component_name}: Error in trade callback: {e}")
# Note: Candle callbacks are handled by _emit_candle_to_callbacks
except Exception as e:
if self.logger:
self.logger.error(f"{self.component_name}: Error processing real-time trade: {e}")
def _emit_candle_to_callbacks(self, candle: OHLCVCandle) -> None:
"""Emit candle to all registered callbacks."""
for callback in self.candle_callbacks:
try:
callback(candle)
except Exception as e:
if self.logger:
self.logger.error(f"{self.component_name}: Error in candle callback: {e}")
def _channel_to_data_type(self, channel: str) -> Optional[DataType]:
"""Convert OKX channel name to DataType enum."""
channel_mapping = {
'trades': DataType.TRADE,
'books5': DataType.ORDERBOOK,
'books50': DataType.ORDERBOOK,
'books-l2-tbt': DataType.ORDERBOOK,
'tickers': DataType.TICKER
}
return channel_mapping.get(channel)
def get_processing_stats(self) -> Dict[str, Any]:
"""Get comprehensive processing statistics."""
return {
'candle_processor': self.candle_processor.get_stats(),
'current_candles': self.candle_processor.get_current_candles(),
'callbacks': {
'trade_callbacks': len(self.trade_callbacks),
'candle_callbacks': len(self.candle_callbacks)
},
'validator_info': self.validator.get_validator_info(),
'transformer_info': self.unified_transformer.get_transformer_info()
}
__all__ = [
'OKXMessageType',
'OKXTradeField',
'OKXOrderbookField',
'OKXTickerField',
'OKXDataValidator',
'OKXDataTransformer',
'OKXDataProcessor'
]