From 58a754414a7e615caf2e249f231f2e15d4e3cb11 Mon Sep 17 00:00:00 2001 From: "Vasily.onl" Date: Fri, 6 Jun 2025 13:33:59 +0800 Subject: [PATCH] Removed Mantine for UI, and used bootstrap for simplicity --- .../bollinger_bands_69b378e2.json | 4 +- dashboard/app.py | 6 +- dashboard/callbacks/charts.py | 7 +- dashboard/callbacks/data_analysis.py | 10 +- dashboard/callbacks/indicators.py | 226 ++------ dashboard/callbacks/system_health.py | 542 +++++++----------- dashboard/components/chart_controls.py | 279 +++------ dashboard/components/data_analysis.py | 198 ++----- dashboard/components/indicator_modal.py | 379 ++++-------- dashboard/layouts/system_health.py | 271 +++------ pyproject.toml | 4 +- uv.lock | 40 +- 12 files changed, 682 insertions(+), 1284 deletions(-) diff --git a/config/indicators/user_indicators/bollinger_bands_69b378e2.json b/config/indicators/user_indicators/bollinger_bands_69b378e2.json index 74f6163..ddb38b0 100644 --- a/config/indicators/user_indicators/bollinger_bands_69b378e2.json +++ b/config/indicators/user_indicators/bollinger_bands_69b378e2.json @@ -6,7 +6,7 @@ "display_type": "overlay", "parameters": { "period": 20, - "std_dev": 2.0 + "std_dev": 2 }, "styling": { "color": "#6f42c1", @@ -16,5 +16,5 @@ }, "visible": true, "created_date": "2025-06-04T04:16:35.460105+00:00", - "modified_date": "2025-06-04T04:16:35.460105+00:00" + "modified_date": "2025-06-06T05:32:24.994486+00:00" } \ No newline at end of file diff --git a/dashboard/app.py b/dashboard/app.py index 800a8a5..1a56c0d 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -4,7 +4,7 @@ Main dashboard application module. import dash from dash import html, dcc -import dash_mantine_components as dmc +import dash_bootstrap_components as dbc from utils.logger import get_logger from dashboard.layouts import ( get_market_data_layout, @@ -20,10 +20,10 @@ logger = get_logger("dashboard_app") def create_app(): """Create and configure the Dash application.""" # Initialize Dash app - app = dash.Dash(__name__, suppress_callback_exceptions=True) + app = dash.Dash(__name__, suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.LUX]) # Define the main layout wrapped in MantineProvider - app.layout = dmc.MantineProvider([ + app.layout = html.Div([ html.Div([ # Page title html.H1("🚀 Crypto Trading Bot Dashboard", diff --git a/dashboard/callbacks/charts.py b/dashboard/callbacks/charts.py index 8b374a3..25380b1 100644 --- a/dashboard/callbacks/charts.py +++ b/dashboard/callbacks/charts.py @@ -3,6 +3,7 @@ Chart-related callbacks for the dashboard. """ from dash import Output, Input, State, Patch, ctx, html, no_update, dcc +import dash_bootstrap_components as dbc from datetime import datetime, timedelta from utils.logger import get_logger from components.charts import ( @@ -137,15 +138,15 @@ def register_chart_callbacks(app): ) def update_market_stats(stored_data, symbol, timeframe): if not stored_data: - return html.Div("Statistics will be available once chart data is loaded.") + return dbc.Alert("Statistics will be available once chart data is loaded.", color="info") try: df = pd.read_json(io.StringIO(stored_data), orient='split') if df.empty: - return html.Div("Not enough data to calculate statistics.") + return dbc.Alert("Not enough data to calculate statistics.", color="warning") return get_market_statistics(df, symbol, timeframe) except Exception as e: logger.error(f"Error updating market stats from stored data: {e}", exc_info=True) - return html.Div(f"Error loading statistics: {e}", style={'color': 'red'}) + return dbc.Alert(f"Error loading statistics: {e}", color="danger") @app.callback( Output("download-chart-data", "data"), diff --git a/dashboard/callbacks/data_analysis.py b/dashboard/callbacks/data_analysis.py index 635bbf9..b5d65fb 100644 --- a/dashboard/callbacks/data_analysis.py +++ b/dashboard/callbacks/data_analysis.py @@ -3,7 +3,7 @@ Data analysis callbacks for the dashboard. """ from dash import Output, Input, html, dcc -import dash_mantine_components as dmc +import dash_bootstrap_components as dbc from utils.logger import get_logger from dashboard.components.data_analysis import ( VolumeAnalyzer, @@ -35,14 +35,14 @@ def register_data_analysis_callbacks(app): logger.info(f"🎯 DATA ANALYSIS CALLBACK TRIGGERED! Type: {analysis_type}, Period: {period}") # Return placeholder message since we're moving to enhanced market stats - info_msg = html.Div([ - html.H4("📊 Statistical Analysis"), + info_msg = dbc.Alert([ + html.H4("📊 Statistical Analysis", className="alert-heading"), html.P("Data analysis has been integrated into the Market Statistics section above."), html.P("The enhanced statistics now include volume analysis, price movement analysis, and trend indicators."), html.P("Change the symbol and timeframe in the main chart to see updated analysis."), html.Hr(), - html.Small("This section will be updated with additional analytical tools in future versions.") - ], style={'border': '2px solid #17a2b8', 'padding': '20px', 'margin': '10px', 'background-color': '#d1ecf1'}) + html.P("This section will be updated with additional analytical tools in future versions.", className="mb-0") + ], color="info") return info_msg, html.Div() diff --git a/dashboard/callbacks/indicators.py b/dashboard/callbacks/indicators.py index ed98eff..0efbc83 100644 --- a/dashboard/callbacks/indicators.py +++ b/dashboard/callbacks/indicators.py @@ -3,7 +3,8 @@ Indicator-related callbacks for the dashboard. """ import dash -from dash import Output, Input, State, html, dcc, callback_context +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 @@ -15,106 +16,36 @@ def register_indicator_callbacks(app): # Modal control callbacks @app.callback( - [Output('indicator-modal', 'style'), - Output('indicator-modal-background', 'style')], - [Input('add-indicator-btn', 'n_clicks'), - Input('close-modal-btn', 'n_clicks'), + Output('indicator-modal', 'is_open'), + [Input('add-indicator-btn-visible', 'n_clicks'), Input('cancel-indicator-btn', 'n_clicks'), - Input('edit-indicator-store', 'data')] - ) - def toggle_indicator_modal(add_clicks, close_clicks, cancel_clicks, edit_data): - """Toggle the visibility of the add indicator modal.""" - - # Default hidden styles - hidden_modal_style = { - 'display': 'none', - 'position': 'fixed', - 'z-index': '1001', - 'left': '0', - 'top': '0', - 'width': '100%', - 'height': '100%', - 'visibility': 'hidden' - } - - hidden_background_style = { - 'display': 'none', - 'position': 'fixed', - 'z-index': '1000', - 'left': '0', - 'top': '0', - 'width': '100%', - 'height': '100%', - 'background-color': 'rgba(0,0,0,0.5)', - 'visibility': 'hidden' - } - - # Visible styles - visible_modal_style = { - 'display': 'block', - 'position': 'fixed', - 'z-index': '1001', - 'left': '0', - 'top': '0', - 'width': '100%', - 'height': '100%', - 'visibility': 'visible' - } - - visible_background_style = { - 'display': 'block', - 'position': 'fixed', - 'z-index': '1000', - 'left': '0', - 'top': '0', - 'width': '100%', - 'height': '100%', - 'background-color': 'rgba(0,0,0,0.5)', - 'visibility': 'visible' - } - - ctx = dash.callback_context - - # If no trigger or initial load, return hidden - if not ctx.triggered: - return [hidden_modal_style, hidden_background_style] - - triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] - - # Only open modal if explicitly requested - should_open = False - - # Check if add button was clicked (and has a click count > 0) - if triggered_id == 'add-indicator-btn' and add_clicks and add_clicks > 0: - should_open = True - - # Check if edit button triggered and should open modal - elif triggered_id == 'edit-indicator-store' and edit_data and edit_data.get('open_modal') and edit_data.get('mode') == 'edit': - should_open = True - - # Check if close/cancel buttons were clicked - elif triggered_id in ['close-modal-btn', 'cancel-indicator-btn']: - should_open = False - - # Default: don't open - else: - should_open = False - - if should_open: - return [visible_modal_style, visible_background_style] - else: - return [hidden_modal_style, hidden_background_style] - - # Sync visible button clicks to hidden button - @app.callback( - Output('add-indicator-btn', 'n_clicks'), - Input('add-indicator-btn-visible', '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 sync_add_button_clicks(visible_clicks): - """Sync clicks from visible button to hidden button.""" - return visible_clicks or 0 + 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'), @@ -190,7 +121,7 @@ def register_indicator_callbacks(app): 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 "", dash.no_update, dash.no_update + return "", no_update, no_update try: # Get indicator manager @@ -218,6 +149,7 @@ def register_indicator_callbacks(app): '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' @@ -233,16 +165,10 @@ def register_indicator_callbacks(app): ) if success: - success_msg = html.Div([ - html.Span("✅ ", style={'color': '#28a745'}), - html.Span(f"Indicator '{name}' updated successfully!", style={'color': '#28a745'}) - ]) + feedback_msg = dbc.Alert(f"Indicator '{name}' updated successfully!", color="success") else: - error_msg = html.Div([ - html.Span("❌ ", style={'color': '#dc3545'}), - html.Span("Failed to update indicator. Please try again.", style={'color': '#dc3545'}) - ]) - return error_msg, dash.no_update, dash.no_update + 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( @@ -254,16 +180,10 @@ def register_indicator_callbacks(app): ) if not new_indicator: - error_msg = html.Div([ - html.Span("❌ ", style={'color': '#dc3545'}), - html.Span("Failed to save indicator. Please try again.", style={'color': '#dc3545'}) - ]) - return error_msg, dash.no_update, dash.no_update + feedback_msg = dbc.Alert("Failed to save indicator.", color="danger") + return feedback_msg, no_update, no_update - success_msg = html.Div([ - html.Span("✅ ", style={'color': '#28a745'}), - html.Span(f"Indicator '{name}' saved successfully!", style={'color': '#28a745'}) - ]) + feedback_msg = dbc.Alert(f"Indicator '{name}' saved successfully!", color="success") # Refresh the indicator options overlay_indicators = manager.get_indicators_by_type('overlay') @@ -279,15 +199,12 @@ def register_indicator_callbacks(app): display_name = f"{indicator.name} ({indicator.type.upper()})" subplot_options.append({'label': display_name, 'value': indicator.id}) - return success_msg, overlay_options, subplot_options + return feedback_msg, overlay_options, subplot_options except Exception as e: logger.error(f"Indicator callback: Error saving indicator: {e}") - error_msg = html.Div([ - html.Span("❌ ", style={'color': '#dc3545'}), - html.Span(f"Error: {str(e)}", style={'color': '#dc3545'}) - ]) - return error_msg, dash.no_update, dash.no_update + 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( @@ -324,27 +241,15 @@ def register_indicator_callbacks(app): "✏️", id={'type': 'edit-indicator-btn', 'index': indicator_id}, title="Edit indicator", - style={ - 'background': 'none', - 'border': 'none', - 'cursor': 'pointer', - 'margin-left': '5px', - 'font-size': '14px', - 'color': '#007bff' - } + className="btn btn-sm btn-outline-primary", + style={'margin-left': '5px'} ), html.Button( "🗑️", id={'type': 'delete-indicator-btn', 'index': indicator_id}, title="Delete indicator", - style={ - 'background': 'none', - 'border': 'none', - 'cursor': 'pointer', - 'margin-left': '5px', - 'font-size': '14px', - 'color': '#dc3545' - } + className="btn btn-sm btn-outline-danger", + style={'margin-left': '5px'} ) ], style={'display': 'inline-block', 'width': '30%', 'text-align': 'right'}) ], style={ @@ -428,9 +333,9 @@ def register_indicator_callbacks(app): ) def delete_indicator(delete_clicks, button_ids): """Delete an indicator when delete button is clicked.""" - ctx = dash.callback_context + ctx = callback_context if not ctx.triggered or not any(delete_clicks): - return dash.no_update, dash.no_update, dash.no_update + return no_update, no_update, no_update # Find which button was clicked triggered_id = ctx.triggered[0]['prop_id'] @@ -461,26 +366,17 @@ def register_indicator_callbacks(app): display_name = f"{indicator.name} ({indicator.type.upper()})" subplot_options.append({'label': display_name, 'value': indicator.id}) - success_msg = html.Div([ - html.Span("🗑️ ", style={'color': '#dc3545'}), - html.Span(f"Indicator '{indicator_name}' deleted successfully!", style={'color': '#dc3545'}) - ]) + success_msg = dbc.Alert(f"Indicator '{indicator_name}' deleted.", color="warning") return success_msg, overlay_options, subplot_options else: - error_msg = html.Div([ - html.Span("❌ ", style={'color': '#dc3545'}), - html.Span("Failed to delete indicator.", style={'color': '#dc3545'}) - ]) - return error_msg, dash.no_update, dash.no_update + 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 = html.Div([ - html.Span("❌ ", style={'color': '#dc3545'}), - html.Span(f"Error: {str(e)}", style={'color': '#dc3545'}) - ]) - return error_msg, dash.no_update, dash.no_update + 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( @@ -505,9 +401,9 @@ def register_indicator_callbacks(app): ) def edit_indicator(edit_clicks, button_ids): """Load indicator data for editing.""" - ctx = dash.callback_context + ctx = callback_context if not ctx.triggered or not any(edit_clicks): - return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update # Find which button was clicked triggered_id = ctx.triggered[0]['prop_id'] @@ -569,13 +465,13 @@ def register_indicator_callbacks(app): bb_stddev ) else: - return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update except Exception as e: logger.error(f"Indicator callback: Error loading indicator for edit: {e}") - return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update - # Reset modal form when closed + # 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), @@ -593,14 +489,14 @@ def register_indicator_callbacks(app): Output('macd-signal-period-input', 'value', allow_duplicate=True), Output('bb-period-input', 'value', allow_duplicate=True), Output('bb-stddev-input', 'value', allow_duplicate=True)], - [Input('close-modal-btn', 'n_clicks'), - Input('cancel-indicator-btn', 'n_clicks')], + [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(close_clicks, cancel_clicks): - """Reset the modal form when it's closed.""" - if close_clicks or cancel_clicks: + def reset_modal_form(cancel_clicks, save_clicks): + """Reset the modal form when it's closed or saved.""" + if cancel_clicks or save_clicks: return "", None, "", "#007bff", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0 - return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update + return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update logger.info("Indicator callbacks: registered successfully") \ No newline at end of file diff --git a/dashboard/callbacks/system_health.py b/dashboard/callbacks/system_health.py index 9167540..15a63ad 100644 --- a/dashboard/callbacks/system_health.py +++ b/dashboard/callbacks/system_health.py @@ -9,7 +9,7 @@ import psutil from datetime import datetime, timedelta from typing import Dict, Any, Optional, List from dash import Output, Input, State, html, callback_context, no_update -import dash_mantine_components as dmc +import dash_bootstrap_components as dbc from utils.logger import get_logger from database.connection import DatabaseManager from database.redis_manager import RedisManager @@ -47,7 +47,7 @@ def register_system_health_callbacks(app): except Exception as e: logger.error(f"Error updating quick status: {e}") - error_status = dmc.Badge("🔴 Error", color="red", variant="light") + error_status = dbc.Badge("🔴 Error", color="danger", className="me-1") return error_status, error_status, error_status, error_status # Detailed Data Collection Service Status @@ -67,11 +67,10 @@ def register_system_health_callbacks(app): except Exception as e: logger.error(f"Error updating data collection status: {e}") - error_div = dmc.Alert( + error_div = dbc.Alert( f"Error: {str(e)}", - title="🔴 Status Check Failed", - color="red", - variant="light" + color="danger", + dismissable=True ) return error_div, error_div @@ -87,11 +86,10 @@ def register_system_health_callbacks(app): return _get_individual_collectors_status() except Exception as e: logger.error(f"Error updating individual collectors status: {e}") - return dmc.Alert( + return dbc.Alert( f"Error: {str(e)}", - title="🔴 Collectors Check Failed", - color="red", - variant="light" + color="danger", + dismissable=True ) # Database Status and Statistics @@ -110,11 +108,10 @@ def register_system_health_callbacks(app): except Exception as e: logger.error(f"Error updating database status: {e}") - error_alert = dmc.Alert( + error_alert = dbc.Alert( f"Error: {str(e)}", - title="🔴 Database Check Failed", - color="red", - variant="light" + color="danger", + dismissable=True ) return error_alert, error_alert @@ -134,11 +131,10 @@ def register_system_health_callbacks(app): except Exception as e: logger.error(f"Error updating Redis status: {e}") - error_alert = dmc.Alert( + error_alert = dbc.Alert( f"Error: {str(e)}", - title="🔴 Redis Check Failed", - color="red", - variant="light" + color="danger", + dismissable=True ) return error_alert, error_alert @@ -153,475 +149,365 @@ def register_system_health_callbacks(app): return _get_system_performance_metrics() except Exception as e: logger.error(f"Error updating system performance: {e}") - return dmc.Alert( + return dbc.Alert( f"Error: {str(e)}", - title="🔴 Performance Check Failed", - color="red", - variant="light" + color="danger", + dismissable=True ) # Data Collection Details Modal @app.callback( - [Output("collection-details-modal", "opened"), + [Output("collection-details-modal", "is_open"), Output("collection-details-content", "children")], [Input("view-collection-details-btn", "n_clicks")], - State("collection-details-modal", "opened") + [State("collection-details-modal", "is_open")] ) - def toggle_collection_details_modal(details_clicks, is_open): + def toggle_collection_details_modal(n_clicks, is_open): """Toggle and populate the collection details modal.""" - if details_clicks: - # Load detailed collection information + if n_clicks: details_content = _get_collection_details_content() - return True, details_content + return not is_open, details_content return is_open, no_update # Collection Logs Modal @app.callback( - [Output("collection-logs-modal", "opened"), + [Output("collection-logs-modal", "is_open"), Output("collection-logs-content", "children")], [Input("view-collection-logs-btn", "n_clicks"), - Input("refresh-logs-btn", "n_clicks"), - Input("close-logs-modal", "n_clicks")], - State("collection-logs-modal", "opened") + Input("refresh-logs-btn", "n_clicks")], + [State("collection-logs-modal", "is_open")], + prevent_initial_call=True ) - def toggle_collection_logs_modal(logs_clicks, refresh_clicks, close_clicks, is_open): + def toggle_collection_logs_modal(logs_clicks, refresh_clicks, is_open): """Toggle and populate the collection logs modal.""" - if logs_clicks or refresh_clicks: - # Load recent logs + ctx = callback_context + if not ctx.triggered: + return is_open, no_update + + triggered_id = ctx.triggered_id + if triggered_id in ["view-collection-logs-btn", "refresh-logs-btn"]: logs_content = _get_collection_logs_content() return True, logs_content - elif close_clicks: - return False, no_update + return is_open, no_update + @app.callback( + Output("collection-logs-modal", "is_open", allow_duplicate=True), + Input("close-logs-modal", "n_clicks"), + State("collection-logs-modal", "is_open"), + prevent_initial_call=True + ) + def close_logs_modal(n_clicks, is_open): + if n_clicks: + return not is_open + return is_open + logger.info("Enhanced system health callbacks registered successfully") # Helper Functions -def _get_data_collection_quick_status() -> dmc.Badge: +def _get_data_collection_quick_status() -> dbc.Badge: """Get quick data collection status.""" try: - # Check if data collection service is running (simplified check) is_running = _check_data_collection_service_running() - if is_running: - return dmc.Badge("🟢 Active", color="green", variant="light") + return dbc.Badge("Active", color="success", className="me-1") else: - return dmc.Badge("🔴 Stopped", color="red", variant="light") + return dbc.Badge("Stopped", color="danger", className="me-1") except: - return dmc.Badge("🟡 Unknown", color="yellow", variant="light") + return dbc.Badge("Unknown", color="warning", className="me-1") -def _get_database_quick_status() -> dmc.Badge: +def _get_database_quick_status() -> dbc.Badge: """Get quick database status.""" try: db_manager = DatabaseManager() - db_manager.initialize() # Initialize the database manager - result = db_manager.test_connection() - if result: - return dmc.Badge("🟢 Connected", color="green", variant="light") + db_manager.initialize() + if db_manager.test_connection(): + return dbc.Badge("Connected", color="success", className="me-1") else: - return dmc.Badge("🔴 Error", color="red", variant="light") + return dbc.Badge("Error", color="danger", className="me-1") except: - return dmc.Badge("🔴 Error", color="red", variant="light") + return dbc.Badge("Error", color="danger", className="me-1") -def _get_redis_quick_status() -> dmc.Badge: +def _get_redis_quick_status() -> dbc.Badge: """Get quick Redis status.""" try: redis_manager = RedisManager() - redis_manager.initialize() # Initialize the Redis manager - result = redis_manager.test_connection() - if result: - return dmc.Badge("🟢 Connected", color="green", variant="light") + redis_manager.initialize() + if redis_manager.test_connection(): + return dbc.Badge("Connected", color="success", className="me-1") else: - return dmc.Badge("🔴 Error", color="red", variant="light") + return dbc.Badge("Error", color="danger", className="me-1") except: - return dmc.Badge("🔴 Error", color="red", variant="light") + return dbc.Badge("Error", color="danger", className="me-1") -def _get_performance_quick_status() -> dmc.Badge: +def _get_performance_quick_status() -> dbc.Badge: """Get quick performance status.""" try: cpu_percent = psutil.cpu_percent(interval=0.1) memory = psutil.virtual_memory() if cpu_percent < 80 and memory.percent < 80: - return dmc.Badge("🟢 Good", color="green", variant="light") + return dbc.Badge("Good", color="success", className="me-1") elif cpu_percent < 90 and memory.percent < 90: - return dmc.Badge("🟡 Warning", color="yellow", variant="light") + return dbc.Badge("Warning", color="warning", className="me-1") else: - return dmc.Badge("🔴 High", color="red", variant="light") + return dbc.Badge("High", color="danger", className="me-1") except: - return dmc.Badge("❓ Unknown", color="gray", variant="light") + return dbc.Badge("Unknown", color="secondary", className="me-1") def _get_data_collection_service_status() -> html.Div: """Get detailed data collection service status.""" try: is_running = _check_data_collection_service_running() - current_time = datetime.now() + current_time = datetime.now().strftime('%H:%M:%S') if is_running: - return dmc.Stack([ - dmc.Group([ - dmc.Badge("🟢 Service Running", color="green", variant="light"), - dmc.Text(f"Checked: {current_time.strftime('%H:%M:%S')}", size="xs", c="dimmed") - ], justify="space-between"), - dmc.Text("Data collection service is actively collecting market data.", - size="sm", c="#2c3e50") - ], gap="xs") + status_badge = dbc.Badge("Service Running", color="success", className="me-2") + status_text = html.P("Data collection service is actively collecting market data.", className="mb-0") + details = html.Div() else: - return dmc.Stack([ - dmc.Group([ - dmc.Badge("🔴 Service Stopped", color="red", variant="light"), - dmc.Text(f"Checked: {current_time.strftime('%H:%M:%S')}", size="xs", c="dimmed") - ], justify="space-between"), - dmc.Text("Data collection service is not running.", size="sm", c="#e74c3c"), - dmc.Code("python scripts/start_data_collection.py", style={'margin-top': '5px'}) - ], gap="xs") + status_badge = dbc.Badge("Service Stopped", color="danger", className="me-2") + status_text = html.P("Data collection service is not running.", className="text-danger") + details = html.Div([ + html.P("To start the service, run:", className="mt-2 mb-1"), + html.Code("python scripts/start_data_collection.py") + ]) + + return html.Div([ + dbc.Row([ + dbc.Col(status_badge, width="auto"), + dbc.Col(html.P(f"Checked: {current_time}", className="text-muted mb-0"), width="auto") + ], align="center", className="mb-2"), + status_text, + details + ]) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Status Check Failed", - color="red", - variant="light" - ) + return dbc.Alert(f"Error checking status: {e}", color="danger") def _get_data_collection_metrics() -> html.Div: """Get data collection metrics.""" try: - # Get database statistics for collected data db_manager = DatabaseManager() - db_manager.initialize() # Initialize the database manager + db_manager.initialize() with db_manager.get_session() as session: from sqlalchemy import text + candles_count = session.execute(text("SELECT COUNT(*) FROM market_data")).scalar() or 0 + tickers_count = session.execute(text("SELECT COUNT(*) FROM raw_trades WHERE data_type = 'ticker'")).scalar() or 0 + latest_market_data = session.execute(text("SELECT MAX(timestamp) FROM market_data")).scalar() + latest_raw_data = session.execute(text("SELECT MAX(timestamp) FROM raw_trades")).scalar() - # Count OHLCV candles from market_data table - candles_count = session.execute( - text("SELECT COUNT(*) FROM market_data") - ).scalar() or 0 + latest_data = max(d for d in [latest_market_data, latest_raw_data] if d) if any([latest_market_data, latest_raw_data]) else None - # Count raw tickers from raw_trades table - tickers_count = session.execute( - text("SELECT COUNT(*) FROM raw_trades WHERE data_type = 'ticker'") - ).scalar() or 0 - - # Get latest data timestamp from both tables - latest_market_data = session.execute( - text("SELECT MAX(timestamp) FROM market_data") - ).scalar() - - latest_raw_data = session.execute( - text("SELECT MAX(timestamp) FROM raw_trades") - ).scalar() - - # Use the most recent timestamp - latest_data = None - if latest_market_data and latest_raw_data: - latest_data = max(latest_market_data, latest_raw_data) - elif latest_market_data: - latest_data = latest_market_data - elif latest_raw_data: - latest_data = latest_raw_data - - # Calculate data freshness - data_freshness_badge = dmc.Badge("No data", color="gray", variant="light") if latest_data: - time_diff = datetime.utcnow() - latest_data.replace(tzinfo=None) if latest_data.tzinfo else datetime.utcnow() - latest_data + time_diff = datetime.utcnow() - (latest_data.replace(tzinfo=None) if latest_data.tzinfo else latest_data) if time_diff < timedelta(minutes=5): - data_freshness_badge = dmc.Badge(f"🟢 Fresh ({time_diff.seconds // 60}m ago)", color="green", variant="light") + freshness_badge = dbc.Badge(f"Fresh ({time_diff.seconds // 60}m ago)", color="success") elif time_diff < timedelta(hours=1): - data_freshness_badge = dmc.Badge(f"🟡 Recent ({time_diff.seconds // 60}m ago)", color="yellow", variant="light") + freshness_badge = dbc.Badge(f"Recent ({time_diff.seconds // 60}m ago)", color="warning") else: - data_freshness_badge = dmc.Badge(f"🔴 Stale ({time_diff.total_seconds() // 3600:.1f}h ago)", color="red", variant="light") + freshness_badge = dbc.Badge(f"Stale ({time_diff.total_seconds() // 3600:.1f}h ago)", color="danger") + else: + freshness_badge = dbc.Badge("No data", color="secondary") - return dmc.Stack([ - dmc.Group([ - dmc.Text(f"Candles: {candles_count:,}", fw=500), - dmc.Text(f"Tickers: {tickers_count:,}", fw=500) - ], justify="space-between"), - dmc.Group([ - dmc.Text("Data Freshness:", fw=500), - data_freshness_badge - ], justify="space-between") - ], gap="xs") + return html.Div([ + dbc.Row([ + dbc.Col(html.Strong("Candles:")), + dbc.Col(f"{candles_count:,}", className="text-end") + ]), + dbc.Row([ + dbc.Col(html.Strong("Tickers:")), + dbc.Col(f"{tickers_count:,}", className="text-end") + ]), + dbc.Row([ + dbc.Col(html.Strong("Data Freshness:")), + dbc.Col(freshness_badge, className="text-end") + ]) + ]) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Metrics Unavailable", - color="red", - variant="light" - ) + return dbc.Alert(f"Error loading metrics: {e}", color="danger") def _get_individual_collectors_status() -> html.Div: """Get individual data collector status.""" try: - # This would connect to a running data collection service - # For now, show a placeholder indicating the status - return dmc.Alert([ - dmc.Text("Individual collector health data would be displayed here when the data collection service is running.", size="sm"), - dmc.Space(h="sm"), - dmc.Group([ - dmc.Text("To start monitoring:", size="sm"), - dmc.Code("python scripts/start_data_collection.py") - ]) - ], title="📊 Collector Health Monitoring", color="blue", variant="light") + return dbc.Alert([ + html.P("Individual collector health data will be displayed here when the data collection service is running.", className="mb-2"), + html.Hr(), + html.P("To start monitoring, run the following command:", className="mb-1"), + html.Code("python scripts/start_data_collection.py") + ], color="info") except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Collector Status Check Failed", - color="red", - variant="light" - ) + return dbc.Alert(f"Error checking collector status: {e}", color="danger") def _get_database_status() -> html.Div: """Get detailed database status.""" try: db_manager = DatabaseManager() - db_manager.initialize() # Initialize the database manager + db_manager.initialize() with db_manager.get_session() as session: - # Test connection and get basic info from sqlalchemy import text result = session.execute(text("SELECT version()")).fetchone() version = result[0] if result else "Unknown" + connections = session.execute(text("SELECT count(*) FROM pg_stat_activity")).scalar() or 0 - # Get connection count - connections = session.execute( - text("SELECT count(*) FROM pg_stat_activity") - ).scalar() or 0 - - return dmc.Stack([ - dmc.Group([ - dmc.Badge("🟢 Database Connected", color="green", variant="light"), - dmc.Text(f"Checked: {datetime.now().strftime('%H:%M:%S')}", size="xs", c="dimmed") - ], justify="space-between"), - dmc.Text(f"Version: PostgreSQL {version.split()[1] if 'PostgreSQL' in version else 'Unknown'}", - size="xs", c="dimmed"), - dmc.Text(f"Active connections: {connections}", size="xs", c="dimmed") - ], gap="xs") + return html.Div([ + dbc.Row([ + dbc.Col(dbc.Badge("Database Connected", color="success"), width="auto"), + dbc.Col(f"Checked: {datetime.now().strftime('%H:%M:%S')}", className="text-muted") + ], align="center", className="mb-2"), + html.P(f"Version: PostgreSQL {version.split()[1] if 'PostgreSQL' in version else 'Unknown'}", className="mb-1"), + html.P(f"Active connections: {connections}", className="mb-0") + ]) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Database Connection Failed", - color="red", - variant="light" - ) + return dbc.Alert(f"Error connecting to database: {e}", color="danger") def _get_database_statistics() -> html.Div: """Get database statistics.""" try: db_manager = DatabaseManager() - db_manager.initialize() # Initialize the database manager + db_manager.initialize() with db_manager.get_session() as session: - # Get table sizes from sqlalchemy import text - table_stats = session.execute(text(""" - SELECT - schemaname, - tablename, - pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size - FROM pg_tables - WHERE schemaname NOT IN ('information_schema', 'pg_catalog') - ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC - LIMIT 5 - """)).fetchall() - - # Get recent activity from both main data tables - market_data_activity = session.execute( - text("SELECT COUNT(*) FROM market_data WHERE timestamp > NOW() - INTERVAL '1 hour'") - ).scalar() or 0 - - raw_data_activity = session.execute( - text("SELECT COUNT(*) FROM raw_trades WHERE timestamp > NOW() - INTERVAL '1 hour'") - ).scalar() or 0 + table_stats_query = """ + SELECT tablename, pg_size_pretty(pg_total_relation_size('public.'||tablename)) as size + FROM pg_tables WHERE schemaname = 'public' + ORDER BY pg_total_relation_size('public.'||tablename) DESC LIMIT 5 + """ + table_stats = session.execute(text(table_stats_query)).fetchall() + market_data_activity = session.execute(text("SELECT COUNT(*) FROM market_data WHERE timestamp > NOW() - INTERVAL '1 hour'")).scalar() or 0 + raw_data_activity = session.execute(text("SELECT COUNT(*) FROM raw_trades WHERE timestamp > NOW() - INTERVAL '1 hour'")).scalar() or 0 total_recent_activity = market_data_activity + raw_data_activity - stats_components = [ - dmc.Group([ - dmc.Text("Recent Activity (1h):", fw=500), - dmc.Text(f"{total_recent_activity:,} records", c="#2c3e50") - ], justify="space-between"), - dmc.Group([ - dmc.Text("• Market Data:", fw=400), - dmc.Text(f"{market_data_activity:,}", c="#7f8c8d") - ], justify="space-between"), - dmc.Group([ - dmc.Text("• Raw Data:", fw=400), - dmc.Text(f"{raw_data_activity:,}", c="#7f8c8d") - ], justify="space-between") + components = [ + dbc.Row([ + dbc.Col(html.Strong("Recent Activity (1h):")), + dbc.Col(f"{total_recent_activity:,} records", className="text-end") + ]), + html.Hr(className="my-2"), + html.Strong("Largest Tables:"), ] - if table_stats: - stats_components.append(dmc.Text("Largest Tables:", fw=500)) - for schema, table, size in table_stats: - stats_components.append( - dmc.Text(f"• {table}: {size}", size="xs", c="dimmed", style={'margin-left': '10px'}) - ) - - return dmc.Stack(stats_components, gap="xs") + for table, size in table_stats: + components.append(dbc.Row([ + dbc.Col(f"• {table}"), + dbc.Col(size, className="text-end text-muted") + ])) + else: + components.append(html.P("No table statistics available.", className="text-muted")) + + return html.Div(components) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Statistics Unavailable", - color="red", - variant="light" - ) + return dbc.Alert(f"Error loading database stats: {e}", color="danger") def _get_redis_status() -> html.Div: """Get Redis status.""" try: redis_manager = RedisManager() - redis_manager.initialize() # Initialize the Redis manager + redis_manager.initialize() info = redis_manager.get_info() - return dmc.Stack([ - dmc.Group([ - dmc.Badge("🟢 Redis Connected", color="green", variant="light"), - dmc.Text(f"Checked: {datetime.now().strftime('%H:%M:%S')}", size="xs", c="dimmed") - ], justify="space-between"), - dmc.Text(f"Host: {redis_manager.config.host}:{redis_manager.config.port}", - size="xs", c="dimmed") - ], gap="xs") + return html.Div([ + dbc.Row([ + dbc.Col(dbc.Badge("Redis Connected", color="success"), width="auto"), + dbc.Col(f"Checked: {datetime.now().strftime('%H:%M:%S')}", className="text-muted") + ], align="center", className="mb-2"), + html.P(f"Host: {redis_manager.config.host}:{redis_manager.config.port}", className="mb-0") + ]) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Redis Connection Failed", - color="red", - variant="light" - ) + return dbc.Alert(f"Error connecting to Redis: {e}", color="danger") def _get_redis_statistics() -> html.Div: """Get Redis statistics.""" try: redis_manager = RedisManager() - redis_manager.initialize() # Initialize the Redis manager - - # Get Redis info + redis_manager.initialize() info = redis_manager.get_info() - return dmc.Stack([ - dmc.Group([ - dmc.Text("Memory Used:", fw=500), - dmc.Text(f"{info.get('used_memory_human', 'Unknown')}", c="#2c3e50") - ], justify="space-between"), - dmc.Group([ - dmc.Text("Connected Clients:", fw=500), - dmc.Text(f"{info.get('connected_clients', 'Unknown')}", c="#2c3e50") - ], justify="space-between"), - dmc.Group([ - dmc.Text("Uptime:", fw=500), - dmc.Text(f"{info.get('uptime_in_seconds', 0) // 3600}h", c="#2c3e50") - ], justify="space-between") - ], gap="xs") - + return html.Div([ + dbc.Row([dbc.Col("Memory Used:"), dbc.Col(info.get('used_memory_human', 'N/A'), className="text-end")]), + dbc.Row([dbc.Col("Connected Clients:"), dbc.Col(info.get('connected_clients', 'N/A'), className="text-end")]), + dbc.Row([dbc.Col("Uptime (hours):"), dbc.Col(f"{info.get('uptime_in_seconds', 0) // 3600}", className="text-end")]) + ]) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Statistics Unavailable", - color="red", - variant="light" - ) + return dbc.Alert(f"Error loading Redis stats: {e}", color="danger") def _get_system_performance_metrics() -> html.Div: """Get system performance metrics.""" try: - # CPU usage cpu_percent = psutil.cpu_percent(interval=0.1) cpu_count = psutil.cpu_count() - - # Memory usage memory = psutil.virtual_memory() - - # Disk usage disk = psutil.disk_usage('/') - - # Network I/O (if available) - try: - network = psutil.net_io_counters() - network_sent = f"{network.bytes_sent / (1024**3):.2f} GB" - network_recv = f"{network.bytes_recv / (1024**3):.2f} GB" - except: - network_sent = "N/A" - network_recv = "N/A" - - # Color coding for metrics - cpu_color = "green" if cpu_percent < 70 else "yellow" if cpu_percent < 85 else "red" - memory_color = "green" if memory.percent < 70 else "yellow" if memory.percent < 85 else "red" - disk_color = "green" if disk.percent < 70 else "yellow" if disk.percent < 85 else "red" - - return dmc.Stack([ - dmc.Group([ - dmc.Text("CPU Usage:", fw=500), - dmc.Badge(f"{cpu_percent:.1f}%", color=cpu_color, variant="light"), - dmc.Text(f"({cpu_count} cores)", size="xs", c="dimmed") - ], justify="space-between"), - dmc.Group([ - dmc.Text("Memory:", fw=500), - dmc.Badge(f"{memory.percent:.1f}%", color=memory_color, variant="light"), - dmc.Text(f"{memory.used // (1024**3)} GB / {memory.total // (1024**3)} GB", - size="xs", c="dimmed") - ], justify="space-between"), - dmc.Group([ - dmc.Text("Disk Usage:", fw=500), - dmc.Badge(f"{disk.percent:.1f}%", color=disk_color, variant="light"), - dmc.Text(f"{disk.used // (1024**3)} GB / {disk.total // (1024**3)} GB", - size="xs", c="dimmed") - ], justify="space-between"), - dmc.Group([ - dmc.Text("Network I/O:", fw=500), - dmc.Text(f"↑ {network_sent} ↓ {network_recv}", size="xs", c="dimmed") - ], justify="space-between") - ], gap="sm") + + def get_color(percent): + if percent < 70: return "success" + if percent < 85: return "warning" + return "danger" + + return html.Div([ + html.Div([ + html.Strong("CPU Usage: "), + dbc.Badge(f"{cpu_percent:.1f}%", color=get_color(cpu_percent)), + html.Span(f" ({cpu_count} cores)", className="text-muted ms-1") + ], className="mb-2"), + dbc.Progress(value=cpu_percent, color=get_color(cpu_percent), style={"height": "10px"}, className="mb-3"), + + html.Div([ + html.Strong("Memory Usage: "), + dbc.Badge(f"{memory.percent:.1f}%", color=get_color(memory.percent)), + html.Span(f" ({memory.used / (1024**3):.1f} / {memory.total / (1024**3):.1f} GB)", className="text-muted ms-1") + ], className="mb-2"), + dbc.Progress(value=memory.percent, color=get_color(memory.percent), style={"height": "10px"}, className="mb-3"), + + html.Div([ + html.Strong("Disk Usage: "), + dbc.Badge(f"{disk.percent:.1f}%", color=get_color(disk.percent)), + html.Span(f" ({disk.used / (1024**3):.1f} / {disk.total / (1024**3):.1f} GB)", className="text-muted ms-1") + ], className="mb-2"), + dbc.Progress(value=disk.percent, color=get_color(disk.percent), style={"height": "10px"}) + ]) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Performance Metrics Unavailable", - color="red", - variant="light" - ) + return dbc.Alert(f"Error loading performance metrics: {e}", color="danger") def _get_collection_details_content() -> html.Div: """Get detailed collection information for modal.""" try: - # Detailed service and collector information - return dmc.Stack([ - dmc.Title("📊 Data Collection Service Details", order=5), - dmc.Text("Comprehensive data collection service information would be displayed here."), - dmc.Divider(), - dmc.Title("Configuration", order=6), - dmc.Text("Service configuration details..."), - dmc.Title("Performance Metrics", order=6), - dmc.Text("Detailed performance analytics..."), - dmc.Title("Health Status", order=6), - dmc.Text("Individual collector health information...") - ], gap="md") + return html.Div([ + html.H5("Data Collection Service Details"), + html.P("Comprehensive data collection service information would be displayed here."), + html.Hr(), + html.H6("Configuration"), + html.P("Service configuration details..."), + html.H6("Performance Metrics"), + html.P("Detailed performance analytics..."), + html.H6("Health Status"), + html.P("Individual collector health information...") + ]) except Exception as e: - return dmc.Alert( - f"Error: {str(e)}", - title="🔴 Error Loading Details", - color="red", - variant="light" - ) + return dbc.Alert(f"Error loading details: {e}", color="danger") def _get_collection_logs_content() -> str: diff --git a/dashboard/components/chart_controls.py b/dashboard/components/chart_controls.py index c43b797..eac8913 100644 --- a/dashboard/components/chart_controls.py +++ b/dashboard/components/chart_controls.py @@ -3,6 +3,7 @@ Chart control components for the market data layout. """ from dash import html, dcc +import dash_bootstrap_components as dbc from utils.logger import get_logger logger = get_logger("default_logger") @@ -10,216 +11,124 @@ logger = get_logger("default_logger") def create_chart_config_panel(strategy_options, overlay_options, subplot_options): """Create the chart configuration panel with add/edit UI.""" - return html.Div([ - html.H5("🎯 Chart Configuration", style={'color': '#2c3e50', 'margin-bottom': '15px'}), - - # Add New Indicator Button - html.Div([ - html.Button( - "➕ Add New Indicator", - id="add-indicator-btn-visible", - className="btn btn-primary", - style={ - 'background-color': '#007bff', - 'color': 'white', - 'border': 'none', - 'padding': '8px 16px', - 'border-radius': '4px', - 'cursor': 'pointer', - 'margin-bottom': '15px', - 'font-weight': 'bold' - } - ) - ]), - - # Strategy Selection - html.Div([ - html.Label("Strategy Template:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Dropdown( - id='strategy-dropdown', - options=strategy_options, - value=None, - placeholder="Select a strategy template (optional)", - style={'margin-bottom': '15px'} - ) - ]), - - # Indicator Controls with Edit Buttons - html.Div([ - # Overlay Indicators + return dbc.Card([ + dbc.CardHeader(html.H5("🎯 Chart Configuration")), + dbc.CardBody([ + dbc.Button("➕ Add New Indicator", id="add-indicator-btn-visible", color="primary", className="mb-3"), + html.Div([ - html.Label("Overlay Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}), - html.Div([ - # Hidden checklist for callback compatibility + html.Label("Strategy Template:", className="form-label"), + dcc.Dropdown( + id='strategy-dropdown', + options=strategy_options, + value=None, + placeholder="Select a strategy template (optional)", + ) + ], className="mb-3"), + + dbc.Row([ + dbc.Col([ + html.Label("Overlay Indicators:", className="form-label"), dcc.Checklist( id='overlay-indicators-checklist', options=overlay_options, - value=[], # Start with no indicators selected - style={'display': 'none'} # Hide the basic checklist + value=[], + style={'display': 'none'} ), - # Custom indicator list with edit buttons - html.Div(id='overlay-indicators-list', children=[ - # This will be populated dynamically - ]) - ]) - ], style={'width': '48%', 'display': 'inline-block', 'margin-right': '4%', 'vertical-align': 'top'}), - - # Subplot Indicators - html.Div([ - html.Label("Subplot Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}), - html.Div([ - # Hidden checklist for callback compatibility + html.Div(id='overlay-indicators-list') + ], width=6), + + dbc.Col([ + html.Label("Subplot Indicators:", className="form-label"), dcc.Checklist( id='subplot-indicators-checklist', options=subplot_options, - value=[], # Start with no indicators selected - style={'display': 'none'} # Hide the basic checklist + value=[], + style={'display': 'none'} ), - # Custom indicator list with edit buttons - html.Div(id='subplot-indicators-list', children=[ - # This will be populated dynamically - ]) - ]) - ], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'}) + html.Div(id='subplot-indicators-list') + ], width=6) + ]) ]) - ], style={ - 'border': '1px solid #bdc3c7', - 'border-radius': '8px', - 'padding': '15px', - 'background-color': '#f8f9fa', - 'margin-bottom': '20px' - }) + ], className="mb-4") def create_auto_update_control(): """Create the auto-update control section.""" return html.Div([ - dcc.Checklist( + dbc.Checkbox( id='auto-update-checkbox', - options=[{'label': ' Auto-update charts', 'value': 'auto'}], - value=['auto'], - style={'margin-bottom': '10px'} + label='Auto-update charts', + value=True, ), html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'}) - ]) + ], className="mb-3") def create_time_range_controls(): """Create the time range control panel.""" - return html.Div([ - html.H5("⏰ Time Range Controls", style={'color': '#2c3e50', 'margin-bottom': '15px'}), - - # Quick Select Dropdown - html.Div([ - html.Label("Quick Select:", style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}), - dcc.Dropdown( - id='time-range-quick-select', - options=[ - {'label': '🕐 Last 1 Hour', 'value': '1h'}, - {'label': '🕐 Last 4 Hours', 'value': '4h'}, - {'label': '🕐 Last 6 Hours', 'value': '6h'}, - {'label': '🕐 Last 12 Hours', 'value': '12h'}, - {'label': '📅 Last 1 Day', 'value': '1d'}, - {'label': '📅 Last 3 Days', 'value': '3d'}, - {'label': '📅 Last 7 Days', 'value': '7d'}, - {'label': '📅 Last 30 Days', 'value': '30d'}, - {'label': '📅 Custom Range', 'value': 'custom'}, - {'label': '🔴 Real-time', 'value': 'realtime'} - ], - value='7d', - placeholder="Select time range", - style={'margin-bottom': '15px'} - ) - ]), - - # Custom Date Range Picker - html.Div([ - html.Label("Custom Date Range:", style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}), + return dbc.Card([ + dbc.CardHeader(html.H5("⏰ Time Range Controls")), + dbc.CardBody([ html.Div([ - dcc.DatePickerRange( - id='custom-date-range', - display_format='YYYY-MM-DD', - style={'display': 'inline-block', 'margin-right': '10px'} - ), - html.Button( - "Clear", - id="clear-date-range-btn", - className="btn btn-sm btn-outline-secondary", - style={ - 'display': 'inline-block', - 'vertical-align': 'top', - 'margin-top': '7px', - 'padding': '5px 10px', - 'font-size': '12px' - } + html.Label("Quick Select:", className="form-label"), + dcc.Dropdown( + id='time-range-quick-select', + options=[ + {'label': '🕐 Last 1 Hour', 'value': '1h'}, + {'label': '🕐 Last 4 Hours', 'value': '4h'}, + {'label': '🕐 Last 6 Hours', 'value': '6h'}, + {'label': '🕐 Last 12 Hours', 'value': '12h'}, + {'label': '📅 Last 1 Day', 'value': '1d'}, + {'label': '📅 Last 3 Days', 'value': '3d'}, + {'label': '📅 Last 7 Days', 'value': '7d'}, + {'label': '📅 Last 30 Days', 'value': '30d'}, + {'label': '📅 Custom Range', 'value': 'custom'}, + {'label': '🔴 Real-time', 'value': 'realtime'} + ], + value='7d', + placeholder="Select time range", ) - ], style={'margin-bottom': '15px'}) - ]), - - # Analysis Mode Toggle - html.Div([ - html.Label("Analysis Mode:", style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}), - dcc.RadioItems( - id='analysis-mode-toggle', - options=[ - {'label': '🔴 Real-time Updates', 'value': 'realtime'}, - {'label': '🔒 Analysis Mode (Locked)', 'value': 'locked'} - ], - value='realtime', - inline=True, - style={'margin-bottom': '10px'} - ) - ]), - - # Time Range Status - html.Div(id='time-range-status', - style={'font-size': '12px', 'color': '#7f8c8d', 'font-style': 'italic'}) - - ], style={ - 'border': '1px solid #bdc3c7', - 'border-radius': '8px', - 'padding': '15px', - 'background-color': '#f0f8ff', - 'margin-bottom': '20px' - }) + ], className="mb-3"), + + html.Div([ + html.Label("Custom Date Range:", className="form-label"), + dbc.InputGroup([ + dcc.DatePickerRange( + id='custom-date-range', + display_format='YYYY-MM-DD', + ), + dbc.Button("Clear", id="clear-date-range-btn", color="secondary", outline=True, size="sm") + ]) + ], className="mb-3"), + + html.Div([ + html.Label("Analysis Mode:", className="form-label"), + dbc.RadioItems( + id='analysis-mode-toggle', + options=[ + {'label': '🔴 Real-time Updates', 'value': 'realtime'}, + {'label': '🔒 Analysis Mode (Locked)', 'value': 'locked'} + ], + value='realtime', + inline=True, + ) + ]), + + html.Div(id='time-range-status', className="text-muted fst-italic mt-2") + ]) + ], className="mb-4") def create_export_controls(): """Create the data export control panel.""" - return html.Div([ - html.H5("💾 Data Export", style={'color': '#2c3e50', 'margin-bottom': '15px'}), - html.Button( - "Export to CSV", - id="export-csv-btn", - className="btn btn-primary", - style={ - 'background-color': '#28a745', - 'color': 'white', - 'border': 'none', - 'padding': '8px 16px', - 'border-radius': '4px', - 'cursor': 'pointer', - 'margin-right': '10px' - } - ), - html.Button( - "Export to JSON", - id="export-json-btn", - className="btn btn-primary", - style={ - 'background-color': '#17a2b8', - 'color': 'white', - 'border': 'none', - 'padding': '8px 16px', - 'border-radius': '4px', - 'cursor': 'pointer' - } - ), - dcc.Download(id="download-chart-data") - ], style={ - 'border': '1px solid #bdc3c7', - 'border-radius': '8px', - 'padding': '15px', - 'background-color': '#f8f9fa', - 'margin-bottom': '20px' - }) \ No newline at end of file + return dbc.Card([ + dbc.CardHeader(html.H5("💾 Data Export")), + dbc.CardBody([ + dbc.ButtonGroup([ + dbc.Button("Export to CSV", id="export-csv-btn", color="primary"), + dbc.Button("Export to JSON", id="export-json-btn", color="secondary"), + ]), + dcc.Download(id="download-chart-data") + ]) + ], className="mb-4") \ No newline at end of file diff --git a/dashboard/components/data_analysis.py b/dashboard/components/data_analysis.py index 43d0bd4..5f5816b 100644 --- a/dashboard/components/data_analysis.py +++ b/dashboard/components/data_analysis.py @@ -3,7 +3,7 @@ Data analysis components for comprehensive market data analysis. """ from dash import html, dcc -import dash_mantine_components as dmc +import dash_bootstrap_components as dbc import plotly.graph_objects as go import plotly.express as px from plotly.subplots import make_subplots @@ -466,7 +466,7 @@ def create_data_analysis_panel(): ]), dcc.Tab(label="Price Movement", value="price-movement", children=[ html.Div(id='price-movement-content', children=[ - dmc.Alert("Select a symbol and timeframe to view price movement analysis.", color="blue") + dbc.Alert("Select a symbol and timeframe to view price movement analysis.", color="primary") ]) ]), ], @@ -492,150 +492,70 @@ def format_number(value: float, decimals: int = 2) -> str: def create_volume_stats_display(stats: Dict[str, Any]) -> html.Div: """Create volume statistics display.""" if 'error' in stats: - return dmc.Alert( + return dbc.Alert( "Error loading volume statistics", - title="Volume Analysis Error", - color="red" + color="danger", + dismissable=True ) - return dmc.SimpleGrid([ - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("📊", size="lg", color="blue"), - dmc.Stack([ - dmc.Text("Total Volume", size="sm", c="dimmed"), - dmc.Text(format_number(stats['total_volume']), fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("📈", size="lg", color="green"), - dmc.Stack([ - dmc.Text("Average Volume", size="sm", c="dimmed"), - dmc.Text(format_number(stats['avg_volume']), fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("🎯", size="lg", color="orange"), - dmc.Stack([ - dmc.Text("Volume Trend", size="sm", c="dimmed"), - dmc.Text(stats['volume_trend'], fw=700, size="lg", - c="green" if stats['volume_trend'] == "Increasing" else "red") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("⚡", size="lg", color="red"), - dmc.Stack([ - dmc.Text("High Volume Periods", size="sm", c="dimmed"), - dmc.Text(str(stats['high_volume_periods']), fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("🔗", size="lg", color="purple"), - dmc.Stack([ - dmc.Text("Volume-Price Correlation", size="sm", c="dimmed"), - dmc.Text(f"{stats['volume_price_correlation']:.3f}", fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("💱", size="lg", color="teal"), - dmc.Stack([ - dmc.Text("Avg Trade Size", size="sm", c="dimmed"), - dmc.Text(format_number(stats['avg_trade_size']), fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm") - - ], cols=3, spacing="md", style={'margin-top': '20px'}) + def create_stat_card(icon, title, value, color="primary"): + return dbc.Col(dbc.Card(dbc.CardBody([ + html.Div([ + html.Div(icon, className="display-6"), + html.Div([ + html.P(title, className="card-title mb-1 text-muted"), + html.H4(value, className=f"card-text fw-bold text-{color}") + ], className="ms-3") + ], className="d-flex align-items-center") + ])), width=4, className="mb-3") + + return dbc.Row([ + create_stat_card("📊", "Total Volume", format_number(stats['total_volume'])), + create_stat_card("📈", "Average Volume", format_number(stats['avg_volume'])), + create_stat_card("🎯", "Volume Trend", stats['volume_trend'], + "success" if stats['volume_trend'] == "Increasing" else "danger"), + create_stat_card("⚡", "High Volume Periods", str(stats['high_volume_periods'])), + create_stat_card("🔗", "Volume-Price Correlation", f"{stats['volume_price_correlation']:.3f}"), + create_stat_card("💱", "Avg Trade Size", format_number(stats['avg_trade_size'])) + ], className="mt-3") def create_price_stats_display(stats: Dict[str, Any]) -> html.Div: """Create price movement statistics display.""" if 'error' in stats: - return dmc.Alert( + return dbc.Alert( "Error loading price statistics", - title="Price Analysis Error", - color="red" + color="danger", + dismissable=True ) - - return dmc.SimpleGrid([ - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("💰", size="lg", color="blue"), - dmc.Stack([ - dmc.Text("Current Price", size="sm", c="dimmed"), - dmc.Text(f"${stats['current_price']:.2f}", fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("📈", size="lg", color="green" if stats['period_return'] >= 0 else "red"), - dmc.Stack([ - dmc.Text("Period Return", size="sm", c="dimmed"), - dmc.Text(f"{stats['period_return']:+.2f}%", fw=700, size="lg", - c="green" if stats['period_return'] >= 0 else "red") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("📊", size="lg", color="orange"), - dmc.Stack([ - dmc.Text("Volatility", size="sm", c="dimmed"), - dmc.Text(f"{stats['volatility']:.2f}%", fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("🎯", size="lg", color="purple"), - dmc.Stack([ - dmc.Text("Bullish Ratio", size="sm", c="dimmed"), - dmc.Text(f"{stats['bullish_ratio']:.1f}%", fw=700, size="lg") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("⚡", size="lg", color="teal"), - dmc.Stack([ - dmc.Text("Momentum", size="sm", c="dimmed"), - dmc.Text(f"{stats['momentum']:+.2f}%", fw=700, size="lg", - c="green" if stats['momentum'] >= 0 else "red") - ], gap="xs") - ]) - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Group([ - dmc.ThemeIcon("📉", size="lg", color="red"), - dmc.Stack([ - dmc.Text("Max Loss", size="sm", c="dimmed"), - dmc.Text(f"{stats['max_loss']:.2f}%", fw=700, size="lg", c="red") - ], gap="xs") - ]) - ], p="md", shadow="sm") - - ], cols=3, spacing="md", style={'margin-top': '20px'}) + + def create_stat_card(icon, title, value, color="primary"): + text_color = "text-dark" + if color == "success": + text_color = "text-success" + elif color == "danger": + text_color = "text-danger" + + return dbc.Col(dbc.Card(dbc.CardBody([ + html.Div([ + html.Div(icon, className="display-6"), + html.Div([ + html.P(title, className="card-title mb-1 text-muted"), + html.H4(value, className=f"card-text fw-bold {text_color}") + ], className="ms-3") + ], className="d-flex align-items-center") + ])), width=4, className="mb-3") + + return dbc.Row([ + create_stat_card("💰", "Current Price", f"${stats['current_price']:.2f}"), + create_stat_card("📈", "Period Return", f"{stats['period_return']:+.2f}%", + "success" if stats['period_return'] >= 0 else "danger"), + create_stat_card("📊", "Volatility", f"{stats['volatility']:.2f}%", color="warning"), + create_stat_card("🎯", "Bullish Ratio", f"{stats['bullish_ratio']:.1f}%"), + create_stat_card("⚡", "Momentum", f"{stats['momentum']:+.2f}%", + "success" if stats['momentum'] >= 0 else "danger"), + create_stat_card("📉", "Max Loss", f"{stats['max_loss']:.2f}%", "danger") + ], className="mt-3") def get_market_statistics(df: pd.DataFrame, symbol: str, timeframe: str) -> html.Div: @@ -660,14 +580,14 @@ def get_market_statistics(df: pd.DataFrame, symbol: str, timeframe: str) -> html time_status = f"📅 Analysis Range: {start_date} to {end_date} (~{days_back} days)" return html.Div([ - html.H3("📊 Enhanced Market Statistics"), + html.H3("📊 Enhanced Market Statistics", className="mb-3"), html.P( time_status, - style={'font-weight': 'bold', 'margin-bottom': '15px', 'color': '#4A4A4A', 'text-align': 'center', 'font-size': '1.1em'} + className="lead text-center text-muted mb-4" ), create_price_stats_display(price_stats), create_volume_stats_display(volume_stats) ]) except Exception as e: logger.error(f"Error in get_market_statistics: {e}", exc_info=True) - return html.Div(f"Error generating statistics display: {e}", style={'color': 'red'}) \ No newline at end of file + return dbc.Alert(f"Error generating statistics display: {e}", color="danger") \ No newline at end of file diff --git a/dashboard/components/indicator_modal.py b/dashboard/components/indicator_modal.py index 7448b67..0de8afb 100644 --- a/dashboard/components/indicator_modal.py +++ b/dashboard/components/indicator_modal.py @@ -3,281 +3,118 @@ Indicator modal component for creating and editing indicators. """ from dash import html, dcc +import dash_bootstrap_components as dbc def create_indicator_modal(): """Create the indicator modal dialog for adding/editing indicators.""" return html.Div([ - dcc.Store(id='edit-indicator-store', data=None), # Store for edit mode - explicitly start with None - - # Modal Background - html.Div( - id='indicator-modal-background', - style={ - 'display': 'none', - 'position': 'fixed', - 'z-index': '1000', - 'left': '0', - 'top': '0', - 'width': '100%', - 'height': '100%', - 'background-color': 'rgba(0,0,0,0.5)', - 'visibility': 'hidden' - } - ), - - # Modal Content + dcc.Store(id='edit-indicator-store', data=None), + dbc.Modal([ + dbc.ModalHeader(dbc.ModalTitle("📊 Add New Indicator", id="modal-title")), + dbc.ModalBody([ + # Basic Settings + html.H5("Basic Settings"), + dbc.Row([ + dbc.Col(dbc.Label("Indicator Name:"), width=12), + dbc.Col(dcc.Input(id='indicator-name-input', type='text', placeholder='e.g., "SMA 30 Custom"', className="w-100"), width=12) + ], className="mb-3"), + dbc.Row([ + dbc.Col(dbc.Label("Indicator Type:"), width=12), + dbc.Col(dcc.Dropdown( + id='indicator-type-dropdown', + options=[ + {'label': 'Simple Moving Average (SMA)', 'value': 'sma'}, + {'label': 'Exponential Moving Average (EMA)', 'value': 'ema'}, + {'label': 'Relative Strength Index (RSI)', 'value': 'rsi'}, + {'label': 'MACD', 'value': 'macd'}, + {'label': 'Bollinger Bands', 'value': 'bollinger_bands'} + ], + placeholder='Select indicator type', + ), width=12) + ], className="mb-3"), + dbc.Row([ + dbc.Col(dbc.Label("Description (Optional):"), width=12), + dbc.Col(dcc.Textarea( + id='indicator-description-input', + placeholder='Brief description of this indicator configuration...', + style={'width': '100%', 'height': '60px'} + ), width=12) + ], className="mb-3"), + html.Hr(), + + # Parameters Section + html.H5("Parameters"), + html.Div( + id='indicator-parameters-message', + children=[html.P("Select an indicator type to configure parameters", className="text-muted fst-italic")] + ), + + # Parameter fields (SMA, EMA, etc.) + create_parameter_fields(), + + html.Hr(), + # Styling Section + html.H5("Styling"), + dbc.Row([ + dbc.Col([ + dbc.Label("Color:"), + dcc.Input(id='indicator-color-input', type='text', value='#007bff', className="w-100") + ], width=6), + dbc.Col([ + dbc.Label("Line Width:"), + dcc.Slider(id='indicator-line-width-slider', min=1, max=5, step=1, value=2, marks={i: str(i) for i in range(1, 6)}) + ], width=6) + ], className="mb-3"), + ]), + dbc.ModalFooter([ + html.Div(id='save-indicator-feedback', className="me-auto"), + dbc.Button("Cancel", id="cancel-indicator-btn", color="secondary"), + dbc.Button("Save Indicator", id="save-indicator-btn", color="primary") + ]) + ], id='indicator-modal', size="lg", is_open=False), + ]) + +def create_parameter_fields(): + """Helper function to create parameter input fields for all indicator types.""" + return html.Div([ + # SMA Parameters html.Div([ - html.Div([ - # Modal Header - html.Div([ - html.H4("📊 Add New Indicator", id="modal-title", style={'margin': '0', 'color': '#2c3e50'}), - html.Button( - "✕", - id="close-modal-btn", - style={ - 'background': 'none', - 'border': 'none', - 'font-size': '24px', - 'cursor': 'pointer', - 'color': '#999', - 'float': 'right' - } - ) - ], style={'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '20px', 'border-bottom': '1px solid #eee', 'padding-bottom': '10px'}), - - # Modal Body - html.Div([ - # Basic Settings - html.Div([ - html.H5("Basic Settings", style={'color': '#2c3e50', 'margin-bottom': '15px'}), - - # Indicator Name - html.Div([ - html.Label("Indicator Name:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='indicator-name-input', - type='text', - placeholder='e.g., "SMA 30 Custom"', - style={'width': '100%', 'padding': '8px', 'margin-bottom': '10px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ) - ]), - - # Indicator Type - html.Div([ - html.Label("Indicator Type:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Dropdown( - id='indicator-type-dropdown', - options=[ - {'label': 'Simple Moving Average (SMA)', 'value': 'sma'}, - {'label': 'Exponential Moving Average (EMA)', 'value': 'ema'}, - {'label': 'Relative Strength Index (RSI)', 'value': 'rsi'}, - {'label': 'MACD', 'value': 'macd'}, - {'label': 'Bollinger Bands', 'value': 'bollinger_bands'} - ], - placeholder='Select indicator type', - style={'margin-bottom': '10px'} - ) - ]), - - # Description - html.Div([ - html.Label("Description (Optional):", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Textarea( - id='indicator-description-input', - placeholder='Brief description of this indicator configuration...', - style={'width': '100%', 'height': '60px', 'padding': '8px', 'margin-bottom': '15px', 'border': '1px solid #ddd', 'border-radius': '4px', 'resize': 'vertical'} - ) - ]) - ], style={'margin-bottom': '20px'}), - - # Parameters Section - html.Div([ - html.H5("Parameters", style={'color': '#2c3e50', 'margin-bottom': '15px'}), - - # Default message - html.Div( - id='indicator-parameters-message', - children=[html.P("Select an indicator type to configure parameters", style={'color': '#7f8c8d', 'font-style': 'italic'})], - style={'display': 'block'} - ), - - # SMA Parameters (hidden by default) - html.Div([ - html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='sma-period-input', - type='number', - value=20, - min=1, max=200, - style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ), - html.P("Number of periods for Simple Moving Average calculation", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) - ], id='sma-parameters', style={'display': 'none', 'margin-bottom': '10px'}), - - # EMA Parameters (hidden by default) - html.Div([ - html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='ema-period-input', - type='number', - value=12, - min=1, max=200, - style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ), - html.P("Number of periods for Exponential Moving Average calculation", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) - ], id='ema-parameters', style={'display': 'none', 'margin-bottom': '10px'}), - - # RSI Parameters (hidden by default) - html.Div([ - html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='rsi-period-input', - type='number', - value=14, - min=2, max=50, - style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ), - html.P("Number of periods for RSI calculation (typically 14)", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) - ], id='rsi-parameters', style={'display': 'none', 'margin-bottom': '10px'}), - - # MACD Parameters (hidden by default) - html.Div([ - html.Div([ - html.Label("Fast Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='macd-fast-period-input', - type='number', - value=12, - min=2, max=50, - style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ) - ], style={'margin-bottom': '10px'}), - html.Div([ - html.Label("Slow Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='macd-slow-period-input', - type='number', - value=26, - min=5, max=100, - style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ) - ], style={'margin-bottom': '10px'}), - html.Div([ - html.Label("Signal Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='macd-signal-period-input', - type='number', - value=9, - min=2, max=30, - style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ) - ]), - html.P("MACD periods: Fast EMA, Slow EMA, and Signal line", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) - ], id='macd-parameters', style={'display': 'none', 'margin-bottom': '10px'}), - - # Bollinger Bands Parameters (hidden by default) - html.Div([ - html.Div([ - html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='bb-period-input', - type='number', - value=20, - min=5, max=100, - style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ) - ], style={'margin-bottom': '10px'}), - html.Div([ - html.Label("Standard Deviation:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='bb-stddev-input', - type='number', - value=2.0, - min=0.5, max=5.0, step=0.1, - style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ) - ]), - html.P("Period for middle line (SMA) and standard deviation multiplier", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'}) - ], id='bb-parameters', style={'display': 'none', 'margin-bottom': '10px'}) - - ], style={'margin-bottom': '20px'}), - - # Styling Section - html.Div([ - html.H5("Styling", style={'color': '#2c3e50', 'margin-bottom': '15px'}), - - html.Div([ - # Color Picker - html.Div([ - html.Label("Color:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Input( - id='indicator-color-input', - type='text', - value='#007bff', - style={'width': '100px', 'padding': '8px', 'margin-bottom': '10px', 'border': '1px solid #ddd', 'border-radius': '4px'} - ) - ], style={'width': '48%', 'display': 'inline-block', 'margin-right': '4%'}), - - # Line Width - html.Div([ - html.Label("Line Width:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), - dcc.Slider( - id='indicator-line-width-slider', - min=1, max=5, step=1, value=2, - marks={i: str(i) for i in range(1, 6)}, - tooltip={'placement': 'bottom', 'always_visible': True} - ) - ], style={'width': '48%', 'display': 'inline-block'}) - ]) - ], style={'margin-bottom': '20px'}) - ]), - - # Modal Footer - html.Div([ - html.Button( - "Cancel", - id="cancel-indicator-btn", - style={ - 'background-color': '#6c757d', - 'color': 'white', - 'border': 'none', - 'padding': '10px 20px', - 'border-radius': '4px', - 'cursor': 'pointer', - 'margin-right': '10px' - } - ), - html.Button( - "Save Indicator", - id="save-indicator-btn", - style={ - 'background-color': '#28a745', - 'color': 'white', - 'border': 'none', - 'padding': '10px 20px', - 'border-radius': '4px', - 'cursor': 'pointer', - 'font-weight': 'bold' - } - ), - html.Div(id='save-indicator-feedback', style={'margin-top': '10px'}) - ], style={'display': 'flex', 'justify-content': 'flex-end', 'margin-top': '20px', 'border-top': '1px solid #eee', 'padding-top': '15px'}) - ], style={ - 'background': 'white', - 'padding': '20px', - 'border-radius': '8px', - 'width': '600px', - 'box-shadow': '0 4px 8px rgba(0,0,0,0.1)' - }) - ], id='indicator-modal-content', style={ - 'display': 'none', - 'position': 'fixed', - 'z-index': '1001', - 'left': '0', - 'top': '0', - 'width': '100%', - 'height': '100%', - 'visibility': 'hidden' - }) + dbc.Label("Period:"), + dcc.Input(id='sma-period-input', type='number', value=20, min=1, max=200), + dbc.FormText("Number of periods for Simple Moving Average calculation") + ], id='sma-parameters', style={'display': 'none'}, className="mb-3"), + + # EMA Parameters + html.Div([ + dbc.Label("Period:"), + dcc.Input(id='ema-period-input', type='number', value=12, min=1, max=200), + dbc.FormText("Number of periods for Exponential Moving Average calculation") + ], id='ema-parameters', style={'display': 'none'}, className="mb-3"), + + # RSI Parameters + html.Div([ + dbc.Label("Period:"), + dcc.Input(id='rsi-period-input', type='number', value=14, min=2, max=50), + dbc.FormText("Number of periods for RSI calculation (typically 14)") + ], id='rsi-parameters', style={'display': 'none'}, className="mb-3"), + + # MACD Parameters + html.Div([ + dbc.Row([ + dbc.Col([dbc.Label("Fast Period:"), dcc.Input(id='macd-fast-period-input', type='number', value=12)], width=4), + dbc.Col([dbc.Label("Slow Period:"), dcc.Input(id='macd-slow-period-input', type='number', value=26)], width=4), + dbc.Col([dbc.Label("Signal Period:"), dcc.Input(id='macd-signal-period-input', type='number', value=9)], width=4), + ]), + dbc.FormText("MACD periods: Fast EMA, Slow EMA, and Signal line") + ], id='macd-parameters', style={'display': 'none'}, className="mb-3"), + + # Bollinger Bands Parameters + html.Div([ + dbc.Row([ + dbc.Col([dbc.Label("Period:"), dcc.Input(id='bb-period-input', type='number', value=20)], width=6), + dbc.Col([dbc.Label("Standard Deviation:"), dcc.Input(id='bb-stddev-input', type='number', value=2.0, step=0.1)], width=6), + ]), + dbc.FormText("Period for middle line (SMA) and standard deviation multiplier") + ], id='bb-parameters', style={'display': 'none'}, className="mb-3") ]) \ No newline at end of file diff --git a/dashboard/layouts/system_health.py b/dashboard/layouts/system_health.py index e5e3ddd..f310a60 100644 --- a/dashboard/layouts/system_health.py +++ b/dashboard/layouts/system_health.py @@ -2,211 +2,130 @@ System health monitoring layout for the dashboard. """ -from dash import html, dcc -import dash_mantine_components as dmc - +from dash import html +import dash_bootstrap_components as dbc def get_system_health_layout(): - """Create the enhanced system health monitoring layout with market data monitoring.""" + """Create the enhanced system health monitoring layout with Bootstrap components.""" + + def create_quick_status_card(title, component_id, icon): + return dbc.Card(dbc.CardBody([ + html.H5(f"{icon} {title}", className="card-title"), + html.Div(id=component_id, children=[ + dbc.Badge("Checking...", color="warning", className="me-1") + ]) + ]), className="text-center") + return html.Div([ # Header section - dmc.Paper([ - dmc.Title("⚙️ System Health & Data Monitoring", order=2, c="#2c3e50"), - dmc.Text("Real-time monitoring of data collection services, database health, and system performance", - c="dimmed", size="sm") - ], p="lg", mb="xl"), + html.Div([ + html.H2("⚙️ System Health & Data Monitoring"), + html.P("Real-time monitoring of data collection services, database health, and system performance", + className="lead") + ], className="p-5 mb-4 bg-light rounded-3"), # Quick Status Overview Row - dmc.Grid([ - dmc.GridCol([ - dmc.Card([ - dmc.CardSection([ - dmc.Group([ - dmc.Text("📊 Data Collection", fw=600, c="#2c3e50"), - ], justify="space-between"), - html.Div(id='data-collection-quick-status', - children=[dmc.Badge("🔄 Checking...", color="yellow", variant="light")]) - ], p="md") - ], shadow="sm", radius="md", withBorder=True) - ], span=3), - - dmc.GridCol([ - dmc.Card([ - dmc.CardSection([ - dmc.Group([ - dmc.Text("🗄️ Database", fw=600, c="#2c3e50"), - ], justify="space-between"), - html.Div(id='database-quick-status', - children=[dmc.Badge("🔄 Checking...", color="yellow", variant="light")]) - ], p="md") - ], shadow="sm", radius="md", withBorder=True) - ], span=3), - - dmc.GridCol([ - dmc.Card([ - dmc.CardSection([ - dmc.Group([ - dmc.Text("🔗 Redis", fw=600, c="#2c3e50"), - ], justify="space-between"), - html.Div(id='redis-quick-status', - children=[dmc.Badge("🔄 Checking...", color="yellow", variant="light")]) - ], p="md") - ], shadow="sm", radius="md", withBorder=True) - ], span=3), - - dmc.GridCol([ - dmc.Card([ - dmc.CardSection([ - dmc.Group([ - dmc.Text("📈 Performance", fw=600, c="#2c3e50"), - ], justify="space-between"), - html.Div(id='performance-quick-status', - children=[dmc.Badge("🔄 Loading...", color="yellow", variant="light")]) - ], p="md") - ], shadow="sm", radius="md", withBorder=True) - ], span=3), - ], gutter="md", mb="xl"), + dbc.Row([ + dbc.Col(create_quick_status_card("Data Collection", "data-collection-quick-status", "📊"), width=3), + dbc.Col(create_quick_status_card("Database", "database-quick-status", "🗄️"), width=3), + dbc.Col(create_quick_status_card("Redis", "redis-quick-status", "🔗"), width=3), + dbc.Col(create_quick_status_card("Performance", "performance-quick-status", "📈"), width=3), + ], className="mb-4"), # Detailed Monitoring Sections - dmc.Grid([ + dbc.Row([ # Left Column - Data Collection Service - dmc.GridCol([ + dbc.Col([ # Data Collection Service Status - dmc.Card([ - dmc.CardSection([ - dmc.Title("📡 Data Collection Service", order=4, c="#2c3e50") - ], inheritPadding=True, py="xs", withBorder=True), - dmc.CardSection([ - # Service Status - dmc.Stack([ - dmc.Title("Service Status", order=5, c="#34495e"), - html.Div(id='data-collection-service-status'), - ], gap="sm"), + dbc.Card([ + dbc.CardHeader(html.H4("📡 Data Collection Service")), + dbc.CardBody([ + html.H5("Service Status", className="card-title"), + html.Div(id='data-collection-service-status', className="mb-4"), - # Data Collection Metrics - dmc.Stack([ - dmc.Title("Collection Metrics", order=5, c="#34495e"), - html.Div(id='data-collection-metrics'), - ], gap="sm"), + html.H5("Collection Metrics", className="card-title"), + html.Div(id='data-collection-metrics', className="mb-4"), - # Service Controls - dmc.Stack([ - dmc.Title("Service Controls", order=5, c="#34495e"), - dmc.Group([ - dmc.Button("🔄 Refresh Status", id="refresh-data-status-btn", - variant="light", color="blue", size="sm"), - dmc.Button("📊 View Details", id="view-collection-details-btn", - variant="outline", color="blue", size="sm"), - dmc.Button("📋 View Logs", id="view-collection-logs-btn", - variant="outline", color="gray", size="sm") - ], gap="xs") - ], gap="sm") - ], p="md") - ], shadow="sm", radius="md", withBorder=True, mb="md"), + html.H5("Service Controls", className="card-title"), + dbc.ButtonGroup([ + dbc.Button("🔄 Refresh Status", id="refresh-data-status-btn", color="primary", outline=True, size="sm"), + dbc.Button("📊 View Details", id="view-collection-details-btn", color="secondary", outline=True, size="sm"), + dbc.Button("📋 View Logs", id="view-collection-logs-btn", color="info", outline=True, size="sm") + ]) + ]) + ], className="mb-4"), # Data Collector Health - dmc.Card([ - dmc.CardSection([ - dmc.Title("🔌 Individual Collectors", order=4, c="#2c3e50") - ], inheritPadding=True, py="xs", withBorder=True), - dmc.CardSection([ + dbc.Card([ + dbc.CardHeader(html.H4("🔌 Individual Collectors")), + dbc.CardBody([ html.Div(id='individual-collectors-status'), html.Div([ - dmc.Alert( + dbc.Alert( "Collector health data will be displayed here when the data collection service is running.", - title="📊 Collector Health Monitoring", - color="blue", - variant="light", - id="collectors-info-alert" + id="collectors-info-alert", + color="info", + is_open=True, ) ], id='collectors-placeholder') - ], p="md") - ], shadow="sm", radius="md", withBorder=True, mb="md") - ], span=6), + ]) + ], className="mb-4"), + ], width=6), # Right Column - System Health - dmc.GridCol([ + dbc.Col([ # Database Status - dmc.Card([ - dmc.CardSection([ - dmc.Title("🗄️ Database Health", order=4, c="#2c3e50") - ], inheritPadding=True, py="xs", withBorder=True), - dmc.CardSection([ - dmc.Stack([ - dmc.Title("Connection Status", order=5, c="#34495e"), - html.Div(id='database-status') - ], gap="sm"), - - dmc.Stack([ - dmc.Title("Database Statistics", order=5, c="#34495e"), - html.Div(id='database-stats') - ], gap="sm") - ], p="md") - ], shadow="sm", radius="md", withBorder=True, mb="md"), + dbc.Card([ + dbc.CardHeader(html.H4("🗄️ Database Health")), + dbc.CardBody([ + html.H5("Connection Status", className="card-title"), + html.Div(id='database-status', className="mb-3"), + html.Hr(), + html.H5("Database Statistics", className="card-title"), + html.Div(id='database-stats') + ]) + ], className="mb-4"), # Redis Status - dmc.Card([ - dmc.CardSection([ - dmc.Title("🔗 Redis Status", order=4, c="#2c3e50") - ], inheritPadding=True, py="xs", withBorder=True), - dmc.CardSection([ - dmc.Stack([ - dmc.Title("Connection Status", order=5, c="#34495e"), - html.Div(id='redis-status') - ], gap="sm"), - - dmc.Stack([ - dmc.Title("Redis Statistics", order=5, c="#34495e"), - html.Div(id='redis-stats') - ], gap="sm") - ], p="md") - ], shadow="sm", radius="md", withBorder=True, mb="md"), + dbc.Card([ + dbc.CardHeader(html.H4("🔗 Redis Status")), + dbc.CardBody([ + html.H5("Connection Status", className="card-title"), + html.Div(id='redis-status', className="mb-3"), + html.Hr(), + html.H5("Redis Statistics", className="card-title"), + html.Div(id='redis-stats') + ]) + ], className="mb-4"), # System Performance - dmc.Card([ - dmc.CardSection([ - dmc.Title("📈 System Performance", order=4, c="#2c3e50") - ], inheritPadding=True, py="xs", withBorder=True), - dmc.CardSection([ + dbc.Card([ + dbc.CardHeader(html.H4("📈 System Performance")), + dbc.CardBody([ html.Div(id='system-performance-metrics') - ], p="md") - ], shadow="sm", radius="md", withBorder=True, mb="md") - ], span=6) - ], gutter="md"), + ]) + ], className="mb-4"), + ], width=6) + ]), # Data Collection Details Modal - dmc.Modal( - title="📊 Data Collection Details", - id="collection-details-modal", - children=[ - html.Div(id="collection-details-content") - ], - size="lg" - ), + dbc.Modal([ + dbc.ModalHeader(dbc.ModalTitle("📊 Data Collection Details")), + dbc.ModalBody(id="collection-details-content") + ], id="collection-details-modal", is_open=False, size="lg"), # Collection Logs Modal - dmc.Modal( - title="📋 Collection Service Logs", - id="collection-logs-modal", - children=[ - dmc.ScrollArea([ - dmc.Code( - id="collection-logs-content", - block=True, - style={ - 'white-space': 'pre-wrap', - 'background-color': '#f8f9fa', - 'padding': '15px', - 'border-radius': '5px', - 'font-family': 'monospace' - } - ) - ], h=400), - dmc.Group([ - dmc.Button("Refresh", id="refresh-logs-btn", variant="light"), - dmc.Button("Close", id="close-logs-modal", variant="outline") - ], justify="flex-end", mt="md") - ], - size="xl" - ) + dbc.Modal([ + dbc.ModalHeader(dbc.ModalTitle("📋 Collection Service Logs")), + dbc.ModalBody( + html.Div( + html.Pre(id="collection-logs-content", style={'max-height': '400px', 'overflow-y': 'auto'}), + style={'white-space': 'pre-wrap', 'background-color': '#f8f9fa', 'padding': '15px', 'border-radius': '5px'} + ) + ), + dbc.ModalFooter([ + dbc.Button("Refresh", id="refresh-logs-btn", color="primary"), + dbc.Button("Close", id="close-logs-modal", color="secondary", className="ms-auto") + ]) + ], id="collection-logs-modal", is_open=False, size="xl") ]) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2e4f842..8ca5b8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,10 @@ requires-python = ">=3.10" dependencies = [ # Core web framework "dash>=2.14.0", - "dash-mantine-components>=0.12.0", + "dash-bootstrap-components>=1.6.0", + "dash-bootstrap-templates>=1.1.0", "plotly>=5.17.0", + "waitress>=3.0.0", # Database "sqlalchemy>=2.0.0", "psycopg2-binary>=2.9.0", diff --git a/uv.lock b/uv.lock index 76c55a9..1045c1d 100644 --- a/uv.lock +++ b/uv.lock @@ -389,15 +389,30 @@ wheels = [ ] [[package]] -name = "dash-mantine-components" -version = "2.0.0" +name = "dash-bootstrap-components" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/1e/535c8312f038ea688171435cefd8b5b03452353646e43bade5d92a8d9da0/dash_mantine_components-2.0.0.tar.gz", hash = "sha256:2e09b7f60b41483a06d270c621b5f23a1a9c9321a7f60d2e2b631cde493456cb", size = 850199 } +sdist = { url = "https://files.pythonhosted.org/packages/49/8d/0f641e7c7878ac65b4bb78a2c7cb707db036f82da13fd61948adec44d5aa/dash_bootstrap_components-2.0.3.tar.gz", hash = "sha256:5c161b04a6e7ed19a7d54e42f070c29fd6c385d5a7797e7a82999aa2fc15b1de", size = 115466 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/45/a1acd23b37af85c8b824ccb3e3e4232900725830a652b762ed0c67afec2a/dash_mantine_components-2.0.0-py3-none-any.whl", hash = "sha256:e084ba1fac9a9ad8672852047d0a97dc3cd7372677d1fa55ef8e655a664fa271", size = 1262158 }, + { url = "https://files.pythonhosted.org/packages/f7/f6/b4652aacfbc8d684c9ca8efc5178860a50b54abf82cd1960013c59f8258f/dash_bootstrap_components-2.0.3-py3-none-any.whl", hash = "sha256:82754d3d001ad5482b8a82b496c7bf98a1c68d2669d607a89dda7ec627304af5", size = 203706 }, +] + +[[package]] +name = "dash-bootstrap-templates" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dash" }, + { name = "dash-bootstrap-components" }, + { name = "numpy" }, + { name = "plotly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/2a/5b109ee6aea69deef649a038147dc1696f6d4152de912315a946ee243640/dash_bootstrap_templates-2.1.0.tar.gz", hash = "sha256:ca9da1060ee2b2c74dc1c26119056f37051a838a58ea07b5d325f9df7fde17fe", size = 114447 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/f7/94fff8c10b57d47311c9d9e9a6b98618f1dcad77ae3fbd7e0659230c04ae/dash_bootstrap_templates-2.1.0-py3-none-any.whl", hash = "sha256:d7a89ce5d1cfec205bff2ec621a8a6382f287eea064917909475477fb32c09d6", size = 100293 }, ] [[package]] @@ -409,7 +424,8 @@ dependencies = [ { name = "alembic" }, { name = "click" }, { name = "dash" }, - { name = "dash-mantine-components" }, + { name = "dash-bootstrap-components" }, + { name = "dash-bootstrap-templates" }, { name = "numpy" }, { name = "pandas" }, { name = "plotly" }, @@ -425,6 +441,7 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, { name = "structlog" }, + { name = "waitress" }, { name = "watchdog" }, { name = "websocket-client" }, { name = "websockets" }, @@ -455,7 +472,8 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, { name = "click", specifier = ">=8.0.0" }, { name = "dash", specifier = ">=2.14.0" }, - { name = "dash-mantine-components", specifier = ">=0.12.0" }, + { name = "dash-bootstrap-components", specifier = ">=1.6.0" }, + { name = "dash-bootstrap-templates", specifier = ">=1.1.0" }, { name = "flake8", marker = "extra == 'dev'", specifier = ">=6.0.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.0" }, @@ -479,6 +497,7 @@ requires-dist = [ { name = "requests", specifier = ">=2.31.0" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "structlog", specifier = ">=23.1.0" }, + { name = "waitress", specifier = ">=3.0.0" }, { name = "watchdog", specifier = ">=3.0.0" }, { name = "websocket-client", specifier = ">=1.6.0" }, { name = "websockets", specifier = ">=11.0.0" }, @@ -1816,6 +1835,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982 }, ] +[[package]] +name = "waitress" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/cb/04ddb054f45faa306a230769e868c28b8065ea196891f09004ebace5b184/waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f", size = 179901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232 }, +] + [[package]] name = "watchdog" version = "6.0.0"