Implement comprehensive chart configuration and validation system - Introduced a modular chart configuration system in `components/charts/config/` to manage indicator definitions, default configurations, and strategy-specific setups. - Added new modules for error handling and validation, enhancing user guidance and error reporting capabilities. - Implemented detailed schema validation for indicators and strategies, ensuring robust configuration management. - Created example strategies and default configurations to facilitate user onboarding and usage. - Enhanced documentation to provide clear guidelines on the configuration system, validation rules, and usage examples. - Added unit tests for all new components to ensure functionality and reliability across the configuration system.
570 lines
22 KiB
Python
570 lines
22 KiB
Python
"""
|
|
Tests for Enhanced Error Handling and User Guidance System
|
|
|
|
Tests the comprehensive error handling system including error detection,
|
|
suggestions, recovery guidance, and configuration validation.
|
|
"""
|
|
|
|
import pytest
|
|
from typing import Set, List
|
|
|
|
from components.charts.config.error_handling import (
|
|
ErrorSeverity,
|
|
ErrorCategory,
|
|
ConfigurationError,
|
|
ErrorReport,
|
|
ConfigurationErrorHandler,
|
|
validate_configuration_strict,
|
|
validate_strategy_name,
|
|
get_indicator_suggestions,
|
|
get_strategy_suggestions,
|
|
check_configuration_health
|
|
)
|
|
|
|
from components.charts.config.strategy_charts import (
|
|
StrategyChartConfig,
|
|
SubplotConfig,
|
|
ChartStyle,
|
|
ChartLayout,
|
|
SubplotType
|
|
)
|
|
|
|
from components.charts.config.defaults import TradingStrategy
|
|
|
|
|
|
class TestConfigurationError:
|
|
"""Test ConfigurationError class."""
|
|
|
|
def test_configuration_error_creation(self):
|
|
"""Test ConfigurationError creation with all fields."""
|
|
error = ConfigurationError(
|
|
category=ErrorCategory.MISSING_INDICATOR,
|
|
severity=ErrorSeverity.HIGH,
|
|
message="Test error message",
|
|
field_path="overlay_indicators[ema_99]",
|
|
missing_item="ema_99",
|
|
suggestions=["Use ema_12 instead", "Try different period"],
|
|
alternatives=["ema_12", "ema_26"],
|
|
recovery_steps=["Replace with ema_12", "Check available indicators"]
|
|
)
|
|
|
|
assert error.category == ErrorCategory.MISSING_INDICATOR
|
|
assert error.severity == ErrorSeverity.HIGH
|
|
assert error.message == "Test error message"
|
|
assert error.field_path == "overlay_indicators[ema_99]"
|
|
assert error.missing_item == "ema_99"
|
|
assert len(error.suggestions) == 2
|
|
assert len(error.alternatives) == 2
|
|
assert len(error.recovery_steps) == 2
|
|
|
|
def test_configuration_error_string_representation(self):
|
|
"""Test string representation with emojis and formatting."""
|
|
error = ConfigurationError(
|
|
category=ErrorCategory.MISSING_INDICATOR,
|
|
severity=ErrorSeverity.CRITICAL,
|
|
message="Indicator 'ema_99' not found",
|
|
suggestions=["Use ema_12"],
|
|
alternatives=["ema_12", "ema_26"],
|
|
recovery_steps=["Replace with available indicator"]
|
|
)
|
|
|
|
error_str = str(error)
|
|
assert "🚨" in error_str # Critical severity emoji
|
|
assert "Indicator 'ema_99' not found" in error_str
|
|
assert "💡 Suggestions:" in error_str
|
|
assert "🔄 Alternatives:" in error_str
|
|
assert "🔧 Recovery steps:" in error_str
|
|
|
|
|
|
class TestErrorReport:
|
|
"""Test ErrorReport class."""
|
|
|
|
def test_error_report_creation(self):
|
|
"""Test ErrorReport creation and basic functionality."""
|
|
report = ErrorReport(is_usable=True)
|
|
|
|
assert report.is_usable is True
|
|
assert len(report.errors) == 0
|
|
assert len(report.missing_strategies) == 0
|
|
assert len(report.missing_indicators) == 0
|
|
assert report.report_time is not None
|
|
|
|
def test_add_error_updates_usability(self):
|
|
"""Test that adding critical/high errors updates usability."""
|
|
report = ErrorReport(is_usable=True)
|
|
|
|
# Add medium error - should remain usable
|
|
medium_error = ConfigurationError(
|
|
category=ErrorCategory.INVALID_PARAMETER,
|
|
severity=ErrorSeverity.MEDIUM,
|
|
message="Medium error"
|
|
)
|
|
report.add_error(medium_error)
|
|
assert report.is_usable is True
|
|
|
|
# Add critical error - should become unusable
|
|
critical_error = ConfigurationError(
|
|
category=ErrorCategory.MISSING_STRATEGY,
|
|
severity=ErrorSeverity.CRITICAL,
|
|
message="Critical error",
|
|
missing_item="test_strategy"
|
|
)
|
|
report.add_error(critical_error)
|
|
assert report.is_usable is False
|
|
assert "test_strategy" in report.missing_strategies
|
|
|
|
def test_add_missing_indicator_tracking(self):
|
|
"""Test tracking of missing indicators."""
|
|
report = ErrorReport(is_usable=True)
|
|
|
|
error = ConfigurationError(
|
|
category=ErrorCategory.MISSING_INDICATOR,
|
|
severity=ErrorSeverity.HIGH,
|
|
message="Indicator missing",
|
|
missing_item="ema_99"
|
|
)
|
|
report.add_error(error)
|
|
|
|
assert "ema_99" in report.missing_indicators
|
|
assert report.is_usable is False # High severity
|
|
|
|
def test_get_critical_and_high_priority_errors(self):
|
|
"""Test filtering errors by severity."""
|
|
report = ErrorReport(is_usable=True)
|
|
|
|
# Add different severity errors
|
|
report.add_error(ConfigurationError(
|
|
category=ErrorCategory.MISSING_INDICATOR,
|
|
severity=ErrorSeverity.CRITICAL,
|
|
message="Critical error"
|
|
))
|
|
|
|
report.add_error(ConfigurationError(
|
|
category=ErrorCategory.MISSING_INDICATOR,
|
|
severity=ErrorSeverity.HIGH,
|
|
message="High error"
|
|
))
|
|
|
|
report.add_error(ConfigurationError(
|
|
category=ErrorCategory.INVALID_PARAMETER,
|
|
severity=ErrorSeverity.MEDIUM,
|
|
message="Medium error"
|
|
))
|
|
|
|
critical_errors = report.get_critical_errors()
|
|
high_errors = report.get_high_priority_errors()
|
|
|
|
assert len(critical_errors) == 1
|
|
assert len(high_errors) == 1
|
|
assert critical_errors[0].message == "Critical error"
|
|
assert high_errors[0].message == "High error"
|
|
|
|
def test_summary_generation(self):
|
|
"""Test error report summary."""
|
|
# Empty report
|
|
empty_report = ErrorReport(is_usable=True)
|
|
assert "✅ No configuration errors found" in empty_report.summary()
|
|
|
|
# Report with errors
|
|
report = ErrorReport(is_usable=False)
|
|
report.add_error(ConfigurationError(
|
|
category=ErrorCategory.MISSING_INDICATOR,
|
|
severity=ErrorSeverity.CRITICAL,
|
|
message="Critical error"
|
|
))
|
|
report.add_error(ConfigurationError(
|
|
category=ErrorCategory.INVALID_PARAMETER,
|
|
severity=ErrorSeverity.MEDIUM,
|
|
message="Medium error"
|
|
))
|
|
|
|
summary = report.summary()
|
|
assert "❌ Cannot proceed" in summary
|
|
assert "2 errors" in summary
|
|
assert "1 critical" in summary
|
|
|
|
|
|
class TestConfigurationErrorHandler:
|
|
"""Test ConfigurationErrorHandler class."""
|
|
|
|
def test_handler_initialization(self):
|
|
"""Test error handler initialization."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
assert len(handler.indicator_names) > 0
|
|
assert len(handler.strategy_names) > 0
|
|
assert "ema_12" in handler.indicator_names
|
|
assert "ema_crossover" in handler.strategy_names
|
|
|
|
def test_validate_existing_strategy(self):
|
|
"""Test validation of existing strategy."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Test existing strategy
|
|
error = handler.validate_strategy_exists("ema_crossover")
|
|
assert error is None
|
|
|
|
def test_validate_missing_strategy(self):
|
|
"""Test validation of missing strategy with suggestions."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Test missing strategy
|
|
error = handler.validate_strategy_exists("non_existent_strategy")
|
|
assert error is not None
|
|
assert error.category == ErrorCategory.MISSING_STRATEGY
|
|
assert error.severity == ErrorSeverity.CRITICAL
|
|
assert "non_existent_strategy" in error.message
|
|
assert len(error.recovery_steps) > 0
|
|
|
|
def test_validate_similar_strategy_name(self):
|
|
"""Test suggestions for similar strategy names."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Test typo in strategy name
|
|
error = handler.validate_strategy_exists("ema_cross") # Similar to "ema_crossover"
|
|
assert error is not None
|
|
assert len(error.alternatives) > 0
|
|
assert "ema_crossover" in error.alternatives or any("ema" in alt for alt in error.alternatives)
|
|
|
|
def test_validate_existing_indicator(self):
|
|
"""Test validation of existing indicator."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Test existing indicator
|
|
error = handler.validate_indicator_exists("ema_12")
|
|
assert error is None
|
|
|
|
def test_validate_missing_indicator(self):
|
|
"""Test validation of missing indicator with suggestions."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Test missing indicator
|
|
error = handler.validate_indicator_exists("ema_999")
|
|
assert error is not None
|
|
assert error.category == ErrorCategory.MISSING_INDICATOR
|
|
assert error.severity in [ErrorSeverity.CRITICAL, ErrorSeverity.HIGH]
|
|
assert "ema_999" in error.message
|
|
assert len(error.recovery_steps) > 0
|
|
|
|
def test_indicator_category_suggestions(self):
|
|
"""Test category-based suggestions for missing indicators."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Test SMA suggestion
|
|
sma_error = handler.validate_indicator_exists("sma_999")
|
|
assert sma_error is not None
|
|
# Check for SMA-related suggestions in any form
|
|
assert any("sma" in suggestion.lower() or "trend" in suggestion.lower()
|
|
for suggestion in sma_error.suggestions)
|
|
|
|
# Test RSI suggestion
|
|
rsi_error = handler.validate_indicator_exists("rsi_999")
|
|
assert rsi_error is not None
|
|
# Check that RSI alternatives contain actual RSI indicators
|
|
assert any("rsi_" in alternative for alternative in rsi_error.alternatives)
|
|
|
|
# Test MACD suggestion
|
|
macd_error = handler.validate_indicator_exists("macd_999")
|
|
assert macd_error is not None
|
|
# Check that MACD alternatives contain actual MACD indicators
|
|
assert any("macd_" in alternative for alternative in macd_error.alternatives)
|
|
|
|
def test_validate_strategy_configuration_empty(self):
|
|
"""Test validation of empty configuration."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Empty configuration
|
|
config = StrategyChartConfig(
|
|
strategy_name="Empty Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Empty strategy",
|
|
timeframes=["1h"],
|
|
overlay_indicators=[],
|
|
subplot_configs=[]
|
|
)
|
|
|
|
report = handler.validate_strategy_configuration(config)
|
|
assert not report.is_usable
|
|
assert len(report.errors) > 0
|
|
assert any(error.category == ErrorCategory.CONFIGURATION_CORRUPT
|
|
for error in report.errors)
|
|
|
|
def test_validate_strategy_configuration_with_missing_indicators(self):
|
|
"""Test validation with missing indicators."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
config = StrategyChartConfig(
|
|
strategy_name="Test Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Test strategy",
|
|
timeframes=["1h"],
|
|
overlay_indicators=["ema_999", "sma_888"], # Missing indicators
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
indicators=["rsi_777"] # Missing indicator
|
|
)
|
|
]
|
|
)
|
|
|
|
report = handler.validate_strategy_configuration(config)
|
|
assert not report.is_usable
|
|
assert len(report.missing_indicators) == 3
|
|
assert "ema_999" in report.missing_indicators
|
|
assert "sma_888" in report.missing_indicators
|
|
assert "rsi_777" in report.missing_indicators
|
|
|
|
def test_strategy_consistency_validation(self):
|
|
"""Test strategy type consistency validation."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Scalping strategy with wrong timeframes
|
|
config = StrategyChartConfig(
|
|
strategy_name="Scalping Strategy",
|
|
strategy_type=TradingStrategy.SCALPING,
|
|
description="Scalping strategy",
|
|
timeframes=["1d", "1w"], # Wrong for scalping
|
|
overlay_indicators=["ema_12"]
|
|
)
|
|
|
|
report = handler.validate_strategy_configuration(config)
|
|
# Should have consistency warning
|
|
consistency_errors = [e for e in report.errors
|
|
if e.category == ErrorCategory.INVALID_PARAMETER]
|
|
assert len(consistency_errors) > 0
|
|
|
|
def test_suggest_alternatives_for_missing_indicators(self):
|
|
"""Test alternative suggestions for missing indicators."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
missing_indicators = {"ema_999", "rsi_777", "unknown_indicator"}
|
|
suggestions = handler.suggest_alternatives_for_missing_indicators(missing_indicators)
|
|
|
|
assert "ema_999" in suggestions
|
|
assert "rsi_777" in suggestions
|
|
# Should have EMA alternatives for ema_999
|
|
assert any("ema_" in alt for alt in suggestions.get("ema_999", []))
|
|
# Should have RSI alternatives for rsi_777
|
|
assert any("rsi_" in alt for alt in suggestions.get("rsi_777", []))
|
|
|
|
|
|
class TestUtilityFunctions:
|
|
"""Test utility functions."""
|
|
|
|
def test_validate_configuration_strict(self):
|
|
"""Test strict configuration validation."""
|
|
# Valid configuration
|
|
valid_config = StrategyChartConfig(
|
|
strategy_name="Valid Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Valid strategy",
|
|
timeframes=["1h"],
|
|
overlay_indicators=["ema_12", "sma_20"]
|
|
)
|
|
|
|
report = validate_configuration_strict(valid_config)
|
|
assert report.is_usable
|
|
|
|
# Invalid configuration
|
|
invalid_config = StrategyChartConfig(
|
|
strategy_name="Invalid Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Invalid strategy",
|
|
timeframes=["1h"],
|
|
overlay_indicators=["ema_999"] # Missing indicator
|
|
)
|
|
|
|
report = validate_configuration_strict(invalid_config)
|
|
assert not report.is_usable
|
|
assert len(report.missing_indicators) > 0
|
|
|
|
def test_validate_strategy_name_function(self):
|
|
"""Test strategy name validation function."""
|
|
# Valid strategy
|
|
error = validate_strategy_name("ema_crossover")
|
|
assert error is None
|
|
|
|
# Invalid strategy
|
|
error = validate_strategy_name("non_existent_strategy")
|
|
assert error is not None
|
|
assert error.category == ErrorCategory.MISSING_STRATEGY
|
|
|
|
def test_get_indicator_suggestions(self):
|
|
"""Test indicator suggestions."""
|
|
# Test exact match suggestions
|
|
suggestions = get_indicator_suggestions("ema")
|
|
assert len(suggestions) > 0
|
|
assert any("ema_" in suggestion for suggestion in suggestions)
|
|
|
|
# Test partial match
|
|
suggestions = get_indicator_suggestions("ema_1")
|
|
assert len(suggestions) > 0
|
|
|
|
# Test no match
|
|
suggestions = get_indicator_suggestions("xyz_999")
|
|
# Should return some suggestions even for no match
|
|
assert isinstance(suggestions, list)
|
|
|
|
def test_get_strategy_suggestions(self):
|
|
"""Test strategy suggestions."""
|
|
# Test exact match suggestions
|
|
suggestions = get_strategy_suggestions("ema")
|
|
assert len(suggestions) > 0
|
|
|
|
# Test partial match
|
|
suggestions = get_strategy_suggestions("cross")
|
|
assert len(suggestions) > 0
|
|
|
|
# Test no match
|
|
suggestions = get_strategy_suggestions("xyz_999")
|
|
assert isinstance(suggestions, list)
|
|
|
|
def test_check_configuration_health(self):
|
|
"""Test configuration health check."""
|
|
# Healthy configuration
|
|
healthy_config = StrategyChartConfig(
|
|
strategy_name="Healthy Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Healthy strategy",
|
|
timeframes=["1h"],
|
|
overlay_indicators=["ema_12", "sma_20"],
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
indicators=["rsi_14"]
|
|
)
|
|
]
|
|
)
|
|
|
|
health = check_configuration_health(healthy_config)
|
|
assert "is_healthy" in health
|
|
assert "error_report" in health
|
|
assert "total_indicators" in health
|
|
assert "has_trend_indicators" in health
|
|
assert "has_momentum_indicators" in health
|
|
assert "recommendations" in health
|
|
|
|
assert health["total_indicators"] == 3
|
|
assert health["has_trend_indicators"] is True
|
|
assert health["has_momentum_indicators"] is True
|
|
|
|
# Unhealthy configuration
|
|
unhealthy_config = StrategyChartConfig(
|
|
strategy_name="Unhealthy Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Unhealthy strategy",
|
|
timeframes=["1h"],
|
|
overlay_indicators=["ema_999"] # Missing indicator
|
|
)
|
|
|
|
health = check_configuration_health(unhealthy_config)
|
|
assert health["is_healthy"] is False
|
|
assert health["missing_indicators"] > 0
|
|
assert len(health["recommendations"]) > 0
|
|
|
|
|
|
class TestErrorSeverityAndCategories:
|
|
"""Test error severity and category enums."""
|
|
|
|
def test_error_severity_values(self):
|
|
"""Test ErrorSeverity enum values."""
|
|
assert ErrorSeverity.CRITICAL == "critical"
|
|
assert ErrorSeverity.HIGH == "high"
|
|
assert ErrorSeverity.MEDIUM == "medium"
|
|
assert ErrorSeverity.LOW == "low"
|
|
|
|
def test_error_category_values(self):
|
|
"""Test ErrorCategory enum values."""
|
|
assert ErrorCategory.MISSING_STRATEGY == "missing_strategy"
|
|
assert ErrorCategory.MISSING_INDICATOR == "missing_indicator"
|
|
assert ErrorCategory.INVALID_PARAMETER == "invalid_parameter"
|
|
assert ErrorCategory.DEPENDENCY_MISSING == "dependency_missing"
|
|
assert ErrorCategory.CONFIGURATION_CORRUPT == "configuration_corrupt"
|
|
|
|
|
|
class TestRecoveryGeneration:
|
|
"""Test recovery configuration generation."""
|
|
|
|
def test_recovery_configuration_generation(self):
|
|
"""Test generating recovery configurations."""
|
|
handler = ConfigurationErrorHandler()
|
|
|
|
# Configuration with missing indicators
|
|
config = StrategyChartConfig(
|
|
strategy_name="Broken Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Strategy with missing indicators",
|
|
timeframes=["1h"],
|
|
overlay_indicators=["ema_999", "ema_12"], # One missing, one valid
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
indicators=["rsi_777"] # Missing
|
|
)
|
|
]
|
|
)
|
|
|
|
# Validate to get error report
|
|
error_report = handler.validate_strategy_configuration(config)
|
|
|
|
# Generate recovery
|
|
recovery_config, recovery_notes = handler.generate_recovery_configuration(config, error_report)
|
|
|
|
assert recovery_config is not None
|
|
assert len(recovery_notes) > 0
|
|
assert "(Recovery)" in recovery_config.strategy_name
|
|
|
|
# Should have valid indicators only
|
|
for indicator in recovery_config.overlay_indicators:
|
|
assert indicator in handler.indicator_names
|
|
|
|
for subplot in recovery_config.subplot_configs:
|
|
for indicator in subplot.indicators:
|
|
assert indicator in handler.indicator_names
|
|
|
|
|
|
class TestIntegrationWithExistingSystems:
|
|
"""Test integration with existing validation and configuration systems."""
|
|
|
|
def test_integration_with_strategy_validation(self):
|
|
"""Test integration with existing strategy validation."""
|
|
from components.charts.config import create_ema_crossover_strategy
|
|
|
|
# Get a known good strategy
|
|
strategy = create_ema_crossover_strategy()
|
|
config = strategy.config
|
|
|
|
# Test with error handler
|
|
report = validate_configuration_strict(config)
|
|
|
|
# Should be usable (might have warnings about missing indicators in test environment)
|
|
assert isinstance(report, ErrorReport)
|
|
assert hasattr(report, 'is_usable')
|
|
assert hasattr(report, 'errors')
|
|
|
|
def test_error_handling_with_custom_configuration(self):
|
|
"""Test error handling with custom configurations."""
|
|
from components.charts.config import create_custom_strategy_config
|
|
|
|
# Try to create config with missing indicators
|
|
config, errors = create_custom_strategy_config(
|
|
strategy_name="Test Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Test strategy",
|
|
timeframes=["1h"],
|
|
overlay_indicators=["ema_999"], # Missing indicator
|
|
subplot_configs=[{
|
|
"subplot_type": "rsi",
|
|
"height_ratio": 0.2,
|
|
"indicators": ["rsi_777"] # Missing indicator
|
|
}]
|
|
)
|
|
|
|
if config: # If config was created despite missing indicators
|
|
report = validate_configuration_strict(config)
|
|
assert not report.is_usable
|
|
assert len(report.missing_indicators) > 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__]) |