Vasily.onl 89b071230e 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.
2025-06-11 18:52:02 +08:00

506 lines
22 KiB
Python

"""
Indicator-related callbacks for the dashboard.
"""
import dash
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
logger = get_logger("default_logger")
def register_indicator_callbacks(app):
"""Register indicator-related callbacks."""
# Modal control callbacks
@app.callback(
Output('indicator-modal', 'is_open'),
[Input('add-indicator-btn-visible', 'n_clicks'),
Input('cancel-indicator-btn', 'n_clicks'),
Input('save-indicator-btn', 'n_clicks'),
Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')],
[State('indicator-modal', 'is_open')],
prevent_initial_call=True
)
def toggle_indicator_modal(add_clicks, cancel_clicks, save_clicks, edit_clicks, is_open):
"""Toggle the visibility of the add indicator modal."""
ctx = callback_context
if not ctx.triggered:
return is_open
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
# Check for add button click
if triggered_id == 'add-indicator-btn-visible' and add_clicks:
return True
# Check for edit button clicks, ensuring a click actually happened
if 'edit-indicator-btn' in triggered_id and any(c for c in edit_clicks if c is not None):
return True
# Check for cancel or save clicks to close the modal
if triggered_id in ['cancel-indicator-btn', 'save-indicator-btn']:
return False
return is_open
# Update parameter fields based on indicator type
@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')],
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
# Save indicator callback
@app.callback(
[Output('save-indicator-feedback', 'children'),
Output('overlay-indicators-checklist', 'options'),
Output('subplot-indicators-checklist', 'options')],
Input('save-indicator-btn', 'n_clicks'),
[State('indicator-name-input', 'value'),
State('indicator-type-dropdown', 'value'),
State('indicator-description-input', 'value'),
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')],
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):
"""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
try:
# Get indicator manager
from components.charts.indicator_manager import get_indicator_manager
manager = get_indicator_manager()
# Collect parameters based on indicator type and actual input values
parameters = {}
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
}
feedback_msg = None
# Check if this is an edit operation
is_edit = edit_data and edit_data.get('mode') == 'edit'
if is_edit:
# Update existing indicator
indicator_id = edit_data.get('indicator_id')
success = manager.update_indicator(
indicator_id,
name=name,
description=description or "",
parameters=parameters,
styling={'color': color or "#007bff", 'line_width': line_width or 2},
timeframe=timeframe or None
)
if success:
feedback_msg = dbc.Alert(f"Indicator '{name}' updated successfully!", color="success")
else:
feedback_msg = dbc.Alert("Failed to update indicator.", color="danger")
return feedback_msg, no_update, no_update
else:
# Create new indicator
new_indicator = manager.create_indicator(
name=name,
indicator_type=indicator_type,
parameters=parameters,
description=description or "",
color=color or "#007bff",
timeframe=timeframe or None
)
if not new_indicator:
feedback_msg = dbc.Alert("Failed to save indicator.", color="danger")
return feedback_msg, no_update, no_update
feedback_msg = dbc.Alert(f"Indicator '{name}' saved successfully!", color="success")
# Refresh the indicator options
overlay_indicators = manager.get_indicators_by_type('overlay')
subplot_indicators = manager.get_indicators_by_type('subplot')
overlay_options = []
for indicator in overlay_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
overlay_options.append({'label': display_name, 'value': indicator.id})
subplot_options = []
for indicator in subplot_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
subplot_options.append({'label': display_name, 'value': indicator.id})
return feedback_msg, overlay_options, subplot_options
except Exception as e:
logger.error(f"Indicator callback: Error saving indicator: {e}")
error_msg = dbc.Alert(f"Error: {str(e)}", color="danger")
return error_msg, no_update, no_update
# Update custom indicator lists with edit/delete buttons
@app.callback(
[Output('overlay-indicators-list', 'children'),
Output('subplot-indicators-list', 'children')],
[Input('overlay-indicators-checklist', 'options'),
Input('subplot-indicators-checklist', 'options'),
Input('overlay-indicators-checklist', 'value'),
Input('subplot-indicators-checklist', 'value')]
)
def update_custom_indicator_lists(overlay_options, subplot_options, overlay_values, subplot_values):
"""Create custom indicator lists with edit and delete buttons."""
def create_indicator_item(option, is_checked):
"""Create a single indicator item with checkbox and buttons."""
indicator_id = option['value']
indicator_name = option['label']
return html.Div([
# Checkbox and name
html.Div([
dcc.Checklist(
options=[{'label': '', 'value': indicator_id}],
value=[indicator_id] if is_checked else [],
id={'type': 'indicator-checkbox', 'index': indicator_id},
style={'display': 'inline-block', 'margin-right': '8px'}
),
html.Span(indicator_name, style={'display': 'inline-block', 'vertical-align': 'top'})
], style={'display': 'inline-block', 'width': '70%'}),
# Edit and Delete buttons
html.Div([
html.Button(
"✏️",
id={'type': 'edit-indicator-btn', 'index': indicator_id},
title="Edit indicator",
className="btn btn-sm btn-outline-primary",
style={'margin-left': '5px'}
),
html.Button(
"🗑️",
id={'type': 'delete-indicator-btn', 'index': indicator_id},
title="Delete indicator",
className="btn btn-sm btn-outline-danger",
style={'margin-left': '5px'}
)
], style={'display': 'inline-block', 'width': '30%', 'text-align': 'right'})
], style={
'display': 'block',
'padding': '5px 0',
'border-bottom': '1px solid #f0f0f0',
'margin-bottom': '5px'
})
# Create overlay indicators list
overlay_list = []
for option in overlay_options:
is_checked = option['value'] in (overlay_values or [])
overlay_list.append(create_indicator_item(option, is_checked))
# Create subplot indicators list
subplot_list = []
for option in subplot_options:
is_checked = option['value'] in (subplot_values or [])
subplot_list.append(create_indicator_item(option, is_checked))
return overlay_list, subplot_list
# Sync individual indicator checkboxes with main checklist
@app.callback(
Output('overlay-indicators-checklist', 'value', allow_duplicate=True),
[Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')],
[State('overlay-indicators-checklist', 'options')],
prevent_initial_call=True
)
def sync_overlay_indicators(checkbox_values, overlay_options):
"""Sync individual indicator checkboxes with main overlay checklist."""
if not checkbox_values or not overlay_options:
return []
selected_indicators = []
overlay_ids = [opt['value'] for opt in overlay_options]
# Flatten the checkbox values and filter for overlay indicators
for values in checkbox_values:
if values: # values is a list, check if not empty
for indicator_id in values:
if indicator_id in overlay_ids:
selected_indicators.append(indicator_id)
# Remove duplicates
return list(set(selected_indicators))
@app.callback(
Output('subplot-indicators-checklist', 'value', allow_duplicate=True),
[Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')],
[State('subplot-indicators-checklist', 'options')],
prevent_initial_call=True
)
def sync_subplot_indicators(checkbox_values, subplot_options):
"""Sync individual indicator checkboxes with main subplot checklist."""
if not checkbox_values or not subplot_options:
return []
selected_indicators = []
subplot_ids = [opt['value'] for opt in subplot_options]
# Flatten the checkbox values and filter for subplot indicators
for values in checkbox_values:
if values: # values is a list, check if not empty
for indicator_id in values:
if indicator_id in subplot_ids:
selected_indicators.append(indicator_id)
# Remove duplicates
return list(set(selected_indicators))
# Handle delete indicator
@app.callback(
[Output('save-indicator-feedback', 'children', allow_duplicate=True),
Output('overlay-indicators-checklist', 'options', allow_duplicate=True),
Output('subplot-indicators-checklist', 'options', allow_duplicate=True)],
[Input({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'n_clicks')],
[State({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'id')],
prevent_initial_call=True
)
def delete_indicator(delete_clicks, button_ids):
"""Delete an indicator when delete button is clicked."""
ctx = callback_context
if not ctx.triggered or not any(delete_clicks):
return no_update, no_update, no_update
# Find which button was clicked
triggered_id = ctx.triggered[0]['prop_id']
button_info = json.loads(triggered_id.split('.')[0])
indicator_id = button_info['index']
try:
# Get indicator manager and delete the indicator
from components.charts.indicator_manager import get_indicator_manager
manager = get_indicator_manager()
# Load indicator to get its name before deletion
indicator = manager.load_indicator(indicator_id)
indicator_name = indicator.name if indicator else indicator_id
if manager.delete_indicator(indicator_id):
# Refresh the indicator options
overlay_indicators = manager.get_indicators_by_type('overlay')
subplot_indicators = manager.get_indicators_by_type('subplot')
overlay_options = []
for indicator in overlay_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
overlay_options.append({'label': display_name, 'value': indicator.id})
subplot_options = []
for indicator in subplot_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
subplot_options.append({'label': display_name, 'value': indicator.id})
success_msg = dbc.Alert(f"Indicator '{indicator_name}' deleted.", color="warning")
return success_msg, overlay_options, subplot_options
else:
error_msg = dbc.Alert("Failed to delete indicator.", color="danger")
return error_msg, no_update, no_update
except Exception as e:
logger.error(f"Indicator callback: Error deleting indicator: {e}")
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
@app.callback(
[Output('modal-title', 'children'),
Output('indicator-name-input', 'value'),
Output('indicator-type-dropdown', 'value'),
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')],
[Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')],
[State({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'id')],
prevent_initial_call=True
)
def edit_indicator(edit_clicks, button_ids):
"""Load indicator data for editing."""
ctx = callback_context
if not ctx.triggered or not any(edit_clicks):
return [no_update] * 15
# Find which button was clicked
triggered_id = ctx.triggered[0]['prop_id']
button_info = json.loads(triggered_id.split('.')[0])
indicator_id = button_info['index']
try:
# Load the indicator data
from components.charts.indicator_manager import get_indicator_manager
manager = get_indicator_manager()
indicator = manager.load_indicator(indicator_id)
if indicator:
# 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')
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
)
else:
return [no_update] * 15
except Exception as e:
logger.error(f"Indicator callback: Error loading indicator for edit: {e}")
return [no_update] * 15
# Reset modal form when closed or saved
@app.callback(
[Output('indicator-name-input', 'value', allow_duplicate=True),
Output('indicator-type-dropdown', 'value', allow_duplicate=True),
Output('indicator-description-input', 'value', allow_duplicate=True),
Output('indicator-timeframe-dropdown', 'value', allow_duplicate=True),
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)],
[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
logger.info("Indicator callbacks: registered successfully")