255 lines
8.1 KiB
Python
255 lines
8.1 KiB
Python
|
|
"""
|
||
|
|
Base validator class for exchange data validation.
|
||
|
|
|
||
|
|
This module provides the abstract base class for exchange-specific data validators,
|
||
|
|
along with common validation patterns and utilities.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from abc import ABC, abstractmethod
|
||
|
|
from typing import Dict, Any, Optional, List, Union
|
||
|
|
from decimal import Decimal
|
||
|
|
from logging import Logger
|
||
|
|
|
||
|
|
from .result import ValidationResult, DataValidationResult
|
||
|
|
from .field_validators import (
|
||
|
|
validate_price,
|
||
|
|
validate_size,
|
||
|
|
validate_volume,
|
||
|
|
validate_trade_side,
|
||
|
|
validate_timestamp,
|
||
|
|
validate_trade_id,
|
||
|
|
validate_symbol_match,
|
||
|
|
validate_required_fields,
|
||
|
|
MIN_PRICE,
|
||
|
|
MAX_PRICE,
|
||
|
|
MIN_SIZE,
|
||
|
|
MAX_SIZE,
|
||
|
|
MIN_TIMESTAMP,
|
||
|
|
MAX_TIMESTAMP,
|
||
|
|
VALID_TRADE_SIDES,
|
||
|
|
NUMERIC_PATTERN,
|
||
|
|
TRADE_ID_PATTERN
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class BaseDataValidator(ABC):
|
||
|
|
"""
|
||
|
|
Abstract base class for exchange data validators.
|
||
|
|
|
||
|
|
This class provides common validation patterns and utilities
|
||
|
|
that can be reused across different exchange implementations.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self,
|
||
|
|
exchange_name: str,
|
||
|
|
component_name: str = "base_data_validator",
|
||
|
|
logger: Optional[Logger] = None):
|
||
|
|
"""
|
||
|
|
Initialize base data validator.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
exchange_name: Name of the exchange (e.g., 'okx', 'binance')
|
||
|
|
component_name: Name for logging
|
||
|
|
logger: Logger instance. If None, no logging will be performed.
|
||
|
|
"""
|
||
|
|
self.exchange_name = exchange_name
|
||
|
|
self.component_name = component_name
|
||
|
|
self.logger = logger
|
||
|
|
|
||
|
|
# Common validation patterns
|
||
|
|
self._numeric_pattern = NUMERIC_PATTERN
|
||
|
|
self._trade_id_pattern = TRADE_ID_PATTERN
|
||
|
|
|
||
|
|
# Valid trade sides
|
||
|
|
self._valid_trade_sides = VALID_TRADE_SIDES
|
||
|
|
|
||
|
|
# Common price and size limits (can be overridden by subclasses)
|
||
|
|
self._min_price = MIN_PRICE
|
||
|
|
self._max_price = MAX_PRICE
|
||
|
|
self._min_size = MIN_SIZE
|
||
|
|
self._max_size = MAX_SIZE
|
||
|
|
|
||
|
|
# Timestamp validation (milliseconds since epoch)
|
||
|
|
self._min_timestamp = MIN_TIMESTAMP
|
||
|
|
self._max_timestamp = MAX_TIMESTAMP
|
||
|
|
|
||
|
|
if self.logger:
|
||
|
|
self.logger.debug(f"{self.component_name}: Initialized {exchange_name} data validator")
|
||
|
|
|
||
|
|
# Abstract methods that must be implemented by subclasses
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
def validate_symbol_format(self, symbol: str) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate exchange-specific symbol format.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
symbol: Symbol to validate
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
def validate_websocket_message(self, message: Dict[str, Any]) -> DataValidationResult:
|
||
|
|
"""
|
||
|
|
Validate complete WebSocket message structure.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
message: WebSocket message to validate
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
DataValidationResult
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Common validation methods available to all subclasses
|
||
|
|
|
||
|
|
def validate_price(self, price: Union[str, int, float, Decimal]) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate price value with common rules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
price: Price value to validate
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult with sanitized decimal price
|
||
|
|
"""
|
||
|
|
return validate_price(price)
|
||
|
|
|
||
|
|
def validate_size(self, size: Union[str, int, float, Decimal]) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate size/quantity value with common rules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
size: Size value to validate
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult with sanitized decimal size
|
||
|
|
"""
|
||
|
|
return validate_size(size)
|
||
|
|
|
||
|
|
def validate_volume(self, volume: Union[str, int, float, Decimal]) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate volume value with common rules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
volume: Volume value to validate
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult
|
||
|
|
"""
|
||
|
|
return validate_volume(volume)
|
||
|
|
|
||
|
|
def validate_trade_side(self, side: str) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate trade side with common rules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
side: Trade side string
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult
|
||
|
|
"""
|
||
|
|
return validate_trade_side(side)
|
||
|
|
|
||
|
|
def validate_timestamp(self, timestamp: Union[str, int], is_milliseconds: bool = True) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate timestamp value with common rules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
timestamp: Timestamp value to validate
|
||
|
|
is_milliseconds: True if timestamp is in milliseconds, False for seconds
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult
|
||
|
|
"""
|
||
|
|
return validate_timestamp(timestamp, is_milliseconds)
|
||
|
|
|
||
|
|
def validate_trade_id(self, trade_id: Union[str, int]) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate trade ID with flexible rules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
trade_id: Trade ID to validate
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult
|
||
|
|
"""
|
||
|
|
return validate_trade_id(trade_id)
|
||
|
|
|
||
|
|
def validate_symbol_match(self, symbol: str, expected_symbol: Optional[str] = None) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate symbol matches expected value.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
symbol: Symbol to validate
|
||
|
|
expected_symbol: Expected symbol value
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult
|
||
|
|
"""
|
||
|
|
return validate_symbol_match(symbol, expected_symbol)
|
||
|
|
|
||
|
|
def validate_orderbook_side(self, side_data: List[List[str]], side_name: str) -> ValidationResult:
|
||
|
|
"""
|
||
|
|
Validate orderbook side (asks or bids) with common rules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
side_data: List of price/size pairs
|
||
|
|
side_name: Name of side for error messages
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
ValidationResult with sanitized data
|
||
|
|
"""
|
||
|
|
errors = []
|
||
|
|
warnings = []
|
||
|
|
sanitized_data = []
|
||
|
|
|
||
|
|
if not isinstance(side_data, list):
|
||
|
|
errors.append(f"{side_name} must be a list")
|
||
|
|
return ValidationResult(False, errors, warnings)
|
||
|
|
|
||
|
|
for i, level in enumerate(side_data):
|
||
|
|
if not isinstance(level, list) or len(level) < 2:
|
||
|
|
errors.append(f"{side_name}[{i}] must be a list with at least 2 elements")
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Validate price and size
|
||
|
|
price_result = self.validate_price(level[0])
|
||
|
|
size_result = self.validate_size(level[1])
|
||
|
|
|
||
|
|
if not price_result.is_valid:
|
||
|
|
errors.extend([f"{side_name}[{i}] price: {error}" for error in price_result.errors])
|
||
|
|
if not size_result.is_valid:
|
||
|
|
errors.extend([f"{side_name}[{i}] size: {error}" for error in size_result.errors])
|
||
|
|
|
||
|
|
# Add sanitized level
|
||
|
|
if price_result.is_valid and size_result.is_valid:
|
||
|
|
sanitized_level = [str(price_result.sanitized_data), str(size_result.sanitized_data)]
|
||
|
|
# Include additional fields if present
|
||
|
|
if len(level) > 2:
|
||
|
|
sanitized_level.extend(level[2:])
|
||
|
|
sanitized_data.append(sanitized_level)
|
||
|
|
|
||
|
|
return ValidationResult(len(errors) == 0, errors, warnings, sanitized_data)
|
||
|
|
|
||
|
|
def get_validator_info(self) -> Dict[str, Any]:
|
||
|
|
"""Get validator configuration information."""
|
||
|
|
return {
|
||
|
|
'exchange': self.exchange_name,
|
||
|
|
'component': self.component_name,
|
||
|
|
'limits': {
|
||
|
|
'min_price': str(self._min_price),
|
||
|
|
'max_price': str(self._max_price),
|
||
|
|
'min_size': str(self._min_size),
|
||
|
|
'max_size': str(self._max_size),
|
||
|
|
'min_timestamp': self._min_timestamp,
|
||
|
|
'max_timestamp': self._max_timestamp
|
||
|
|
},
|
||
|
|
'patterns': {
|
||
|
|
'numeric': self._numeric_pattern.pattern,
|
||
|
|
'trade_id': self._trade_id_pattern.pattern
|
||
|
|
}
|
||
|
|
}
|