- Introduced a comprehensive data collection framework, including `CollectorServiceConfig`, `BaseDataCollector`, and `CollectorManager`, enhancing modularity and maintainability. - Developed `CollectorFactory` for streamlined collector creation, promoting separation of concerns and improved configuration handling. - Enhanced `DataCollectionService` to utilize the new architecture, ensuring robust error handling and logging practices. - Added `TaskManager` for efficient management of asynchronous tasks, improving performance and resource management. - Implemented health monitoring and auto-recovery features in `CollectorManager`, ensuring reliable operation of data collectors. - Updated imports across the codebase to reflect the new structure, ensuring consistent access to components. These changes significantly improve the architecture and maintainability of the data collection service, aligning with project standards for modularity, performance, and error handling.
739 lines
31 KiB
Python
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 ...collector.base_collector import DataType, MarketDataPoint
|
|
from ...common import (
|
|
DataValidationResult,
|
|
StandardizedTrade,
|
|
OHLCVCandle,
|
|
CandleProcessingConfig,
|
|
BaseDataValidator,
|
|
ValidationResult,
|
|
BaseDataTransformer,
|
|
UnifiedDataTransformer,
|
|
create_standardized_trade
|
|
)
|
|
from ...common.aggregation.realtime import RealTimeCandleProcessor
|
|
|
|
|
|
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'
|
|
] |