""" Configuration Validation and Error Handling System This module provides comprehensive validation for chart configurations with detailed error reporting, warnings, and configurable validation rules. """ from typing import Dict, List, Any, Optional, Union, Tuple, Set from dataclasses import dataclass, field from enum import Enum import re from datetime import datetime from .indicator_defs import ChartIndicatorConfig, INDICATOR_SCHEMAS, validate_indicator_configuration from .defaults import get_all_default_indicators, TradingStrategy, IndicatorCategory from .strategy_charts import StrategyChartConfig, SubplotConfig, ChartStyle, ChartLayout, SubplotType from utils.logger import get_logger # Initialize logger logger = get_logger() class ValidationLevel(str, Enum): """Validation severity levels.""" ERROR = "error" WARNING = "warning" INFO = "info" DEBUG = "debug" class ValidationRule(str, Enum): """Available validation rules.""" REQUIRED_FIELDS = "required_fields" HEIGHT_RATIOS = "height_ratios" INDICATOR_EXISTENCE = "indicator_existence" TIMEFRAME_FORMAT = "timeframe_format" CHART_STYLE = "chart_style" SUBPLOT_CONFIG = "subplot_config" STRATEGY_CONSISTENCY = "strategy_consistency" PERFORMANCE_IMPACT = "performance_impact" INDICATOR_CONFLICTS = "indicator_conflicts" RESOURCE_USAGE = "resource_usage" @dataclass class ValidationIssue: """Represents a validation issue.""" level: ValidationLevel rule: ValidationRule message: str field_path: str = "" suggestion: Optional[str] = None auto_fix: Optional[str] = None context: Dict[str, Any] = field(default_factory=dict) def __str__(self) -> str: """String representation of the validation issue.""" prefix = f"[{self.level.value.upper()}]" location = f" at {self.field_path}" if self.field_path else "" suggestion = f" Suggestion: {self.suggestion}" if self.suggestion else "" return f"{prefix} {self.message}{location}.{suggestion}" @dataclass class ValidationReport: """Comprehensive validation report.""" is_valid: bool errors: List[ValidationIssue] = field(default_factory=list) warnings: List[ValidationIssue] = field(default_factory=list) info: List[ValidationIssue] = field(default_factory=list) debug: List[ValidationIssue] = field(default_factory=list) validation_time: Optional[datetime] = None rules_applied: Set[ValidationRule] = field(default_factory=set) def add_issue(self, issue: ValidationIssue) -> None: """Add a validation issue to the appropriate list.""" if issue.level == ValidationLevel.ERROR: self.errors.append(issue) self.is_valid = False elif issue.level == ValidationLevel.WARNING: self.warnings.append(issue) elif issue.level == ValidationLevel.INFO: self.info.append(issue) elif issue.level == ValidationLevel.DEBUG: self.debug.append(issue) def get_all_issues(self) -> List[ValidationIssue]: """Get all validation issues sorted by severity.""" return self.errors + self.warnings + self.info + self.debug def get_issues_by_rule(self, rule: ValidationRule) -> List[ValidationIssue]: """Get all issues for a specific validation rule.""" return [issue for issue in self.get_all_issues() if issue.rule == rule] def has_errors(self) -> bool: """Check if there are any errors.""" return len(self.errors) > 0 def has_warnings(self) -> bool: """Check if there are any warnings.""" return len(self.warnings) > 0 def summary(self) -> str: """Get a summary of the validation report.""" total_issues = len(self.get_all_issues()) status = "INVALID" if not self.is_valid else "VALID" return (f"Validation {status}: {len(self.errors)} errors, " f"{len(self.warnings)} warnings, {total_issues} total issues") class ConfigurationValidator: """Comprehensive configuration validator.""" def __init__(self, enabled_rules: Optional[Set[ValidationRule]] = None): """ Initialize validator with optional rule filtering. Args: enabled_rules: Set of rules to apply. If None, applies all rules. """ self.enabled_rules = enabled_rules or set(ValidationRule) self.timeframe_pattern = re.compile(r'^(\d+)(m|h|d|w)$') self.color_pattern = re.compile(r'^#[0-9a-fA-F]{6}$') # Load indicator information for validation self._load_indicator_info() def _load_indicator_info(self) -> None: """Load indicator information for validation.""" try: self.available_indicators = get_all_default_indicators() self.indicator_schemas = INDICATOR_SCHEMAS except Exception as e: logger.warning(f"Validation: Failed to load indicator information: {e}") self.available_indicators = {} self.indicator_schemas = {} def validate_strategy_config(self, config: StrategyChartConfig) -> ValidationReport: """ Perform comprehensive validation of a strategy configuration. Args: config: Strategy configuration to validate Returns: Detailed validation report """ report = ValidationReport(is_valid=True, validation_time=datetime.now()) # Apply validation rules if ValidationRule.REQUIRED_FIELDS in self.enabled_rules: self._validate_required_fields(config, report) if ValidationRule.HEIGHT_RATIOS in self.enabled_rules: self._validate_height_ratios(config, report) if ValidationRule.INDICATOR_EXISTENCE in self.enabled_rules: self._validate_indicator_existence(config, report) if ValidationRule.TIMEFRAME_FORMAT in self.enabled_rules: self._validate_timeframe_format(config, report) if ValidationRule.CHART_STYLE in self.enabled_rules: self._validate_chart_style(config, report) if ValidationRule.SUBPLOT_CONFIG in self.enabled_rules: self._validate_subplot_configs(config, report) if ValidationRule.STRATEGY_CONSISTENCY in self.enabled_rules: self._validate_strategy_consistency(config, report) if ValidationRule.PERFORMANCE_IMPACT in self.enabled_rules: self._validate_performance_impact(config, report) if ValidationRule.INDICATOR_CONFLICTS in self.enabled_rules: self._validate_indicator_conflicts(config, report) if ValidationRule.RESOURCE_USAGE in self.enabled_rules: self._validate_resource_usage(config, report) # Update applied rules report.rules_applied = self.enabled_rules return report def _validate_required_fields(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate required fields.""" # Strategy name if not config.strategy_name or not config.strategy_name.strip(): report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.REQUIRED_FIELDS, message="Strategy name is required and cannot be empty", field_path="strategy_name", suggestion="Provide a descriptive name for your strategy" )) elif len(config.strategy_name.strip()) < 3: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.REQUIRED_FIELDS, message="Strategy name is very short", field_path="strategy_name", suggestion="Use a more descriptive name (at least 3 characters)" )) # Strategy type if not isinstance(config.strategy_type, TradingStrategy): report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.REQUIRED_FIELDS, message="Invalid strategy type", field_path="strategy_type", suggestion=f"Must be one of: {[s.value for s in TradingStrategy]}" )) # Description if not config.description or not config.description.strip(): report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.REQUIRED_FIELDS, message="Strategy description is missing", field_path="description", suggestion="Add a description to help users understand the strategy" )) # Timeframes if not config.timeframes: report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.REQUIRED_FIELDS, message="At least one timeframe must be specified", field_path="timeframes", suggestion="Add recommended timeframes for this strategy" )) def _validate_height_ratios(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate chart height ratios.""" # Main chart height if config.main_chart_height <= 0 or config.main_chart_height > 1.0: report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.HEIGHT_RATIOS, message=f"Main chart height ({config.main_chart_height}) must be between 0 and 1.0", field_path="main_chart_height", suggestion="Set a value between 0.1 and 0.9", auto_fix="0.7" )) elif config.main_chart_height < 0.3: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.HEIGHT_RATIOS, message=f"Main chart height ({config.main_chart_height}) is very small", field_path="main_chart_height", suggestion="Consider using at least 0.3 for better visibility" )) # Subplot heights total_subplot_height = sum(subplot.height_ratio for subplot in config.subplot_configs) total_height = config.main_chart_height + total_subplot_height if total_height > 1.0: excess = total_height - 1.0 report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.HEIGHT_RATIOS, message=f"Total chart height ({total_height:.3f}) exceeds 1.0 by {excess:.3f}", field_path="height_ratios", suggestion="Reduce main chart height or subplot heights", context={"total_height": total_height, "excess": excess} )) elif total_height < 0.8: unused = 1.0 - total_height report.add_issue(ValidationIssue( level=ValidationLevel.INFO, rule=ValidationRule.HEIGHT_RATIOS, message=f"Chart height ({total_height:.3f}) leaves {unused:.3f} unused space", field_path="height_ratios", suggestion="Consider increasing chart or subplot heights for better space utilization" )) # Individual subplot heights for i, subplot in enumerate(config.subplot_configs): if subplot.height_ratio <= 0 or subplot.height_ratio > 1.0: report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.HEIGHT_RATIOS, message=f"Subplot {i} height ratio ({subplot.height_ratio}) must be between 0 and 1.0", field_path=f"subplot_configs[{i}].height_ratio", suggestion="Set a value between 0.1 and 0.5" )) elif subplot.height_ratio < 0.1: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.HEIGHT_RATIOS, message=f"Subplot {i} height ratio ({subplot.height_ratio}) is very small", field_path=f"subplot_configs[{i}].height_ratio", suggestion="Consider using at least 0.1 for better readability" )) def _validate_indicator_existence(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate that indicators exist in the available indicators.""" # Check overlay indicators for indicator in config.overlay_indicators: if indicator not in self.available_indicators: report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.INDICATOR_EXISTENCE, message=f"Overlay indicator '{indicator}' not found", field_path=f"overlay_indicators.{indicator}", suggestion="Check indicator name or add it to defaults", context={"available_count": len(self.available_indicators)} )) # Check subplot indicators for i, subplot in enumerate(config.subplot_configs): for indicator in subplot.indicators: if indicator not in self.available_indicators: report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.INDICATOR_EXISTENCE, message=f"Subplot indicator '{indicator}' not found", field_path=f"subplot_configs[{i}].indicators.{indicator}", suggestion="Check indicator name or add it to defaults" )) def _validate_timeframe_format(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate timeframe format.""" valid_timeframes = ['1m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M'] for timeframe in config.timeframes: if timeframe not in valid_timeframes: if self.timeframe_pattern.match(timeframe): report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.TIMEFRAME_FORMAT, message=f"Timeframe '{timeframe}' format is valid but not in common list", field_path=f"timeframes.{timeframe}", suggestion=f"Consider using standard timeframes: {valid_timeframes[:8]}" )) else: report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.TIMEFRAME_FORMAT, message=f"Invalid timeframe format '{timeframe}'", field_path=f"timeframes.{timeframe}", suggestion="Use format like '1m', '5m', '1h', '4h', '1d', '1w'", context={"valid_timeframes": valid_timeframes} )) def _validate_chart_style(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate chart style configuration.""" style = config.chart_style # Validate colors color_fields = [ ('background_color', style.background_color), ('grid_color', style.grid_color), ('text_color', style.text_color), ('candlestick_up_color', style.candlestick_up_color), ('candlestick_down_color', style.candlestick_down_color), ('volume_color', style.volume_color) ] for field_name, color_value in color_fields: if color_value and not self.color_pattern.match(color_value): report.add_issue(ValidationIssue( level=ValidationLevel.ERROR, rule=ValidationRule.CHART_STYLE, message=f"Invalid color format for {field_name}: '{color_value}'", field_path=f"chart_style.{field_name}", suggestion="Use hex color format like '#ffffff' or '#123456'" )) # Validate font size if style.font_size < 6 or style.font_size > 24: level = ValidationLevel.ERROR if style.font_size < 1 or style.font_size > 48 else ValidationLevel.WARNING report.add_issue(ValidationIssue( level=level, rule=ValidationRule.CHART_STYLE, message=f"Font size {style.font_size} may cause readability issues", field_path="chart_style.font_size", suggestion="Use font size between 8 and 18 for optimal readability" )) # Validate theme valid_themes = ['plotly', 'plotly_white', 'plotly_dark', 'ggplot2', 'seaborn', 'simple_white'] if style.theme not in valid_themes: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.CHART_STYLE, message=f"Theme '{style.theme}' may not be supported", field_path="chart_style.theme", suggestion=f"Consider using: {valid_themes[:3]}", context={"valid_themes": valid_themes} )) def _validate_subplot_configs(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate subplot configurations.""" subplot_types = [subplot.subplot_type for subplot in config.subplot_configs] # Check for duplicate subplot types seen_types = set() for i, subplot in enumerate(config.subplot_configs): if subplot.subplot_type in seen_types: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.SUBPLOT_CONFIG, message=f"Duplicate subplot type '{subplot.subplot_type.value}' at position {i}", field_path=f"subplot_configs[{i}].subplot_type", suggestion="Consider using different subplot types or combining indicators" )) seen_types.add(subplot.subplot_type) # Validate subplot-specific indicators if subplot.subplot_type == SubplotType.RSI and subplot.indicators: for indicator in subplot.indicators: if indicator in self.available_indicators: indicator_config = self.available_indicators[indicator].config indicator_type = indicator_config.indicator_type # Handle both string and enum types if hasattr(indicator_type, 'value'): indicator_type_value = indicator_type.value else: indicator_type_value = str(indicator_type) if indicator_type_value != 'rsi': report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.SUBPLOT_CONFIG, message=f"Non-RSI indicator '{indicator}' in RSI subplot", field_path=f"subplot_configs[{i}].indicators.{indicator}", suggestion="Use RSI indicators in RSI subplots for consistency" )) elif subplot.subplot_type == SubplotType.MACD and subplot.indicators: for indicator in subplot.indicators: if indicator in self.available_indicators: indicator_config = self.available_indicators[indicator].config indicator_type = indicator_config.indicator_type # Handle both string and enum types if hasattr(indicator_type, 'value'): indicator_type_value = indicator_type.value else: indicator_type_value = str(indicator_type) if indicator_type_value != 'macd': report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.SUBPLOT_CONFIG, message=f"Non-MACD indicator '{indicator}' in MACD subplot", field_path=f"subplot_configs[{i}].indicators.{indicator}", suggestion="Use MACD indicators in MACD subplots for consistency" )) def _validate_strategy_consistency(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate strategy consistency with indicator choices.""" strategy_type = config.strategy_type timeframes = config.timeframes # Check timeframe consistency with strategy strategy_timeframe_recommendations = { TradingStrategy.SCALPING: ['1m', '5m'], TradingStrategy.DAY_TRADING: ['5m', '15m', '1h'], TradingStrategy.SWING_TRADING: ['1h', '4h', '1d'], TradingStrategy.POSITION_TRADING: ['4h', '1d', '1w'], TradingStrategy.MOMENTUM: ['15m', '1h', '4h'], TradingStrategy.MEAN_REVERSION: ['15m', '1h', '4h'] } recommended = strategy_timeframe_recommendations.get(strategy_type, []) if recommended: mismatched_timeframes = [tf for tf in timeframes if tf not in recommended] if mismatched_timeframes: report.add_issue(ValidationIssue( level=ValidationLevel.INFO, rule=ValidationRule.STRATEGY_CONSISTENCY, message=f"Timeframes {mismatched_timeframes} may not be optimal for {strategy_type.value}", field_path="timeframes", suggestion=f"Consider using: {recommended}", context={"recommended": recommended, "current": timeframes} )) def _validate_performance_impact(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate potential performance impact.""" total_indicators = len(config.overlay_indicators) for subplot in config.subplot_configs: total_indicators += len(subplot.indicators) if total_indicators > 10: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.PERFORMANCE_IMPACT, message=f"High indicator count ({total_indicators}) may impact performance", field_path="indicators", suggestion="Consider reducing the number of indicators for better performance", context={"indicator_count": total_indicators} )) # Check for complex indicators complex_indicators = ['bollinger_bands', 'macd'] complex_count = 0 all_indicators = config.overlay_indicators.copy() for subplot in config.subplot_configs: all_indicators.extend(subplot.indicators) for indicator in all_indicators: if indicator in self.available_indicators: indicator_config = self.available_indicators[indicator].config indicator_type = indicator_config.indicator_type # Handle both string and enum types if hasattr(indicator_type, 'value'): indicator_type_value = indicator_type.value else: indicator_type_value = str(indicator_type) if indicator_type_value in complex_indicators: complex_count += 1 if complex_count > 3: report.add_issue(ValidationIssue( level=ValidationLevel.INFO, rule=ValidationRule.PERFORMANCE_IMPACT, message=f"Multiple complex indicators ({complex_count}) detected", field_path="indicators", suggestion="Complex indicators may increase calculation time", context={"complex_count": complex_count} )) def _validate_indicator_conflicts(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate for potential indicator conflicts or redundancy.""" all_indicators = config.overlay_indicators.copy() for subplot in config.subplot_configs: all_indicators.extend(subplot.indicators) # Check for similar indicators sma_indicators = [ind for ind in all_indicators if 'sma_' in ind] ema_indicators = [ind for ind in all_indicators if 'ema_' in ind] rsi_indicators = [ind for ind in all_indicators if 'rsi_' in ind] if len(sma_indicators) > 3: report.add_issue(ValidationIssue( level=ValidationLevel.INFO, rule=ValidationRule.INDICATOR_CONFLICTS, message=f"Multiple SMA indicators ({len(sma_indicators)}) may create visual clutter", field_path="overlay_indicators", suggestion="Consider using fewer SMA periods for cleaner charts" )) if len(ema_indicators) > 3: report.add_issue(ValidationIssue( level=ValidationLevel.INFO, rule=ValidationRule.INDICATOR_CONFLICTS, message=f"Multiple EMA indicators ({len(ema_indicators)}) may create visual clutter", field_path="overlay_indicators", suggestion="Consider using fewer EMA periods for cleaner charts" )) if len(rsi_indicators) > 2: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.INDICATOR_CONFLICTS, message=f"Multiple RSI indicators ({len(rsi_indicators)}) provide redundant information", field_path="subplot_indicators", suggestion="Usually one or two RSI periods are sufficient" )) def _validate_resource_usage(self, config: StrategyChartConfig, report: ValidationReport) -> None: """Validate estimated resource usage.""" # Estimate memory usage based on indicators and subplots base_memory = 1.0 # Base chart memory in MB indicator_memory = len(config.overlay_indicators) * 0.1 # 0.1 MB per overlay indicator subplot_memory = len(config.subplot_configs) * 0.5 # 0.5 MB per subplot total_memory = base_memory + indicator_memory + subplot_memory if total_memory > 5.0: report.add_issue(ValidationIssue( level=ValidationLevel.WARNING, rule=ValidationRule.RESOURCE_USAGE, message=f"Estimated memory usage ({total_memory:.1f} MB) is high", field_path="configuration", suggestion="Consider reducing indicators or subplots for lower memory usage", context={"estimated_memory_mb": total_memory} )) # Check for potential rendering complexity rendering_complexity = len(config.overlay_indicators) + (len(config.subplot_configs) * 2) if rendering_complexity > 15: report.add_issue(ValidationIssue( level=ValidationLevel.INFO, rule=ValidationRule.RESOURCE_USAGE, message=f"High rendering complexity ({rendering_complexity}) detected", field_path="configuration", suggestion="Complex charts may have slower rendering times" )) def validate_configuration( config: StrategyChartConfig, rules: Optional[Set[ValidationRule]] = None, strict: bool = False ) -> ValidationReport: """ Validate a strategy configuration with comprehensive error checking. Args: config: Strategy configuration to validate rules: Optional set of validation rules to apply strict: If True, treats warnings as errors Returns: Comprehensive validation report """ validator = ConfigurationValidator(enabled_rules=rules) report = validator.validate_strategy_config(config) # In strict mode, treat warnings as errors if strict and report.warnings: for warning in report.warnings: warning.level = ValidationLevel.ERROR report.errors.append(warning) report.warnings.clear() report.is_valid = False return report def get_validation_rules_info() -> Dict[ValidationRule, Dict[str, str]]: """ Get information about available validation rules. Returns: Dictionary mapping rules to their descriptions """ return { ValidationRule.REQUIRED_FIELDS: { "name": "Required Fields", "description": "Validates that all required configuration fields are present and valid" }, ValidationRule.HEIGHT_RATIOS: { "name": "Height Ratios", "description": "Validates chart and subplot height ratios sum correctly" }, ValidationRule.INDICATOR_EXISTENCE: { "name": "Indicator Existence", "description": "Validates that all referenced indicators exist in the defaults" }, ValidationRule.TIMEFRAME_FORMAT: { "name": "Timeframe Format", "description": "Validates timeframe format and common usage patterns" }, ValidationRule.CHART_STYLE: { "name": "Chart Style", "description": "Validates chart styling options like colors, fonts, and themes" }, ValidationRule.SUBPLOT_CONFIG: { "name": "Subplot Configuration", "description": "Validates subplot configurations and indicator compatibility" }, ValidationRule.STRATEGY_CONSISTENCY: { "name": "Strategy Consistency", "description": "Validates that configuration matches strategy type recommendations" }, ValidationRule.PERFORMANCE_IMPACT: { "name": "Performance Impact", "description": "Warns about configurations that may impact performance" }, ValidationRule.INDICATOR_CONFLICTS: { "name": "Indicator Conflicts", "description": "Detects redundant or conflicting indicator combinations" }, ValidationRule.RESOURCE_USAGE: { "name": "Resource Usage", "description": "Estimates and warns about high resource usage configurations" } }