- Introduced dynamic generation of parameter fields and callback handling for indicators, enhancing modularity and maintainability. - Updated `config_utils.py` with new utility functions to load indicator templates and generate dynamic outputs and states for parameter fields. - Refactored `indicators.py` to utilize these utilities, streamlining the callback logic and improving user experience by reducing hardcoded elements. - Modified `indicator_modal.py` to create parameter fields dynamically based on JSON templates, eliminating the need for manual updates when adding new indicators. - Added documentation outlining the new data-driven architecture for indicators, improving clarity and guidance for future development. These changes significantly enhance the flexibility and scalability of the indicator system, aligning with project goals for maintainability and performance.
338 lines
11 KiB
Python
338 lines
11 KiB
Python
"""
|
|
Utility functions for loading and managing indicator 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_indicator_templates() -> Dict[str, Dict[str, Any]]:
|
|
"""Load all indicator templates from the templates directory.
|
|
|
|
Returns:
|
|
Dict[str, Dict[str, Any]]: Dictionary mapping indicator 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)
|
|
indicator_type = template.get('type')
|
|
if indicator_type:
|
|
templates[indicator_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 indicator templates: {e}")
|
|
|
|
return templates
|
|
|
|
|
|
def get_indicator_dropdown_options() -> List[Dict[str, str]]:
|
|
"""Generate dropdown options for indicator types from templates.
|
|
|
|
Returns:
|
|
List[Dict[str, str]]: List of dropdown options with label and value
|
|
"""
|
|
templates = load_indicator_templates()
|
|
options = []
|
|
|
|
for indicator_type, template in templates.items():
|
|
option = {
|
|
'label': template.get('name', indicator_type.upper()),
|
|
'value': indicator_type
|
|
}
|
|
options.append(option)
|
|
|
|
# Sort by label for consistent UI
|
|
options.sort(key=lambda x: x['label'])
|
|
|
|
return options
|
|
|
|
|
|
def get_indicator_parameter_schema(indicator_type: str) -> Optional[Dict[str, Any]]:
|
|
"""Get parameter schema for a specific indicator type.
|
|
|
|
Args:
|
|
indicator_type (str): The indicator type (e.g., 'sma', 'ema')
|
|
|
|
Returns:
|
|
Optional[Dict[str, Any]]: Parameter schema or None if not found
|
|
"""
|
|
templates = load_indicator_templates()
|
|
template = templates.get(indicator_type)
|
|
|
|
if template:
|
|
return template.get('parameter_schema', {})
|
|
|
|
return None
|
|
|
|
|
|
def get_indicator_default_parameters(indicator_type: str) -> Optional[Dict[str, Any]]:
|
|
"""Get default parameters for a specific indicator type.
|
|
|
|
Args:
|
|
indicator_type (str): The indicator type (e.g., 'sma', 'ema')
|
|
|
|
Returns:
|
|
Optional[Dict[str, Any]]: Default parameters or None if not found
|
|
"""
|
|
templates = load_indicator_templates()
|
|
template = templates.get(indicator_type)
|
|
|
|
if template:
|
|
return template.get('default_parameters', {})
|
|
|
|
return None
|
|
|
|
|
|
def get_indicator_default_styling(indicator_type: str) -> Optional[Dict[str, Any]]:
|
|
"""Get default styling for a specific indicator type.
|
|
|
|
Args:
|
|
indicator_type (str): The indicator type (e.g., 'sma', 'ema')
|
|
|
|
Returns:
|
|
Optional[Dict[str, Any]]: Default styling or None if not found
|
|
"""
|
|
templates = load_indicator_templates()
|
|
template = templates.get(indicator_type)
|
|
|
|
if template:
|
|
return template.get('default_styling', {})
|
|
|
|
return None
|
|
|
|
|
|
def generate_parameter_fields_config(indicator_type: str) -> Optional[Dict[str, Any]]:
|
|
"""Generate parameter field configuration for dynamic UI generation.
|
|
|
|
Args:
|
|
indicator_type (str): The indicator type (e.g., 'sma', 'ema')
|
|
|
|
Returns:
|
|
Optional[Dict[str, Any]]: Configuration for generating parameter input fields
|
|
"""
|
|
schema = get_indicator_parameter_schema(indicator_type)
|
|
defaults = get_indicator_default_parameters(indicator_type)
|
|
|
|
if not schema or not defaults:
|
|
return None
|
|
|
|
fields_config = {}
|
|
|
|
for param_name, param_schema in schema.items():
|
|
# Skip timeframe as it's handled separately in the modal
|
|
if param_name == 'timeframe':
|
|
continue
|
|
|
|
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'{indicator_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']
|
|
|
|
fields_config[param_name] = field_config
|
|
|
|
return fields_config
|
|
|
|
|
|
def get_parameter_field_outputs() -> List[Output]:
|
|
"""Generate dynamic Output components for parameter field visibility callbacks.
|
|
|
|
Returns:
|
|
List[Output]: List of Output components for all parameter containers
|
|
"""
|
|
templates = load_indicator_templates()
|
|
outputs = [Output('indicator-parameters-message', 'style')]
|
|
|
|
for indicator_type in templates.keys():
|
|
outputs.append(Output(f'{indicator_type}-parameters', 'style'))
|
|
|
|
return outputs
|
|
|
|
|
|
def get_parameter_field_states() -> List[State]:
|
|
"""Generate dynamic State components for parameter input fields.
|
|
|
|
Returns:
|
|
List[State]: List of State components for all parameter input fields
|
|
"""
|
|
templates = load_indicator_templates()
|
|
states = []
|
|
|
|
for indicator_type, template in templates.items():
|
|
config = generate_parameter_fields_config(indicator_type)
|
|
if config:
|
|
for field_config in config.values():
|
|
states.append(State(field_config['input_id'], 'value'))
|
|
|
|
return states
|
|
|
|
|
|
def get_parameter_field_edit_outputs() -> List[Output]:
|
|
"""Generate dynamic Output components for parameter fields in edit mode.
|
|
|
|
Returns:
|
|
List[Output]: List of Output components for setting parameter values
|
|
"""
|
|
templates = load_indicator_templates()
|
|
outputs = []
|
|
|
|
for indicator_type, template in templates.items():
|
|
config = generate_parameter_fields_config(indicator_type)
|
|
if config:
|
|
for field_config in config.values():
|
|
outputs.append(Output(field_config['input_id'], 'value'))
|
|
|
|
return outputs
|
|
|
|
|
|
def get_parameter_field_reset_outputs() -> List[Output]:
|
|
"""Generate dynamic Output components for resetting parameter fields.
|
|
|
|
Returns:
|
|
List[Output]: List of Output components for resetting parameter values
|
|
"""
|
|
templates = load_indicator_templates()
|
|
outputs = []
|
|
|
|
for indicator_type, template in templates.items():
|
|
config = generate_parameter_fields_config(indicator_type)
|
|
if config:
|
|
for field_config in config.values():
|
|
outputs.append(Output(field_config['input_id'], 'value', allow_duplicate=True))
|
|
|
|
return outputs
|
|
|
|
|
|
def collect_parameter_values(indicator_type: str, all_parameter_values: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Collect parameter values for a specific indicator type from callback arguments.
|
|
|
|
Args:
|
|
indicator_type (str): The indicator type
|
|
all_parameter_values (Dict[str, Any]): All parameter values from callback
|
|
|
|
Returns:
|
|
Dict[str, Any]: Parameters specific to the indicator type
|
|
"""
|
|
config = generate_parameter_fields_config(indicator_type)
|
|
if not config:
|
|
return {}
|
|
|
|
parameters = {}
|
|
defaults = get_indicator_default_parameters(indicator_type)
|
|
|
|
for param_name, field_config in config.items():
|
|
field_id = field_config['input_id']
|
|
value = all_parameter_values.get(field_id)
|
|
default_value = defaults.get(param_name, field_config.get('default'))
|
|
|
|
# Use provided value or fall back to default
|
|
parameters[param_name] = value if value is not None else default_value
|
|
|
|
return parameters
|
|
|
|
|
|
def set_parameter_values(indicator_type: str, parameters: Dict[str, Any]) -> List[Any]:
|
|
"""Generate parameter values for setting in edit mode.
|
|
|
|
Args:
|
|
indicator_type (str): The indicator type
|
|
parameters (Dict[str, Any]): Parameter values to set
|
|
|
|
Returns:
|
|
List[Any]: Values in the order expected by the callback outputs
|
|
"""
|
|
templates = load_indicator_templates()
|
|
values = []
|
|
|
|
for current_type, template in templates.items():
|
|
config = generate_parameter_fields_config(current_type)
|
|
if config:
|
|
for param_name, field_config in config.items():
|
|
if current_type == indicator_type:
|
|
# Set the actual parameter value for the matching indicator type
|
|
value = parameters.get(param_name)
|
|
else:
|
|
# Set None for other indicator types
|
|
value = None
|
|
values.append(value)
|
|
|
|
return values
|
|
|
|
|
|
def reset_parameter_values() -> List[Any]:
|
|
"""Generate default parameter values for resetting the form.
|
|
|
|
Returns:
|
|
List[Any]: Default values in the order expected by reset callback outputs
|
|
"""
|
|
templates = load_indicator_templates()
|
|
values = []
|
|
|
|
for indicator_type, template in templates.items():
|
|
config = generate_parameter_fields_config(indicator_type)
|
|
if config:
|
|
defaults = get_indicator_default_parameters(indicator_type)
|
|
for param_name, field_config in config.items():
|
|
default_value = defaults.get(param_name, field_config.get('default'))
|
|
values.append(default_value)
|
|
|
|
return values
|
|
|
|
|
|
def get_parameter_visibility_styles(selected_indicator_type: str) -> List[Dict[str, str]]:
|
|
"""Generate visibility styles for parameter containers.
|
|
|
|
Args:
|
|
selected_indicator_type (str): The currently selected indicator type
|
|
|
|
Returns:
|
|
List[Dict[str, str]]: Visibility styles for each parameter container
|
|
"""
|
|
templates = load_indicator_templates()
|
|
hidden_style = {'display': 'none'}
|
|
visible_style = {'display': 'block'}
|
|
|
|
# First style is for the message
|
|
message_style = {'display': 'block'} if not selected_indicator_type else {'display': 'none'}
|
|
styles = [message_style]
|
|
|
|
# Then styles for each indicator type container
|
|
for indicator_type in templates.keys():
|
|
style = visible_style if indicator_type == selected_indicator_type else hidden_style
|
|
styles.append(style)
|
|
|
|
return styles |