605 lines
24 KiB
Python
605 lines
24 KiB
Python
"""
|
||
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 |