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.
539 lines
21 KiB
Python
539 lines
21 KiB
Python
"""
|
|
Tests for Configuration Validation and Error Handling System
|
|
|
|
Tests the comprehensive validation system including validation rules,
|
|
error reporting, warnings, and detailed diagnostics.
|
|
"""
|
|
|
|
import pytest
|
|
from typing import Set
|
|
from datetime import datetime
|
|
|
|
from components.charts.config.validation import (
|
|
ValidationLevel,
|
|
ValidationRule,
|
|
ValidationIssue,
|
|
ValidationReport,
|
|
ConfigurationValidator,
|
|
validate_configuration,
|
|
get_validation_rules_info
|
|
)
|
|
|
|
from components.charts.config.strategy_charts import (
|
|
StrategyChartConfig,
|
|
SubplotConfig,
|
|
ChartStyle,
|
|
ChartLayout,
|
|
SubplotType
|
|
)
|
|
|
|
from components.charts.config.defaults import TradingStrategy
|
|
|
|
|
|
class TestValidationComponents:
|
|
"""Test validation component classes."""
|
|
|
|
def test_validation_level_enum(self):
|
|
"""Test ValidationLevel enum values."""
|
|
levels = [level.value for level in ValidationLevel]
|
|
expected_levels = ["error", "warning", "info", "debug"]
|
|
|
|
for expected in expected_levels:
|
|
assert expected in levels
|
|
|
|
def test_validation_rule_enum(self):
|
|
"""Test ValidationRule enum values."""
|
|
rules = [rule.value for rule in ValidationRule]
|
|
expected_rules = [
|
|
"required_fields", "height_ratios", "indicator_existence",
|
|
"timeframe_format", "chart_style", "subplot_config",
|
|
"strategy_consistency", "performance_impact", "indicator_conflicts",
|
|
"resource_usage"
|
|
]
|
|
|
|
for expected in expected_rules:
|
|
assert expected in rules
|
|
|
|
def test_validation_issue_creation(self):
|
|
"""Test ValidationIssue creation and string representation."""
|
|
issue = ValidationIssue(
|
|
level=ValidationLevel.ERROR,
|
|
rule=ValidationRule.REQUIRED_FIELDS,
|
|
message="Test error message",
|
|
field_path="test.field",
|
|
suggestion="Test suggestion"
|
|
)
|
|
|
|
assert issue.level == ValidationLevel.ERROR
|
|
assert issue.rule == ValidationRule.REQUIRED_FIELDS
|
|
assert issue.message == "Test error message"
|
|
assert issue.field_path == "test.field"
|
|
assert issue.suggestion == "Test suggestion"
|
|
|
|
# Test string representation
|
|
issue_str = str(issue)
|
|
assert "[ERROR]" in issue_str
|
|
assert "Test error message" in issue_str
|
|
assert "test.field" in issue_str
|
|
assert "Test suggestion" in issue_str
|
|
|
|
def test_validation_report_creation(self):
|
|
"""Test ValidationReport creation and methods."""
|
|
report = ValidationReport(is_valid=True)
|
|
|
|
assert report.is_valid is True
|
|
assert len(report.errors) == 0
|
|
assert len(report.warnings) == 0
|
|
assert len(report.info) == 0
|
|
assert len(report.debug) == 0
|
|
|
|
# Test adding issues
|
|
error_issue = ValidationIssue(
|
|
level=ValidationLevel.ERROR,
|
|
rule=ValidationRule.REQUIRED_FIELDS,
|
|
message="Error message"
|
|
)
|
|
|
|
warning_issue = ValidationIssue(
|
|
level=ValidationLevel.WARNING,
|
|
rule=ValidationRule.HEIGHT_RATIOS,
|
|
message="Warning message"
|
|
)
|
|
|
|
report.add_issue(error_issue)
|
|
report.add_issue(warning_issue)
|
|
|
|
assert not report.is_valid # Should be False after adding error
|
|
assert len(report.errors) == 1
|
|
assert len(report.warnings) == 1
|
|
assert report.has_errors()
|
|
assert report.has_warnings()
|
|
|
|
# Test get_all_issues
|
|
all_issues = report.get_all_issues()
|
|
assert len(all_issues) == 2
|
|
|
|
# Test get_issues_by_rule
|
|
field_issues = report.get_issues_by_rule(ValidationRule.REQUIRED_FIELDS)
|
|
assert len(field_issues) == 1
|
|
assert field_issues[0] == error_issue
|
|
|
|
# Test summary
|
|
summary = report.summary()
|
|
assert "1 errors" in summary
|
|
assert "1 warnings" in summary
|
|
|
|
|
|
class TestConfigurationValidator:
|
|
"""Test ConfigurationValidator class."""
|
|
|
|
def create_valid_config(self) -> StrategyChartConfig:
|
|
"""Create a valid test configuration."""
|
|
return StrategyChartConfig(
|
|
strategy_name="Valid Test Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Valid strategy for testing",
|
|
timeframes=["5m", "15m", "1h"],
|
|
main_chart_height=0.7,
|
|
overlay_indicators=["sma_20"], # Using simple indicators
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
height_ratio=0.2,
|
|
indicators=[], # Empty to avoid indicator existence issues
|
|
title="RSI"
|
|
)
|
|
]
|
|
)
|
|
|
|
def test_validator_initialization(self):
|
|
"""Test validator initialization."""
|
|
# Test with all rules
|
|
validator = ConfigurationValidator()
|
|
assert len(validator.enabled_rules) == len(ValidationRule)
|
|
|
|
# Test with specific rules
|
|
specific_rules = {ValidationRule.REQUIRED_FIELDS, ValidationRule.HEIGHT_RATIOS}
|
|
validator = ConfigurationValidator(enabled_rules=specific_rules)
|
|
assert validator.enabled_rules == specific_rules
|
|
|
|
def test_validate_strategy_config_valid(self):
|
|
"""Test validation of a valid configuration."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator()
|
|
report = validator.validate_strategy_config(config)
|
|
|
|
# Should have some validation applied
|
|
assert isinstance(report, ValidationReport)
|
|
assert report.validation_time is not None
|
|
assert len(report.rules_applied) > 0
|
|
|
|
def test_required_fields_validation(self):
|
|
"""Test required fields validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.REQUIRED_FIELDS})
|
|
|
|
# Test missing strategy name
|
|
config.strategy_name = ""
|
|
report = validator.validate_strategy_config(config)
|
|
assert not report.is_valid
|
|
assert len(report.errors) > 0
|
|
assert any("Strategy name is required" in str(error) for error in report.errors)
|
|
|
|
# Test short strategy name (should be warning)
|
|
config.strategy_name = "AB"
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.warnings) > 0
|
|
assert any("very short" in str(warning) for warning in report.warnings)
|
|
|
|
# Test missing timeframes
|
|
config.strategy_name = "Valid Name"
|
|
config.timeframes = []
|
|
report = validator.validate_strategy_config(config)
|
|
assert not report.is_valid
|
|
assert any("timeframe must be specified" in str(error) for error in report.errors)
|
|
|
|
def test_height_ratios_validation(self):
|
|
"""Test height ratios validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.HEIGHT_RATIOS})
|
|
|
|
# Test invalid main chart height
|
|
config.main_chart_height = 1.5 # Invalid: > 1.0
|
|
report = validator.validate_strategy_config(config)
|
|
assert not report.is_valid
|
|
assert any("Main chart height" in str(error) for error in report.errors)
|
|
|
|
# Test total height exceeding 1.0
|
|
config.main_chart_height = 0.8
|
|
config.subplot_configs[0].height_ratio = 0.3 # Total = 1.1
|
|
report = validator.validate_strategy_config(config)
|
|
assert not report.is_valid
|
|
assert any("exceeds 1.0" in str(error) for error in report.errors)
|
|
|
|
# Test very small main chart height (should be warning)
|
|
config.main_chart_height = 0.1
|
|
config.subplot_configs[0].height_ratio = 0.2
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.warnings) > 0
|
|
assert any("very small" in str(warning) for warning in report.warnings)
|
|
|
|
def test_timeframe_format_validation(self):
|
|
"""Test timeframe format validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.TIMEFRAME_FORMAT})
|
|
|
|
# Test invalid timeframe format
|
|
config.timeframes = ["invalid", "1h", "5m"]
|
|
report = validator.validate_strategy_config(config)
|
|
assert not report.is_valid
|
|
assert any("Invalid timeframe format" in str(error) for error in report.errors)
|
|
|
|
# Test valid but uncommon timeframe (should be warning)
|
|
config.timeframes = ["7m", "1h"] # 7m is valid format but uncommon
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.warnings) > 0
|
|
assert any("not in common list" in str(warning) for warning in report.warnings)
|
|
|
|
def test_chart_style_validation(self):
|
|
"""Test chart style validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.CHART_STYLE})
|
|
|
|
# Test invalid color format
|
|
config.chart_style.background_color = "invalid_color"
|
|
report = validator.validate_strategy_config(config)
|
|
assert not report.is_valid
|
|
assert any("Invalid color format" in str(error) for error in report.errors)
|
|
|
|
# Test extreme font size (should be warning or error)
|
|
config.chart_style.background_color = "#ffffff" # Fix color
|
|
config.chart_style.font_size = 2 # Too small
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.errors) > 0 or len(report.warnings) > 0
|
|
|
|
# Test unsupported theme (should be warning)
|
|
config.chart_style.font_size = 12 # Fix font size
|
|
config.chart_style.theme = "unsupported_theme"
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.warnings) > 0
|
|
assert any("may not be supported" in str(warning) for warning in report.warnings)
|
|
|
|
def test_subplot_config_validation(self):
|
|
"""Test subplot configuration validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.SUBPLOT_CONFIG})
|
|
|
|
# Test duplicate subplot types
|
|
config.subplot_configs.append(SubplotConfig(
|
|
subplot_type=SubplotType.RSI, # Duplicate
|
|
height_ratio=0.1,
|
|
indicators=[],
|
|
title="RSI 2"
|
|
))
|
|
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.warnings) > 0
|
|
assert any("Duplicate subplot type" in str(warning) for warning in report.warnings)
|
|
|
|
def test_strategy_consistency_validation(self):
|
|
"""Test strategy consistency validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.STRATEGY_CONSISTENCY})
|
|
|
|
# Test mismatched timeframes for scalping strategy
|
|
config.strategy_type = TradingStrategy.SCALPING
|
|
config.timeframes = ["4h", "1d"] # Not optimal for scalping
|
|
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.info) > 0
|
|
assert any("may not be optimal" in str(info) for info in report.info)
|
|
|
|
def test_performance_impact_validation(self):
|
|
"""Test performance impact validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.PERFORMANCE_IMPACT})
|
|
|
|
# Test high indicator count
|
|
config.overlay_indicators = [f"indicator_{i}" for i in range(12)] # 12 indicators
|
|
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.warnings) > 0
|
|
assert any("may impact performance" in str(warning) for warning in report.warnings)
|
|
|
|
def test_indicator_conflicts_validation(self):
|
|
"""Test indicator conflicts validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.INDICATOR_CONFLICTS})
|
|
|
|
# Test multiple SMA indicators
|
|
config.overlay_indicators = ["sma_5", "sma_10", "sma_20", "sma_50"] # 4 SMA indicators
|
|
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.info) > 0
|
|
assert any("visual clutter" in str(info) for info in report.info)
|
|
|
|
def test_resource_usage_validation(self):
|
|
"""Test resource usage validation."""
|
|
config = self.create_valid_config()
|
|
validator = ConfigurationValidator(enabled_rules={ValidationRule.RESOURCE_USAGE})
|
|
|
|
# Test high memory usage configuration
|
|
config.overlay_indicators = [f"indicator_{i}" for i in range(10)]
|
|
config.subplot_configs = [
|
|
SubplotConfig(subplot_type=SubplotType.RSI, height_ratio=0.1, indicators=[])
|
|
for _ in range(10)
|
|
] # Many subplots
|
|
|
|
report = validator.validate_strategy_config(config)
|
|
assert len(report.warnings) > 0 or len(report.info) > 0
|
|
|
|
|
|
class TestValidationFunctions:
|
|
"""Test standalone validation functions."""
|
|
|
|
def create_test_config(self) -> StrategyChartConfig:
|
|
"""Create a test configuration."""
|
|
return StrategyChartConfig(
|
|
strategy_name="Test Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Test strategy",
|
|
timeframes=["15m", "1h"],
|
|
main_chart_height=0.8,
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
height_ratio=0.2,
|
|
indicators=[]
|
|
)
|
|
]
|
|
)
|
|
|
|
def test_validate_configuration_function(self):
|
|
"""Test the standalone validate_configuration function."""
|
|
config = self.create_test_config()
|
|
|
|
# Test with default rules
|
|
report = validate_configuration(config)
|
|
assert isinstance(report, ValidationReport)
|
|
assert report.validation_time is not None
|
|
|
|
# Test with specific rules
|
|
specific_rules = {ValidationRule.REQUIRED_FIELDS, ValidationRule.HEIGHT_RATIOS}
|
|
report = validate_configuration(config, rules=specific_rules)
|
|
assert report.rules_applied == specific_rules
|
|
|
|
# Test strict mode
|
|
config.strategy_name = "AB" # Short name (should be warning)
|
|
report = validate_configuration(config, strict=False)
|
|
normal_errors = len(report.errors)
|
|
|
|
report = validate_configuration(config, strict=True)
|
|
strict_errors = len(report.errors)
|
|
assert strict_errors >= normal_errors # Strict mode may have more errors
|
|
|
|
def test_get_validation_rules_info(self):
|
|
"""Test getting validation rules information."""
|
|
rules_info = get_validation_rules_info()
|
|
|
|
assert isinstance(rules_info, dict)
|
|
assert len(rules_info) == len(ValidationRule)
|
|
|
|
# Check that all rules have information
|
|
for rule in ValidationRule:
|
|
assert rule in rules_info
|
|
rule_info = rules_info[rule]
|
|
assert "name" in rule_info
|
|
assert "description" in rule_info
|
|
assert isinstance(rule_info["name"], str)
|
|
assert isinstance(rule_info["description"], str)
|
|
|
|
|
|
class TestValidationIntegration:
|
|
"""Test integration with existing systems."""
|
|
|
|
def test_strategy_config_validate_method(self):
|
|
"""Test the updated validate method in StrategyChartConfig."""
|
|
config = StrategyChartConfig(
|
|
strategy_name="Integration Test",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Integration test strategy",
|
|
timeframes=["15m"],
|
|
main_chart_height=0.8,
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
height_ratio=0.2,
|
|
indicators=[]
|
|
)
|
|
]
|
|
)
|
|
|
|
# Test basic validate method (backward compatibility)
|
|
is_valid, errors = config.validate()
|
|
assert isinstance(is_valid, bool)
|
|
assert isinstance(errors, list)
|
|
|
|
# Test comprehensive validation method
|
|
report = config.validate_comprehensive()
|
|
assert isinstance(report, ValidationReport)
|
|
assert report.validation_time is not None
|
|
|
|
def test_validation_with_invalid_config(self):
|
|
"""Test validation with an invalid configuration."""
|
|
config = StrategyChartConfig(
|
|
strategy_name="", # Invalid: empty name
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="", # Warning: empty description
|
|
timeframes=[], # Invalid: no timeframes
|
|
main_chart_height=1.5, # Invalid: > 1.0
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
height_ratio=-0.1, # Invalid: negative
|
|
indicators=[]
|
|
)
|
|
]
|
|
)
|
|
|
|
# Test basic validation
|
|
is_valid, errors = config.validate()
|
|
assert not is_valid
|
|
assert len(errors) > 0
|
|
|
|
# Test comprehensive validation
|
|
report = config.validate_comprehensive()
|
|
assert not report.is_valid
|
|
assert len(report.errors) > 0
|
|
assert len(report.warnings) > 0 # Should have warnings too
|
|
|
|
def test_validation_error_handling(self):
|
|
"""Test validation error handling."""
|
|
config = StrategyChartConfig(
|
|
strategy_name="Error Test",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Error test strategy",
|
|
timeframes=["15m"],
|
|
main_chart_height=0.8,
|
|
subplot_configs=[]
|
|
)
|
|
|
|
# The validation should handle errors gracefully
|
|
is_valid, errors = config.validate()
|
|
assert isinstance(is_valid, bool)
|
|
assert isinstance(errors, list)
|
|
|
|
|
|
class TestValidationEdgeCases:
|
|
"""Test edge cases and boundary conditions."""
|
|
|
|
def test_empty_configuration(self):
|
|
"""Test validation with minimal configuration."""
|
|
config = StrategyChartConfig(
|
|
strategy_name="Minimal",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Minimal config",
|
|
timeframes=["1h"],
|
|
overlay_indicators=[],
|
|
subplot_configs=[]
|
|
)
|
|
|
|
report = validate_configuration(config)
|
|
# Should be valid even with minimal configuration
|
|
assert isinstance(report, ValidationReport)
|
|
|
|
def test_maximum_configuration(self):
|
|
"""Test validation with maximum complexity configuration."""
|
|
config = StrategyChartConfig(
|
|
strategy_name="Maximum Complexity Strategy",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Strategy with maximum complexity for testing",
|
|
timeframes=["1m", "5m", "15m", "1h", "4h"],
|
|
main_chart_height=0.4,
|
|
overlay_indicators=[f"indicator_{i}" for i in range(15)],
|
|
subplot_configs=[
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.RSI,
|
|
height_ratio=0.15,
|
|
indicators=[f"rsi_{i}" for i in range(5)]
|
|
),
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.MACD,
|
|
height_ratio=0.15,
|
|
indicators=[f"macd_{i}" for i in range(5)]
|
|
),
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.VOLUME,
|
|
height_ratio=0.1,
|
|
indicators=[]
|
|
),
|
|
SubplotConfig(
|
|
subplot_type=SubplotType.MOMENTUM,
|
|
height_ratio=0.2,
|
|
indicators=[f"momentum_{i}" for i in range(3)]
|
|
)
|
|
]
|
|
)
|
|
|
|
report = validate_configuration(config)
|
|
# Should have warnings about performance and complexity
|
|
assert len(report.warnings) > 0 or len(report.info) > 0
|
|
|
|
def test_boundary_values(self):
|
|
"""Test validation with boundary values."""
|
|
config = StrategyChartConfig(
|
|
strategy_name="Boundary Test",
|
|
strategy_type=TradingStrategy.DAY_TRADING,
|
|
description="Boundary test strategy",
|
|
timeframes=["1h"],
|
|
main_chart_height=1.0, # Maximum allowed
|
|
subplot_configs=[] # No subplots (total height = 1.0)
|
|
)
|
|
|
|
report = validate_configuration(config)
|
|
# Should be valid with exact boundary values
|
|
assert isinstance(report, ValidationReport)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__]) |