4.0 - 2.0 Implement strategy configuration utilities and templates

- Introduced `config_utils.py` for loading and managing strategy configurations, including functions for loading templates, generating dropdown options, and retrieving parameter schemas and default values.
- Added JSON templates for EMA Crossover, MACD, and RSI strategies, defining their parameters and validation rules to enhance modularity and maintainability.
- Implemented `StrategyManager` in `manager.py` for managing user-defined strategies with file-based storage, supporting easy sharing and portability.
- Updated `__init__.py` to include new components and ensure proper module exports.
- Enhanced error handling and logging practices across the new modules for improved reliability.

These changes establish a robust foundation for strategy management and configuration, aligning with project goals for modularity, performance, and maintainability.
This commit is contained in:
Vasily.onl
2025-06-12 15:17:35 +08:00
parent fd5a59fc39
commit d34da789ec
17 changed files with 2220 additions and 243 deletions

View File

@@ -0,0 +1,368 @@
"""
Utility functions for loading and managing strategy configurations.
"""
import json
import os
import logging
from typing import List, Dict, Any, Optional
from dash import Output, Input, State
logger = logging.getLogger(__name__)
def load_strategy_templates() -> Dict[str, Dict[str, Any]]:
"""Load all strategy templates from the templates directory.
Returns:
Dict[str, Dict[str, Any]]: Dictionary mapping strategy type to template configuration
"""
templates = {}
try:
# Get the templates directory path
templates_dir = os.path.join(os.path.dirname(__file__), 'templates')
if not os.path.exists(templates_dir):
logger.error(f"Templates directory not found at {templates_dir}")
return {}
# Load all JSON files from templates directory
for filename in os.listdir(templates_dir):
if filename.endswith('_template.json'):
file_path = os.path.join(templates_dir, filename)
try:
with open(file_path, 'r', encoding='utf-8') as f:
template = json.load(f)
strategy_type = template.get('type')
if strategy_type:
templates[strategy_type] = template
else:
logger.warning(f"Template {filename} missing 'type' field")
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON from {filename}: {e}")
except Exception as e:
logger.error(f"Error loading template {filename}: {e}")
except Exception as e:
logger.error(f"Error loading strategy templates: {e}")
return templates
def get_strategy_dropdown_options() -> List[Dict[str, str]]:
"""Generate dropdown options for strategy types from templates.
Returns:
List[Dict[str, str]]: List of dropdown options with label and value
"""
templates = load_strategy_templates()
options = []
for strategy_type, template in templates.items():
option = {
'label': template.get('name', strategy_type.upper()),
'value': strategy_type
}
options.append(option)
# Sort by label for consistent UI
options.sort(key=lambda x: x['label'])
return options
def get_strategy_parameter_schema(strategy_type: str) -> Optional[Dict[str, Any]]:
"""Get parameter schema for a specific strategy type.
Args:
strategy_type (str): The strategy type (e.g., 'ema_crossover', 'rsi')
Returns:
Optional[Dict[str, Any]]: Parameter schema or None if not found
"""
templates = load_strategy_templates()
template = templates.get(strategy_type)
if template:
return template.get('parameter_schema', {})
return None
def get_strategy_default_parameters(strategy_type: str) -> Optional[Dict[str, Any]]:
"""Get default parameters for a specific strategy type.
Args:
strategy_type (str): The strategy type (e.g., 'ema_crossover', 'rsi')
Returns:
Optional[Dict[str, Any]]: Default parameters or None if not found
"""
templates = load_strategy_templates()
template = templates.get(strategy_type)
if template:
return template.get('default_parameters', {})
return None
def get_strategy_metadata(strategy_type: str) -> Optional[Dict[str, Any]]:
"""Get metadata for a specific strategy type.
Args:
strategy_type (str): The strategy type (e.g., 'ema_crossover', 'rsi')
Returns:
Optional[Dict[str, Any]]: Strategy metadata or None if not found
"""
templates = load_strategy_templates()
template = templates.get(strategy_type)
if template:
return template.get('metadata', {})
return None
def get_strategy_required_indicators(strategy_type: str) -> List[str]:
"""Get required indicators for a specific strategy type.
Args:
strategy_type (str): The strategy type (e.g., 'ema_crossover', 'rsi')
Returns:
List[str]: List of required indicator types
"""
metadata = get_strategy_metadata(strategy_type)
if metadata:
return metadata.get('required_indicators', [])
return []
def generate_parameter_fields_config(strategy_type: str) -> Optional[Dict[str, Any]]:
"""Generate parameter field configuration for dynamic UI generation.
Args:
strategy_type (str): The strategy type (e.g., 'ema_crossover', 'rsi')
Returns:
Optional[Dict[str, Any]]: Configuration for generating parameter input fields
"""
schema = get_strategy_parameter_schema(strategy_type)
defaults = get_strategy_default_parameters(strategy_type)
if not schema or not defaults:
return None
fields_config = {}
for param_name, param_schema in schema.items():
field_config = {
'type': param_schema.get('type', 'int'),
'label': param_name.replace('_', ' ').title(),
'default': defaults.get(param_name, param_schema.get('default')),
'description': param_schema.get('description', ''),
'input_id': f'{strategy_type}-{param_name.replace("_", "-")}-input'
}
# Add validation constraints if present
if 'min' in param_schema:
field_config['min'] = param_schema['min']
if 'max' in param_schema:
field_config['max'] = param_schema['max']
if 'step' in param_schema:
field_config['step'] = param_schema['step']
if 'options' in param_schema:
field_config['options'] = param_schema['options']
fields_config[param_name] = field_config
return fields_config
def validate_strategy_parameters(strategy_type: str, parameters: Dict[str, Any]) -> tuple[bool, List[str]]:
"""Validate strategy parameters against schema.
Args:
strategy_type (str): The strategy type
parameters (Dict[str, Any]): Parameters to validate
Returns:
tuple[bool, List[str]]: (is_valid, list_of_errors)
"""
schema = get_strategy_parameter_schema(strategy_type)
if not schema:
return False, [f"No schema found for strategy type: {strategy_type}"]
errors = []
# Check required parameters
for param_name, param_schema in schema.items():
if param_schema.get('required', True) and param_name not in parameters:
errors.append(f"Missing required parameter: {param_name}")
continue
if param_name not in parameters:
continue
value = parameters[param_name]
param_type = param_schema.get('type', 'int')
# Type validation
if param_type == 'int' and not isinstance(value, int):
errors.append(f"Parameter {param_name} must be an integer")
elif param_type == 'float' and not isinstance(value, (int, float)):
errors.append(f"Parameter {param_name} must be a number")
elif param_type == 'str' and not isinstance(value, str):
errors.append(f"Parameter {param_name} must be a string")
elif param_type == 'bool' and not isinstance(value, bool):
errors.append(f"Parameter {param_name} must be a boolean")
# Range validation
if 'min' in param_schema and value < param_schema['min']:
errors.append(f"Parameter {param_name} must be >= {param_schema['min']}")
if 'max' in param_schema and value > param_schema['max']:
errors.append(f"Parameter {param_name} must be <= {param_schema['max']}")
# Options validation
if 'options' in param_schema and value not in param_schema['options']:
errors.append(f"Parameter {param_name} must be one of: {param_schema['options']}")
return len(errors) == 0, errors
def save_user_strategy(strategy_name: str, config: Dict[str, Any]) -> bool:
"""Save a user-defined strategy configuration.
Args:
strategy_name (str): Name of the strategy configuration
config (Dict[str, Any]): Strategy configuration
Returns:
bool: True if saved successfully, False otherwise
"""
try:
user_strategies_dir = os.path.join(os.path.dirname(__file__), 'user_strategies')
os.makedirs(user_strategies_dir, exist_ok=True)
filename = f"{strategy_name.lower().replace(' ', '_')}.json"
file_path = os.path.join(user_strategies_dir, filename)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
logger.info(f"Saved user strategy configuration: {strategy_name}")
return True
except Exception as e:
logger.error(f"Error saving user strategy {strategy_name}: {e}")
return False
def load_user_strategies() -> Dict[str, Dict[str, Any]]:
"""Load all user-defined strategy configurations.
Returns:
Dict[str, Dict[str, Any]]: Dictionary mapping strategy name to configuration
"""
strategies = {}
try:
user_strategies_dir = os.path.join(os.path.dirname(__file__), 'user_strategies')
if not os.path.exists(user_strategies_dir):
return {}
for filename in os.listdir(user_strategies_dir):
if filename.endswith('.json'):
file_path = os.path.join(user_strategies_dir, filename)
try:
with open(file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
strategy_name = config.get('name', filename.replace('.json', ''))
strategies[strategy_name] = config
except Exception as e:
logger.error(f"Error loading user strategy {filename}: {e}")
except Exception as e:
logger.error(f"Error loading user strategies: {e}")
return strategies
def delete_user_strategy(strategy_name: str) -> bool:
"""Delete a user-defined strategy configuration.
Args:
strategy_name (str): Name of the strategy to delete
Returns:
bool: True if deleted successfully, False otherwise
"""
try:
user_strategies_dir = os.path.join(os.path.dirname(__file__), 'user_strategies')
filename = f"{strategy_name.lower().replace(' ', '_')}.json"
file_path = os.path.join(user_strategies_dir, filename)
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"Deleted user strategy configuration: {strategy_name}")
return True
else:
logger.warning(f"User strategy file not found: {file_path}")
return False
except Exception as e:
logger.error(f"Error deleting user strategy {strategy_name}: {e}")
return False
def export_strategy_config(strategy_name: str, config: Dict[str, Any]) -> str:
"""Export strategy configuration as JSON string.
Args:
strategy_name (str): Name of the strategy
config (Dict[str, Any]): Strategy configuration
Returns:
str: JSON string representation of the configuration
"""
export_data = {
'name': strategy_name,
'config': config,
'exported_at': str(os.times()),
'version': '1.0'
}
return json.dumps(export_data, indent=2, ensure_ascii=False)
def import_strategy_config(json_string: str) -> tuple[bool, Optional[Dict[str, Any]], List[str]]:
"""Import strategy configuration from JSON string.
Args:
json_string (str): JSON string containing strategy configuration
Returns:
tuple[bool, Optional[Dict[str, Any]], List[str]]: (success, config, errors)
"""
try:
data = json.loads(json_string)
if 'name' not in data or 'config' not in data:
return False, None, ['Invalid format: missing name or config fields']
# Validate the configuration if it has a strategy type
config = data['config']
if 'strategy' in config:
is_valid, errors = validate_strategy_parameters(config['strategy'], config)
if not is_valid:
return False, None, errors
return True, data, []
except json.JSONDecodeError as e:
return False, None, [f'Invalid JSON format: {e}']
except Exception as e:
return False, None, [f'Error importing configuration: {e}']

