Add utility functions for managing indicator configurations
- Introduced `config_utils.py` to provide utility functions for loading and managing indicator templates, enhancing modularity and maintainability. - Implemented functions to load templates, generate dropdown options, and retrieve parameter schemas, default parameters, and styling for various indicators. - Updated the indicator modal to dynamically create parameter fields based on the loaded configurations, improving user experience and reducing redundancy. - Refactored existing parameter field creation logic to utilize the new utility functions, streamlining the codebase and adhering to project standards for clarity and maintainability. These changes significantly enhance the configuration management for indicators, aligning with project goals for modularity and performance.
This commit is contained in:
parent
dbe58e5cef
commit
89b071230e
167
config/indicators/config_utils.py
Normal file
167
config/indicators/config_utils.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""
|
||||
Utility functions for loading and managing indicator configurations.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
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
|
||||
@ -13,7 +13,8 @@
|
||||
"opacity": 1.0,
|
||||
"line_style": "solid"
|
||||
},
|
||||
"timeframe": null,
|
||||
"visible": true,
|
||||
"created_date": "2025-06-04T04:16:35.456253+00:00",
|
||||
"modified_date": "2025-06-04T04:16:35.456253+00:00"
|
||||
"modified_date": "2025-06-11T10:50:38.809797+00:00"
|
||||
}
|
||||
@ -53,15 +53,15 @@ def register_indicator_callbacks(app):
|
||||
Output('ema-parameters', 'style'),
|
||||
Output('rsi-parameters', 'style'),
|
||||
Output('macd-parameters', 'style'),
|
||||
Output('bb-parameters', 'style')],
|
||||
Output('bollinger_bands-parameters', 'style')],
|
||||
Input('indicator-type-dropdown', 'value'),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def update_parameter_fields(indicator_type):
|
||||
"""Show/hide parameter input fields based on selected indicator type."""
|
||||
# Default styles
|
||||
hidden_style = {'display': 'none', 'margin-bottom': '10px'}
|
||||
visible_style = {'display': 'block', 'margin-bottom': '10px'}
|
||||
hidden_style = {'display': 'none'}
|
||||
visible_style = {'display': 'block'}
|
||||
|
||||
# Default message visibility
|
||||
message_style = {'display': 'block'} if not indicator_type else {'display': 'none'}
|
||||
@ -110,8 +110,8 @@ def register_indicator_callbacks(app):
|
||||
State('macd-slow-period-input', 'value'),
|
||||
State('macd-signal-period-input', 'value'),
|
||||
# Bollinger Bands parameters
|
||||
State('bb-period-input', 'value'),
|
||||
State('bb-stddev-input', 'value'),
|
||||
State('bollinger_bands-period-input', 'value'),
|
||||
State('bollinger_bands-std-dev-input', 'value'),
|
||||
# Edit mode data
|
||||
State('edit-indicator-store', 'data')],
|
||||
prevent_initial_call=True
|
||||
@ -397,8 +397,8 @@ def register_indicator_callbacks(app):
|
||||
Output('macd-fast-period-input', 'value'),
|
||||
Output('macd-slow-period-input', 'value'),
|
||||
Output('macd-signal-period-input', 'value'),
|
||||
Output('bb-period-input', 'value'),
|
||||
Output('bb-stddev-input', 'value')],
|
||||
Output('bollinger_bands-period-input', 'value'),
|
||||
Output('bollinger_bands-std-dev-input', 'value')],
|
||||
[Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')],
|
||||
[State({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'id')],
|
||||
prevent_initial_call=True
|
||||
@ -493,8 +493,8 @@ def register_indicator_callbacks(app):
|
||||
Output('macd-fast-period-input', 'value', allow_duplicate=True),
|
||||
Output('macd-slow-period-input', 'value', allow_duplicate=True),
|
||||
Output('macd-signal-period-input', 'value', allow_duplicate=True),
|
||||
Output('bb-period-input', 'value', allow_duplicate=True),
|
||||
Output('bb-stddev-input', 'value', allow_duplicate=True)],
|
||||
Output('bollinger_bands-period-input', 'value', allow_duplicate=True),
|
||||
Output('bollinger_bands-std-dev-input', 'value', allow_duplicate=True)],
|
||||
[Input('cancel-indicator-btn', 'n_clicks'),
|
||||
Input('save-indicator-btn', 'n_clicks')], # Also reset on successful save
|
||||
prevent_initial_call=True
|
||||
|
||||
@ -5,6 +5,89 @@ Indicator modal component for creating and editing indicators.
|
||||
from dash import html, dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
from utils.timeframe_utils import load_timeframe_options
|
||||
from config.indicators.config_utils import get_indicator_dropdown_options, generate_parameter_fields_config
|
||||
|
||||
|
||||
def create_dynamic_parameter_fields(indicator_type: str) -> html.Div:
|
||||
"""Create parameter input fields dynamically based on indicator configuration.
|
||||
|
||||
Args:
|
||||
indicator_type (str): The indicator type (e.g., 'sma', 'ema')
|
||||
|
||||
Returns:
|
||||
html.Div: Div containing the parameter input fields
|
||||
"""
|
||||
fields_config = generate_parameter_fields_config(indicator_type)
|
||||
|
||||
if not fields_config:
|
||||
return html.Div(
|
||||
html.P("No parameters available for this indicator", className="text-muted fst-italic"),
|
||||
id=f'{indicator_type}-parameters',
|
||||
style={'display': 'none'},
|
||||
className="mb-3"
|
||||
)
|
||||
|
||||
field_elements = []
|
||||
|
||||
# Handle single parameter (like SMA, EMA, RSI)
|
||||
if len(fields_config) == 1:
|
||||
param_name, field_config = next(iter(fields_config.items()))
|
||||
field_elements.append(
|
||||
html.Div([
|
||||
dbc.Label(f"{field_config['label']}:"),
|
||||
dcc.Input(
|
||||
id=field_config['input_id'],
|
||||
type='number',
|
||||
value=field_config['default'],
|
||||
min=field_config.get('min'),
|
||||
max=field_config.get('max'),
|
||||
step=field_config.get('step', 1 if field_config['type'] == 'int' else 0.1)
|
||||
),
|
||||
dbc.FormText(field_config['description']) if field_config['description'] else None
|
||||
])
|
||||
)
|
||||
else:
|
||||
# Handle multiple parameters (like MACD, Bollinger Bands)
|
||||
rows = []
|
||||
params_per_row = min(len(fields_config), 4) # Max 4 parameters per row
|
||||
|
||||
param_items = list(fields_config.items())
|
||||
for i in range(0, len(param_items), params_per_row):
|
||||
row_params = param_items[i:i + params_per_row]
|
||||
cols = []
|
||||
|
||||
for param_name, field_config in row_params:
|
||||
col = dbc.Col([
|
||||
dbc.Label(f"{field_config['label']}:"),
|
||||
dcc.Input(
|
||||
id=field_config['input_id'],
|
||||
type='number',
|
||||
value=field_config['default'],
|
||||
min=field_config.get('min'),
|
||||
max=field_config.get('max'),
|
||||
step=field_config.get('step', 1 if field_config['type'] == 'int' else 0.1)
|
||||
)
|
||||
], width=12 // len(row_params))
|
||||
cols.append(col)
|
||||
|
||||
rows.append(dbc.Row(cols))
|
||||
|
||||
field_elements.extend(rows)
|
||||
|
||||
# Add description for multi-parameter indicators
|
||||
if any(config['description'] for config in fields_config.values()):
|
||||
descriptions = [f"{config['label']}: {config['description']}"
|
||||
for config in fields_config.values() if config['description']]
|
||||
field_elements.append(
|
||||
dbc.FormText("; ".join(descriptions))
|
||||
)
|
||||
|
||||
return html.Div(
|
||||
field_elements,
|
||||
id=f'{indicator_type}-parameters',
|
||||
style={'display': 'none'},
|
||||
className="mb-3"
|
||||
)
|
||||
|
||||
|
||||
def create_indicator_modal():
|
||||
@ -24,13 +107,7 @@ def create_indicator_modal():
|
||||
dbc.Col(dbc.Label("Indicator Type:"), width=12),
|
||||
dbc.Col(dcc.Dropdown(
|
||||
id='indicator-type-dropdown',
|
||||
options=[
|
||||
{'label': 'Simple Moving Average (SMA)', 'value': 'sma'},
|
||||
{'label': 'Exponential Moving Average (EMA)', 'value': 'ema'},
|
||||
{'label': 'Relative Strength Index (RSI)', 'value': 'rsi'},
|
||||
{'label': 'MACD', 'value': 'macd'},
|
||||
{'label': 'Bollinger Bands', 'value': 'bollinger_bands'}
|
||||
],
|
||||
options=get_indicator_dropdown_options(),
|
||||
placeholder='Select indicator type',
|
||||
), width=12)
|
||||
], className="mb-3"),
|
||||
@ -61,7 +138,11 @@ def create_indicator_modal():
|
||||
),
|
||||
|
||||
# Parameter fields (SMA, EMA, etc.)
|
||||
create_parameter_fields(),
|
||||
create_dynamic_parameter_fields('sma'),
|
||||
create_dynamic_parameter_fields('ema'),
|
||||
create_dynamic_parameter_fields('rsi'),
|
||||
create_dynamic_parameter_fields('macd'),
|
||||
create_dynamic_parameter_fields('bollinger_bands'),
|
||||
|
||||
html.Hr(),
|
||||
# Styling Section
|
||||
@ -85,46 +166,4 @@ def create_indicator_modal():
|
||||
], id='indicator-modal', size="lg", is_open=False),
|
||||
])
|
||||
|
||||
def create_parameter_fields():
|
||||
"""Helper function to create parameter input fields for all indicator types."""
|
||||
return html.Div([
|
||||
# SMA Parameters
|
||||
html.Div([
|
||||
dbc.Label("Period:"),
|
||||
dcc.Input(id='sma-period-input', type='number', value=20, min=1, max=200),
|
||||
dbc.FormText("Number of periods for Simple Moving Average calculation")
|
||||
], id='sma-parameters', style={'display': 'none'}, className="mb-3"),
|
||||
|
||||
# EMA Parameters
|
||||
html.Div([
|
||||
dbc.Label("Period:"),
|
||||
dcc.Input(id='ema-period-input', type='number', value=12, min=1, max=200),
|
||||
dbc.FormText("Number of periods for Exponential Moving Average calculation")
|
||||
], id='ema-parameters', style={'display': 'none'}, className="mb-3"),
|
||||
|
||||
# RSI Parameters
|
||||
html.Div([
|
||||
dbc.Label("Period:"),
|
||||
dcc.Input(id='rsi-period-input', type='number', value=14, min=2, max=50),
|
||||
dbc.FormText("Number of periods for RSI calculation (typically 14)")
|
||||
], id='rsi-parameters', style={'display': 'none'}, className="mb-3"),
|
||||
|
||||
# MACD Parameters
|
||||
html.Div([
|
||||
dbc.Row([
|
||||
dbc.Col([dbc.Label("Fast Period:"), dcc.Input(id='macd-fast-period-input', type='number', value=12)], width=4),
|
||||
dbc.Col([dbc.Label("Slow Period:"), dcc.Input(id='macd-slow-period-input', type='number', value=26)], width=4),
|
||||
dbc.Col([dbc.Label("Signal Period:"), dcc.Input(id='macd-signal-period-input', type='number', value=9)], width=4),
|
||||
]),
|
||||
dbc.FormText("MACD periods: Fast EMA, Slow EMA, and Signal line")
|
||||
], id='macd-parameters', style={'display': 'none'}, className="mb-3"),
|
||||
|
||||
# Bollinger Bands Parameters
|
||||
html.Div([
|
||||
dbc.Row([
|
||||
dbc.Col([dbc.Label("Period:"), dcc.Input(id='bb-period-input', type='number', value=20)], width=6),
|
||||
dbc.Col([dbc.Label("Standard Deviation:"), dcc.Input(id='bb-stddev-input', type='number', value=2.0, step=0.1)], width=6),
|
||||
]),
|
||||
dbc.FormText("Period for middle line (SMA) and standard deviation multiplier")
|
||||
], id='bb-parameters', style={'display': 'none'}, className="mb-3")
|
||||
])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user