Vasily.onl 3e0e89b826 Refactor indicator management to a data-driven approach
- 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.
2025-06-11 19:09:52 +08:00

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