View File

@@ -0,0 +1,55 @@
{
"type": "ema_crossover",
"name": "EMA Crossover",
"description": "Exponential Moving Average crossover strategy that generates buy signals when fast EMA crosses above slow EMA and sell signals when fast EMA crosses below slow EMA.",
"category": "trend_following",
"parameter_schema": {
"fast_period": {
"type": "int",
"description": "Period for fast EMA calculation",
"min": 5,
"max": 50,
"default": 12,
"required": true
},
"slow_period": {
"type": "int",
"description": "Period for slow EMA calculation",
"min": 10,
"max": 200,
"default": 26,
"required": true
},
"min_price_change": {
"type": "float",
"description": "Minimum price change percentage to validate signal",
"min": 0.0,
"max": 10.0,
"default": 0.5,
"required": false
}
},
"default_parameters": {
"fast_period": 12,
"slow_period": 26,
"min_price_change": 0.5
},
"metadata": {
"required_indicators": ["ema"],
"timeframes": ["1h", "4h", "1d"],
"market_conditions": ["trending"],
"risk_level": "medium",
"difficulty": "beginner",
"signals": {
"buy": "Fast EMA crosses above slow EMA",
"sell": "Fast EMA crosses below slow EMA"
},
"performance_notes": "Works best in trending markets, may generate false signals in sideways markets"
},
"validation_rules": {
"fast_period_less_than_slow": {
"rule": "fast_period < slow_period",
"message": "Fast period must be less than slow period"
}
}
}

