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

462 lines
18 KiB
Python

"""
Error Handling Utilities for Chart Layers
This module provides comprehensive error handling for chart creation,
including custom exceptions, error recovery strategies, and user-friendly
error messaging for various insufficient data scenarios.
"""
import pandas as pd
from datetime import datetime, timezone, timedelta
from typing import List, Dict, Any, Optional, Union, Tuple, Callable
from dataclasses import dataclass
from enum import Enum
from utils.logger import get_logger
# Initialize logger
logger = get_logger()
class ErrorSeverity(Enum):
"""Error severity levels for chart operations"""
INFO = "info" # Informational, chart can proceed
WARNING = "warning" # Warning, chart proceeds with limitations
ERROR = "error" # Error, chart creation may fail
CRITICAL = "critical" # Critical error, chart creation impossible
@dataclass
class ChartError:
"""Container for chart error information"""
code: str
message: str
severity: ErrorSeverity
context: Dict[str, Any]
recovery_suggestion: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert error to dictionary for logging/serialization"""
return {
'code': self.code,
'message': self.message,
'severity': self.severity.value,
'context': self.context,
'recovery_suggestion': self.recovery_suggestion
}
class ChartDataError(Exception):
"""Base exception for chart data-related errors"""
def __init__(self, error: ChartError):
self.error = error
super().__init__(error.message)
class InsufficientDataError(ChartDataError):
"""Raised when there's insufficient data for chart/indicator calculations"""
pass
class DataValidationError(ChartDataError):
"""Raised when data validation fails"""
pass
class IndicatorCalculationError(ChartDataError):
"""Raised when indicator calculations fail"""
pass
class DataConnectionError(ChartDataError):
"""Raised when database/data source connection fails"""
pass
class DataRequirements:
"""Data requirements checker for charts and indicators"""
# Minimum data requirements for different indicators
INDICATOR_MIN_PERIODS = {
'sma': lambda period: period + 5, # SMA needs period + buffer
'ema': lambda period: period * 2, # EMA needs 2x period for stability
'rsi': lambda period: period + 10, # RSI needs period + warmup
'macd': lambda fast, slow, signal: slow + signal + 10, # MACD most demanding
'bollinger_bands': lambda period: period + 5, # BB needs period + buffer
'candlestick': lambda: 10, # Basic candlestick minimum
'volume': lambda: 5 # Volume minimum
}
@classmethod
def check_candlestick_requirements(cls, data_count: int) -> ChartError:
"""Check if we have enough data for basic candlestick chart"""
min_required = cls.INDICATOR_MIN_PERIODS['candlestick']()
if data_count == 0:
return ChartError(
code='NO_DATA',
message='No market data available',
severity=ErrorSeverity.CRITICAL,
context={'data_count': data_count, 'required': min_required},
recovery_suggestion='Check data collection service or select different symbol/timeframe'
)
elif data_count < min_required:
return ChartError(
code='INSUFFICIENT_CANDLESTICK_DATA',
message=f'Insufficient data for candlestick chart: {data_count} candles (need {min_required})',
severity=ErrorSeverity.WARNING,
context={'data_count': data_count, 'required': min_required},
recovery_suggestion='Chart will display with limited data - consider longer time range'
)
else:
return ChartError(
code='SUFFICIENT_DATA',
message='Sufficient data for candlestick chart',
severity=ErrorSeverity.INFO,
context={'data_count': data_count, 'required': min_required}
)
@classmethod
def check_indicator_requirements(cls, indicator_type: str, data_count: int,
parameters: Dict[str, Any]) -> ChartError:
"""Check if we have enough data for specific indicator"""
if indicator_type not in cls.INDICATOR_MIN_PERIODS:
return ChartError(
code='UNKNOWN_INDICATOR',
message=f'Unknown indicator type: {indicator_type}',
severity=ErrorSeverity.ERROR,
context={'indicator_type': indicator_type, 'data_count': data_count},
recovery_suggestion='Check indicator type spelling or implementation'
)
# Calculate minimum required data
try:
if indicator_type in ['sma', 'ema', 'rsi', 'bollinger_bands']:
period = parameters.get('period', 20)
min_required = cls.INDICATOR_MIN_PERIODS[indicator_type](period)
elif indicator_type == 'macd':
fast = parameters.get('fast_period', 12)
slow = parameters.get('slow_period', 26)
signal = parameters.get('signal_period', 9)
min_required = cls.INDICATOR_MIN_PERIODS[indicator_type](fast, slow, signal)
else:
min_required = cls.INDICATOR_MIN_PERIODS[indicator_type]()
except Exception as e:
return ChartError(
code='PARAMETER_ERROR',
message=f'Invalid parameters for {indicator_type}: {e}',
severity=ErrorSeverity.ERROR,
context={'indicator_type': indicator_type, 'parameters': parameters},
recovery_suggestion='Check indicator parameters for valid values'
)
if data_count < min_required:
# Determine severity based on how insufficient the data is
if data_count < min_required // 2:
# Severely insufficient - less than half the required data
severity = ErrorSeverity.ERROR
else:
# Slightly insufficient - can potentially adjust parameters
severity = ErrorSeverity.WARNING
return ChartError(
code='INSUFFICIENT_INDICATOR_DATA',
message=f'Insufficient data for {indicator_type}: {data_count} candles (need {min_required})',
severity=severity,
context={
'indicator_type': indicator_type,
'data_count': data_count,
'required': min_required,
'parameters': parameters
},
recovery_suggestion=f'Increase data range to at least {min_required} candles or adjust {indicator_type} parameters'
)
else:
return ChartError(
code='SUFFICIENT_INDICATOR_DATA',
message=f'Sufficient data for {indicator_type}',
severity=ErrorSeverity.INFO,
context={
'indicator_type': indicator_type,
'data_count': data_count,
'required': min_required
}
)
class ErrorRecoveryStrategies:
"""Error recovery strategies for different chart scenarios"""
@staticmethod
def handle_insufficient_data(error: ChartError, fallback_options: Dict[str, Any]) -> Dict[str, Any]:
"""Handle insufficient data by providing fallback strategies"""
strategy = {
'can_proceed': False,
'fallback_action': None,
'modified_config': None,
'user_message': error.message
}
if error.code == 'INSUFFICIENT_CANDLESTICK_DATA':
# For candlestick, we can proceed with warnings
strategy.update({
'can_proceed': True,
'fallback_action': 'display_with_warning',
'user_message': f"{error.message}. Chart will display available data."
})
elif error.code == 'INSUFFICIENT_INDICATOR_DATA':
# For indicators, try to adjust parameters or skip
indicator_type = error.context.get('indicator_type')
data_count = error.context.get('data_count', 0)
if indicator_type in ['sma', 'ema', 'bollinger_bands']:
# Try reducing period to fit available data
max_period = max(5, data_count // 2) # Conservative estimate
strategy.update({
'can_proceed': True,
'fallback_action': 'adjust_parameters',
'modified_config': {'period': max_period},
'user_message': f"Adjusted {indicator_type} period to {max_period} due to limited data"
})
elif indicator_type == 'rsi':
# RSI can work with reduced period
max_period = max(7, data_count // 3)
strategy.update({
'can_proceed': True,
'fallback_action': 'adjust_parameters',
'modified_config': {'period': max_period},
'user_message': f"Adjusted RSI period to {max_period} due to limited data"
})
else:
# Skip the indicator entirely
strategy.update({
'can_proceed': True,
'fallback_action': 'skip_indicator',
'user_message': f"Skipped {indicator_type} due to insufficient data"
})
return strategy
@staticmethod
def handle_data_validation_error(error: ChartError) -> Dict[str, Any]:
"""Handle data validation errors"""
return {
'can_proceed': False,
'fallback_action': 'show_error',
'user_message': f"Data validation failed: {error.message}",
'recovery_suggestion': error.recovery_suggestion
}
@staticmethod
def handle_connection_error(error: ChartError) -> Dict[str, Any]:
"""Handle database/connection errors"""
return {
'can_proceed': False,
'fallback_action': 'show_error',
'user_message': "Unable to connect to data source",
'recovery_suggestion': "Check database connection or try again later"
}
class ChartErrorHandler:
"""Main error handler for chart operations"""
def __init__(self):
self.logger = logger
self.errors: List[ChartError] = []
self.warnings: List[ChartError] = []
def clear_errors(self):
"""Clear accumulated errors and warnings"""
self.errors.clear()
self.warnings.clear()
def validate_data_sufficiency(self, data: Union[pd.DataFrame, List[Dict[str, Any]]],
chart_type: str = 'candlestick',
indicators: List[Dict[str, Any]] = None) -> bool:
"""
Validate if data is sufficient for chart and indicator requirements.
Args:
data: Chart data (DataFrame or list of candle dicts)
chart_type: Type of chart being created
indicators: List of indicator configurations
Returns:
True if data is sufficient, False otherwise
"""
self.clear_errors()
# Get data count
if isinstance(data, pd.DataFrame):
data_count = len(data)
elif isinstance(data, list):
data_count = len(data)
else:
self.errors.append(ChartError(
code='INVALID_DATA_TYPE',
message=f'Invalid data type: {type(data)}',
severity=ErrorSeverity.ERROR,
context={'data_type': str(type(data))}
))
return False
# Check basic chart requirements
chart_error = DataRequirements.check_candlestick_requirements(data_count)
if chart_error.severity in [ErrorSeverity.WARNING]:
self.warnings.append(chart_error)
elif chart_error.severity in [ErrorSeverity.ERROR, ErrorSeverity.CRITICAL]:
self.errors.append(chart_error)
return False
# Check indicator requirements
if indicators:
for indicator_config in indicators:
indicator_type = indicator_config.get('type', 'unknown')
parameters = indicator_config.get('parameters', {})
indicator_error = DataRequirements.check_indicator_requirements(
indicator_type, data_count, parameters
)
if indicator_error.severity == ErrorSeverity.WARNING:
self.warnings.append(indicator_error)
elif indicator_error.severity in [ErrorSeverity.ERROR, ErrorSeverity.CRITICAL]:
self.errors.append(indicator_error)
# Return True if no critical errors
return len(self.errors) == 0
def get_error_summary(self) -> Dict[str, Any]:
"""Get summary of all errors and warnings"""
return {
'has_errors': len(self.errors) > 0,
'has_warnings': len(self.warnings) > 0,
'error_count': len(self.errors),
'warning_count': len(self.warnings),
'errors': [error.to_dict() for error in self.errors],
'warnings': [warning.to_dict() for warning in self.warnings],
'can_proceed': len(self.errors) == 0
}
def get_user_friendly_message(self) -> str:
"""Get a user-friendly message summarizing errors and warnings"""
if not self.errors and not self.warnings:
return "Chart data is ready"
messages = []
if self.errors:
error_msg = f"{len(self.errors)} error(s) prevent chart creation"
messages.append(error_msg)
# Add most relevant error message
if self.errors:
main_error = self.errors[0] # Show first error
messages.append(f"{main_error.message}")
if main_error.recovery_suggestion:
messages.append(f" 💡 {main_error.recovery_suggestion}")
if self.warnings:
warning_msg = f"⚠️ {len(self.warnings)} warning(s)"
messages.append(warning_msg)
# Add most relevant warning
if self.warnings:
main_warning = self.warnings[0]
messages.append(f"{main_warning.message}")
return "\n".join(messages)
def apply_error_recovery(self, error: ChartError,
fallback_options: Dict[str, Any] = None) -> Dict[str, Any]:
"""Apply error recovery strategy for a specific error"""
fallback_options = fallback_options or {}
if error.code.startswith('INSUFFICIENT'):
return ErrorRecoveryStrategies.handle_insufficient_data(error, fallback_options)
elif 'VALIDATION' in error.code:
return ErrorRecoveryStrategies.handle_data_validation_error(error)
elif 'CONNECTION' in error.code:
return ErrorRecoveryStrategies.handle_connection_error(error)
else:
# Default recovery strategy
return {
'can_proceed': False,
'fallback_action': 'show_error',
'user_message': error.message,
'recovery_suggestion': error.recovery_suggestion
}
# Convenience functions
def check_data_sufficiency(data: Union[pd.DataFrame, List[Dict[str, Any]]],
indicators: List[Dict[str, Any]] = None) -> Tuple[bool, Dict[str, Any]]:
"""
Convenience function to check data sufficiency.
Args:
data: Chart data
indicators: List of indicator configurations
Returns:
Tuple of (is_sufficient, error_summary)
"""
handler = ChartErrorHandler()
is_sufficient = handler.validate_data_sufficiency(data, indicators=indicators)
return is_sufficient, handler.get_error_summary()
def get_error_message(data: Union[pd.DataFrame, List[Dict[str, Any]]],
indicators: List[Dict[str, Any]] = None) -> str:
"""
Get user-friendly error message for data issues.
Args:
data: Chart data
indicators: List of indicator configurations
Returns:
User-friendly error message
"""
handler = ChartErrorHandler()
handler.validate_data_sufficiency(data, indicators=indicators)
return handler.get_user_friendly_message()
def create_error_annotation(error_message: str, position: str = "top") -> Dict[str, Any]:
"""
Create a Plotly annotation for error display.
Args:
error_message: Error message to display
position: Position of annotation ('top', 'center', 'bottom')
Returns:
Plotly annotation configuration
"""
positions = {
'top': {'x': 0.5, 'y': 0.9},
'center': {'x': 0.5, 'y': 0.5},
'bottom': {'x': 0.5, 'y': 0.1}
}
pos = positions.get(position, positions['center'])
return {
'text': error_message,
'xref': 'paper',
'yref': 'paper',
'x': pos['x'],
'y': pos['y'],
'xanchor': 'center',
'yanchor': 'middle',
'showarrow': False,
'font': {'size': 14, 'color': '#e74c3c'},
'bgcolor': 'rgba(255,255,255,0.8)',
'bordercolor': '#e74c3c',
'borderwidth': 1
}