TCPDashboard/components/charts/config/error_handling.py
2025-06-12 13:27:30 +08:00

605 lines
24 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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