2025-06-11 18:52:02 +08:00
|
|
|
"""
|
|
|
|
|
Utility functions for loading and managing indicator configurations.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import logging
|
|
|
|
|
from typing import List, Dict, Any, Optional
|
2025-06-11 19:09:52 +08:00
|
|
|
from dash import Output, Input, State
|
2025-06-11 18:52:02 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2025-06-11 19:09:52 +08:00
|
|
|
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
|