""" 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")