""" 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 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() 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 - now fully dynamic! @app.callback( 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.""" return get_parameter_visibility_styles(indicator_type) # Save indicator callback - now fully dynamic! @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'), 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, 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 try: # Get indicator manager from components.charts.indicator_manager import get_indicator_manager manager = get_indicator_manager() # 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] # 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 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 - now fully dynamic! @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')] + 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 ) 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 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'] 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'} # 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] + parameter_values ) else: 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}") 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 - now fully dynamic! @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)] + 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.""" # 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")