""" Enhanced Error Handling and User Guidance System This module provides comprehensive error handling for missing strategies and indicators, with clear error messages, suggestions, and recovery guidance rather than silent fallbacks. """ from typing import Dict, List, Optional, Set, Tuple, Any from dataclasses import dataclass, field from enum import Enum import difflib from datetime import datetime from .indicator_defs import IndicatorType, ChartIndicatorConfig from .defaults import get_all_default_indicators, IndicatorCategory, TradingStrategy from .strategy_charts import StrategyChartConfig from .example_strategies import get_all_example_strategies from utils.logger import get_logger # Initialize logger logger = get_logger() class ErrorSeverity(str, Enum): """Severity levels for configuration errors.""" CRITICAL = "critical" # Cannot proceed at all HIGH = "high" # Major functionality missing MEDIUM = "medium" # Some features unavailable LOW = "low" # Minor issues, mostly cosmetic class ErrorCategory(str, Enum): """Categories of configuration errors.""" MISSING_STRATEGY = "missing_strategy" MISSING_INDICATOR = "missing_indicator" INVALID_PARAMETER = "invalid_parameter" DEPENDENCY_MISSING = "dependency_missing" CONFIGURATION_CORRUPT = "configuration_corrupt" @dataclass class ConfigurationError: """Detailed configuration error with guidance.""" category: ErrorCategory severity: ErrorSeverity message: str field_path: str = "" missing_item: str = "" suggestions: List[str] = field(default_factory=list) alternatives: List[str] = field(default_factory=list) recovery_steps: List[str] = field(default_factory=list) context: Dict[str, Any] = field(default_factory=dict) def __str__(self) -> str: """String representation of the error.""" severity_emoji = { ErrorSeverity.CRITICAL: "🚨", ErrorSeverity.HIGH: "❌", ErrorSeverity.MEDIUM: "âš ī¸", ErrorSeverity.LOW: "â„šī¸" } result = f"{severity_emoji.get(self.severity, '❓')} {self.message}" if self.suggestions: result += f"\n 💡 Suggestions: {', '.join(self.suggestions)}" if self.alternatives: result += f"\n 🔄 Alternatives: {', '.join(self.alternatives)}" if self.recovery_steps: result += f"\n 🔧 Recovery steps:" for step in self.recovery_steps: result += f"\n â€ĸ {step}" return result @dataclass class ErrorReport: """Comprehensive error report with categorized issues.""" is_usable: bool errors: List[ConfigurationError] = field(default_factory=list) missing_strategies: Set[str] = field(default_factory=set) missing_indicators: Set[str] = field(default_factory=set) report_time: datetime = field(default_factory=datetime.now) def add_error(self, error: ConfigurationError) -> None: """Add an error to the report.""" self.errors.append(error) # Track missing items if error.category == ErrorCategory.MISSING_STRATEGY: self.missing_strategies.add(error.missing_item) elif error.category == ErrorCategory.MISSING_INDICATOR: self.missing_indicators.add(error.missing_item) # Update usability based on severity if error.severity in [ErrorSeverity.CRITICAL, ErrorSeverity.HIGH]: self.is_usable = False def get_critical_errors(self) -> List[ConfigurationError]: """Get only critical errors that prevent usage.""" return [e for e in self.errors if e.severity == ErrorSeverity.CRITICAL] def get_high_priority_errors(self) -> List[ConfigurationError]: """Get high priority errors that significantly impact functionality.""" return [e for e in self.errors if e.severity == ErrorSeverity.HIGH] def summary(self) -> str: """Get a summary of the error report.""" if not self.errors: return "✅ No configuration errors found" critical = len(self.get_critical_errors()) high = len(self.get_high_priority_errors()) total = len(self.errors) status = "❌ Cannot proceed" if not self.is_usable else "âš ī¸ Has issues but usable" return f"{status} - {total} errors ({critical} critical, {high} high priority)" class ConfigurationErrorHandler: """Enhanced error handler for configuration issues.""" def __init__(self): """Initialize the error handler.""" self.available_indicators = get_all_default_indicators() self.available_strategies = get_all_example_strategies() # Cache indicator names for fuzzy matching self.indicator_names = set(self.available_indicators.keys()) self.strategy_names = set(self.available_strategies.keys()) logger.info(f"Error Handling: Error handler initialized with {len(self.indicator_names)} indicators and {len(self.strategy_names)} strategies") def validate_strategy_exists(self, strategy_name: str) -> Optional[ConfigurationError]: """Check if a strategy exists and provide guidance if not.""" if strategy_name in self.strategy_names: return None # Find similar strategy names similar = difflib.get_close_matches( strategy_name, self.strategy_names, n=3, cutoff=0.6 ) suggestions = [] alternatives = list(similar) if similar else [] recovery_steps = [] if similar: suggestions.append(f"Did you mean one of: {', '.join(similar)}?") recovery_steps.append(f"Try using: {similar[0]}") else: suggestions.append("Check available strategies with get_all_example_strategies()") recovery_steps.append("List available strategies: get_strategy_summary()") # Add general recovery steps recovery_steps.extend([ "Create a custom strategy with create_custom_strategy_config()", "Use a pre-built strategy like 'ema_crossover' or 'swing_trading'" ]) return ConfigurationError( category=ErrorCategory.MISSING_STRATEGY, severity=ErrorSeverity.CRITICAL, message=f"Strategy '{strategy_name}' not found", missing_item=strategy_name, suggestions=suggestions, alternatives=alternatives, recovery_steps=recovery_steps, context={"available_count": len(self.strategy_names)} ) def validate_indicator_exists(self, indicator_name: str) -> Optional[ConfigurationError]: """Check if an indicator exists and provide guidance if not.""" if indicator_name in self.indicator_names: return None # Find similar indicator names similar = difflib.get_close_matches( indicator_name, self.indicator_names, n=3, cutoff=0.6 ) suggestions = [] alternatives = list(similar) if similar else [] recovery_steps = [] if similar: suggestions.append(f"Did you mean: {', '.join(similar)}?") recovery_steps.append(f"Try using: {similar[0]}") else: # Suggest by category if no close matches suggestions.append("Check available indicators with get_all_default_indicators()") # Try to guess category and suggest alternatives if "sma" in indicator_name.lower() or "ema" in indicator_name.lower(): trend_indicators = [name for name in self.indicator_names if name.startswith(("sma_", "ema_"))] alternatives.extend(trend_indicators[:3]) suggestions.append("For trend indicators, try SMA or EMA with different periods") elif "rsi" in indicator_name.lower(): rsi_indicators = [name for name in self.indicator_names if name.startswith("rsi_")] alternatives.extend(rsi_indicators) suggestions.append("For RSI, try rsi_14, rsi_7, or rsi_21") elif "macd" in indicator_name.lower(): macd_indicators = [name for name in self.indicator_names if name.startswith("macd_")] alternatives.extend(macd_indicators) suggestions.append("For MACD, try macd_12_26_9 or other period combinations") elif "bb" in indicator_name.lower() or "bollinger" in indicator_name.lower(): bb_indicators = [name for name in self.indicator_names if name.startswith("bb_")] alternatives.extend(bb_indicators) suggestions.append("For Bollinger Bands, try bb_20_20 or bb_20_15") # Add general recovery steps recovery_steps.extend([ "List available indicators by category: get_indicators_by_category()", "Create custom indicator with create_indicator_config()", "Remove this indicator from your configuration if not essential" ]) # Determine severity based on indicator type severity = ErrorSeverity.HIGH if indicator_name.startswith(("sma_", "ema_")): severity = ErrorSeverity.CRITICAL # Trend indicators are often essential return ConfigurationError( category=ErrorCategory.MISSING_INDICATOR, severity=severity, message=f"Indicator '{indicator_name}' not found", missing_item=indicator_name, suggestions=suggestions, alternatives=alternatives, recovery_steps=recovery_steps, context={"available_count": len(self.indicator_names)} ) def validate_strategy_configuration(self, config: StrategyChartConfig) -> ErrorReport: """Comprehensively validate a strategy configuration.""" report = ErrorReport(is_usable=True) # Validate overlay indicators for indicator in config.overlay_indicators: error = self.validate_indicator_exists(indicator) if error: error.field_path = f"overlay_indicators[{indicator}]" report.add_error(error) # Validate subplot indicators for i, subplot in enumerate(config.subplot_configs): for indicator in subplot.indicators: error = self.validate_indicator_exists(indicator) if error: error.field_path = f"subplot_configs[{i}].indicators[{indicator}]" report.add_error(error) # Check for empty configuration total_indicators = len(config.overlay_indicators) + sum( len(subplot.indicators) for subplot in config.subplot_configs ) if total_indicators == 0: report.add_error(ConfigurationError( category=ErrorCategory.CONFIGURATION_CORRUPT, severity=ErrorSeverity.CRITICAL, message="Configuration has no indicators defined", suggestions=[ "Add at least one overlay indicator (e.g., 'ema_12', 'sma_20')", "Add subplot indicators for momentum analysis (e.g., 'rsi_14')" ], recovery_steps=[ "Use a pre-built strategy: create_ema_crossover_strategy()", "Add basic indicators: ['ema_12', 'ema_26'] for trend analysis", "Add RSI subplot for momentum: subplot with 'rsi_14'" ] )) # Validate strategy consistency if hasattr(config, 'strategy_type'): consistency_error = self._validate_strategy_consistency(config) if consistency_error: report.add_error(consistency_error) return report def _validate_strategy_consistency(self, config: StrategyChartConfig) -> Optional[ConfigurationError]: """Validate that strategy configuration is consistent with strategy type.""" strategy_type = config.strategy_type timeframes = config.timeframes # Define expected timeframes for different strategies expected_timeframes = { TradingStrategy.SCALPING: ["1m", "5m"], TradingStrategy.DAY_TRADING: ["5m", "15m", "1h", "4h"], TradingStrategy.SWING_TRADING: ["1h", "4h", "1d"], TradingStrategy.MOMENTUM: ["5m", "15m", "1h"], TradingStrategy.MEAN_REVERSION: ["15m", "1h", "4h"] } if strategy_type in expected_timeframes: expected = expected_timeframes[strategy_type] overlap = set(timeframes) & set(expected) if not overlap: return ConfigurationError( category=ErrorCategory.INVALID_PARAMETER, severity=ErrorSeverity.MEDIUM, message=f"Timeframes {timeframes} may not be optimal for {strategy_type.value} strategy", field_path="timeframes", suggestions=[f"Consider using timeframes: {', '.join(expected)}"], alternatives=expected, recovery_steps=[ f"Update timeframes to include: {expected[0]}", f"Or change strategy type to match timeframes" ] ) return None def suggest_alternatives_for_missing_indicators(self, missing_indicators: Set[str]) -> Dict[str, List[str]]: """Suggest alternative indicators for missing ones.""" suggestions = {} for indicator in missing_indicators: alternatives = [] # Extract base type and period if possible parts = indicator.split('_') if len(parts) >= 2: base_type = parts[0] # Find similar indicators of the same type similar_type = [name for name in self.indicator_names if name.startswith(f"{base_type}_")] alternatives.extend(similar_type[:3]) # If no similar type, suggest by category if not similar_type: if base_type in ["sma", "ema"]: alternatives = ["sma_20", "ema_12", "ema_26"] elif base_type == "rsi": alternatives = ["rsi_14", "rsi_7", "rsi_21"] elif base_type == "macd": alternatives = ["macd_12_26_9", "macd_8_17_6"] elif base_type == "bb": alternatives = ["bb_20_20", "bb_20_15"] if alternatives: suggestions[indicator] = alternatives return suggestions def generate_recovery_configuration(self, config: StrategyChartConfig, error_report: ErrorReport) -> Tuple[Optional[StrategyChartConfig], List[str]]: """Generate a recovery configuration with working alternatives.""" if not error_report.missing_indicators: return config, [] recovery_notes = [] recovery_config = StrategyChartConfig( strategy_name=f"{config.strategy_name} (Recovery)", strategy_type=config.strategy_type, description=f"{config.description} (Auto-recovered from missing indicators)", timeframes=config.timeframes, layout=config.layout, main_chart_height=config.main_chart_height, overlay_indicators=[], subplot_configs=[], chart_style=config.chart_style ) # Replace missing overlay indicators for indicator in config.overlay_indicators: if indicator in error_report.missing_indicators: # Find replacement alternatives = self.suggest_alternatives_for_missing_indicators({indicator}) if indicator in alternatives and alternatives[indicator]: replacement = alternatives[indicator][0] recovery_config.overlay_indicators.append(replacement) recovery_notes.append(f"Replaced '{indicator}' with '{replacement}'") else: recovery_notes.append(f"Could not find replacement for '{indicator}' - removed") else: recovery_config.overlay_indicators.append(indicator) # Handle subplot configurations for subplot in config.subplot_configs: recovered_subplot = subplot.__class__( subplot_type=subplot.subplot_type, height_ratio=subplot.height_ratio, indicators=[], title=subplot.title, y_axis_label=subplot.y_axis_label, show_grid=subplot.show_grid, show_legend=subplot.show_legend ) for indicator in subplot.indicators: if indicator in error_report.missing_indicators: alternatives = self.suggest_alternatives_for_missing_indicators({indicator}) if indicator in alternatives and alternatives[indicator]: replacement = alternatives[indicator][0] recovered_subplot.indicators.append(replacement) recovery_notes.append(f"In subplot: Replaced '{indicator}' with '{replacement}'") else: recovery_notes.append(f"In subplot: Could not find replacement for '{indicator}' - removed") else: recovered_subplot.indicators.append(indicator) # Only add subplot if it has indicators if recovered_subplot.indicators: recovery_config.subplot_configs.append(recovered_subplot) else: recovery_notes.append(f"Removed empty subplot: {subplot.subplot_type.value}") # Add fallback indicators if configuration is empty if not recovery_config.overlay_indicators and not any( subplot.indicators for subplot in recovery_config.subplot_configs ): recovery_config.overlay_indicators = ["ema_12", "ema_26", "sma_20"] recovery_notes.append("Added basic trend indicators: EMA 12, EMA 26, SMA 20") # Add basic RSI subplot from .strategy_charts import SubplotConfig, SubplotType recovery_config.subplot_configs.append( SubplotConfig( subplot_type=SubplotType.RSI, height_ratio=0.2, indicators=["rsi_14"], title="RSI" ) ) recovery_notes.append("Added basic RSI subplot") return recovery_config, recovery_notes def validate_configuration_strict(config: StrategyChartConfig) -> ErrorReport: """ Strict validation that fails on any missing dependencies. Args: config: Strategy configuration to validate Returns: ErrorReport with detailed error information """ handler = ConfigurationErrorHandler() return handler.validate_strategy_configuration(config) def validate_strategy_name(strategy_name: str) -> Optional[ConfigurationError]: """ Validate that a strategy name exists. Args: strategy_name: Name of the strategy to validate Returns: ConfigurationError if strategy not found, None otherwise """ handler = ConfigurationErrorHandler() return handler.validate_strategy_exists(strategy_name) def get_indicator_suggestions(partial_name: str, limit: int = 5) -> List[str]: """ Get indicator suggestions based on partial name. Args: partial_name: Partial indicator name limit: Maximum number of suggestions Returns: List of suggested indicator names """ handler = ConfigurationErrorHandler() # Fuzzy match against available indicators matches = difflib.get_close_matches( partial_name, handler.indicator_names, n=limit, cutoff=0.3 ) # If no fuzzy matches, try substring matching if not matches: substring_matches = [ name for name in handler.indicator_names if partial_name.lower() in name.lower() ] matches = substring_matches[:limit] return matches def get_strategy_suggestions(partial_name: str, limit: int = 5) -> List[str]: """ Get strategy suggestions based on partial name. Args: partial_name: Partial strategy name limit: Maximum number of suggestions Returns: List of suggested strategy names """ handler = ConfigurationErrorHandler() matches = difflib.get_close_matches( partial_name, handler.strategy_names, n=limit, cutoff=0.3 ) if not matches: substring_matches = [ name for name in handler.strategy_names if partial_name.lower() in name.lower() ] matches = substring_matches[:limit] return matches def check_configuration_health(config: StrategyChartConfig) -> Dict[str, Any]: """ Perform a comprehensive health check on a configuration. Args: config: Strategy configuration to check Returns: Dictionary with health check results """ handler = ConfigurationErrorHandler() error_report = handler.validate_strategy_configuration(config) # Count indicators by category indicator_counts = {} all_indicators = config.overlay_indicators + [ indicator for subplot in config.subplot_configs for indicator in subplot.indicators ] for indicator in all_indicators: if indicator in handler.available_indicators: category = handler.available_indicators[indicator].category.value indicator_counts[category] = indicator_counts.get(category, 0) + 1 return { "is_healthy": error_report.is_usable and len(error_report.errors) == 0, "error_report": error_report, "total_indicators": len(all_indicators), "missing_indicators": len(error_report.missing_indicators), "indicator_by_category": indicator_counts, "has_trend_indicators": "trend" in indicator_counts, "has_momentum_indicators": "momentum" in indicator_counts, "recommendations": _generate_health_recommendations(config, error_report, indicator_counts) } def _generate_health_recommendations( config: StrategyChartConfig, error_report: ErrorReport, indicator_counts: Dict[str, int] ) -> List[str]: """Generate health recommendations for a configuration.""" recommendations = [] # Missing indicators if error_report.missing_indicators: recommendations.append(f"Fix {len(error_report.missing_indicators)} missing indicators") # Category balance if not indicator_counts.get("trend", 0): recommendations.append("Add trend indicators (SMA, EMA) for direction analysis") if not indicator_counts.get("momentum", 0): recommendations.append("Add momentum indicators (RSI, MACD) for entry timing") # Strategy-specific recommendations if config.strategy_type == TradingStrategy.SCALPING: if "1m" not in config.timeframes and "5m" not in config.timeframes: recommendations.append("Add short timeframes (1m, 5m) for scalping strategy") elif config.strategy_type == TradingStrategy.SWING_TRADING: if not any(tf in config.timeframes for tf in ["4h", "1d"]): recommendations.append("Add longer timeframes (4h, 1d) for swing trading") # Performance recommendations total_indicators = sum(indicator_counts.values()) if total_indicators > 10: recommendations.append("Consider reducing indicators for better performance") elif total_indicators < 3: recommendations.append("Add more indicators for comprehensive analysis") return recommendations