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.
This commit is contained in:
@@ -7,6 +7,16 @@ from dash import Output, Input, State, html, dcc, callback_context, no_update
|
||||
import dash_bootstrap_components as dbc
|
||||
import json
|
||||
from utils.logger import get_logger
|
||||
from config.indicators.config_utils import (
|
||||
get_parameter_field_outputs,
|
||||
get_parameter_field_states,
|
||||
get_parameter_field_edit_outputs,
|
||||
get_parameter_field_reset_outputs,
|
||||
get_parameter_visibility_styles,
|
||||
collect_parameter_values,
|
||||
set_parameter_values,
|
||||
reset_parameter_values
|
||||
)
|
||||
|
||||
logger = get_logger("default_logger")
|
||||
|
||||
@@ -46,48 +56,17 @@ def register_indicator_callbacks(app):
|
||||
|
||||
return is_open
|
||||
|
||||
# Update parameter fields based on indicator type
|
||||
# Update parameter fields based on indicator type - now fully dynamic!
|
||||
@app.callback(
|
||||
[Output('indicator-parameters-message', 'style'),
|
||||
Output('sma-parameters', 'style'),
|
||||
Output('ema-parameters', 'style'),
|
||||
Output('rsi-parameters', 'style'),
|
||||
Output('macd-parameters', 'style'),
|
||||
Output('bollinger_bands-parameters', 'style')],
|
||||
get_parameter_field_outputs(),
|
||||
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'}
|
||||
visible_style = {'display': 'block'}
|
||||
|
||||
# Default message visibility
|
||||
message_style = {'display': 'block'} if not indicator_type else {'display': 'none'}
|
||||
|
||||
# Initialize all as hidden
|
||||
sma_style = hidden_style
|
||||
ema_style = hidden_style
|
||||
rsi_style = hidden_style
|
||||
macd_style = hidden_style
|
||||
bb_style = hidden_style
|
||||
|
||||
# Show the relevant parameter section
|
||||
if indicator_type == 'sma':
|
||||
sma_style = visible_style
|
||||
elif indicator_type == 'ema':
|
||||
ema_style = visible_style
|
||||
elif indicator_type == 'rsi':
|
||||
rsi_style = visible_style
|
||||
elif indicator_type == 'macd':
|
||||
macd_style = visible_style
|
||||
elif indicator_type == 'bollinger_bands':
|
||||
bb_style = visible_style
|
||||
|
||||
return message_style, sma_style, ema_style, rsi_style, macd_style, bb_style
|
||||
return get_parameter_visibility_styles(indicator_type)
|
||||
|
||||
# Save indicator callback
|
||||
# Save indicator callback - now fully dynamic!
|
||||
@app.callback(
|
||||
[Output('save-indicator-feedback', 'children'),
|
||||
Output('overlay-indicators-checklist', 'options'),
|
||||
@@ -99,27 +78,10 @@ def register_indicator_callbacks(app):
|
||||
State('indicator-timeframe-dropdown', 'value'),
|
||||
State('indicator-color-input', 'value'),
|
||||
State('indicator-line-width-slider', 'value'),
|
||||
# SMA parameters
|
||||
State('sma-period-input', 'value'),
|
||||
# EMA parameters
|
||||
State('ema-period-input', 'value'),
|
||||
# RSI parameters
|
||||
State('rsi-period-input', 'value'),
|
||||
# MACD parameters
|
||||
State('macd-fast-period-input', 'value'),
|
||||
State('macd-slow-period-input', 'value'),
|
||||
State('macd-signal-period-input', 'value'),
|
||||
# Bollinger Bands parameters
|
||||
State('bollinger_bands-period-input', 'value'),
|
||||
State('bollinger_bands-std-dev-input', 'value'),
|
||||
# Edit mode data
|
||||
State('edit-indicator-store', 'data')],
|
||||
State('edit-indicator-store', 'data')] + get_parameter_field_states(),
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def save_new_indicator(n_clicks, name, indicator_type, description, timeframe, color, line_width,
|
||||
sma_period, ema_period, rsi_period,
|
||||
macd_fast, macd_slow, macd_signal,
|
||||
bb_period, bb_stddev, edit_data):
|
||||
def save_new_indicator(n_clicks, name, indicator_type, description, timeframe, color, line_width, edit_data, *parameter_values):
|
||||
"""Save a new indicator or update an existing one."""
|
||||
if not n_clicks or not name or not indicator_type:
|
||||
return "", no_update, no_update
|
||||
@@ -129,26 +91,16 @@ def register_indicator_callbacks(app):
|
||||
from components.charts.indicator_manager import get_indicator_manager
|
||||
manager = get_indicator_manager()
|
||||
|
||||
# Collect parameters based on indicator type and actual input values
|
||||
parameters = {}
|
||||
# Create mapping of parameter field IDs to values
|
||||
parameter_states = get_parameter_field_states()
|
||||
all_parameter_values = {}
|
||||
for i, state in enumerate(parameter_states):
|
||||
if i < len(parameter_values):
|
||||
field_id = state.component_id
|
||||
all_parameter_values[field_id] = parameter_values[i]
|
||||
|
||||
if indicator_type == 'sma':
|
||||
parameters = {'period': sma_period or 20}
|
||||
elif indicator_type == 'ema':
|
||||
parameters = {'period': ema_period or 12}
|
||||
elif indicator_type == 'rsi':
|
||||
parameters = {'period': rsi_period or 14}
|
||||
elif indicator_type == 'macd':
|
||||
parameters = {
|
||||
'fast_period': macd_fast or 12,
|
||||
'slow_period': macd_slow or 26,
|
||||
'signal_period': macd_signal or 9
|
||||
}
|
||||
elif indicator_type == 'bollinger_bands':
|
||||
parameters = {
|
||||
'period': bb_period or 20,
|
||||
'std_dev': bb_stddev or 2.0
|
||||
}
|
||||
# Collect parameters for the specific indicator type
|
||||
parameters = collect_parameter_values(indicator_type, all_parameter_values)
|
||||
|
||||
feedback_msg = None
|
||||
# Check if this is an edit operation
|
||||
@@ -381,7 +333,7 @@ def register_indicator_callbacks(app):
|
||||
error_msg = dbc.Alert(f"Error: {str(e)}", color="danger")
|
||||
return error_msg, no_update, no_update
|
||||
|
||||
# Handle edit indicator - open modal with existing data
|
||||
# Handle edit indicator - open modal with existing data - now fully dynamic!
|
||||
@app.callback(
|
||||
[Output('modal-title', 'children'),
|
||||
Output('indicator-name-input', 'value'),
|
||||
@@ -389,16 +341,7 @@ def register_indicator_callbacks(app):
|
||||
Output('indicator-description-input', 'value'),
|
||||
Output('indicator-timeframe-dropdown', 'value'),
|
||||
Output('indicator-color-input', 'value'),
|
||||
Output('edit-indicator-store', 'data'),
|
||||
# Add parameter field outputs
|
||||
Output('sma-period-input', 'value'),
|
||||
Output('ema-period-input', 'value'),
|
||||
Output('rsi-period-input', 'value'),
|
||||
Output('macd-fast-period-input', 'value'),
|
||||
Output('macd-slow-period-input', 'value'),
|
||||
Output('macd-signal-period-input', 'value'),
|
||||
Output('bollinger_bands-period-input', 'value'),
|
||||
Output('bollinger_bands-std-dev-input', 'value')],
|
||||
Output('edit-indicator-store', 'data')] + get_parameter_field_edit_outputs(),
|
||||
[Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')],
|
||||
[State({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'id')],
|
||||
prevent_initial_call=True
|
||||
@@ -407,7 +350,10 @@ def register_indicator_callbacks(app):
|
||||
"""Load indicator data for editing."""
|
||||
ctx = callback_context
|
||||
if not ctx.triggered or not any(edit_clicks):
|
||||
return [no_update] * 15
|
||||
# Return the correct number of no_updates for all outputs
|
||||
basic_outputs = 7 # Modal title, name, type, description, timeframe, color, edit_data
|
||||
parameter_outputs = len(get_parameter_field_edit_outputs())
|
||||
return [no_update] * (basic_outputs + parameter_outputs)
|
||||
|
||||
# Find which button was clicked
|
||||
triggered_id = ctx.triggered[0]['prop_id']
|
||||
@@ -424,59 +370,31 @@ def register_indicator_callbacks(app):
|
||||
# Store indicator ID for update
|
||||
edit_data = {'indicator_id': indicator_id, 'mode': 'edit'}
|
||||
|
||||
# Extract parameter values based on indicator type
|
||||
params = indicator.parameters
|
||||
|
||||
# Default parameter values
|
||||
sma_period = None
|
||||
ema_period = None
|
||||
rsi_period = None
|
||||
macd_fast = None
|
||||
macd_slow = None
|
||||
macd_signal = None
|
||||
bb_period = None
|
||||
bb_stddev = None
|
||||
|
||||
# Update with actual saved values
|
||||
if indicator.type == 'sma':
|
||||
sma_period = params.get('period')
|
||||
elif indicator.type == 'ema':
|
||||
ema_period = params.get('period')
|
||||
elif indicator.type == 'rsi':
|
||||
rsi_period = params.get('period')
|
||||
elif indicator.type == 'macd':
|
||||
macd_fast = params.get('fast_period')
|
||||
macd_slow = params.get('slow_period')
|
||||
macd_signal = params.get('signal_period')
|
||||
elif indicator.type == 'bollinger_bands':
|
||||
bb_period = params.get('period')
|
||||
bb_stddev = params.get('std_dev')
|
||||
# Generate parameter values for all fields
|
||||
parameter_values = set_parameter_values(indicator.type, indicator.parameters)
|
||||
|
||||
# Return all values: basic fields + dynamic parameter fields
|
||||
return (
|
||||
f"✏️ Edit Indicator: {indicator.name}",
|
||||
indicator.name,
|
||||
indicator.type,
|
||||
indicator.description,
|
||||
indicator.timeframe,
|
||||
indicator.styling.color,
|
||||
edit_data,
|
||||
sma_period,
|
||||
ema_period,
|
||||
rsi_period,
|
||||
macd_fast,
|
||||
macd_slow,
|
||||
macd_signal,
|
||||
bb_period,
|
||||
bb_stddev
|
||||
[f"✏️ Edit Indicator: {indicator.name}",
|
||||
indicator.name,
|
||||
indicator.type,
|
||||
indicator.description,
|
||||
indicator.timeframe,
|
||||
indicator.styling.color,
|
||||
edit_data] + parameter_values
|
||||
)
|
||||
else:
|
||||
return [no_update] * 15
|
||||
basic_outputs = 7
|
||||
parameter_outputs = len(get_parameter_field_edit_outputs())
|
||||
return [no_update] * (basic_outputs + parameter_outputs)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Indicator callback: Error loading indicator for edit: {e}")
|
||||
return [no_update] * 15
|
||||
basic_outputs = 7
|
||||
parameter_outputs = len(get_parameter_field_edit_outputs())
|
||||
return [no_update] * (basic_outputs + parameter_outputs)
|
||||
|
||||
# Reset modal form when closed or saved
|
||||
# Reset modal form when closed or saved - now fully dynamic!
|
||||
@app.callback(
|
||||
[Output('indicator-name-input', 'value', allow_duplicate=True),
|
||||
Output('indicator-type-dropdown', 'value', allow_duplicate=True),
|
||||
@@ -485,22 +403,19 @@ def register_indicator_callbacks(app):
|
||||
Output('indicator-color-input', 'value', allow_duplicate=True),
|
||||
Output('indicator-line-width-slider', 'value'),
|
||||
Output('modal-title', 'children', allow_duplicate=True),
|
||||
Output('edit-indicator-store', 'data', allow_duplicate=True),
|
||||
# Add parameter field resets
|
||||
Output('sma-period-input', 'value', allow_duplicate=True),
|
||||
Output('ema-period-input', 'value', allow_duplicate=True),
|
||||
Output('rsi-period-input', 'value', allow_duplicate=True),
|
||||
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('bollinger_bands-period-input', 'value', allow_duplicate=True),
|
||||
Output('bollinger_bands-std-dev-input', 'value', allow_duplicate=True)],
|
||||
Output('edit-indicator-store', 'data', allow_duplicate=True)] + get_parameter_field_reset_outputs(),
|
||||
[Input('cancel-indicator-btn', 'n_clicks'),
|
||||
Input('save-indicator-btn', 'n_clicks')], # Also reset on successful save
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def reset_modal_form(cancel_clicks, save_clicks):
|
||||
"""Reset the modal form to its default state."""
|
||||
return "", "", "", "", "", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0
|
||||
# Basic form reset values
|
||||
basic_values = ["", "", "", "", "", 2, "📊 Add New Indicator", None]
|
||||
|
||||
# Dynamic parameter reset values
|
||||
parameter_values = reset_parameter_values()
|
||||
|
||||
return basic_values + parameter_values
|
||||
|
||||
logger.info("Indicator callbacks: registered successfully")
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
from config.indicators.config_utils import get_indicator_dropdown_options, generate_parameter_fields_config, load_indicator_templates
|
||||
|
||||
|
||||
def create_dynamic_parameter_fields(indicator_type: str) -> html.Div:
|
||||
@@ -137,12 +137,8 @@ def create_indicator_modal():
|
||||
children=[html.P("Select an indicator type to configure parameters", className="text-muted fst-italic")]
|
||||
),
|
||||
|
||||
# Parameter fields (SMA, EMA, etc.)
|
||||
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'),
|
||||
# Dynamically generate parameter fields for all indicator types
|
||||
*[create_dynamic_parameter_fields(indicator_type) for indicator_type in load_indicator_templates().keys()],
|
||||
|
||||
html.Hr(),
|
||||
# Styling Section
|
||||
|
||||
Reference in New Issue
Block a user