- Removed the existing `validation.py` file and replaced it with a modular structure, introducing separate files for validation results, field validators, and the base validator class. - Implemented comprehensive validation functions for common data types, enhancing reusability and maintainability. - Added a new `__init__.py` to expose the validation utilities, ensuring a clean public interface. - Created detailed documentation for the validation module, including usage examples and architectural details. - Introduced extensive unit tests to cover the new validation framework, ensuring reliability and preventing regressions. These changes enhance the overall architecture of the data validation module, making it more scalable and easier to manage.
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
|
|
}
|
|
} |