2025-06-04 13:30:16 +08:00
|
|
|
"""
|
|
|
|
|
Indicator-related callbacks for the dashboard.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import dash
|
2025-06-06 13:33:59 +08:00
|
|
|
from dash import Output, Input, State, html, dcc, callback_context, no_update
|
|
|
|
|
import dash_bootstrap_components as dbc
|
2025-06-04 13:30:16 +08:00
|
|
|
import json
|
|
|
|
|
from utils.logger import get_logger
|
|
|
|
|
|
2025-06-04 17:03:35 +08:00
|
|
|
logger = get_logger("default_logger")
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_indicator_callbacks(app):
|
|
|
|
|
"""Register indicator-related callbacks."""
|
|
|
|
|
|
|
|
|
|
# Modal control callbacks
|
|
|
|
|
@app.callback(
|
2025-06-06 13:33:59 +08:00
|
|
|
Output('indicator-modal', 'is_open'),
|
|
|
|
|
[Input('add-indicator-btn-visible', 'n_clicks'),
|
2025-06-04 13:30:16 +08:00
|
|
|
Input('cancel-indicator-btn', 'n_clicks'),
|
2025-06-06 13:33:59 +08:00
|
|
|
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
|
2025-06-04 13:30:16 +08:00
|
|
|
)
|
2025-06-06 13:33:59 +08:00
|
|
|
def toggle_indicator_modal(add_clicks, cancel_clicks, save_clicks, edit_clicks, is_open):
|
2025-06-04 13:30:16 +08:00
|
|
|
"""Toggle the visibility of the add indicator modal."""
|
2025-06-06 13:33:59 +08:00
|
|
|
ctx = callback_context
|
2025-06-04 13:30:16 +08:00
|
|
|
if not ctx.triggered:
|
2025-06-06 13:33:59 +08:00
|
|
|
return is_open
|
|
|
|
|
|
2025-06-04 13:30:16 +08:00
|
|
|
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
|
|
|
|
|
2025-06-06 13:33:59 +08:00
|
|
|
# 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
|
2025-06-04 13:30:16 +08:00
|
|
|
|
2025-06-06 13:33:59 +08:00
|
|
|
# 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
|
|
|
|
|
|
2025-06-04 13:30:16 +08:00
|
|
|
# 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'),
|
2025-06-11 18:52:02 +08:00
|
|
|
Output('bollinger_bands-parameters', 'style')],
|
2025-06-04 13:30:16 +08:00
|
|
|
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
|
2025-06-11 18:52:02 +08:00
|
|
|
hidden_style = {'display': 'none'}
|
|
|
|
|
visible_style = {'display': 'block'}
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# 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'),
|
2025-06-06 15:06:17 +08:00
|
|
|
State('indicator-timeframe-dropdown', 'value'),
|
2025-06-04 13:30:16 +08:00
|
|
|
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
|
2025-06-11 18:52:02 +08:00
|
|
|
State('bollinger_bands-period-input', 'value'),
|
|
|
|
|
State('bollinger_bands-std-dev-input', 'value'),
|
2025-06-04 13:30:16 +08:00
|
|
|
# Edit mode data
|
|
|
|
|
State('edit-indicator-store', 'data')],
|
|
|
|
|
prevent_initial_call=True
|
|
|
|
|
)
|
2025-06-06 15:06:17 +08:00
|
|
|
def save_new_indicator(n_clicks, name, indicator_type, description, timeframe, color, line_width,
|
2025-06-04 13:30:16 +08:00
|
|
|
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:
|
2025-06-06 13:33:59 +08:00
|
|
|
return "", no_update, no_update
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 13:33:59 +08:00
|
|
|
feedback_msg = None
|
2025-06-04 13:30:16 +08:00
|
|
|
# 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,
|
2025-06-06 15:06:17 +08:00
|
|
|
styling={'color': color or "#007bff", 'line_width': line_width or 2},
|
|
|
|
|
timeframe=timeframe or None
|
2025-06-04 13:30:16 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if success:
|
2025-06-06 13:33:59 +08:00
|
|
|
feedback_msg = dbc.Alert(f"Indicator '{name}' updated successfully!", color="success")
|
2025-06-04 13:30:16 +08:00
|
|
|
else:
|
2025-06-06 13:33:59 +08:00
|
|
|
feedback_msg = dbc.Alert("Failed to update indicator.", color="danger")
|
|
|
|
|
return feedback_msg, no_update, no_update
|
2025-06-04 13:30:16 +08:00
|
|
|
else:
|
|
|
|
|
# Create new indicator
|
|
|
|
|
new_indicator = manager.create_indicator(
|
|
|
|
|
name=name,
|
|
|
|
|
indicator_type=indicator_type,
|
|
|
|
|
parameters=parameters,
|
|
|
|
|
description=description or "",
|
2025-06-06 15:06:17 +08:00
|
|
|
color=color or "#007bff",
|
|
|
|
|
timeframe=timeframe or None
|
2025-06-04 13:30:16 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not new_indicator:
|
2025-06-06 13:33:59 +08:00
|
|
|
feedback_msg = dbc.Alert("Failed to save indicator.", color="danger")
|
|
|
|
|
return feedback_msg, no_update, no_update
|
2025-06-04 13:30:16 +08:00
|
|
|
|
2025-06-06 13:33:59 +08:00
|
|
|
feedback_msg = dbc.Alert(f"Indicator '{name}' saved successfully!", color="success")
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# 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})
|
|
|
|
|
|
2025-06-06 13:33:59 +08:00
|
|
|
return feedback_msg, overlay_options, subplot_options
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-06-04 17:03:35 +08:00
|
|
|
logger.error(f"Indicator callback: Error saving indicator: {e}")
|
2025-06-06 13:33:59 +08:00
|
|
|
error_msg = dbc.Alert(f"Error: {str(e)}", color="danger")
|
|
|
|
|
return error_msg, no_update, no_update
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# 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",
|
2025-06-06 13:33:59 +08:00
|
|
|
className="btn btn-sm btn-outline-primary",
|
|
|
|
|
style={'margin-left': '5px'}
|
2025-06-04 13:30:16 +08:00
|
|
|
),
|
|
|
|
|
html.Button(
|
|
|
|
|
"🗑️",
|
|
|
|
|
id={'type': 'delete-indicator-btn', 'index': indicator_id},
|
|
|
|
|
title="Delete indicator",
|
2025-06-06 13:33:59 +08:00
|
|
|
className="btn btn-sm btn-outline-danger",
|
|
|
|
|
style={'margin-left': '5px'}
|
2025-06-04 13:30:16 +08:00
|
|
|
)
|
|
|
|
|
], 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."""
|
2025-06-06 13:33:59 +08:00
|
|
|
ctx = callback_context
|
2025-06-04 13:30:16 +08:00
|
|
|
if not ctx.triggered or not any(delete_clicks):
|
2025-06-06 13:33:59 +08:00
|
|
|
return no_update, no_update, no_update
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# 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})
|
|
|
|
|
|
2025-06-06 13:33:59 +08:00
|
|
|
success_msg = dbc.Alert(f"Indicator '{indicator_name}' deleted.", color="warning")
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
return success_msg, overlay_options, subplot_options
|
|
|
|
|
else:
|
2025-06-06 13:33:59 +08:00
|
|
|
error_msg = dbc.Alert("Failed to delete indicator.", color="danger")
|
|
|
|
|
return error_msg, no_update, no_update
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-06-04 17:03:35 +08:00
|
|
|
logger.error(f"Indicator callback: Error deleting indicator: {e}")
|
2025-06-06 13:33:59 +08:00
|
|
|
error_msg = dbc.Alert(f"Error: {str(e)}", color="danger")
|
|
|
|
|
return error_msg, no_update, no_update
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# 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'),
|
2025-06-06 15:06:17 +08:00
|
|
|
Output('indicator-timeframe-dropdown', 'value'),
|
2025-06-04 13:30:16 +08:00
|
|
|
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'),
|
2025-06-11 18:52:02 +08:00
|
|
|
Output('bollinger_bands-period-input', 'value'),
|
|
|
|
|
Output('bollinger_bands-std-dev-input', 'value')],
|
2025-06-04 13:30:16 +08:00
|
|
|
[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."""
|
2025-06-06 13:33:59 +08:00
|
|
|
ctx = callback_context
|
2025-06-04 13:30:16 +08:00
|
|
|
if not ctx.triggered or not any(edit_clicks):
|
2025-06-06 15:06:17 +08:00
|
|
|
return [no_update] * 15
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# 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
|
2025-06-06 15:06:17 +08:00
|
|
|
edit_data = {'indicator_id': indicator_id, 'mode': 'edit'}
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# Extract parameter values based on indicator type
|
|
|
|
|
params = indicator.parameters
|
|
|
|
|
|
|
|
|
|
# Default parameter values
|
2025-06-06 15:06:17 +08:00
|
|
|
sma_period = None
|
|
|
|
|
ema_period = None
|
|
|
|
|
rsi_period = None
|
|
|
|
|
macd_fast = None
|
|
|
|
|
macd_slow = None
|
|
|
|
|
macd_signal = None
|
|
|
|
|
bb_period = None
|
|
|
|
|
bb_stddev = None
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
# Update with actual saved values
|
|
|
|
|
if indicator.type == 'sma':
|
2025-06-06 15:06:17 +08:00
|
|
|
sma_period = params.get('period')
|
2025-06-04 13:30:16 +08:00
|
|
|
elif indicator.type == 'ema':
|
2025-06-06 15:06:17 +08:00
|
|
|
ema_period = params.get('period')
|
2025-06-04 13:30:16 +08:00
|
|
|
elif indicator.type == 'rsi':
|
2025-06-06 15:06:17 +08:00
|
|
|
rsi_period = params.get('period')
|
2025-06-04 13:30:16 +08:00
|
|
|
elif indicator.type == 'macd':
|
2025-06-06 15:06:17 +08:00
|
|
|
macd_fast = params.get('fast_period')
|
|
|
|
|
macd_slow = params.get('slow_period')
|
|
|
|
|
macd_signal = params.get('signal_period')
|
2025-06-04 13:30:16 +08:00
|
|
|
elif indicator.type == 'bollinger_bands':
|
2025-06-06 15:06:17 +08:00
|
|
|
bb_period = params.get('period')
|
|
|
|
|
bb_stddev = params.get('std_dev')
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
return (
|
2025-06-06 15:06:17 +08:00
|
|
|
f"✏️ Edit Indicator: {indicator.name}",
|
2025-06-04 13:30:16 +08:00
|
|
|
indicator.name,
|
|
|
|
|
indicator.type,
|
|
|
|
|
indicator.description,
|
2025-06-06 15:06:17 +08:00
|
|
|
indicator.timeframe,
|
2025-06-04 13:30:16 +08:00
|
|
|
indicator.styling.color,
|
|
|
|
|
edit_data,
|
|
|
|
|
sma_period,
|
|
|
|
|
ema_period,
|
|
|
|
|
rsi_period,
|
|
|
|
|
macd_fast,
|
|
|
|
|
macd_slow,
|
|
|
|
|
macd_signal,
|
|
|
|
|
bb_period,
|
|
|
|
|
bb_stddev
|
|
|
|
|
)
|
|
|
|
|
else:
|
2025-06-06 15:06:17 +08:00
|
|
|
return [no_update] * 15
|
2025-06-04 13:30:16 +08:00
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-06-04 17:03:35 +08:00
|
|
|
logger.error(f"Indicator callback: Error loading indicator for edit: {e}")
|
2025-06-06 15:06:17 +08:00
|
|
|
return [no_update] * 15
|
2025-06-04 13:30:16 +08:00
|
|
|
|
2025-06-06 13:33:59 +08:00
|
|
|
# Reset modal form when closed or saved
|
2025-06-04 13:30:16 +08:00
|
|
|
@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),
|
2025-06-06 15:06:17 +08:00
|
|
|
Output('indicator-timeframe-dropdown', 'value', allow_duplicate=True),
|
2025-06-04 13:30:16 +08:00
|
|
|
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),
|
2025-06-11 18:52:02 +08:00
|
|
|
Output('bollinger_bands-period-input', 'value', allow_duplicate=True),
|
|
|
|
|
Output('bollinger_bands-std-dev-input', 'value', allow_duplicate=True)],
|
2025-06-06 13:33:59 +08:00
|
|
|
[Input('cancel-indicator-btn', 'n_clicks'),
|
|
|
|
|
Input('save-indicator-btn', 'n_clicks')], # Also reset on successful save
|
2025-06-04 13:30:16 +08:00
|
|
|
prevent_initial_call=True
|
|
|
|
|
)
|
2025-06-06 13:33:59 +08:00
|
|
|
def reset_modal_form(cancel_clicks, save_clicks):
|
2025-06-06 15:06:17 +08:00
|
|
|
"""Reset the modal form to its default state."""
|
|
|
|
|
return "", "", "", "", "", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0
|
2025-06-04 13:30:16 +08:00
|
|
|
|
2025-06-04 17:03:35 +08:00
|
|
|
logger.info("Indicator callbacks: registered successfully")
|