View File

@@ -0,0 +1,77 @@
{
"type": "macd",
"name": "MACD Strategy",
"description": "Moving Average Convergence Divergence strategy that generates signals based on MACD line crossovers with the signal line and zero line.",
"category": "trend_following",
"parameter_schema": {
"fast_period": {
"type": "int",
"description": "Fast EMA period for MACD calculation",
"min": 5,
"max": 30,
"default": 12,
"required": true
},
"slow_period": {
"type": "int",
"description": "Slow EMA period for MACD calculation",
"min": 15,
"max": 50,
"default": 26,
"required": true
},
"signal_period": {
"type": "int",
"description": "Signal line EMA period",
"min": 5,
"max": 20,
"default": 9,
"required": true
},
"signal_type": {
"type": "str",
"description": "Type of MACD signal to use",
"options": ["line_cross", "zero_cross", "histogram"],
"default": "line_cross",
"required": true
},
"histogram_threshold": {
"type": "float",
"description": "Minimum histogram value for signal confirmation",
"min": 0.0,
"max": 1.0,
"default": 0.0,
"required": false
}
},
"default_parameters": {
"fast_period": 12,
"slow_period": 26,
"signal_period": 9,
"signal_type": "line_cross",
"histogram_threshold": 0.0
},
"metadata": {
"required_indicators": ["macd"],
"timeframes": ["1h", "4h", "1d"],
"market_conditions": ["trending", "volatile"],
"risk_level": "medium",
"difficulty": "intermediate",
"signals": {
"buy": "MACD line crosses above signal line (or zero line)",
"sell": "MACD line crosses below signal line (or zero line)",
"confirmation": "Histogram supports signal direction"
},
"performance_notes": "Effective in trending markets but may lag during rapid price changes"
},
"validation_rules": {
"fast_period_less_than_slow": {
"rule": "fast_period < slow_period",
"message": "Fast period must be less than slow period"
},
"valid_signal_type": {
"rule": "signal_type in ['line_cross', 'zero_cross', 'histogram']",
"message": "Signal type must be one of: line_cross, zero_cross, histogram"
}
}
}

