3.4 - 3.0 Strategy Configuration System
Implement comprehensive chart configuration and validation system - Introduced a modular chart configuration system in `components/charts/config/` to manage indicator definitions, default configurations, and strategy-specific setups. - Added new modules for error handling and validation, enhancing user guidance and error reporting capabilities. - Implemented detailed schema validation for indicators and strategies, ensuring robust configuration management. - Created example strategies and default configurations to facilitate user onboarding and usage. - Enhanced documentation to provide clear guidelines on the configuration system, validation rules, and usage examples. - Added unit tests for all new components to ensure functionality and reliability across the configuration system.
This commit is contained in:
@@ -1,37 +1,242 @@
|
||||
"""
|
||||
Chart Configuration Package
|
||||
|
||||
This package contains configuration management for the modular chart system,
|
||||
including indicator definitions, strategy-specific configurations, and defaults.
|
||||
This package provides configuration management for the modular chart system,
|
||||
including indicator definitions, schema validation, and default configurations.
|
||||
"""
|
||||
|
||||
from .indicator_defs import (
|
||||
INDICATOR_DEFINITIONS,
|
||||
# Core classes
|
||||
IndicatorType,
|
||||
DisplayType,
|
||||
LineStyle,
|
||||
PriceColumn,
|
||||
IndicatorParameterSchema,
|
||||
IndicatorSchema,
|
||||
ChartIndicatorConfig,
|
||||
calculate_indicators,
|
||||
convert_database_candles_to_ohlcv,
|
||||
|
||||
# Schema definitions
|
||||
INDICATOR_SCHEMAS,
|
||||
INDICATOR_DEFINITIONS,
|
||||
|
||||
# Utility functions
|
||||
validate_indicator_configuration,
|
||||
create_indicator_config,
|
||||
get_indicator_schema,
|
||||
get_available_indicator_types,
|
||||
get_indicator_parameter_info,
|
||||
validate_parameters_for_type,
|
||||
create_configuration_from_json,
|
||||
|
||||
# Legacy functions
|
||||
get_indicator_display_config,
|
||||
get_available_indicators,
|
||||
get_overlay_indicators,
|
||||
get_subplot_indicators,
|
||||
get_default_indicator_params
|
||||
get_default_indicator_params,
|
||||
calculate_indicators
|
||||
)
|
||||
|
||||
from .defaults import (
|
||||
# Categories and strategies
|
||||
IndicatorCategory,
|
||||
TradingStrategy,
|
||||
IndicatorPreset,
|
||||
|
||||
# Color schemes
|
||||
CATEGORY_COLORS,
|
||||
|
||||
# Default indicators
|
||||
get_all_default_indicators,
|
||||
get_indicators_by_category,
|
||||
get_indicators_for_timeframe,
|
||||
|
||||
# Strategy presets
|
||||
get_strategy_indicators,
|
||||
get_strategy_info,
|
||||
get_available_strategies,
|
||||
get_available_categories,
|
||||
|
||||
# Custom presets
|
||||
create_custom_preset
|
||||
)
|
||||
|
||||
from .strategy_charts import (
|
||||
# Chart configuration classes
|
||||
ChartLayout,
|
||||
SubplotType,
|
||||
SubplotConfig,
|
||||
ChartStyle,
|
||||
StrategyChartConfig,
|
||||
|
||||
# Strategy configuration functions
|
||||
create_default_strategy_configurations,
|
||||
validate_strategy_configuration,
|
||||
create_custom_strategy_config,
|
||||
load_strategy_config_from_json,
|
||||
export_strategy_config_to_json,
|
||||
get_strategy_config,
|
||||
get_all_strategy_configs,
|
||||
get_available_strategy_names
|
||||
)
|
||||
|
||||
from .validation import (
|
||||
# Validation classes
|
||||
ValidationLevel,
|
||||
ValidationRule,
|
||||
ValidationIssue,
|
||||
ValidationReport,
|
||||
ConfigurationValidator,
|
||||
|
||||
# Validation functions
|
||||
validate_configuration,
|
||||
get_validation_rules_info
|
||||
)
|
||||
|
||||
from .example_strategies import (
|
||||
# Example strategy classes
|
||||
StrategyExample,
|
||||
|
||||
# Example strategy functions
|
||||
create_ema_crossover_strategy,
|
||||
create_momentum_breakout_strategy,
|
||||
create_mean_reversion_strategy,
|
||||
create_scalping_strategy,
|
||||
create_swing_trading_strategy,
|
||||
get_all_example_strategies,
|
||||
get_example_strategy,
|
||||
get_strategies_by_difficulty,
|
||||
get_strategies_by_risk_level,
|
||||
get_strategies_by_market_condition,
|
||||
get_strategy_summary,
|
||||
export_example_strategies_to_json
|
||||
)
|
||||
|
||||
from .error_handling import (
|
||||
# Error handling classes
|
||||
ErrorSeverity,
|
||||
ErrorCategory,
|
||||
ConfigurationError,
|
||||
ErrorReport,
|
||||
ConfigurationErrorHandler,
|
||||
|
||||
# Error handling functions
|
||||
validate_configuration_strict,
|
||||
validate_strategy_name,
|
||||
get_indicator_suggestions,
|
||||
get_strategy_suggestions,
|
||||
check_configuration_health
|
||||
)
|
||||
|
||||
# Package metadata
|
||||
__version__ = "0.1.0"
|
||||
__package_name__ = "config"
|
||||
|
||||
# Public exports
|
||||
__all__ = [
|
||||
"INDICATOR_DEFINITIONS",
|
||||
"ChartIndicatorConfig",
|
||||
"calculate_indicators",
|
||||
"convert_database_candles_to_ohlcv",
|
||||
"get_indicator_display_config",
|
||||
"get_available_indicators",
|
||||
"get_overlay_indicators",
|
||||
"get_subplot_indicators",
|
||||
"get_default_indicator_params"
|
||||
# Core classes from indicator_defs
|
||||
'IndicatorType',
|
||||
'DisplayType',
|
||||
'LineStyle',
|
||||
'PriceColumn',
|
||||
'IndicatorParameterSchema',
|
||||
'IndicatorSchema',
|
||||
'ChartIndicatorConfig',
|
||||
|
||||
# Schema and definitions
|
||||
'INDICATOR_SCHEMAS',
|
||||
'INDICATOR_DEFINITIONS',
|
||||
|
||||
# Validation and creation functions
|
||||
'validate_indicator_configuration',
|
||||
'create_indicator_config',
|
||||
'get_indicator_schema',
|
||||
'get_available_indicator_types',
|
||||
'get_indicator_parameter_info',
|
||||
'validate_parameters_for_type',
|
||||
'create_configuration_from_json',
|
||||
|
||||
# Legacy compatibility functions
|
||||
'get_indicator_display_config',
|
||||
'get_available_indicators',
|
||||
'get_overlay_indicators',
|
||||
'get_subplot_indicators',
|
||||
'get_default_indicator_params',
|
||||
'calculate_indicators',
|
||||
|
||||
# Categories and strategies from defaults
|
||||
'IndicatorCategory',
|
||||
'TradingStrategy',
|
||||
'IndicatorPreset',
|
||||
'CATEGORY_COLORS',
|
||||
|
||||
# Default configuration functions
|
||||
'get_all_default_indicators',
|
||||
'get_indicators_by_category',
|
||||
'get_indicators_for_timeframe',
|
||||
'get_strategy_indicators',
|
||||
'get_strategy_info',
|
||||
'get_available_strategies',
|
||||
'get_available_categories',
|
||||
'create_custom_preset',
|
||||
|
||||
# Strategy chart configuration classes
|
||||
'ChartLayout',
|
||||
'SubplotType',
|
||||
'SubplotConfig',
|
||||
'ChartStyle',
|
||||
'StrategyChartConfig',
|
||||
|
||||
# Strategy configuration functions
|
||||
'create_default_strategy_configurations',
|
||||
'validate_strategy_configuration',
|
||||
'create_custom_strategy_config',
|
||||
'load_strategy_config_from_json',
|
||||
'export_strategy_config_to_json',
|
||||
'get_strategy_config',
|
||||
'get_all_strategy_configs',
|
||||
'get_available_strategy_names',
|
||||
|
||||
# Validation classes
|
||||
'ValidationLevel',
|
||||
'ValidationRule',
|
||||
'ValidationIssue',
|
||||
'ValidationReport',
|
||||
'ConfigurationValidator',
|
||||
|
||||
# Validation functions
|
||||
'validate_configuration',
|
||||
'get_validation_rules_info',
|
||||
|
||||
# Example strategy classes
|
||||
'StrategyExample',
|
||||
|
||||
# Example strategy functions
|
||||
'create_ema_crossover_strategy',
|
||||
'create_momentum_breakout_strategy',
|
||||
'create_mean_reversion_strategy',
|
||||
'create_scalping_strategy',
|
||||
'create_swing_trading_strategy',
|
||||
'get_all_example_strategies',
|
||||
'get_example_strategy',
|
||||
'get_strategies_by_difficulty',
|
||||
'get_strategies_by_risk_level',
|
||||
'get_strategies_by_market_condition',
|
||||
'get_strategy_summary',
|
||||
'export_example_strategies_to_json',
|
||||
|
||||
# Error handling classes
|
||||
'ErrorSeverity',
|
||||
'ErrorCategory',
|
||||
'ConfigurationError',
|
||||
'ErrorReport',
|
||||
'ConfigurationErrorHandler',
|
||||
|
||||
# Error handling functions
|
||||
'validate_configuration_strict',
|
||||
'validate_strategy_name',
|
||||
'get_indicator_suggestions',
|
||||
'get_strategy_suggestions',
|
||||
'check_configuration_health'
|
||||
]
|
||||
|
||||
# Legacy function names for backward compatibility
|
||||
|
||||
460
components/charts/config/defaults.py
Normal file
460
components/charts/config/defaults.py
Normal file
@@ -0,0 +1,460 @@
|
||||
"""
|
||||
Default Indicator Configurations and Parameters
|
||||
|
||||
This module provides comprehensive default indicator configurations
|
||||
organized by categories, trading strategies, and common use cases.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from .indicator_defs import ChartIndicatorConfig, create_indicator_config, IndicatorType
|
||||
|
||||
|
||||
class IndicatorCategory(str, Enum):
|
||||
"""Categories for organizing indicators."""
|
||||
TREND = "trend"
|
||||
MOMENTUM = "momentum"
|
||||
VOLATILITY = "volatility"
|
||||
VOLUME = "volume"
|
||||
SUPPORT_RESISTANCE = "support_resistance"
|
||||
|
||||
|
||||
class TradingStrategy(str, Enum):
|
||||
"""Common trading strategy types."""
|
||||
SCALPING = "scalping"
|
||||
DAY_TRADING = "day_trading"
|
||||
SWING_TRADING = "swing_trading"
|
||||
POSITION_TRADING = "position_trading"
|
||||
MOMENTUM = "momentum"
|
||||
MEAN_REVERSION = "mean_reversion"
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndicatorPreset:
|
||||
"""
|
||||
Predefined indicator configuration preset.
|
||||
"""
|
||||
name: str
|
||||
description: str
|
||||
category: IndicatorCategory
|
||||
recommended_timeframes: List[str]
|
||||
config: ChartIndicatorConfig
|
||||
|
||||
|
||||
# Color schemes for different indicator categories
|
||||
CATEGORY_COLORS = {
|
||||
IndicatorCategory.TREND: {
|
||||
'primary': '#007bff', # Blue
|
||||
'secondary': '#28a745', # Green
|
||||
'tertiary': '#17a2b8', # Cyan
|
||||
'quaternary': '#6c757d' # Gray
|
||||
},
|
||||
IndicatorCategory.MOMENTUM: {
|
||||
'primary': '#dc3545', # Red
|
||||
'secondary': '#fd7e14', # Orange
|
||||
'tertiary': '#e83e8c', # Pink
|
||||
'quaternary': '#6f42c1' # Purple
|
||||
},
|
||||
IndicatorCategory.VOLATILITY: {
|
||||
'primary': '#6f42c1', # Purple
|
||||
'secondary': '#e83e8c', # Pink
|
||||
'tertiary': '#20c997', # Teal
|
||||
'quaternary': '#ffc107' # Yellow
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_trend_indicators() -> Dict[str, IndicatorPreset]:
|
||||
"""Create default trend indicator configurations."""
|
||||
trend_indicators = {}
|
||||
|
||||
# Simple Moving Averages
|
||||
sma_configs = [
|
||||
(5, "Very Short Term", ['1m', '5m']),
|
||||
(10, "Short Term", ['5m', '15m']),
|
||||
(20, "Short-Medium Term", ['15m', '1h']),
|
||||
(50, "Medium Term", ['1h', '4h']),
|
||||
(100, "Long Term", ['4h', '1d']),
|
||||
(200, "Very Long Term", ['1d', '1w'])
|
||||
]
|
||||
|
||||
for period, desc, timeframes in sma_configs:
|
||||
config, _ = create_indicator_config(
|
||||
name=f"SMA ({period})",
|
||||
indicator_type="sma",
|
||||
parameters={"period": period},
|
||||
color=CATEGORY_COLORS[IndicatorCategory.TREND]['primary'] if period <= 20 else
|
||||
CATEGORY_COLORS[IndicatorCategory.TREND]['secondary'] if period <= 50 else
|
||||
CATEGORY_COLORS[IndicatorCategory.TREND]['tertiary'],
|
||||
line_width=2 if period <= 50 else 3
|
||||
)
|
||||
|
||||
trend_indicators[f"sma_{period}"] = IndicatorPreset(
|
||||
name=f"SMA {period}",
|
||||
description=f"{desc} Simple Moving Average - {period} periods",
|
||||
category=IndicatorCategory.TREND,
|
||||
recommended_timeframes=timeframes,
|
||||
config=config
|
||||
)
|
||||
|
||||
# Exponential Moving Averages
|
||||
ema_configs = [
|
||||
(5, "Very Short Term", ['1m', '5m']),
|
||||
(12, "Short Term (MACD Fast)", ['5m', '15m', '1h']),
|
||||
(21, "Fibonacci Short Term", ['15m', '1h']),
|
||||
(26, "Medium Term (MACD Slow)", ['1h', '4h']),
|
||||
(50, "Medium-Long Term", ['4h', '1d']),
|
||||
(100, "Long Term", ['1d', '1w']),
|
||||
(200, "Very Long Term", ['1d', '1w'])
|
||||
]
|
||||
|
||||
for period, desc, timeframes in ema_configs:
|
||||
config, _ = create_indicator_config(
|
||||
name=f"EMA ({period})",
|
||||
indicator_type="ema",
|
||||
parameters={"period": period},
|
||||
color=CATEGORY_COLORS[IndicatorCategory.TREND]['secondary'] if period <= 21 else
|
||||
CATEGORY_COLORS[IndicatorCategory.TREND]['tertiary'] if period <= 50 else
|
||||
CATEGORY_COLORS[IndicatorCategory.TREND]['quaternary'],
|
||||
line_width=2,
|
||||
line_style='dash' if period in [12, 26] else 'solid'
|
||||
)
|
||||
|
||||
trend_indicators[f"ema_{period}"] = IndicatorPreset(
|
||||
name=f"EMA {period}",
|
||||
description=f"{desc} Exponential Moving Average - {period} periods",
|
||||
category=IndicatorCategory.TREND,
|
||||
recommended_timeframes=timeframes,
|
||||
config=config
|
||||
)
|
||||
|
||||
return trend_indicators
|
||||
|
||||
|
||||
def create_momentum_indicators() -> Dict[str, IndicatorPreset]:
|
||||
"""Create default momentum indicator configurations."""
|
||||
momentum_indicators = {}
|
||||
|
||||
# RSI configurations
|
||||
rsi_configs = [
|
||||
(7, "Fast RSI", ['1m', '5m', '15m']),
|
||||
(14, "Standard RSI", ['15m', '1h', '4h']),
|
||||
(21, "Slow RSI", ['1h', '4h', '1d']),
|
||||
(30, "Very Slow RSI", ['4h', '1d', '1w'])
|
||||
]
|
||||
|
||||
for period, desc, timeframes in rsi_configs:
|
||||
config, _ = create_indicator_config(
|
||||
name=f"RSI ({period})",
|
||||
indicator_type="rsi",
|
||||
parameters={"period": period},
|
||||
color=CATEGORY_COLORS[IndicatorCategory.MOMENTUM]['primary'] if period == 14 else
|
||||
CATEGORY_COLORS[IndicatorCategory.MOMENTUM]['secondary'],
|
||||
line_width=2,
|
||||
subplot_height_ratio=0.25
|
||||
)
|
||||
|
||||
momentum_indicators[f"rsi_{period}"] = IndicatorPreset(
|
||||
name=f"RSI {period}",
|
||||
description=f"{desc} - Relative Strength Index with {period} periods",
|
||||
category=IndicatorCategory.MOMENTUM,
|
||||
recommended_timeframes=timeframes,
|
||||
config=config
|
||||
)
|
||||
|
||||
# MACD configurations
|
||||
macd_configs = [
|
||||
((5, 13, 4), "Fast MACD", ['1m', '5m']),
|
||||
((8, 17, 6), "Scalping MACD", ['5m', '15m']),
|
||||
((12, 26, 9), "Standard MACD", ['15m', '1h', '4h']),
|
||||
((19, 39, 13), "Slow MACD", ['1h', '4h', '1d']),
|
||||
((26, 52, 18), "Very Slow MACD", ['4h', '1d', '1w'])
|
||||
]
|
||||
|
||||
for (fast, slow, signal), desc, timeframes in macd_configs:
|
||||
config, _ = create_indicator_config(
|
||||
name=f"MACD ({fast},{slow},{signal})",
|
||||
indicator_type="macd",
|
||||
parameters={
|
||||
"fast_period": fast,
|
||||
"slow_period": slow,
|
||||
"signal_period": signal
|
||||
},
|
||||
color=CATEGORY_COLORS[IndicatorCategory.MOMENTUM]['secondary'] if (fast, slow, signal) == (12, 26, 9) else
|
||||
CATEGORY_COLORS[IndicatorCategory.MOMENTUM]['tertiary'],
|
||||
line_width=2,
|
||||
subplot_height_ratio=0.3
|
||||
)
|
||||
|
||||
momentum_indicators[f"macd_{fast}_{slow}_{signal}"] = IndicatorPreset(
|
||||
name=f"MACD {fast}/{slow}/{signal}",
|
||||
description=f"{desc} - MACD with {fast}/{slow}/{signal} periods",
|
||||
category=IndicatorCategory.MOMENTUM,
|
||||
recommended_timeframes=timeframes,
|
||||
config=config
|
||||
)
|
||||
|
||||
return momentum_indicators
|
||||
|
||||
|
||||
def create_volatility_indicators() -> Dict[str, IndicatorPreset]:
|
||||
"""Create default volatility indicator configurations."""
|
||||
volatility_indicators = {}
|
||||
|
||||
# Bollinger Bands configurations
|
||||
bb_configs = [
|
||||
((10, 1.5), "Tight Bollinger Bands", ['1m', '5m']),
|
||||
((20, 2.0), "Standard Bollinger Bands", ['15m', '1h', '4h']),
|
||||
((20, 2.5), "Wide Bollinger Bands", ['1h', '4h']),
|
||||
((50, 2.0), "Long-term Bollinger Bands", ['4h', '1d', '1w'])
|
||||
]
|
||||
|
||||
for (period, std_dev), desc, timeframes in bb_configs:
|
||||
config, _ = create_indicator_config(
|
||||
name=f"BB ({period}, {std_dev})",
|
||||
indicator_type="bollinger_bands",
|
||||
parameters={"period": period, "std_dev": std_dev},
|
||||
color=CATEGORY_COLORS[IndicatorCategory.VOLATILITY]['primary'] if (period, std_dev) == (20, 2.0) else
|
||||
CATEGORY_COLORS[IndicatorCategory.VOLATILITY]['secondary'],
|
||||
line_width=1,
|
||||
opacity=0.7
|
||||
)
|
||||
|
||||
volatility_indicators[f"bb_{period}_{int(std_dev*10)}"] = IndicatorPreset(
|
||||
name=f"Bollinger Bands {period}/{std_dev}",
|
||||
description=f"{desc} - {period} period with {std_dev} standard deviations",
|
||||
category=IndicatorCategory.VOLATILITY,
|
||||
recommended_timeframes=timeframes,
|
||||
config=config
|
||||
)
|
||||
|
||||
return volatility_indicators
|
||||
|
||||
|
||||
def create_strategy_presets() -> Dict[str, Dict[str, List[str]]]:
|
||||
"""Create predefined indicator combinations for common trading strategies."""
|
||||
|
||||
strategy_presets = {
|
||||
TradingStrategy.SCALPING.value: {
|
||||
"name": "Scalping Strategy",
|
||||
"description": "Fast indicators for 1-5 minute scalping",
|
||||
"timeframes": ["1m", "5m"],
|
||||
"indicators": [
|
||||
"ema_5", "ema_12", "ema_21",
|
||||
"rsi_7", "macd_5_13_4",
|
||||
"bb_10_15"
|
||||
]
|
||||
},
|
||||
|
||||
TradingStrategy.DAY_TRADING.value: {
|
||||
"name": "Day Trading Strategy",
|
||||
"description": "Balanced indicators for intraday trading",
|
||||
"timeframes": ["5m", "15m", "1h"],
|
||||
"indicators": [
|
||||
"sma_20", "ema_12", "ema_26",
|
||||
"rsi_14", "macd_12_26_9",
|
||||
"bb_20_20"
|
||||
]
|
||||
},
|
||||
|
||||
TradingStrategy.SWING_TRADING.value: {
|
||||
"name": "Swing Trading Strategy",
|
||||
"description": "Medium-term indicators for swing trading",
|
||||
"timeframes": ["1h", "4h", "1d"],
|
||||
"indicators": [
|
||||
"sma_50", "ema_21", "ema_50",
|
||||
"rsi_14", "rsi_21", "macd_12_26_9",
|
||||
"bb_20_20"
|
||||
]
|
||||
},
|
||||
|
||||
TradingStrategy.POSITION_TRADING.value: {
|
||||
"name": "Position Trading Strategy",
|
||||
"description": "Long-term indicators for position trading",
|
||||
"timeframes": ["4h", "1d", "1w"],
|
||||
"indicators": [
|
||||
"sma_100", "sma_200", "ema_50", "ema_100",
|
||||
"rsi_21", "macd_19_39_13",
|
||||
"bb_50_20"
|
||||
]
|
||||
},
|
||||
|
||||
TradingStrategy.MOMENTUM.value: {
|
||||
"name": "Momentum Strategy",
|
||||
"description": "Momentum-focused indicators",
|
||||
"timeframes": ["15m", "1h", "4h"],
|
||||
"indicators": [
|
||||
"ema_12", "ema_26",
|
||||
"rsi_7", "rsi_14", "macd_8_17_6", "macd_12_26_9"
|
||||
]
|
||||
},
|
||||
|
||||
TradingStrategy.MEAN_REVERSION.value: {
|
||||
"name": "Mean Reversion Strategy",
|
||||
"description": "Indicators for mean reversion trading",
|
||||
"timeframes": ["15m", "1h", "4h"],
|
||||
"indicators": [
|
||||
"sma_20", "sma_50", "bb_20_20", "bb_20_25",
|
||||
"rsi_14", "rsi_21"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return strategy_presets
|
||||
|
||||
|
||||
def get_all_default_indicators() -> Dict[str, IndicatorPreset]:
|
||||
"""
|
||||
Get all default indicator configurations.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping indicator names to their preset configurations
|
||||
"""
|
||||
all_indicators = {}
|
||||
|
||||
# Combine all indicator categories
|
||||
all_indicators.update(create_trend_indicators())
|
||||
all_indicators.update(create_momentum_indicators())
|
||||
all_indicators.update(create_volatility_indicators())
|
||||
|
||||
return all_indicators
|
||||
|
||||
|
||||
def get_indicators_by_category(category: IndicatorCategory) -> Dict[str, IndicatorPreset]:
|
||||
"""
|
||||
Get default indicators filtered by category.
|
||||
|
||||
Args:
|
||||
category: Indicator category to filter by
|
||||
|
||||
Returns:
|
||||
Dictionary of indicators in the specified category
|
||||
"""
|
||||
all_indicators = get_all_default_indicators()
|
||||
return {name: preset for name, preset in all_indicators.items()
|
||||
if preset.category == category}
|
||||
|
||||
|
||||
def get_indicators_for_timeframe(timeframe: str) -> Dict[str, IndicatorPreset]:
|
||||
"""
|
||||
Get indicators recommended for a specific timeframe.
|
||||
|
||||
Args:
|
||||
timeframe: Timeframe string (e.g., '1m', '5m', '1h', '4h', '1d')
|
||||
|
||||
Returns:
|
||||
Dictionary of indicators suitable for the timeframe
|
||||
"""
|
||||
all_indicators = get_all_default_indicators()
|
||||
return {name: preset for name, preset in all_indicators.items()
|
||||
if timeframe in preset.recommended_timeframes}
|
||||
|
||||
|
||||
def get_strategy_indicators(strategy: TradingStrategy) -> List[str]:
|
||||
"""
|
||||
Get indicator names for a specific trading strategy.
|
||||
|
||||
Args:
|
||||
strategy: Trading strategy type
|
||||
|
||||
Returns:
|
||||
List of indicator names for the strategy
|
||||
"""
|
||||
presets = create_strategy_presets()
|
||||
strategy_config = presets.get(strategy.value, {})
|
||||
return strategy_config.get("indicators", [])
|
||||
|
||||
|
||||
def get_strategy_info(strategy: TradingStrategy) -> Dict[str, Any]:
|
||||
"""
|
||||
Get complete information about a trading strategy.
|
||||
|
||||
Args:
|
||||
strategy: Trading strategy type
|
||||
|
||||
Returns:
|
||||
Dictionary with strategy details including indicators and timeframes
|
||||
"""
|
||||
presets = create_strategy_presets()
|
||||
return presets.get(strategy.value, {})
|
||||
|
||||
|
||||
def create_custom_preset(
|
||||
name: str,
|
||||
description: str,
|
||||
category: IndicatorCategory,
|
||||
indicator_configs: List[Dict[str, Any]],
|
||||
recommended_timeframes: Optional[List[str]] = None
|
||||
) -> Dict[str, IndicatorPreset]:
|
||||
"""
|
||||
Create custom indicator presets.
|
||||
|
||||
Args:
|
||||
name: Preset name
|
||||
description: Preset description
|
||||
category: Indicator category
|
||||
indicator_configs: List of indicator configuration dictionaries
|
||||
recommended_timeframes: Optional list of recommended timeframes
|
||||
|
||||
Returns:
|
||||
Dictionary of created indicator presets
|
||||
"""
|
||||
custom_presets = {}
|
||||
|
||||
for i, config_data in enumerate(indicator_configs):
|
||||
try:
|
||||
config, errors = create_indicator_config(**config_data)
|
||||
if errors:
|
||||
continue
|
||||
|
||||
preset_name = f"{name.lower().replace(' ', '_')}_{i}"
|
||||
custom_presets[preset_name] = IndicatorPreset(
|
||||
name=f"{name} {i+1}",
|
||||
description=description,
|
||||
category=category,
|
||||
recommended_timeframes=recommended_timeframes or ["15m", "1h", "4h"],
|
||||
config=config
|
||||
)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return custom_presets
|
||||
|
||||
|
||||
def get_available_strategies() -> List[Dict[str, str]]:
|
||||
"""
|
||||
Get list of available trading strategies.
|
||||
|
||||
Returns:
|
||||
List of dictionaries with strategy information
|
||||
"""
|
||||
presets = create_strategy_presets()
|
||||
return [
|
||||
{
|
||||
"value": strategy,
|
||||
"name": info["name"],
|
||||
"description": info["description"],
|
||||
"timeframes": ", ".join(info["timeframes"])
|
||||
}
|
||||
for strategy, info in presets.items()
|
||||
]
|
||||
|
||||
|
||||
def get_available_categories() -> List[Dict[str, str]]:
|
||||
"""
|
||||
Get list of available indicator categories.
|
||||
|
||||
Returns:
|
||||
List of dictionaries with category information
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"value": category.value,
|
||||
"name": category.value.replace("_", " ").title(),
|
||||
"description": f"Indicators for {category.value.replace('_', ' ')} analysis"
|
||||
}
|
||||
for category in IndicatorCategory
|
||||
]
|
||||
605
components/charts/config/error_handling.py
Normal file
605
components/charts/config/error_handling.py
Normal file
@@ -0,0 +1,605 @@
|
||||
"""
|
||||
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("error_handling")
|
||||
|
||||
|
||||
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 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
|
||||
651
components/charts/config/example_strategies.py
Normal file
651
components/charts/config/example_strategies.py
Normal file
@@ -0,0 +1,651 @@
|
||||
"""
|
||||
Example Strategy Configurations
|
||||
|
||||
This module provides real-world trading strategy configurations that demonstrate
|
||||
how to combine indicators for specific trading approaches like EMA crossover,
|
||||
momentum trading, and other popular strategies.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from .strategy_charts import (
|
||||
StrategyChartConfig,
|
||||
SubplotConfig,
|
||||
ChartStyle,
|
||||
ChartLayout,
|
||||
SubplotType
|
||||
)
|
||||
from .defaults import TradingStrategy
|
||||
from utils.logger import get_logger
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger("example_strategies")
|
||||
|
||||
|
||||
@dataclass
|
||||
class StrategyExample:
|
||||
"""Represents an example trading strategy with metadata."""
|
||||
config: StrategyChartConfig
|
||||
description: str
|
||||
author: str = "TCPDashboard"
|
||||
difficulty: str = "Beginner" # Beginner, Intermediate, Advanced
|
||||
expected_return: Optional[str] = None
|
||||
risk_level: str = "Medium" # Low, Medium, High
|
||||
market_conditions: List[str] = None # Trending, Sideways, Volatile
|
||||
notes: List[str] = None
|
||||
references: List[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.market_conditions is None:
|
||||
self.market_conditions = ["Trending"]
|
||||
if self.notes is None:
|
||||
self.notes = []
|
||||
if self.references is None:
|
||||
self.references = []
|
||||
|
||||
|
||||
def create_ema_crossover_strategy() -> StrategyExample:
|
||||
"""
|
||||
Create EMA crossover strategy configuration.
|
||||
|
||||
Classic trend-following strategy using fast and slow EMA crossovers
|
||||
for entry and exit signals.
|
||||
"""
|
||||
config = StrategyChartConfig(
|
||||
strategy_name="EMA Crossover Strategy",
|
||||
strategy_type=TradingStrategy.DAY_TRADING,
|
||||
description="Trend-following strategy using EMA crossovers for entry/exit signals",
|
||||
timeframes=["15m", "1h", "4h"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.65,
|
||||
overlay_indicators=[
|
||||
"ema_12", # Fast EMA
|
||||
"ema_26", # Slow EMA
|
||||
"ema_50", # Trend filter
|
||||
"bb_20_20" # Bollinger Bands for volatility context
|
||||
],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.15,
|
||||
indicators=["rsi_14"],
|
||||
title="RSI Momentum",
|
||||
y_axis_label="RSI",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.2,
|
||||
indicators=["macd_12_26_9"],
|
||||
title="MACD Confirmation",
|
||||
y_axis_label="MACD",
|
||||
show_grid=True
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=12,
|
||||
candlestick_up_color="#26a69a",
|
||||
candlestick_down_color="#ef5350",
|
||||
show_volume=True,
|
||||
show_grid=True
|
||||
),
|
||||
tags=["trend-following", "ema-crossover", "day-trading", "intermediate"]
|
||||
)
|
||||
|
||||
return StrategyExample(
|
||||
config=config,
|
||||
description="""
|
||||
EMA Crossover Strategy uses the crossing of fast (12-period) and slow (26-period)
|
||||
exponential moving averages to generate buy and sell signals. The 50-period EMA
|
||||
acts as a trend filter to avoid false signals in sideways markets.
|
||||
|
||||
Entry Rules:
|
||||
- Buy when fast EMA crosses above slow EMA and price is above 50 EMA
|
||||
- RSI should be above 30 (not oversold)
|
||||
- MACD line should be above signal line
|
||||
|
||||
Exit Rules:
|
||||
- Sell when fast EMA crosses below slow EMA
|
||||
- Or when RSI reaches overbought levels (>70)
|
||||
- Stop loss: 2% below entry or below recent swing low
|
||||
""",
|
||||
author="TCPDashboard Team",
|
||||
difficulty="Intermediate",
|
||||
expected_return="8-15% monthly (in trending markets)",
|
||||
risk_level="Medium",
|
||||
market_conditions=["Trending", "Breakout"],
|
||||
notes=[
|
||||
"Works best in trending markets",
|
||||
"Can produce whipsaws in sideways markets",
|
||||
"Use 50 EMA as trend filter to reduce false signals",
|
||||
"Consider volume confirmation for stronger signals",
|
||||
"Best timeframes: 15m, 1h, 4h for day trading"
|
||||
],
|
||||
references=[
|
||||
"Moving Average Convergence Divergence - Gerald Appel",
|
||||
"Technical Analysis of Financial Markets - John Murphy"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_momentum_breakout_strategy() -> StrategyExample:
|
||||
"""
|
||||
Create momentum breakout strategy configuration.
|
||||
|
||||
Strategy focused on capturing momentum moves using RSI,
|
||||
MACD, and volume confirmation.
|
||||
"""
|
||||
config = StrategyChartConfig(
|
||||
strategy_name="Momentum Breakout Strategy",
|
||||
strategy_type=TradingStrategy.MOMENTUM,
|
||||
description="Momentum strategy capturing strong directional moves with volume confirmation",
|
||||
timeframes=["5m", "15m", "1h"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.6,
|
||||
overlay_indicators=[
|
||||
"ema_8", # Fast trend
|
||||
"ema_21", # Medium trend
|
||||
"bb_20_25" # Volatility bands (wider for breakouts)
|
||||
],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.15,
|
||||
indicators=["rsi_7", "rsi_14"], # Fast and standard RSI
|
||||
title="RSI Momentum (7 & 14)",
|
||||
y_axis_label="RSI",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.15,
|
||||
indicators=["macd_8_17_6"], # Faster MACD for momentum
|
||||
title="MACD Fast",
|
||||
y_axis_label="MACD",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.VOLUME,
|
||||
height_ratio=0.1,
|
||||
indicators=[],
|
||||
title="Volume Confirmation",
|
||||
y_axis_label="Volume",
|
||||
show_grid=True
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=11,
|
||||
candlestick_up_color="#00d4aa",
|
||||
candlestick_down_color="#fe6a85",
|
||||
show_volume=True,
|
||||
show_grid=True
|
||||
),
|
||||
tags=["momentum", "breakout", "volume", "short-term"]
|
||||
)
|
||||
|
||||
return StrategyExample(
|
||||
config=config,
|
||||
description="""
|
||||
Momentum Breakout Strategy captures strong directional moves by identifying
|
||||
momentum acceleration with volume confirmation. Uses faster indicators
|
||||
to catch moves early while avoiding false breakouts.
|
||||
|
||||
Entry Rules:
|
||||
- Price breaks above/below Bollinger Bands with strong volume
|
||||
- RSI-7 > 70 for bullish momentum (< 30 for bearish)
|
||||
- MACD line crosses above signal line with rising histogram
|
||||
- Volume should be 1.5x average volume
|
||||
- EMA-8 above EMA-21 for trend confirmation
|
||||
|
||||
Exit Rules:
|
||||
- RSI-7 reaches extreme levels (>80 or <20)
|
||||
- MACD histogram starts declining
|
||||
- Volume drops significantly
|
||||
- Price returns inside Bollinger Bands
|
||||
- Stop loss: 1.5% or below previous swing point
|
||||
""",
|
||||
author="TCPDashboard Team",
|
||||
difficulty="Advanced",
|
||||
expected_return="15-25% monthly (high volatility)",
|
||||
risk_level="High",
|
||||
market_conditions=["Volatile", "Breakout", "High Volume"],
|
||||
notes=[
|
||||
"Requires quick execution and tight risk management",
|
||||
"Best during high volatility periods",
|
||||
"Monitor volume closely for confirmation",
|
||||
"Use smaller position sizes due to higher risk",
|
||||
"Consider market hours for better volume",
|
||||
"Avoid during low liquidity periods"
|
||||
],
|
||||
references=[
|
||||
"Momentum Stock Selection - Richard Driehaus",
|
||||
"High Probability Trading - Marcel Link"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_mean_reversion_strategy() -> StrategyExample:
|
||||
"""
|
||||
Create mean reversion strategy configuration.
|
||||
|
||||
Counter-trend strategy using oversold/overbought conditions
|
||||
and support/resistance levels.
|
||||
"""
|
||||
config = StrategyChartConfig(
|
||||
strategy_name="Mean Reversion Strategy",
|
||||
strategy_type=TradingStrategy.MEAN_REVERSION,
|
||||
description="Counter-trend strategy exploiting oversold/overbought conditions",
|
||||
timeframes=["15m", "1h", "4h"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.7,
|
||||
overlay_indicators=[
|
||||
"sma_20", # Mean reversion line
|
||||
"sma_50", # Trend context
|
||||
"bb_20_20", # Standard Bollinger Bands
|
||||
"bb_20_15" # Tighter bands for entry signals
|
||||
],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.2,
|
||||
indicators=["rsi_14", "rsi_21"], # Standard and slower RSI
|
||||
title="RSI Mean Reversion",
|
||||
y_axis_label="RSI",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.VOLUME,
|
||||
height_ratio=0.1,
|
||||
indicators=[],
|
||||
title="Volume Analysis",
|
||||
y_axis_label="Volume",
|
||||
show_grid=True
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=12,
|
||||
candlestick_up_color="#4caf50",
|
||||
candlestick_down_color="#f44336",
|
||||
show_volume=True,
|
||||
show_grid=True
|
||||
),
|
||||
tags=["mean-reversion", "counter-trend", "oversold-overbought"]
|
||||
)
|
||||
|
||||
return StrategyExample(
|
||||
config=config,
|
||||
description="""
|
||||
Mean Reversion Strategy exploits the tendency of prices to return to their
|
||||
average after extreme moves. Uses multiple RSI periods and Bollinger Bands
|
||||
to identify oversold/overbought conditions with high probability reversals.
|
||||
|
||||
Entry Rules (Long):
|
||||
- Price touches or breaks lower Bollinger Band (20,2.0)
|
||||
- RSI-14 < 30 and RSI-21 < 35 (oversold conditions)
|
||||
- Price shows bullish divergence with RSI
|
||||
- Volume spike on the reversal candle
|
||||
- Entry on first green candle after oversold signal
|
||||
|
||||
Entry Rules (Short):
|
||||
- Price touches or breaks upper Bollinger Band
|
||||
- RSI-14 > 70 and RSI-21 > 65 (overbought conditions)
|
||||
- Bearish divergence with RSI
|
||||
- High volume on reversal
|
||||
|
||||
Exit Rules:
|
||||
- Price returns to SMA-20 (mean)
|
||||
- RSI reaches neutral zone (45-55)
|
||||
- Stop loss: Beyond recent swing high/low
|
||||
- Take profit: Opposite Bollinger Band
|
||||
""",
|
||||
author="TCPDashboard Team",
|
||||
difficulty="Intermediate",
|
||||
expected_return="10-18% monthly (ranging markets)",
|
||||
risk_level="Medium",
|
||||
market_conditions=["Sideways", "Ranging", "Oversold/Overbought"],
|
||||
notes=[
|
||||
"Works best in ranging/sideways markets",
|
||||
"Avoid during strong trending periods",
|
||||
"Look for divergences for higher probability setups",
|
||||
"Use proper position sizing due to counter-trend nature",
|
||||
"Consider market structure and support/resistance levels",
|
||||
"Best during regular market hours for better volume"
|
||||
],
|
||||
references=[
|
||||
"Mean Reversion Trading Systems - Howard Bandy",
|
||||
"Contrarian Investment Strategies - David Dreman"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_scalping_strategy() -> StrategyExample:
|
||||
"""
|
||||
Create scalping strategy configuration.
|
||||
|
||||
High-frequency strategy for quick profits using
|
||||
very fast indicators and tight risk management.
|
||||
"""
|
||||
config = StrategyChartConfig(
|
||||
strategy_name="Scalping Strategy",
|
||||
strategy_type=TradingStrategy.SCALPING,
|
||||
description="High-frequency scalping strategy for quick profits",
|
||||
timeframes=["1m", "5m"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.55,
|
||||
overlay_indicators=[
|
||||
"ema_5", # Very fast EMA
|
||||
"ema_12", # Fast EMA
|
||||
"ema_21", # Reference EMA
|
||||
"bb_10_15" # Tight Bollinger Bands
|
||||
],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.2,
|
||||
indicators=["rsi_7"], # Very fast RSI
|
||||
title="RSI Fast (7)",
|
||||
y_axis_label="RSI",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.15,
|
||||
indicators=["macd_5_13_4"], # Very fast MACD
|
||||
title="MACD Ultra Fast",
|
||||
y_axis_label="MACD",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.VOLUME,
|
||||
height_ratio=0.1,
|
||||
indicators=[],
|
||||
title="Volume",
|
||||
y_axis_label="Volume",
|
||||
show_grid=True
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=10,
|
||||
candlestick_up_color="#00e676",
|
||||
candlestick_down_color="#ff1744",
|
||||
show_volume=True,
|
||||
show_grid=True
|
||||
),
|
||||
tags=["scalping", "high-frequency", "fast", "1-minute"]
|
||||
)
|
||||
|
||||
return StrategyExample(
|
||||
config=config,
|
||||
description="""
|
||||
Scalping Strategy designed for rapid-fire trading with small profits
|
||||
and very tight risk management. Uses ultra-fast indicators to capture
|
||||
small price movements multiple times per session.
|
||||
|
||||
Entry Rules:
|
||||
- EMA-5 crosses above EMA-12 (bullish scalp)
|
||||
- Price is above EMA-21 for trend alignment
|
||||
- RSI-7 between 40-60 (avoid extremes)
|
||||
- MACD line above signal line
|
||||
- Strong volume confirmation
|
||||
- Enter on pullback to EMA-5 after crossover
|
||||
|
||||
Exit Rules:
|
||||
- Target: 5-10 pips profit (0.1-0.2% for stocks)
|
||||
- Stop loss: 3-5 pips (0.05-0.1%)
|
||||
- Exit if RSI reaches extreme (>75 or <25)
|
||||
- Exit if EMA-5 crosses back below EMA-12
|
||||
- Maximum hold time: 5-15 minutes
|
||||
""",
|
||||
author="TCPDashboard Team",
|
||||
difficulty="Advanced",
|
||||
expected_return="Small profits, high frequency (2-5% daily)",
|
||||
risk_level="High",
|
||||
market_conditions=["High Liquidity", "Volatile", "Active Sessions"],
|
||||
notes=[
|
||||
"Requires very fast execution and low latency",
|
||||
"Best during active market hours (overlapping sessions)",
|
||||
"Use tight spreads and low commission brokers",
|
||||
"Requires constant monitoring and quick decisions",
|
||||
"Risk management is critical - small stops",
|
||||
"Not suitable for beginners",
|
||||
"Consider transaction costs carefully",
|
||||
"Practice on demo account extensively first"
|
||||
],
|
||||
references=[
|
||||
"A Complete Guide to Volume Price Analysis - Anna Coulling",
|
||||
"The Complete Guide to Day Trading - Markus Heitkoetter"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_swing_trading_strategy() -> StrategyExample:
|
||||
"""
|
||||
Create swing trading strategy configuration.
|
||||
|
||||
Medium-term strategy capturing price swings over
|
||||
several days to weeks using trend and momentum indicators.
|
||||
"""
|
||||
config = StrategyChartConfig(
|
||||
strategy_name="Swing Trading Strategy",
|
||||
strategy_type=TradingStrategy.SWING_TRADING,
|
||||
description="Medium-term strategy capturing multi-day price swings",
|
||||
timeframes=["4h", "1d"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.7,
|
||||
overlay_indicators=[
|
||||
"sma_20", # Short-term trend
|
||||
"sma_50", # Medium-term trend
|
||||
"ema_21", # Dynamic support/resistance
|
||||
"bb_20_20" # Volatility bands
|
||||
],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.15,
|
||||
indicators=["rsi_14"],
|
||||
title="RSI (14)",
|
||||
y_axis_label="RSI",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.15,
|
||||
indicators=["macd_12_26_9"],
|
||||
title="MACD Standard",
|
||||
y_axis_label="MACD",
|
||||
show_grid=True
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=13,
|
||||
candlestick_up_color="#388e3c",
|
||||
candlestick_down_color="#d32f2f",
|
||||
show_volume=True,
|
||||
show_grid=True
|
||||
),
|
||||
tags=["swing-trading", "medium-term", "trend-following"]
|
||||
)
|
||||
|
||||
return StrategyExample(
|
||||
config=config,
|
||||
description="""
|
||||
Swing Trading Strategy captures price swings over several days to weeks
|
||||
by identifying trend changes and momentum shifts. Suitable for traders
|
||||
who cannot monitor markets constantly but want to catch significant moves.
|
||||
|
||||
Entry Rules (Long):
|
||||
- Price above SMA-50 (uptrend confirmation)
|
||||
- SMA-20 crosses above SMA-50 or price bounces off SMA-20
|
||||
- RSI pullback to 35-45 then rises above 50
|
||||
- MACD line crosses above signal line
|
||||
- Price finds support at EMA-21 or lower Bollinger Band
|
||||
|
||||
Entry Rules (Short):
|
||||
- Price below SMA-50 (downtrend confirmation)
|
||||
- SMA-20 crosses below SMA-50 or price rejected at SMA-20
|
||||
- RSI pullback to 55-65 then falls below 50
|
||||
- MACD line crosses below signal line
|
||||
|
||||
Exit Rules:
|
||||
- Price reaches opposite Bollinger Band
|
||||
- RSI reaches overbought/oversold extremes (>70/<30)
|
||||
- MACD shows divergence or histogram weakens
|
||||
- Stop loss: Below/above recent swing point (2-4%)
|
||||
- Take profit: 1:2 or 1:3 risk-reward ratio
|
||||
""",
|
||||
author="TCPDashboard Team",
|
||||
difficulty="Beginner",
|
||||
expected_return="15-25% annually",
|
||||
risk_level="Medium",
|
||||
market_conditions=["Trending", "Swing Markets"],
|
||||
notes=[
|
||||
"Suitable for part-time traders",
|
||||
"Requires patience for proper setups",
|
||||
"Good for building trading discipline",
|
||||
"Consider fundamental analysis for direction",
|
||||
"Use proper position sizing (1-2% risk per trade)",
|
||||
"Best on daily timeframes for trend following",
|
||||
"Monitor weekly charts for overall direction"
|
||||
],
|
||||
references=[
|
||||
"Swing Trading For Beginners - Matthew Maybury",
|
||||
"The Master Swing Trader - Alan Farley"
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def get_all_example_strategies() -> Dict[str, StrategyExample]:
|
||||
"""
|
||||
Get all available example strategies.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping strategy names to StrategyExample objects
|
||||
"""
|
||||
return {
|
||||
"ema_crossover": create_ema_crossover_strategy(),
|
||||
"momentum_breakout": create_momentum_breakout_strategy(),
|
||||
"mean_reversion": create_mean_reversion_strategy(),
|
||||
"scalping": create_scalping_strategy(),
|
||||
"swing_trading": create_swing_trading_strategy()
|
||||
}
|
||||
|
||||
|
||||
def get_example_strategy(strategy_name: str) -> Optional[StrategyExample]:
|
||||
"""
|
||||
Get a specific example strategy by name.
|
||||
|
||||
Args:
|
||||
strategy_name: Name of the strategy to retrieve
|
||||
|
||||
Returns:
|
||||
StrategyExample object or None if not found
|
||||
"""
|
||||
strategies = get_all_example_strategies()
|
||||
return strategies.get(strategy_name)
|
||||
|
||||
|
||||
def get_strategies_by_difficulty(difficulty: str) -> List[StrategyExample]:
|
||||
"""
|
||||
Get example strategies filtered by difficulty level.
|
||||
|
||||
Args:
|
||||
difficulty: Difficulty level ("Beginner", "Intermediate", "Advanced")
|
||||
|
||||
Returns:
|
||||
List of StrategyExample objects matching the difficulty
|
||||
"""
|
||||
strategies = get_all_example_strategies()
|
||||
return [strategy for strategy in strategies.values()
|
||||
if strategy.difficulty == difficulty]
|
||||
|
||||
|
||||
def get_strategies_by_risk_level(risk_level: str) -> List[StrategyExample]:
|
||||
"""
|
||||
Get example strategies filtered by risk level.
|
||||
|
||||
Args:
|
||||
risk_level: Risk level ("Low", "Medium", "High")
|
||||
|
||||
Returns:
|
||||
List of StrategyExample objects matching the risk level
|
||||
"""
|
||||
strategies = get_all_example_strategies()
|
||||
return [strategy for strategy in strategies.values()
|
||||
if strategy.risk_level == risk_level]
|
||||
|
||||
|
||||
def get_strategies_by_market_condition(condition: str) -> List[StrategyExample]:
|
||||
"""
|
||||
Get example strategies suitable for specific market conditions.
|
||||
|
||||
Args:
|
||||
condition: Market condition ("Trending", "Sideways", "Volatile", etc.)
|
||||
|
||||
Returns:
|
||||
List of StrategyExample objects suitable for the condition
|
||||
"""
|
||||
strategies = get_all_example_strategies()
|
||||
return [strategy for strategy in strategies.values()
|
||||
if condition in strategy.market_conditions]
|
||||
|
||||
|
||||
def get_strategy_summary() -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
Get a summary of all example strategies with key information.
|
||||
|
||||
Returns:
|
||||
Dictionary with strategy summaries
|
||||
"""
|
||||
strategies = get_all_example_strategies()
|
||||
summary = {}
|
||||
|
||||
for name, strategy in strategies.items():
|
||||
summary[name] = {
|
||||
"name": strategy.config.strategy_name,
|
||||
"type": strategy.config.strategy_type.value,
|
||||
"difficulty": strategy.difficulty,
|
||||
"risk_level": strategy.risk_level,
|
||||
"timeframes": ", ".join(strategy.config.timeframes),
|
||||
"market_conditions": ", ".join(strategy.market_conditions),
|
||||
"expected_return": strategy.expected_return or "N/A"
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
def export_example_strategies_to_json() -> str:
|
||||
"""
|
||||
Export all example strategies to JSON format.
|
||||
|
||||
Returns:
|
||||
JSON string containing all example strategies
|
||||
"""
|
||||
import json
|
||||
from .strategy_charts import export_strategy_config_to_json
|
||||
|
||||
strategies = get_all_example_strategies()
|
||||
export_data = {}
|
||||
|
||||
for name, strategy in strategies.items():
|
||||
export_data[name] = {
|
||||
"config": json.loads(export_strategy_config_to_json(strategy.config)),
|
||||
"metadata": {
|
||||
"description": strategy.description,
|
||||
"author": strategy.author,
|
||||
"difficulty": strategy.difficulty,
|
||||
"expected_return": strategy.expected_return,
|
||||
"risk_level": strategy.risk_level,
|
||||
"market_conditions": strategy.market_conditions,
|
||||
"notes": strategy.notes,
|
||||
"references": strategy.references
|
||||
}
|
||||
}
|
||||
|
||||
return json.dumps(export_data, indent=2)
|
||||
@@ -5,10 +5,12 @@ This module defines indicator configurations and provides integration
|
||||
with the existing data/common/indicators.py technical indicators module.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Any, Optional, Union, Literal
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
from data.common.indicators import TechnicalIndicators, IndicatorResult, create_default_indicators_config, validate_indicator_config
|
||||
from data.common.data_types import OHLCVCandle
|
||||
@@ -18,6 +20,278 @@ from utils.logger import get_logger
|
||||
logger = get_logger("indicator_defs")
|
||||
|
||||
|
||||
class IndicatorType(str, Enum):
|
||||
"""Supported indicator types."""
|
||||
SMA = "sma"
|
||||
EMA = "ema"
|
||||
RSI = "rsi"
|
||||
MACD = "macd"
|
||||
BOLLINGER_BANDS = "bollinger_bands"
|
||||
|
||||
|
||||
class DisplayType(str, Enum):
|
||||
"""Chart display types for indicators."""
|
||||
OVERLAY = "overlay"
|
||||
SUBPLOT = "subplot"
|
||||
|
||||
|
||||
class LineStyle(str, Enum):
|
||||
"""Available line styles for chart display."""
|
||||
SOLID = "solid"
|
||||
DASH = "dash"
|
||||
DOT = "dot"
|
||||
DASHDOT = "dashdot"
|
||||
|
||||
|
||||
class PriceColumn(str, Enum):
|
||||
"""Available price columns for calculations."""
|
||||
OPEN = "open"
|
||||
HIGH = "high"
|
||||
LOW = "low"
|
||||
CLOSE = "close"
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndicatorParameterSchema:
|
||||
"""
|
||||
Schema definition for an indicator parameter.
|
||||
"""
|
||||
name: str
|
||||
type: type
|
||||
required: bool = True
|
||||
default: Any = None
|
||||
min_value: Optional[Union[int, float]] = None
|
||||
max_value: Optional[Union[int, float]] = None
|
||||
description: str = ""
|
||||
|
||||
def validate(self, value: Any) -> tuple[bool, str]:
|
||||
"""
|
||||
Validate a parameter value against this schema.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
if value is None:
|
||||
if self.required:
|
||||
return False, f"Parameter '{self.name}' is required"
|
||||
return True, ""
|
||||
|
||||
# Type validation
|
||||
if not isinstance(value, self.type):
|
||||
return False, f"Parameter '{self.name}' must be of type {self.type.__name__}, got {type(value).__name__}"
|
||||
|
||||
# Range validation for numeric types
|
||||
if isinstance(value, (int, float)):
|
||||
if self.min_value is not None and value < self.min_value:
|
||||
return False, f"Parameter '{self.name}' must be >= {self.min_value}, got {value}"
|
||||
if self.max_value is not None and value > self.max_value:
|
||||
return False, f"Parameter '{self.name}' must be <= {self.max_value}, got {value}"
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndicatorSchema:
|
||||
"""
|
||||
Complete schema definition for an indicator type.
|
||||
"""
|
||||
indicator_type: IndicatorType
|
||||
display_type: DisplayType
|
||||
required_parameters: List[IndicatorParameterSchema]
|
||||
optional_parameters: List[IndicatorParameterSchema] = field(default_factory=list)
|
||||
min_data_points: int = 1
|
||||
description: str = ""
|
||||
|
||||
def get_parameter_schema(self, param_name: str) -> Optional[IndicatorParameterSchema]:
|
||||
"""Get schema for a specific parameter."""
|
||||
for param in self.required_parameters + self.optional_parameters:
|
||||
if param.name == param_name:
|
||||
return param
|
||||
return None
|
||||
|
||||
def validate_parameters(self, parameters: Dict[str, Any]) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate all parameters against this schema.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_error_messages)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Check required parameters
|
||||
for param_schema in self.required_parameters:
|
||||
value = parameters.get(param_schema.name)
|
||||
is_valid, error = param_schema.validate(value)
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Check optional parameters if provided
|
||||
for param_schema in self.optional_parameters:
|
||||
if param_schema.name in parameters:
|
||||
value = parameters[param_schema.name]
|
||||
is_valid, error = param_schema.validate(value)
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Check for unknown parameters
|
||||
known_params = {p.name for p in self.required_parameters + self.optional_parameters}
|
||||
for param_name in parameters:
|
||||
if param_name not in known_params:
|
||||
errors.append(f"Unknown parameter '{param_name}' for {self.indicator_type.value} indicator")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
# Define schema for each indicator type
|
||||
INDICATOR_SCHEMAS = {
|
||||
IndicatorType.SMA: IndicatorSchema(
|
||||
indicator_type=IndicatorType.SMA,
|
||||
display_type=DisplayType.OVERLAY,
|
||||
required_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="period",
|
||||
type=int,
|
||||
min_value=1,
|
||||
max_value=200,
|
||||
description="Number of periods for moving average"
|
||||
)
|
||||
],
|
||||
optional_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="price_column",
|
||||
type=str,
|
||||
required=False,
|
||||
default="close",
|
||||
description="Price column to use (open, high, low, close)"
|
||||
)
|
||||
],
|
||||
min_data_points=1,
|
||||
description="Simple Moving Average - arithmetic mean of closing prices over a specified period"
|
||||
),
|
||||
|
||||
IndicatorType.EMA: IndicatorSchema(
|
||||
indicator_type=IndicatorType.EMA,
|
||||
display_type=DisplayType.OVERLAY,
|
||||
required_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="period",
|
||||
type=int,
|
||||
min_value=1,
|
||||
max_value=200,
|
||||
description="Number of periods for exponential moving average"
|
||||
)
|
||||
],
|
||||
optional_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="price_column",
|
||||
type=str,
|
||||
required=False,
|
||||
default="close",
|
||||
description="Price column to use (open, high, low, close)"
|
||||
)
|
||||
],
|
||||
min_data_points=1,
|
||||
description="Exponential Moving Average - gives more weight to recent prices"
|
||||
),
|
||||
|
||||
IndicatorType.RSI: IndicatorSchema(
|
||||
indicator_type=IndicatorType.RSI,
|
||||
display_type=DisplayType.SUBPLOT,
|
||||
required_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="period",
|
||||
type=int,
|
||||
min_value=2,
|
||||
max_value=100,
|
||||
description="Number of periods for RSI calculation"
|
||||
)
|
||||
],
|
||||
optional_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="price_column",
|
||||
type=str,
|
||||
required=False,
|
||||
default="close",
|
||||
description="Price column to use (open, high, low, close)"
|
||||
)
|
||||
],
|
||||
min_data_points=2,
|
||||
description="Relative Strength Index - momentum oscillator measuring speed and magnitude of price changes"
|
||||
),
|
||||
|
||||
IndicatorType.MACD: IndicatorSchema(
|
||||
indicator_type=IndicatorType.MACD,
|
||||
display_type=DisplayType.SUBPLOT,
|
||||
required_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="fast_period",
|
||||
type=int,
|
||||
min_value=1,
|
||||
max_value=50,
|
||||
description="Fast EMA period"
|
||||
),
|
||||
IndicatorParameterSchema(
|
||||
name="slow_period",
|
||||
type=int,
|
||||
min_value=1,
|
||||
max_value=100,
|
||||
description="Slow EMA period"
|
||||
),
|
||||
IndicatorParameterSchema(
|
||||
name="signal_period",
|
||||
type=int,
|
||||
min_value=1,
|
||||
max_value=50,
|
||||
description="Signal line EMA period"
|
||||
)
|
||||
],
|
||||
optional_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="price_column",
|
||||
type=str,
|
||||
required=False,
|
||||
default="close",
|
||||
description="Price column to use (open, high, low, close)"
|
||||
)
|
||||
],
|
||||
min_data_points=3,
|
||||
description="Moving Average Convergence Divergence - trend-following momentum indicator"
|
||||
),
|
||||
|
||||
IndicatorType.BOLLINGER_BANDS: IndicatorSchema(
|
||||
indicator_type=IndicatorType.BOLLINGER_BANDS,
|
||||
display_type=DisplayType.OVERLAY,
|
||||
required_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="period",
|
||||
type=int,
|
||||
min_value=2,
|
||||
max_value=100,
|
||||
description="Number of periods for moving average"
|
||||
),
|
||||
IndicatorParameterSchema(
|
||||
name="std_dev",
|
||||
type=float,
|
||||
min_value=0.1,
|
||||
max_value=5.0,
|
||||
description="Number of standard deviations for bands"
|
||||
)
|
||||
],
|
||||
optional_parameters=[
|
||||
IndicatorParameterSchema(
|
||||
name="price_column",
|
||||
type=str,
|
||||
required=False,
|
||||
default="close",
|
||||
description="Price column to use (open, high, low, close)"
|
||||
)
|
||||
],
|
||||
min_data_points=2,
|
||||
description="Bollinger Bands - volatility bands placed above and below a moving average"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChartIndicatorConfig:
|
||||
"""
|
||||
@@ -42,6 +316,50 @@ class ChartIndicatorConfig:
|
||||
config = {'type': self.indicator_type}
|
||||
config.update(self.parameters)
|
||||
return config
|
||||
|
||||
def validate(self) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate this indicator configuration against its schema.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_error_messages)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Check if indicator type is supported
|
||||
try:
|
||||
indicator_type = IndicatorType(self.indicator_type)
|
||||
except ValueError:
|
||||
return False, [f"Unsupported indicator type: {self.indicator_type}"]
|
||||
|
||||
# Get schema for this indicator type
|
||||
schema = INDICATOR_SCHEMAS.get(indicator_type)
|
||||
if not schema:
|
||||
return False, [f"No schema found for indicator type: {self.indicator_type}"]
|
||||
|
||||
# Validate parameters against schema
|
||||
is_valid, param_errors = schema.validate_parameters(self.parameters)
|
||||
if not is_valid:
|
||||
errors.extend(param_errors)
|
||||
|
||||
# Validate display properties
|
||||
if self.display_type not in [DisplayType.OVERLAY.value, DisplayType.SUBPLOT.value]:
|
||||
errors.append(f"Invalid display_type: {self.display_type}")
|
||||
|
||||
if self.line_style not in [style.value for style in LineStyle]:
|
||||
errors.append(f"Invalid line_style: {self.line_style}")
|
||||
|
||||
if not isinstance(self.line_width, int) or self.line_width < 1:
|
||||
errors.append("line_width must be a positive integer")
|
||||
|
||||
if not isinstance(self.opacity, (int, float)) or not (0.0 <= self.opacity <= 1.0):
|
||||
errors.append("opacity must be a number between 0.0 and 1.0")
|
||||
|
||||
if self.display_type == DisplayType.SUBPLOT.value:
|
||||
if not isinstance(self.subplot_height_ratio, (int, float)) or not (0.1 <= self.subplot_height_ratio <= 1.0):
|
||||
errors.append("subplot_height_ratio must be a number between 0.1 and 1.0")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
|
||||
# Built-in indicator definitions with chart display properties
|
||||
@@ -263,4 +581,206 @@ def get_default_indicator_params(indicator_type: str) -> Dict[str, Any]:
|
||||
'bollinger_bands': {'period': 20, 'std_dev': 2.0, 'price_column': 'close'}
|
||||
}
|
||||
|
||||
return defaults.get(indicator_type, {})
|
||||
return defaults.get(indicator_type, {})
|
||||
|
||||
|
||||
def validate_indicator_configuration(config: ChartIndicatorConfig) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate an indicator configuration against its schema.
|
||||
|
||||
Args:
|
||||
config: Chart indicator configuration to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_error_messages)
|
||||
"""
|
||||
return config.validate()
|
||||
|
||||
|
||||
def create_indicator_config(
|
||||
name: str,
|
||||
indicator_type: str,
|
||||
parameters: Dict[str, Any],
|
||||
display_type: Optional[str] = None,
|
||||
color: str = "#007bff",
|
||||
**display_options
|
||||
) -> tuple[Optional[ChartIndicatorConfig], List[str]]:
|
||||
"""
|
||||
Create and validate a new indicator configuration.
|
||||
|
||||
Args:
|
||||
name: Display name for the indicator
|
||||
indicator_type: Type of indicator (sma, ema, rsi, etc.)
|
||||
parameters: Indicator parameters
|
||||
display_type: Optional override for display type
|
||||
color: Color for chart display
|
||||
**display_options: Additional display configuration options
|
||||
|
||||
Returns:
|
||||
Tuple of (config_object_or_None, list_of_error_messages)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Validate indicator type
|
||||
try:
|
||||
indicator_enum = IndicatorType(indicator_type)
|
||||
except ValueError:
|
||||
return None, [f"Unsupported indicator type: {indicator_type}"]
|
||||
|
||||
# Get schema for validation
|
||||
schema = INDICATOR_SCHEMAS.get(indicator_enum)
|
||||
if not schema:
|
||||
return None, [f"No schema found for indicator type: {indicator_type}"]
|
||||
|
||||
# Use schema display type if not overridden
|
||||
if display_type is None:
|
||||
display_type = schema.display_type.value
|
||||
|
||||
# Fill in default parameters
|
||||
final_parameters = {}
|
||||
|
||||
# Add required parameters with defaults if missing
|
||||
for param_schema in schema.required_parameters:
|
||||
if param_schema.name in parameters:
|
||||
final_parameters[param_schema.name] = parameters[param_schema.name]
|
||||
elif param_schema.default is not None:
|
||||
final_parameters[param_schema.name] = param_schema.default
|
||||
# Required parameters without defaults will be caught by validation
|
||||
|
||||
# Add optional parameters
|
||||
for param_schema in schema.optional_parameters:
|
||||
if param_schema.name in parameters:
|
||||
final_parameters[param_schema.name] = parameters[param_schema.name]
|
||||
elif param_schema.default is not None:
|
||||
final_parameters[param_schema.name] = param_schema.default
|
||||
|
||||
# Create configuration
|
||||
config = ChartIndicatorConfig(
|
||||
name=name,
|
||||
indicator_type=indicator_type,
|
||||
parameters=final_parameters,
|
||||
display_type=display_type,
|
||||
color=color,
|
||||
line_style=display_options.get('line_style', 'solid'),
|
||||
line_width=display_options.get('line_width', 2),
|
||||
opacity=display_options.get('opacity', 1.0),
|
||||
visible=display_options.get('visible', True),
|
||||
subplot_height_ratio=display_options.get('subplot_height_ratio', 0.3)
|
||||
)
|
||||
|
||||
# Validate the configuration
|
||||
is_valid, validation_errors = config.validate()
|
||||
if not is_valid:
|
||||
return None, validation_errors
|
||||
|
||||
return config, []
|
||||
|
||||
|
||||
def get_indicator_schema(indicator_type: str) -> Optional[IndicatorSchema]:
|
||||
"""
|
||||
Get the schema for an indicator type.
|
||||
|
||||
Args:
|
||||
indicator_type: Type of indicator
|
||||
|
||||
Returns:
|
||||
IndicatorSchema object or None if not found
|
||||
"""
|
||||
try:
|
||||
indicator_enum = IndicatorType(indicator_type)
|
||||
return INDICATOR_SCHEMAS.get(indicator_enum)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def get_available_indicator_types() -> List[str]:
|
||||
"""
|
||||
Get list of available indicator types.
|
||||
|
||||
Returns:
|
||||
List of supported indicator type strings
|
||||
"""
|
||||
return [indicator_type.value for indicator_type in IndicatorType]
|
||||
|
||||
|
||||
def get_indicator_parameter_info(indicator_type: str) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Get detailed parameter information for an indicator type.
|
||||
|
||||
Args:
|
||||
indicator_type: Type of indicator
|
||||
|
||||
Returns:
|
||||
Dictionary with parameter information including types, ranges, and descriptions
|
||||
"""
|
||||
schema = get_indicator_schema(indicator_type)
|
||||
if not schema:
|
||||
return {}
|
||||
|
||||
param_info = {}
|
||||
|
||||
for param in schema.required_parameters + schema.optional_parameters:
|
||||
param_info[param.name] = {
|
||||
'type': param.type.__name__,
|
||||
'required': param.required,
|
||||
'default': param.default,
|
||||
'min_value': param.min_value,
|
||||
'max_value': param.max_value,
|
||||
'description': param.description
|
||||
}
|
||||
|
||||
return param_info
|
||||
|
||||
|
||||
def validate_parameters_for_type(indicator_type: str, parameters: Dict[str, Any]) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate parameters for a specific indicator type.
|
||||
|
||||
Args:
|
||||
indicator_type: Type of indicator
|
||||
parameters: Parameters to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_error_messages)
|
||||
"""
|
||||
schema = get_indicator_schema(indicator_type)
|
||||
if not schema:
|
||||
return False, [f"Unknown indicator type: {indicator_type}"]
|
||||
|
||||
return schema.validate_parameters(parameters)
|
||||
|
||||
|
||||
def create_configuration_from_json(json_data: Union[str, Dict[str, Any]]) -> tuple[Optional[ChartIndicatorConfig], List[str]]:
|
||||
"""
|
||||
Create indicator configuration from JSON data.
|
||||
|
||||
Args:
|
||||
json_data: JSON string or dictionary with configuration data
|
||||
|
||||
Returns:
|
||||
Tuple of (config_object_or_None, list_of_error_messages)
|
||||
"""
|
||||
try:
|
||||
if isinstance(json_data, str):
|
||||
data = json.loads(json_data)
|
||||
else:
|
||||
data = json_data
|
||||
|
||||
required_fields = ['name', 'indicator_type', 'parameters']
|
||||
missing_fields = [field for field in required_fields if field not in data]
|
||||
if missing_fields:
|
||||
return None, [f"Missing required fields: {', '.join(missing_fields)}"]
|
||||
|
||||
return create_indicator_config(
|
||||
name=data['name'],
|
||||
indicator_type=data['indicator_type'],
|
||||
parameters=data['parameters'],
|
||||
display_type=data.get('display_type'),
|
||||
color=data.get('color', '#007bff'),
|
||||
**{k: v for k, v in data.items() if k not in ['name', 'indicator_type', 'parameters', 'display_type', 'color']}
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return None, [f"Invalid JSON: {e}"]
|
||||
except Exception as e:
|
||||
return None, [f"Error creating configuration: {e}"]
|
||||
640
components/charts/config/strategy_charts.py
Normal file
640
components/charts/config/strategy_charts.py
Normal file
@@ -0,0 +1,640 @@
|
||||
"""
|
||||
Strategy-Specific Chart Configuration System
|
||||
|
||||
This module provides complete chart configurations for different trading strategies,
|
||||
including indicator combinations, chart layouts, subplot arrangements, and display settings.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from .indicator_defs import ChartIndicatorConfig, create_indicator_config, validate_indicator_configuration
|
||||
from .defaults import (
|
||||
TradingStrategy,
|
||||
IndicatorCategory,
|
||||
get_all_default_indicators,
|
||||
get_strategy_indicators,
|
||||
get_strategy_info
|
||||
)
|
||||
from utils.logger import get_logger
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger("strategy_charts")
|
||||
|
||||
|
||||
class ChartLayout(str, Enum):
|
||||
"""Chart layout types."""
|
||||
SINGLE_CHART = "single_chart"
|
||||
MAIN_WITH_SUBPLOTS = "main_with_subplots"
|
||||
MULTI_CHART = "multi_chart"
|
||||
GRID_LAYOUT = "grid_layout"
|
||||
|
||||
|
||||
class SubplotType(str, Enum):
|
||||
"""Types of subplots available."""
|
||||
VOLUME = "volume"
|
||||
RSI = "rsi"
|
||||
MACD = "macd"
|
||||
MOMENTUM = "momentum"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubplotConfig:
|
||||
"""Configuration for a chart subplot."""
|
||||
subplot_type: SubplotType
|
||||
height_ratio: float = 0.3
|
||||
indicators: List[str] = field(default_factory=list)
|
||||
title: Optional[str] = None
|
||||
y_axis_label: Optional[str] = None
|
||||
show_grid: bool = True
|
||||
show_legend: bool = True
|
||||
background_color: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChartStyle:
|
||||
"""Chart styling configuration."""
|
||||
theme: str = "plotly_white"
|
||||
background_color: str = "#ffffff"
|
||||
grid_color: str = "#e6e6e6"
|
||||
text_color: str = "#2c3e50"
|
||||
font_family: str = "Arial, sans-serif"
|
||||
font_size: int = 12
|
||||
candlestick_up_color: str = "#26a69a"
|
||||
candlestick_down_color: str = "#ef5350"
|
||||
volume_color: str = "#78909c"
|
||||
show_volume: bool = True
|
||||
show_grid: bool = True
|
||||
show_legend: bool = True
|
||||
show_toolbar: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class StrategyChartConfig:
|
||||
"""Complete chart configuration for a trading strategy."""
|
||||
strategy_name: str
|
||||
strategy_type: TradingStrategy
|
||||
description: str
|
||||
timeframes: List[str]
|
||||
|
||||
# Chart layout
|
||||
layout: ChartLayout = ChartLayout.MAIN_WITH_SUBPLOTS
|
||||
main_chart_height: float = 0.7
|
||||
|
||||
# Indicators
|
||||
overlay_indicators: List[str] = field(default_factory=list)
|
||||
subplot_configs: List[SubplotConfig] = field(default_factory=list)
|
||||
|
||||
# Style
|
||||
chart_style: ChartStyle = field(default_factory=ChartStyle)
|
||||
|
||||
# Metadata
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
version: str = "1.0"
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
def validate(self) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate the strategy chart configuration.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_error_messages)
|
||||
"""
|
||||
# Use the new comprehensive validation system
|
||||
from .validation import validate_configuration
|
||||
|
||||
try:
|
||||
report = validate_configuration(self)
|
||||
|
||||
# Convert validation report to simple format for backward compatibility
|
||||
error_messages = [str(issue) for issue in report.errors]
|
||||
return report.is_valid, error_messages
|
||||
|
||||
except ImportError:
|
||||
# Fallback to original validation if new system unavailable
|
||||
logger.warning("Enhanced validation system unavailable, using basic validation")
|
||||
return self._basic_validate()
|
||||
except Exception as e:
|
||||
logger.error(f"Validation error: {e}")
|
||||
return False, [f"Validation system error: {e}"]
|
||||
|
||||
def validate_comprehensive(self) -> 'ValidationReport':
|
||||
"""
|
||||
Perform comprehensive validation with detailed reporting.
|
||||
|
||||
Returns:
|
||||
Detailed validation report with errors, warnings, and suggestions
|
||||
"""
|
||||
from .validation import validate_configuration
|
||||
return validate_configuration(self)
|
||||
|
||||
def _basic_validate(self) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Basic validation method (fallback).
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_error_messages)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Validate basic fields
|
||||
if not self.strategy_name:
|
||||
errors.append("Strategy name is required")
|
||||
|
||||
if not isinstance(self.strategy_type, TradingStrategy):
|
||||
errors.append("Invalid strategy type")
|
||||
|
||||
if not self.timeframes:
|
||||
errors.append("At least one timeframe must be specified")
|
||||
|
||||
# Validate height ratios
|
||||
total_subplot_height = sum(config.height_ratio for config in self.subplot_configs)
|
||||
if self.main_chart_height + total_subplot_height > 1.0:
|
||||
errors.append("Total chart height ratios exceed 1.0")
|
||||
|
||||
if self.main_chart_height <= 0 or self.main_chart_height > 1.0:
|
||||
errors.append("Main chart height must be between 0 and 1.0")
|
||||
|
||||
# Validate indicators exist
|
||||
try:
|
||||
all_default_indicators = get_all_default_indicators()
|
||||
|
||||
for indicator_name in self.overlay_indicators:
|
||||
if indicator_name not in all_default_indicators:
|
||||
errors.append(f"Overlay indicator '{indicator_name}' not found in defaults")
|
||||
|
||||
for subplot_config in self.subplot_configs:
|
||||
for indicator_name in subplot_config.indicators:
|
||||
if indicator_name not in all_default_indicators:
|
||||
errors.append(f"Subplot indicator '{indicator_name}' not found in defaults")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not validate indicator existence: {e}")
|
||||
|
||||
# Validate subplot height ratios
|
||||
for i, subplot_config in enumerate(self.subplot_configs):
|
||||
if subplot_config.height_ratio <= 0 or subplot_config.height_ratio > 1.0:
|
||||
errors.append(f"Subplot {i} height ratio must be between 0 and 1.0")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def get_all_indicators(self) -> List[str]:
|
||||
"""Get all indicators used in this strategy configuration."""
|
||||
all_indicators = list(self.overlay_indicators)
|
||||
for subplot_config in self.subplot_configs:
|
||||
all_indicators.extend(subplot_config.indicators)
|
||||
return list(set(all_indicators))
|
||||
|
||||
def get_indicator_configs(self) -> Dict[str, ChartIndicatorConfig]:
|
||||
"""
|
||||
Get the actual indicator configuration objects for all indicators.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping indicator names to their configurations
|
||||
"""
|
||||
all_default_indicators = get_all_default_indicators()
|
||||
indicator_configs = {}
|
||||
|
||||
for indicator_name in self.get_all_indicators():
|
||||
if indicator_name in all_default_indicators:
|
||||
preset = all_default_indicators[indicator_name]
|
||||
indicator_configs[indicator_name] = preset.config
|
||||
|
||||
return indicator_configs
|
||||
|
||||
|
||||
def create_default_strategy_configurations() -> Dict[str, StrategyChartConfig]:
|
||||
"""Create default chart configurations for all trading strategies."""
|
||||
strategy_configs = {}
|
||||
|
||||
# Scalping Strategy
|
||||
strategy_configs["scalping"] = StrategyChartConfig(
|
||||
strategy_name="Scalping Strategy",
|
||||
strategy_type=TradingStrategy.SCALPING,
|
||||
description="Fast-paced trading with quick entry/exit on 1-5 minute charts",
|
||||
timeframes=["1m", "5m"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.6,
|
||||
overlay_indicators=["ema_5", "ema_12", "ema_21", "bb_10_15"],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.2,
|
||||
indicators=["rsi_7"],
|
||||
title="RSI (7)",
|
||||
y_axis_label="RSI",
|
||||
show_grid=True
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.2,
|
||||
indicators=["macd_5_13_4"],
|
||||
title="MACD Fast",
|
||||
y_axis_label="MACD",
|
||||
show_grid=True
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=10,
|
||||
show_volume=True,
|
||||
candlestick_up_color="#00d4aa",
|
||||
candlestick_down_color="#fe6a85"
|
||||
),
|
||||
tags=["scalping", "short-term", "fast"]
|
||||
)
|
||||
|
||||
# Day Trading Strategy
|
||||
strategy_configs["day_trading"] = StrategyChartConfig(
|
||||
strategy_name="Day Trading Strategy",
|
||||
strategy_type=TradingStrategy.DAY_TRADING,
|
||||
description="Intraday trading with balanced indicator mix for 5m-1h charts",
|
||||
timeframes=["5m", "15m", "1h"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.65,
|
||||
overlay_indicators=["sma_20", "ema_12", "ema_26", "bb_20_20"],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.15,
|
||||
indicators=["rsi_14"],
|
||||
title="RSI (14)",
|
||||
y_axis_label="RSI"
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.2,
|
||||
indicators=["macd_12_26_9"],
|
||||
title="MACD",
|
||||
y_axis_label="MACD"
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=12,
|
||||
show_volume=True
|
||||
),
|
||||
tags=["day-trading", "intraday", "balanced"]
|
||||
)
|
||||
|
||||
# Swing Trading Strategy
|
||||
strategy_configs["swing_trading"] = StrategyChartConfig(
|
||||
strategy_name="Swing Trading Strategy",
|
||||
strategy_type=TradingStrategy.SWING_TRADING,
|
||||
description="Medium-term trading for multi-day holds on 1h-1d charts",
|
||||
timeframes=["1h", "4h", "1d"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.7,
|
||||
overlay_indicators=["sma_50", "ema_21", "ema_50", "bb_20_20"],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.15,
|
||||
indicators=["rsi_14", "rsi_21"],
|
||||
title="RSI Comparison",
|
||||
y_axis_label="RSI"
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.15,
|
||||
indicators=["macd_12_26_9"],
|
||||
title="MACD",
|
||||
y_axis_label="MACD"
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=12,
|
||||
show_volume=True
|
||||
),
|
||||
tags=["swing-trading", "medium-term", "multi-day"]
|
||||
)
|
||||
|
||||
# Position Trading Strategy
|
||||
strategy_configs["position_trading"] = StrategyChartConfig(
|
||||
strategy_name="Position Trading Strategy",
|
||||
strategy_type=TradingStrategy.POSITION_TRADING,
|
||||
description="Long-term trading for weeks/months holds on 4h-1w charts",
|
||||
timeframes=["4h", "1d", "1w"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.75,
|
||||
overlay_indicators=["sma_100", "sma_200", "ema_50", "ema_100", "bb_50_20"],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.12,
|
||||
indicators=["rsi_21"],
|
||||
title="RSI (21)",
|
||||
y_axis_label="RSI"
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.13,
|
||||
indicators=["macd_19_39_13"],
|
||||
title="MACD Slow",
|
||||
y_axis_label="MACD"
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=14,
|
||||
show_volume=False # Less important for long-term
|
||||
),
|
||||
tags=["position-trading", "long-term", "weeks-months"]
|
||||
)
|
||||
|
||||
# Momentum Strategy
|
||||
strategy_configs["momentum"] = StrategyChartConfig(
|
||||
strategy_name="Momentum Strategy",
|
||||
strategy_type=TradingStrategy.MOMENTUM,
|
||||
description="Trend-following momentum strategy for strong directional moves",
|
||||
timeframes=["15m", "1h", "4h"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.6,
|
||||
overlay_indicators=["ema_12", "ema_26"],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.15,
|
||||
indicators=["rsi_7", "rsi_14"],
|
||||
title="RSI Momentum",
|
||||
y_axis_label="RSI"
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.MACD,
|
||||
height_ratio=0.25,
|
||||
indicators=["macd_8_17_6", "macd_12_26_9"],
|
||||
title="MACD Momentum",
|
||||
y_axis_label="MACD"
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=12,
|
||||
candlestick_up_color="#26a69a",
|
||||
candlestick_down_color="#ef5350"
|
||||
),
|
||||
tags=["momentum", "trend-following", "directional"]
|
||||
)
|
||||
|
||||
# Mean Reversion Strategy
|
||||
strategy_configs["mean_reversion"] = StrategyChartConfig(
|
||||
strategy_name="Mean Reversion Strategy",
|
||||
strategy_type=TradingStrategy.MEAN_REVERSION,
|
||||
description="Counter-trend strategy for oversold/overbought conditions",
|
||||
timeframes=["15m", "1h", "4h"],
|
||||
layout=ChartLayout.MAIN_WITH_SUBPLOTS,
|
||||
main_chart_height=0.65,
|
||||
overlay_indicators=["sma_20", "sma_50", "bb_20_20", "bb_20_25"],
|
||||
subplot_configs=[
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.RSI,
|
||||
height_ratio=0.2,
|
||||
indicators=["rsi_14", "rsi_21"],
|
||||
title="RSI Mean Reversion",
|
||||
y_axis_label="RSI"
|
||||
),
|
||||
SubplotConfig(
|
||||
subplot_type=SubplotType.VOLUME,
|
||||
height_ratio=0.15,
|
||||
indicators=[],
|
||||
title="Volume",
|
||||
y_axis_label="Volume"
|
||||
)
|
||||
],
|
||||
chart_style=ChartStyle(
|
||||
theme="plotly_white",
|
||||
font_size=12,
|
||||
show_volume=True
|
||||
),
|
||||
tags=["mean-reversion", "counter-trend", "oversold-overbought"]
|
||||
)
|
||||
|
||||
return strategy_configs
|
||||
|
||||
|
||||
def validate_strategy_configuration(config: StrategyChartConfig) -> tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate a strategy chart configuration.
|
||||
|
||||
Args:
|
||||
config: Strategy chart configuration to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_error_messages)
|
||||
"""
|
||||
return config.validate()
|
||||
|
||||
|
||||
def create_custom_strategy_config(
|
||||
strategy_name: str,
|
||||
strategy_type: TradingStrategy,
|
||||
description: str,
|
||||
timeframes: List[str],
|
||||
overlay_indicators: List[str],
|
||||
subplot_configs: List[Dict[str, Any]],
|
||||
chart_style: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> tuple[Optional[StrategyChartConfig], List[str]]:
|
||||
"""
|
||||
Create a custom strategy chart configuration.
|
||||
|
||||
Args:
|
||||
strategy_name: Name of the strategy
|
||||
strategy_type: Type of trading strategy
|
||||
description: Strategy description
|
||||
timeframes: List of recommended timeframes
|
||||
overlay_indicators: List of overlay indicator names
|
||||
subplot_configs: List of subplot configuration dictionaries
|
||||
chart_style: Optional chart style configuration
|
||||
**kwargs: Additional configuration options
|
||||
|
||||
Returns:
|
||||
Tuple of (config_object_or_None, list_of_error_messages)
|
||||
"""
|
||||
try:
|
||||
# Create subplot configurations
|
||||
subplots = []
|
||||
for subplot_data in subplot_configs:
|
||||
subplot_type = SubplotType(subplot_data.get("subplot_type", "custom"))
|
||||
subplot = SubplotConfig(
|
||||
subplot_type=subplot_type,
|
||||
height_ratio=subplot_data.get("height_ratio", 0.2),
|
||||
indicators=subplot_data.get("indicators", []),
|
||||
title=subplot_data.get("title"),
|
||||
y_axis_label=subplot_data.get("y_axis_label"),
|
||||
show_grid=subplot_data.get("show_grid", True),
|
||||
show_legend=subplot_data.get("show_legend", True),
|
||||
background_color=subplot_data.get("background_color")
|
||||
)
|
||||
subplots.append(subplot)
|
||||
|
||||
# Create chart style
|
||||
style = ChartStyle()
|
||||
if chart_style:
|
||||
for key, value in chart_style.items():
|
||||
if hasattr(style, key):
|
||||
setattr(style, key, value)
|
||||
|
||||
# Create configuration
|
||||
config = StrategyChartConfig(
|
||||
strategy_name=strategy_name,
|
||||
strategy_type=strategy_type,
|
||||
description=description,
|
||||
timeframes=timeframes,
|
||||
layout=ChartLayout(kwargs.get("layout", ChartLayout.MAIN_WITH_SUBPLOTS.value)),
|
||||
main_chart_height=kwargs.get("main_chart_height", 0.7),
|
||||
overlay_indicators=overlay_indicators,
|
||||
subplot_configs=subplots,
|
||||
chart_style=style,
|
||||
created_at=datetime.now(),
|
||||
version=kwargs.get("version", "1.0"),
|
||||
tags=kwargs.get("tags", [])
|
||||
)
|
||||
|
||||
# Validate configuration
|
||||
is_valid, errors = config.validate()
|
||||
if not is_valid:
|
||||
return None, errors
|
||||
|
||||
return config, []
|
||||
|
||||
except Exception as e:
|
||||
return None, [f"Error creating strategy configuration: {e}"]
|
||||
|
||||
|
||||
def load_strategy_config_from_json(json_data: Union[str, Dict[str, Any]]) -> tuple[Optional[StrategyChartConfig], List[str]]:
|
||||
"""
|
||||
Load strategy configuration from JSON data.
|
||||
|
||||
Args:
|
||||
json_data: JSON string or dictionary with configuration data
|
||||
|
||||
Returns:
|
||||
Tuple of (config_object_or_None, list_of_error_messages)
|
||||
"""
|
||||
try:
|
||||
if isinstance(json_data, str):
|
||||
data = json.loads(json_data)
|
||||
else:
|
||||
data = json_data
|
||||
|
||||
# Extract required fields
|
||||
required_fields = ["strategy_name", "strategy_type", "description", "timeframes"]
|
||||
missing_fields = [field for field in required_fields if field not in data]
|
||||
if missing_fields:
|
||||
return None, [f"Missing required fields: {', '.join(missing_fields)}"]
|
||||
|
||||
# Convert strategy type
|
||||
try:
|
||||
strategy_type = TradingStrategy(data["strategy_type"])
|
||||
except ValueError:
|
||||
return None, [f"Invalid strategy type: {data['strategy_type']}"]
|
||||
|
||||
return create_custom_strategy_config(
|
||||
strategy_name=data["strategy_name"],
|
||||
strategy_type=strategy_type,
|
||||
description=data["description"],
|
||||
timeframes=data["timeframes"],
|
||||
overlay_indicators=data.get("overlay_indicators", []),
|
||||
subplot_configs=data.get("subplot_configs", []),
|
||||
chart_style=data.get("chart_style"),
|
||||
**{k: v for k, v in data.items() if k not in required_fields + ["overlay_indicators", "subplot_configs", "chart_style"]}
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return None, [f"Invalid JSON: {e}"]
|
||||
except Exception as e:
|
||||
return None, [f"Error loading configuration: {e}"]
|
||||
|
||||
|
||||
def export_strategy_config_to_json(config: StrategyChartConfig) -> str:
|
||||
"""
|
||||
Export strategy configuration to JSON string.
|
||||
|
||||
Args:
|
||||
config: Strategy configuration to export
|
||||
|
||||
Returns:
|
||||
JSON string representation of the configuration
|
||||
"""
|
||||
# Convert to dictionary
|
||||
config_dict = {
|
||||
"strategy_name": config.strategy_name,
|
||||
"strategy_type": config.strategy_type.value,
|
||||
"description": config.description,
|
||||
"timeframes": config.timeframes,
|
||||
"layout": config.layout.value,
|
||||
"main_chart_height": config.main_chart_height,
|
||||
"overlay_indicators": config.overlay_indicators,
|
||||
"subplot_configs": [
|
||||
{
|
||||
"subplot_type": subplot.subplot_type.value,
|
||||
"height_ratio": subplot.height_ratio,
|
||||
"indicators": subplot.indicators,
|
||||
"title": subplot.title,
|
||||
"y_axis_label": subplot.y_axis_label,
|
||||
"show_grid": subplot.show_grid,
|
||||
"show_legend": subplot.show_legend,
|
||||
"background_color": subplot.background_color
|
||||
}
|
||||
for subplot in config.subplot_configs
|
||||
],
|
||||
"chart_style": {
|
||||
"theme": config.chart_style.theme,
|
||||
"background_color": config.chart_style.background_color,
|
||||
"grid_color": config.chart_style.grid_color,
|
||||
"text_color": config.chart_style.text_color,
|
||||
"font_family": config.chart_style.font_family,
|
||||
"font_size": config.chart_style.font_size,
|
||||
"candlestick_up_color": config.chart_style.candlestick_up_color,
|
||||
"candlestick_down_color": config.chart_style.candlestick_down_color,
|
||||
"volume_color": config.chart_style.volume_color,
|
||||
"show_volume": config.chart_style.show_volume,
|
||||
"show_grid": config.chart_style.show_grid,
|
||||
"show_legend": config.chart_style.show_legend,
|
||||
"show_toolbar": config.chart_style.show_toolbar
|
||||
},
|
||||
"version": config.version,
|
||||
"tags": config.tags
|
||||
}
|
||||
|
||||
return json.dumps(config_dict, indent=2)
|
||||
|
||||
|
||||
def get_strategy_config(strategy_name: str) -> Optional[StrategyChartConfig]:
|
||||
"""
|
||||
Get a default strategy configuration by name.
|
||||
|
||||
Args:
|
||||
strategy_name: Name of the strategy
|
||||
|
||||
Returns:
|
||||
Strategy configuration or None if not found
|
||||
"""
|
||||
default_configs = create_default_strategy_configurations()
|
||||
return default_configs.get(strategy_name)
|
||||
|
||||
|
||||
def get_all_strategy_configs() -> Dict[str, StrategyChartConfig]:
|
||||
"""
|
||||
Get all default strategy configurations.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping strategy names to their configurations
|
||||
"""
|
||||
return create_default_strategy_configurations()
|
||||
|
||||
|
||||
def get_available_strategy_names() -> List[str]:
|
||||
"""
|
||||
Get list of available default strategy names.
|
||||
|
||||
Returns:
|
||||
List of strategy names
|
||||
"""
|
||||
return list(create_default_strategy_configurations().keys())
|
||||
676
components/charts/config/validation.py
Normal file
676
components/charts/config/validation.py
Normal file
@@ -0,0 +1,676 @@
|
||||
"""
|
||||
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("config_validation")
|
||||
|
||||
|
||||
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"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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user