diff --git a/config/indicators/config_utils.py b/config/indicators/config_utils.py new file mode 100644 index 0000000..d4c3638 --- /dev/null +++ b/config/indicators/config_utils.py @@ -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 \ No newline at end of file diff --git a/config/indicators/user_indicators/ema_de4fc14c.json b/config/indicators/user_indicators/ema_de4fc14c.json index c08e9b3..c2eeb30 100644 --- a/config/indicators/user_indicators/ema_de4fc14c.json +++ b/config/indicators/user_indicators/ema_de4fc14c.json @@ -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" } \ No newline at end of file diff --git a/dashboard/callbacks/indicators.py b/dashboard/callbacks/indicators.py index e502cd1..c66aeb9 100644 --- a/dashboard/callbacks/indicators.py +++ b/dashboard/callbacks/indicators.py @@ -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 diff --git a/dashboard/components/indicator_modal.py b/dashboard/components/indicator_modal.py index a757ac4..3697ba4 100644 --- a/dashboard/components/indicator_modal.py +++ b/dashboard/components/indicator_modal.py @@ -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") - ]) \ No newline at end of file + \ No newline at end of file