View File

@@ -0,0 +1,67 @@
{
"type": "rsi",
"name": "RSI Strategy",
"description": "Relative Strength Index momentum strategy that generates buy signals when RSI is oversold and sell signals when RSI is overbought.",
"category": "momentum",
"parameter_schema": {
"period": {
"type": "int",
"description": "Period for RSI calculation",
"min": 2,
"max": 50,
"default": 14,
"required": true
},
"overbought": {
"type": "float",
"description": "RSI overbought threshold (sell signal)",
"min": 50.0,
"max": 95.0,
"default": 70.0,
"required": true
},
"oversold": {
"type": "float",
"description": "RSI oversold threshold (buy signal)",
"min": 5.0,
"max": 50.0,
"default": 30.0,
"required": true
},
"neutrality_zone": {
"type": "bool",
"description": "Enable neutrality zone between 40-60 RSI",
"default": false,
"required": false
}
},
"default_parameters": {
"period": 14,
"overbought": 70.0,
"oversold": 30.0,
"neutrality_zone": false
},
"metadata": {
"required_indicators": ["rsi"],
"timeframes": ["15m", "1h", "4h", "1d"],
"market_conditions": ["volatile", "ranging"],
"risk_level": "medium",
"difficulty": "beginner",
"signals": {
"buy": "RSI below oversold threshold",
"sell": "RSI above overbought threshold",
"hold": "RSI in neutral zone (if enabled)"
},
"performance_notes": "Works well in ranging markets, may lag in strong trending markets"
},
"validation_rules": {
"oversold_less_than_overbought": {
"rule": "oversold < overbought",
"message": "Oversold threshold must be less than overbought threshold"
},
"valid_threshold_range": {
"rule": "oversold >= 5 and overbought <= 95",
"message": "Thresholds must be within valid RSI range (5-95)"
}
}
}