""" Tests for Strategy Signal Validation Pipeline This module tests signal validation, filtering, and quality assessment functionality for strategy-generated signals. """ import pytest from datetime import datetime, timezone from unittest.mock import patch from strategies.validation import StrategySignalValidator, ValidationConfig from strategies.data_types import StrategySignal, SignalType class TestValidationConfig: """Tests for ValidationConfig dataclass.""" def test_default_config(self): """Test default validation configuration.""" config = ValidationConfig() assert config.min_confidence == 0.0 assert config.max_confidence == 1.0 assert config.required_metadata_fields == [] assert config.allowed_signal_types == list(SignalType) assert config.price_tolerance_percent == 5.0 def test_custom_config(self): """Test custom validation configuration.""" config = ValidationConfig( min_confidence=0.3, max_confidence=0.9, required_metadata_fields=['indicator1', 'indicator2'], allowed_signal_types=[SignalType.BUY, SignalType.SELL], price_tolerance_percent=2.0 ) assert config.min_confidence == 0.3 assert config.max_confidence == 0.9 assert config.required_metadata_fields == ['indicator1', 'indicator2'] assert config.allowed_signal_types == [SignalType.BUY, SignalType.SELL] assert config.price_tolerance_percent == 2.0 class TestStrategySignalValidator: """Tests for StrategySignalValidator class.""" @pytest.fixture def validator(self): """Create validator with default configuration.""" return StrategySignalValidator() @pytest.fixture def strict_validator(self): """Create validator with strict configuration.""" config = ValidationConfig( min_confidence=0.5, max_confidence=1.0, required_metadata_fields=['rsi', 'macd'], allowed_signal_types=[SignalType.BUY, SignalType.SELL] ) return StrategySignalValidator(config) @pytest.fixture def valid_signal(self): """Create a valid strategy signal for testing.""" return StrategySignal( timestamp=datetime.now(timezone.utc), symbol='BTC-USDT', timeframe='1h', signal_type=SignalType.BUY, price=50000.0, confidence=0.8, metadata={'rsi': 30, 'macd': 0.05} ) def test_initialization(self, validator): """Test validator initialization.""" assert validator.config is not None assert validator.logger is not None assert validator._validation_stats['total_signals_validated'] == 0 assert validator._validation_stats['valid_signals'] == 0 assert validator._validation_stats['invalid_signals'] == 0 def test_validate_valid_signal(self, validator, valid_signal): """Test validation of a completely valid signal.""" is_valid, errors = validator.validate_signal(valid_signal) assert is_valid is True assert errors == [] assert validator._validation_stats['total_signals_validated'] == 1 assert validator._validation_stats['valid_signals'] == 1 assert validator._validation_stats['invalid_signals'] == 0 def test_validate_invalid_confidence_low(self, validator, valid_signal): """Test validation with confidence too low.""" valid_signal.confidence = -0.1 is_valid, errors = validator.validate_signal(valid_signal) assert is_valid is False assert len(errors) == 1 assert "Invalid confidence" in errors[0] assert validator._validation_stats['invalid_signals'] == 1 def test_validate_invalid_confidence_high(self, validator, valid_signal): """Test validation with confidence too high.""" valid_signal.confidence = 1.5 is_valid, errors = validator.validate_signal(valid_signal) assert is_valid is False assert len(errors) == 1 assert "Invalid confidence" in errors[0] def test_validate_invalid_signal_type(self, strict_validator, valid_signal): """Test validation with disallowed signal type.""" valid_signal.signal_type = SignalType.HOLD is_valid, errors = strict_validator.validate_signal(valid_signal) assert is_valid is False assert len(errors) == 1 assert "Signal type" in errors[0] and "not in allowed types" in errors[0] def test_validate_invalid_price(self, validator, valid_signal): """Test validation with invalid price.""" valid_signal.price = -100.0 is_valid, errors = validator.validate_signal(valid_signal) assert is_valid is False assert len(errors) == 1 assert "Invalid price" in errors[0] def test_validate_missing_required_metadata(self, strict_validator, valid_signal): """Test validation with missing required metadata.""" valid_signal.metadata = {'rsi': 30} # Missing 'macd' is_valid, errors = strict_validator.validate_signal(valid_signal) assert is_valid is False assert len(errors) == 1 assert "Missing required metadata fields" in errors[0] assert "macd" in errors[0] def test_validate_multiple_errors(self, strict_validator, valid_signal): """Test validation with multiple errors.""" valid_signal.confidence = 1.5 # Too high valid_signal.price = -100.0 # Invalid valid_signal.signal_type = SignalType.HOLD # Not allowed valid_signal.metadata = {} # Missing required fields is_valid, errors = strict_validator.validate_signal(valid_signal) assert is_valid is False assert len(errors) == 4 assert any("confidence" in error for error in errors) assert any("price" in error for error in errors) assert any("Signal type" in error for error in errors) assert any("Missing required metadata" in error for error in errors) def test_validation_statistics_tracking(self, validator, valid_signal): """Test that validation statistics are properly tracked.""" # Validate multiple signals validator.validate_signal(valid_signal) # Valid invalid_signal = valid_signal invalid_signal.confidence = 1.5 # Invalid validator.validate_signal(invalid_signal) # Invalid stats = validator._validation_stats assert stats['total_signals_validated'] == 2 assert stats['valid_signals'] == 1 assert stats['invalid_signals'] == 1 assert len(stats['validation_errors']) > 0 def test_validate_signals_batch(self, validator, valid_signal): """Test batch validation of multiple signals.""" # Create a mix of valid and invalid signals signals = [ valid_signal, # Valid StrategySignal( # Invalid confidence timestamp=datetime.now(timezone.utc), symbol='ETH-USDT', timeframe='1h', signal_type=SignalType.SELL, price=3000.0, confidence=1.5, # Invalid metadata={} ), StrategySignal( # Valid timestamp=datetime.now(timezone.utc), symbol='BNB-USDT', timeframe='1h', signal_type=SignalType.BUY, price=300.0, confidence=0.7, metadata={} ) ] valid_signals, invalid_signals = validator.validate_signals_batch(signals) assert len(valid_signals) == 2 assert len(invalid_signals) == 1 assert invalid_signals[0].confidence == 1.5 def test_filter_signals_by_confidence(self, validator, valid_signal): """Test filtering signals by confidence threshold.""" signals = [ valid_signal, # confidence 0.8 StrategySignal( timestamp=datetime.now(timezone.utc), symbol='ETH-USDT', timeframe='1h', signal_type=SignalType.SELL, price=3000.0, confidence=0.3, # Low confidence metadata={} ), StrategySignal( timestamp=datetime.now(timezone.utc), symbol='BNB-USDT', timeframe='1h', signal_type=SignalType.BUY, price=300.0, confidence=0.9, # High confidence metadata={} ) ] # Filter with threshold 0.5 filtered_signals = validator.filter_signals_by_confidence(signals, min_confidence=0.5) assert len(filtered_signals) == 2 assert all(signal.confidence >= 0.5 for signal in filtered_signals) assert filtered_signals[0].confidence == 0.8 assert filtered_signals[1].confidence == 0.9 def test_filter_signals_by_type(self, validator, valid_signal): """Test filtering signals by allowed types.""" signals = [ valid_signal, # BUY StrategySignal( timestamp=datetime.now(timezone.utc), symbol='ETH-USDT', timeframe='1h', signal_type=SignalType.SELL, price=3000.0, confidence=0.8, metadata={} ), StrategySignal( timestamp=datetime.now(timezone.utc), symbol='BNB-USDT', timeframe='1h', signal_type=SignalType.HOLD, price=300.0, confidence=0.7, metadata={} ) ] # Filter to only allow BUY and SELL filtered_signals = validator.filter_signals_by_type( signals, allowed_types=[SignalType.BUY, SignalType.SELL] ) assert len(filtered_signals) == 2 assert filtered_signals[0].signal_type == SignalType.BUY assert filtered_signals[1].signal_type == SignalType.SELL def test_get_validation_statistics(self, validator, valid_signal): """Test comprehensive validation statistics.""" # Validate some signals to generate statistics validator.validate_signal(valid_signal) # Valid invalid_signal = valid_signal invalid_signal.confidence = -0.1 # Invalid validator.validate_signal(invalid_signal) # Invalid stats = validator.get_validation_statistics() assert stats['total_signals_validated'] == 2 assert stats['valid_signals'] == 1 assert stats['invalid_signals'] == 1 assert stats['validation_success_rate'] == 0.5 assert stats['validation_failure_rate'] == 0.5 assert 'validation_errors' in stats def test_transform_signal_confidence(self, validator, valid_signal): """Test signal confidence transformation.""" original_confidence = valid_signal.confidence # 0.8 # Test confidence multiplier transformed_signal = validator.transform_signal_confidence( valid_signal, confidence_multiplier=1.2 ) assert transformed_signal.confidence == original_confidence * 1.2 assert transformed_signal.symbol == valid_signal.symbol assert transformed_signal.signal_type == valid_signal.signal_type assert transformed_signal.price == valid_signal.price # Test confidence cap capped_signal = validator.transform_signal_confidence( valid_signal, confidence_multiplier=2.0, # Would exceed 1.0 max_confidence=1.0 ) assert capped_signal.confidence == 1.0 # Capped at max def test_enrich_signal_metadata(self, validator, valid_signal): """Test signal metadata enrichment.""" additional_metadata = { 'validation_timestamp': datetime.now(timezone.utc).isoformat(), 'validation_status': 'approved', 'risk_score': 0.2 } enriched_signal = validator.enrich_signal_metadata(valid_signal, additional_metadata) # Original metadata should be preserved assert enriched_signal.metadata['rsi'] == 30 assert enriched_signal.metadata['macd'] == 0.05 # New metadata should be added assert enriched_signal.metadata['validation_status'] == 'approved' assert enriched_signal.metadata['risk_score'] == 0.2 assert 'validation_timestamp' in enriched_signal.metadata # Other properties should remain unchanged assert enriched_signal.confidence == valid_signal.confidence assert enriched_signal.signal_type == valid_signal.signal_type def test_transform_signals_batch(self, validator, valid_signal): """Test batch signal transformation.""" signals = [ valid_signal, StrategySignal( timestamp=datetime.now(timezone.utc), symbol='ETH-USDT', timeframe='1h', signal_type=SignalType.SELL, price=3000.0, confidence=0.6, metadata={'ema': 2950} ) ] additional_metadata = {'batch_id': 'test_batch_001'} transformed_signals = validator.transform_signals_batch( signals, confidence_multiplier=1.1, additional_metadata=additional_metadata ) assert len(transformed_signals) == 2 # Check confidence transformation assert transformed_signals[0].confidence == 0.8 * 1.1 assert transformed_signals[1].confidence == 0.6 * 1.1 # Check metadata enrichment assert transformed_signals[0].metadata['batch_id'] == 'test_batch_001' assert transformed_signals[1].metadata['batch_id'] == 'test_batch_001' # Verify original metadata preserved assert transformed_signals[0].metadata['rsi'] == 30 assert transformed_signals[1].metadata['ema'] == 2950 def test_calculate_signal_quality_metrics(self, validator, valid_signal): """Test signal quality metrics calculation.""" signals = [ valid_signal, # confidence 0.8, has metadata StrategySignal( timestamp=datetime.now(timezone.utc), symbol='ETH-USDT', timeframe='1h', signal_type=SignalType.SELL, price=3000.0, confidence=0.9, # High confidence metadata={'volume_spike': True} ), StrategySignal( timestamp=datetime.now(timezone.utc), symbol='BNB-USDT', timeframe='1h', signal_type=SignalType.HOLD, price=300.0, confidence=0.4, # Low confidence metadata=None # No metadata ) ] metrics = validator.calculate_signal_quality_metrics(signals) assert metrics['total_signals'] == 3 assert metrics['confidence_metrics']['average'] == round((0.8 + 0.9 + 0.4) / 3, 3) assert metrics['confidence_metrics']['minimum'] == 0.4 assert metrics['confidence_metrics']['maximum'] == 0.9 assert metrics['confidence_metrics']['high_confidence_count'] == 2 # >= 0.7 assert metrics['quality_score'] == round((2/3) * 100, 1) # 66.7% assert metrics['metadata_completeness_percentage'] == round((2/3) * 100, 1) # Check signal type distribution assert metrics['signal_type_distribution']['buy'] == 1 assert metrics['signal_type_distribution']['sell'] == 1 assert metrics['signal_type_distribution']['hold'] == 1 # Check recommendations assert isinstance(metrics['recommendations'], list) assert len(metrics['recommendations']) > 0 def test_calculate_signal_quality_metrics_empty(self, validator): """Test quality metrics with empty signal list.""" metrics = validator.calculate_signal_quality_metrics([]) assert 'error' in metrics assert metrics['error'] == 'No signals provided for quality analysis' def test_generate_quality_recommendations(self, validator): """Test quality recommendation generation.""" # Test low confidence signals low_confidence_signals = [ StrategySignal( timestamp=datetime.now(timezone.utc), symbol='BTC-USDT', timeframe='1h', signal_type=SignalType.BUY, price=50000.0, confidence=0.3, # Low confidence metadata=None # No metadata ) ] recommendations = validator._generate_quality_recommendations(low_confidence_signals) assert any("confidence" in rec.lower() for rec in recommendations) assert any("metadata" in rec.lower() for rec in recommendations) def test_generate_validation_report(self, validator, valid_signal): """Test comprehensive validation report generation.""" # Generate some validation activity validator.validate_signal(valid_signal) # Valid invalid_signal = valid_signal invalid_signal.confidence = -0.1 # Invalid validator.validate_signal(invalid_signal) # Invalid report = validator.generate_validation_report() assert 'report_timestamp' in report assert 'validation_summary' in report assert 'error_analysis' in report assert 'configuration' in report assert 'health_status' in report # Check validation summary summary = report['validation_summary'] assert summary['total_validated'] == 2 assert '50.0%' in summary['success_rate'] assert '50.0%' in summary['failure_rate'] # Check configuration config = report['configuration'] assert config['min_confidence'] == 0.0 assert config['max_confidence'] == 1.0 assert isinstance(config['allowed_signal_types'], list) # Check health status assert report['health_status'] in ['good', 'needs_attention']