Removed Mantine for UI, and used bootstrap for simplicity

This commit is contained in:
Vasily.onl 2025-06-06 13:33:59 +08:00
parent 38cbf9cd2f
commit 58a754414a
12 changed files with 682 additions and 1284 deletions

View File

@ -6,7 +6,7 @@
"display_type": "overlay", "display_type": "overlay",
"parameters": { "parameters": {
"period": 20, "period": 20,
"std_dev": 2.0 "std_dev": 2
}, },
"styling": { "styling": {
"color": "#6f42c1", "color": "#6f42c1",
@ -16,5 +16,5 @@
}, },
"visible": true, "visible": true,
"created_date": "2025-06-04T04:16:35.460105+00:00", "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"
} }

View File

@ -4,7 +4,7 @@ Main dashboard application module.
import dash import dash
from dash import html, dcc from dash import html, dcc
import dash_mantine_components as dmc import dash_bootstrap_components as dbc
from utils.logger import get_logger from utils.logger import get_logger
from dashboard.layouts import ( from dashboard.layouts import (
get_market_data_layout, get_market_data_layout,
@ -20,10 +20,10 @@ logger = get_logger("dashboard_app")
def create_app(): def create_app():
"""Create and configure the Dash application.""" """Create and configure the Dash application."""
# Initialize Dash app # 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 # Define the main layout wrapped in MantineProvider
app.layout = dmc.MantineProvider([ app.layout = html.Div([
html.Div([ html.Div([
# Page title # Page title
html.H1("🚀 Crypto Trading Bot Dashboard", html.H1("🚀 Crypto Trading Bot Dashboard",

View File

@ -3,6 +3,7 @@ Chart-related callbacks for the dashboard.
""" """
from dash import Output, Input, State, Patch, ctx, html, no_update, dcc from dash import Output, Input, State, Patch, ctx, html, no_update, dcc
import dash_bootstrap_components as dbc
from datetime import datetime, timedelta from datetime import datetime, timedelta
from utils.logger import get_logger from utils.logger import get_logger
from components.charts import ( from components.charts import (
@ -137,15 +138,15 @@ def register_chart_callbacks(app):
) )
def update_market_stats(stored_data, symbol, timeframe): def update_market_stats(stored_data, symbol, timeframe):
if not stored_data: 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: try:
df = pd.read_json(io.StringIO(stored_data), orient='split') df = pd.read_json(io.StringIO(stored_data), orient='split')
if df.empty: 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) return get_market_statistics(df, symbol, timeframe)
except Exception as e: except Exception as e:
logger.error(f"Error updating market stats from stored data: {e}", exc_info=True) 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( @app.callback(
Output("download-chart-data", "data"), Output("download-chart-data", "data"),

View File

@ -3,7 +3,7 @@ Data analysis callbacks for the dashboard.
""" """
from dash import Output, Input, html, dcc 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 utils.logger import get_logger
from dashboard.components.data_analysis import ( from dashboard.components.data_analysis import (
VolumeAnalyzer, VolumeAnalyzer,
@ -35,14 +35,14 @@ def register_data_analysis_callbacks(app):
logger.info(f"🎯 DATA ANALYSIS CALLBACK TRIGGERED! Type: {analysis_type}, Period: {period}") logger.info(f"🎯 DATA ANALYSIS CALLBACK TRIGGERED! Type: {analysis_type}, Period: {period}")
# Return placeholder message since we're moving to enhanced market stats # Return placeholder message since we're moving to enhanced market stats
info_msg = html.Div([ info_msg = dbc.Alert([
html.H4("📊 Statistical Analysis"), html.H4("📊 Statistical Analysis", className="alert-heading"),
html.P("Data analysis has been integrated into the Market Statistics section above."), 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("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.P("Change the symbol and timeframe in the main chart to see updated analysis."),
html.Hr(), html.Hr(),
html.Small("This section will be updated with additional analytical tools in future versions.") html.P("This section will be updated with additional analytical tools in future versions.", className="mb-0")
], style={'border': '2px solid #17a2b8', 'padding': '20px', 'margin': '10px', 'background-color': '#d1ecf1'}) ], color="info")
return info_msg, html.Div() return info_msg, html.Div()

View File

@ -3,7 +3,8 @@ Indicator-related callbacks for the dashboard.
""" """
import dash 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 import json
from utils.logger import get_logger from utils.logger import get_logger
@ -15,105 +16,35 @@ def register_indicator_callbacks(app):
# Modal control callbacks # Modal control callbacks
@app.callback( @app.callback(
[Output('indicator-modal', 'style'), Output('indicator-modal', 'is_open'),
Output('indicator-modal-background', 'style')], [Input('add-indicator-btn-visible', 'n_clicks'),
[Input('add-indicator-btn', 'n_clicks'),
Input('close-modal-btn', 'n_clicks'),
Input('cancel-indicator-btn', 'n_clicks'), Input('cancel-indicator-btn', 'n_clicks'),
Input('edit-indicator-store', 'data')] 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, close_clicks, cancel_clicks, edit_data): def toggle_indicator_modal(add_clicks, cancel_clicks, save_clicks, edit_clicks, is_open):
"""Toggle the visibility of the add indicator modal.""" """Toggle the visibility of the add indicator modal."""
ctx = callback_context
# 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: if not ctx.triggered:
return [hidden_modal_style, hidden_background_style] return is_open
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
# Only open modal if explicitly requested # Check for add button click
should_open = False if triggered_id == 'add-indicator-btn-visible' and add_clicks:
return True
# Check if add button was clicked (and has a click count > 0) # Check for edit button clicks, ensuring a click actually happened
if triggered_id == 'add-indicator-btn' and add_clicks and add_clicks > 0: if 'edit-indicator-btn' in triggered_id and any(c for c in edit_clicks if c is not None):
should_open = True return True
# Check if edit button triggered and should open modal # Check for cancel or save clicks to close the modal
elif triggered_id == 'edit-indicator-store' and edit_data and edit_data.get('open_modal') and edit_data.get('mode') == 'edit': if triggered_id in ['cancel-indicator-btn', 'save-indicator-btn']:
should_open = True return False
# Check if close/cancel buttons were clicked return is_open
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'),
prevent_initial_call=True
)
def sync_add_button_clicks(visible_clicks):
"""Sync clicks from visible button to hidden button."""
return visible_clicks or 0
# Update parameter fields based on indicator type # Update parameter fields based on indicator type
@app.callback( @app.callback(
@ -190,7 +121,7 @@ def register_indicator_callbacks(app):
bb_period, bb_stddev, edit_data): bb_period, bb_stddev, edit_data):
"""Save a new indicator or update an existing one.""" """Save a new indicator or update an existing one."""
if not n_clicks or not name or not indicator_type: if not n_clicks or not name or not indicator_type:
return "", dash.no_update, dash.no_update return "", no_update, no_update
try: try:
# Get indicator manager # Get indicator manager
@ -218,6 +149,7 @@ def register_indicator_callbacks(app):
'std_dev': bb_stddev or 2.0 'std_dev': bb_stddev or 2.0
} }
feedback_msg = None
# Check if this is an edit operation # Check if this is an edit operation
is_edit = edit_data and edit_data.get('mode') == 'edit' is_edit = edit_data and edit_data.get('mode') == 'edit'
@ -233,16 +165,10 @@ def register_indicator_callbacks(app):
) )
if success: if success:
success_msg = html.Div([ feedback_msg = dbc.Alert(f"Indicator '{name}' updated successfully!", color="success")
html.Span("", style={'color': '#28a745'}),
html.Span(f"Indicator '{name}' updated successfully!", style={'color': '#28a745'})
])
else: else:
error_msg = html.Div([ feedback_msg = dbc.Alert("Failed to update indicator.", color="danger")
html.Span("", style={'color': '#dc3545'}), return feedback_msg, no_update, no_update
html.Span("Failed to update indicator. Please try again.", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
else: else:
# Create new indicator # Create new indicator
new_indicator = manager.create_indicator( new_indicator = manager.create_indicator(
@ -254,16 +180,10 @@ def register_indicator_callbacks(app):
) )
if not new_indicator: if not new_indicator:
error_msg = html.Div([ feedback_msg = dbc.Alert("Failed to save indicator.", color="danger")
html.Span("", style={'color': '#dc3545'}), return feedback_msg, no_update, no_update
html.Span("Failed to save indicator. Please try again.", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
success_msg = html.Div([ feedback_msg = dbc.Alert(f"Indicator '{name}' saved successfully!", color="success")
html.Span("", style={'color': '#28a745'}),
html.Span(f"Indicator '{name}' saved successfully!", style={'color': '#28a745'})
])
# Refresh the indicator options # Refresh the indicator options
overlay_indicators = manager.get_indicators_by_type('overlay') 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()})" display_name = f"{indicator.name} ({indicator.type.upper()})"
subplot_options.append({'label': display_name, 'value': indicator.id}) 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: except Exception as e:
logger.error(f"Indicator callback: Error saving indicator: {e}") logger.error(f"Indicator callback: Error saving indicator: {e}")
error_msg = html.Div([ error_msg = dbc.Alert(f"Error: {str(e)}", color="danger")
html.Span("", style={'color': '#dc3545'}), return error_msg, no_update, no_update
html.Span(f"Error: {str(e)}", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
# Update custom indicator lists with edit/delete buttons # Update custom indicator lists with edit/delete buttons
@app.callback( @app.callback(
@ -324,27 +241,15 @@ def register_indicator_callbacks(app):
"✏️", "✏️",
id={'type': 'edit-indicator-btn', 'index': indicator_id}, id={'type': 'edit-indicator-btn', 'index': indicator_id},
title="Edit indicator", title="Edit indicator",
style={ className="btn btn-sm btn-outline-primary",
'background': 'none', style={'margin-left': '5px'}
'border': 'none',
'cursor': 'pointer',
'margin-left': '5px',
'font-size': '14px',
'color': '#007bff'
}
), ),
html.Button( html.Button(
"🗑️", "🗑️",
id={'type': 'delete-indicator-btn', 'index': indicator_id}, id={'type': 'delete-indicator-btn', 'index': indicator_id},
title="Delete indicator", title="Delete indicator",
style={ className="btn btn-sm btn-outline-danger",
'background': 'none', style={'margin-left': '5px'}
'border': 'none',
'cursor': 'pointer',
'margin-left': '5px',
'font-size': '14px',
'color': '#dc3545'
}
) )
], style={'display': 'inline-block', 'width': '30%', 'text-align': 'right'}) ], style={'display': 'inline-block', 'width': '30%', 'text-align': 'right'})
], style={ ], style={
@ -428,9 +333,9 @@ def register_indicator_callbacks(app):
) )
def delete_indicator(delete_clicks, button_ids): def delete_indicator(delete_clicks, button_ids):
"""Delete an indicator when delete button is clicked.""" """Delete an indicator when delete button is clicked."""
ctx = dash.callback_context ctx = callback_context
if not ctx.triggered or not any(delete_clicks): 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 # Find which button was clicked
triggered_id = ctx.triggered[0]['prop_id'] triggered_id = ctx.triggered[0]['prop_id']
@ -461,26 +366,17 @@ def register_indicator_callbacks(app):
display_name = f"{indicator.name} ({indicator.type.upper()})" display_name = f"{indicator.name} ({indicator.type.upper()})"
subplot_options.append({'label': display_name, 'value': indicator.id}) subplot_options.append({'label': display_name, 'value': indicator.id})
success_msg = html.Div([ success_msg = dbc.Alert(f"Indicator '{indicator_name}' deleted.", color="warning")
html.Span("🗑️ ", style={'color': '#dc3545'}),
html.Span(f"Indicator '{indicator_name}' deleted successfully!", style={'color': '#dc3545'})
])
return success_msg, overlay_options, subplot_options return success_msg, overlay_options, subplot_options
else: else:
error_msg = html.Div([ error_msg = dbc.Alert("Failed to delete indicator.", color="danger")
html.Span("", style={'color': '#dc3545'}), return error_msg, no_update, no_update
html.Span("Failed to delete indicator.", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
except Exception as e: except Exception as e:
logger.error(f"Indicator callback: Error deleting indicator: {e}") logger.error(f"Indicator callback: Error deleting indicator: {e}")
error_msg = html.Div([ error_msg = dbc.Alert(f"Error: {str(e)}", color="danger")
html.Span("", style={'color': '#dc3545'}), return error_msg, no_update, no_update
html.Span(f"Error: {str(e)}", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
# Handle edit indicator - open modal with existing data # Handle edit indicator - open modal with existing data
@app.callback( @app.callback(
@ -505,9 +401,9 @@ def register_indicator_callbacks(app):
) )
def edit_indicator(edit_clicks, button_ids): def edit_indicator(edit_clicks, button_ids):
"""Load indicator data for editing.""" """Load indicator data for editing."""
ctx = dash.callback_context ctx = callback_context
if not ctx.triggered or not any(edit_clicks): 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 # Find which button was clicked
triggered_id = ctx.triggered[0]['prop_id'] triggered_id = ctx.triggered[0]['prop_id']
@ -569,13 +465,13 @@ def register_indicator_callbacks(app):
bb_stddev bb_stddev
) )
else: 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: except Exception as e:
logger.error(f"Indicator callback: Error loading indicator for edit: {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( @app.callback(
[Output('indicator-name-input', 'value', allow_duplicate=True), [Output('indicator-name-input', 'value', allow_duplicate=True),
Output('indicator-type-dropdown', '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('macd-signal-period-input', 'value', allow_duplicate=True),
Output('bb-period-input', 'value', allow_duplicate=True), Output('bb-period-input', 'value', allow_duplicate=True),
Output('bb-stddev-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 prevent_initial_call=True
) )
def reset_modal_form(close_clicks, cancel_clicks): def reset_modal_form(cancel_clicks, save_clicks):
"""Reset the modal form when it's closed.""" """Reset the modal form when it's closed or saved."""
if close_clicks or cancel_clicks: if cancel_clicks or save_clicks:
return "", None, "", "#007bff", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0 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") logger.info("Indicator callbacks: registered successfully")

View File

@ -9,7 +9,7 @@ import psutil
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from dash import Output, Input, State, html, callback_context, no_update 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 utils.logger import get_logger
from database.connection import DatabaseManager from database.connection import DatabaseManager
from database.redis_manager import RedisManager from database.redis_manager import RedisManager
@ -47,7 +47,7 @@ def register_system_health_callbacks(app):
except Exception as e: except Exception as e:
logger.error(f"Error updating quick status: {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 return error_status, error_status, error_status, error_status
# Detailed Data Collection Service Status # Detailed Data Collection Service Status
@ -67,11 +67,10 @@ def register_system_health_callbacks(app):
except Exception as e: except Exception as e:
logger.error(f"Error updating data collection status: {e}") logger.error(f"Error updating data collection status: {e}")
error_div = dmc.Alert( error_div = dbc.Alert(
f"Error: {str(e)}", f"Error: {str(e)}",
title="🔴 Status Check Failed", color="danger",
color="red", dismissable=True
variant="light"
) )
return error_div, error_div return error_div, error_div
@ -87,11 +86,10 @@ def register_system_health_callbacks(app):
return _get_individual_collectors_status() return _get_individual_collectors_status()
except Exception as e: except Exception as e:
logger.error(f"Error updating individual collectors status: {e}") logger.error(f"Error updating individual collectors status: {e}")
return dmc.Alert( return dbc.Alert(
f"Error: {str(e)}", f"Error: {str(e)}",
title="🔴 Collectors Check Failed", color="danger",
color="red", dismissable=True
variant="light"
) )
# Database Status and Statistics # Database Status and Statistics
@ -110,11 +108,10 @@ def register_system_health_callbacks(app):
except Exception as e: except Exception as e:
logger.error(f"Error updating database status: {e}") logger.error(f"Error updating database status: {e}")
error_alert = dmc.Alert( error_alert = dbc.Alert(
f"Error: {str(e)}", f"Error: {str(e)}",
title="🔴 Database Check Failed", color="danger",
color="red", dismissable=True
variant="light"
) )
return error_alert, error_alert return error_alert, error_alert
@ -134,11 +131,10 @@ def register_system_health_callbacks(app):
except Exception as e: except Exception as e:
logger.error(f"Error updating Redis status: {e}") logger.error(f"Error updating Redis status: {e}")
error_alert = dmc.Alert( error_alert = dbc.Alert(
f"Error: {str(e)}", f"Error: {str(e)}",
title="🔴 Redis Check Failed", color="danger",
color="red", dismissable=True
variant="light"
) )
return error_alert, error_alert return error_alert, error_alert
@ -153,475 +149,365 @@ def register_system_health_callbacks(app):
return _get_system_performance_metrics() return _get_system_performance_metrics()
except Exception as e: except Exception as e:
logger.error(f"Error updating system performance: {e}") logger.error(f"Error updating system performance: {e}")
return dmc.Alert( return dbc.Alert(
f"Error: {str(e)}", f"Error: {str(e)}",
title="🔴 Performance Check Failed", color="danger",
color="red", dismissable=True
variant="light"
) )
# Data Collection Details Modal # Data Collection Details Modal
@app.callback( @app.callback(
[Output("collection-details-modal", "opened"), [Output("collection-details-modal", "is_open"),
Output("collection-details-content", "children")], Output("collection-details-content", "children")],
[Input("view-collection-details-btn", "n_clicks")], [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.""" """Toggle and populate the collection details modal."""
if details_clicks: if n_clicks:
# Load detailed collection information
details_content = _get_collection_details_content() details_content = _get_collection_details_content()
return True, details_content return not is_open, details_content
return is_open, no_update return is_open, no_update
# Collection Logs Modal # Collection Logs Modal
@app.callback( @app.callback(
[Output("collection-logs-modal", "opened"), [Output("collection-logs-modal", "is_open"),
Output("collection-logs-content", "children")], Output("collection-logs-content", "children")],
[Input("view-collection-logs-btn", "n_clicks"), [Input("view-collection-logs-btn", "n_clicks"),
Input("refresh-logs-btn", "n_clicks"), Input("refresh-logs-btn", "n_clicks")],
Input("close-logs-modal", "n_clicks")], [State("collection-logs-modal", "is_open")],
State("collection-logs-modal", "opened") 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.""" """Toggle and populate the collection logs modal."""
if logs_clicks or refresh_clicks: ctx = callback_context
# Load recent logs 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() logs_content = _get_collection_logs_content()
return True, logs_content return True, logs_content
elif close_clicks:
return False, no_update
return is_open, 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") logger.info("Enhanced system health callbacks registered successfully")
# Helper Functions # Helper Functions
def _get_data_collection_quick_status() -> dmc.Badge: def _get_data_collection_quick_status() -> dbc.Badge:
"""Get quick data collection status.""" """Get quick data collection status."""
try: try:
# Check if data collection service is running (simplified check)
is_running = _check_data_collection_service_running() is_running = _check_data_collection_service_running()
if is_running: if is_running:
return dmc.Badge("🟢 Active", color="green", variant="light") return dbc.Badge("Active", color="success", className="me-1")
else: else:
return dmc.Badge("🔴 Stopped", color="red", variant="light") return dbc.Badge("Stopped", color="danger", className="me-1")
except: 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.""" """Get quick database status."""
try: try:
db_manager = DatabaseManager() db_manager = DatabaseManager()
db_manager.initialize() # Initialize the database manager db_manager.initialize()
result = db_manager.test_connection() if db_manager.test_connection():
if result: return dbc.Badge("Connected", color="success", className="me-1")
return dmc.Badge("🟢 Connected", color="green", variant="light")
else: else:
return dmc.Badge("🔴 Error", color="red", variant="light") return dbc.Badge("Error", color="danger", className="me-1")
except: 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.""" """Get quick Redis status."""
try: try:
redis_manager = RedisManager() redis_manager = RedisManager()
redis_manager.initialize() # Initialize the Redis manager redis_manager.initialize()
result = redis_manager.test_connection() if redis_manager.test_connection():
if result: return dbc.Badge("Connected", color="success", className="me-1")
return dmc.Badge("🟢 Connected", color="green", variant="light")
else: else:
return dmc.Badge("🔴 Error", color="red", variant="light") return dbc.Badge("Error", color="danger", className="me-1")
except: 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.""" """Get quick performance status."""
try: try:
cpu_percent = psutil.cpu_percent(interval=0.1) cpu_percent = psutil.cpu_percent(interval=0.1)
memory = psutil.virtual_memory() memory = psutil.virtual_memory()
if cpu_percent < 80 and memory.percent < 80: 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: 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: else:
return dmc.Badge("🔴 High", color="red", variant="light") return dbc.Badge("High", color="danger", className="me-1")
except: 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: def _get_data_collection_service_status() -> html.Div:
"""Get detailed data collection service status.""" """Get detailed data collection service status."""
try: try:
is_running = _check_data_collection_service_running() is_running = _check_data_collection_service_running()
current_time = datetime.now() current_time = datetime.now().strftime('%H:%M:%S')
if is_running: if is_running:
return dmc.Stack([ status_badge = dbc.Badge("Service Running", color="success", className="me-2")
dmc.Group([ status_text = html.P("Data collection service is actively collecting market data.", className="mb-0")
dmc.Badge("🟢 Service Running", color="green", variant="light"), details = html.Div()
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")
else: else:
return dmc.Stack([ status_badge = dbc.Badge("Service Stopped", color="danger", className="me-2")
dmc.Group([ status_text = html.P("Data collection service is not running.", className="text-danger")
dmc.Badge("🔴 Service Stopped", color="red", variant="light"), details = html.Div([
dmc.Text(f"Checked: {current_time.strftime('%H:%M:%S')}", size="xs", c="dimmed") html.P("To start the service, run:", className="mt-2 mb-1"),
], justify="space-between"), html.Code("python scripts/start_data_collection.py")
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") 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: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error checking status: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Status Check Failed",
color="red",
variant="light"
)
def _get_data_collection_metrics() -> html.Div: def _get_data_collection_metrics() -> html.Div:
"""Get data collection metrics.""" """Get data collection metrics."""
try: try:
# Get database statistics for collected data
db_manager = DatabaseManager() db_manager = DatabaseManager()
db_manager.initialize() # Initialize the database manager db_manager.initialize()
with db_manager.get_session() as session: with db_manager.get_session() as session:
from sqlalchemy import text 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 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
candles_count = session.execute(
text("SELECT COUNT(*) FROM market_data")
).scalar() or 0
# 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: 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): 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): 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: 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([ return html.Div([
dmc.Group([ dbc.Row([
dmc.Text(f"Candles: {candles_count:,}", fw=500), dbc.Col(html.Strong("Candles:")),
dmc.Text(f"Tickers: {tickers_count:,}", fw=500) dbc.Col(f"{candles_count:,}", className="text-end")
], justify="space-between"), ]),
dmc.Group([ dbc.Row([
dmc.Text("Data Freshness:", fw=500), dbc.Col(html.Strong("Tickers:")),
data_freshness_badge dbc.Col(f"{tickers_count:,}", className="text-end")
], justify="space-between") ]),
], gap="xs") dbc.Row([
dbc.Col(html.Strong("Data Freshness:")),
dbc.Col(freshness_badge, className="text-end")
])
])
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error loading metrics: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Metrics Unavailable",
color="red",
variant="light"
)
def _get_individual_collectors_status() -> html.Div: def _get_individual_collectors_status() -> html.Div:
"""Get individual data collector status.""" """Get individual data collector status."""
try: try:
# This would connect to a running data collection service return dbc.Alert([
# For now, show a placeholder indicating the status html.P("Individual collector health data will be displayed here when the data collection service is running.", className="mb-2"),
return dmc.Alert([ html.Hr(),
dmc.Text("Individual collector health data would be displayed here when the data collection service is running.", size="sm"), html.P("To start monitoring, run the following command:", className="mb-1"),
dmc.Space(h="sm"), html.Code("python scripts/start_data_collection.py")
dmc.Group([ ], color="info")
dmc.Text("To start monitoring:", size="sm"),
dmc.Code("python scripts/start_data_collection.py")
])
], title="📊 Collector Health Monitoring", color="blue", variant="light")
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error checking collector status: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Collector Status Check Failed",
color="red",
variant="light"
)
def _get_database_status() -> html.Div: def _get_database_status() -> html.Div:
"""Get detailed database status.""" """Get detailed database status."""
try: try:
db_manager = DatabaseManager() db_manager = DatabaseManager()
db_manager.initialize() # Initialize the database manager db_manager.initialize()
with db_manager.get_session() as session: with db_manager.get_session() as session:
# Test connection and get basic info
from sqlalchemy import text from sqlalchemy import text
result = session.execute(text("SELECT version()")).fetchone() result = session.execute(text("SELECT version()")).fetchone()
version = result[0] if result else "Unknown" version = result[0] if result else "Unknown"
connections = session.execute(text("SELECT count(*) FROM pg_stat_activity")).scalar() or 0
# Get connection count return html.Div([
connections = session.execute( dbc.Row([
text("SELECT count(*) FROM pg_stat_activity") dbc.Col(dbc.Badge("Database Connected", color="success"), width="auto"),
).scalar() or 0 dbc.Col(f"Checked: {datetime.now().strftime('%H:%M:%S')}", className="text-muted")
], align="center", className="mb-2"),
return dmc.Stack([ html.P(f"Version: PostgreSQL {version.split()[1] if 'PostgreSQL' in version else 'Unknown'}", className="mb-1"),
dmc.Group([ html.P(f"Active connections: {connections}", className="mb-0")
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")
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error connecting to database: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Database Connection Failed",
color="red",
variant="light"
)
def _get_database_statistics() -> html.Div: def _get_database_statistics() -> html.Div:
"""Get database statistics.""" """Get database statistics."""
try: try:
db_manager = DatabaseManager() db_manager = DatabaseManager()
db_manager.initialize() # Initialize the database manager db_manager.initialize()
with db_manager.get_session() as session: with db_manager.get_session() as session:
# Get table sizes
from sqlalchemy import text from sqlalchemy import text
table_stats = session.execute(text(""" table_stats_query = """
SELECT SELECT tablename, pg_size_pretty(pg_total_relation_size('public.'||tablename)) as size
schemaname, FROM pg_tables WHERE schemaname = 'public'
tablename, ORDER BY pg_total_relation_size('public.'||tablename) DESC LIMIT 5
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size """
FROM pg_tables table_stats = session.execute(text(table_stats_query)).fetchall()
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
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 total_recent_activity = market_data_activity + raw_data_activity
stats_components = [ components = [
dmc.Group([ dbc.Row([
dmc.Text("Recent Activity (1h):", fw=500), dbc.Col(html.Strong("Recent Activity (1h):")),
dmc.Text(f"{total_recent_activity:,} records", c="#2c3e50") dbc.Col(f"{total_recent_activity:,} records", className="text-end")
], justify="space-between"), ]),
dmc.Group([ html.Hr(className="my-2"),
dmc.Text("• Market Data:", fw=400), html.Strong("Largest Tables:"),
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")
] ]
if table_stats: if table_stats:
stats_components.append(dmc.Text("Largest Tables:", fw=500)) for table, size in table_stats:
for schema, table, size in table_stats: components.append(dbc.Row([
stats_components.append( dbc.Col(f"{table}"),
dmc.Text(f"{table}: {size}", size="xs", c="dimmed", style={'margin-left': '10px'}) dbc.Col(size, className="text-end text-muted")
) ]))
else:
components.append(html.P("No table statistics available.", className="text-muted"))
return dmc.Stack(stats_components, gap="xs") return html.Div(components)
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error loading database stats: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Statistics Unavailable",
color="red",
variant="light"
)
def _get_redis_status() -> html.Div: def _get_redis_status() -> html.Div:
"""Get Redis status.""" """Get Redis status."""
try: try:
redis_manager = RedisManager() redis_manager = RedisManager()
redis_manager.initialize() # Initialize the Redis manager redis_manager.initialize()
info = redis_manager.get_info() info = redis_manager.get_info()
return dmc.Stack([ return html.Div([
dmc.Group([ dbc.Row([
dmc.Badge("🟢 Redis Connected", color="green", variant="light"), dbc.Col(dbc.Badge("Redis Connected", color="success"), width="auto"),
dmc.Text(f"Checked: {datetime.now().strftime('%H:%M:%S')}", size="xs", c="dimmed") dbc.Col(f"Checked: {datetime.now().strftime('%H:%M:%S')}", className="text-muted")
], justify="space-between"), ], align="center", className="mb-2"),
dmc.Text(f"Host: {redis_manager.config.host}:{redis_manager.config.port}", html.P(f"Host: {redis_manager.config.host}:{redis_manager.config.port}", className="mb-0")
size="xs", c="dimmed") ])
], gap="xs")
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error connecting to Redis: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Redis Connection Failed",
color="red",
variant="light"
)
def _get_redis_statistics() -> html.Div: def _get_redis_statistics() -> html.Div:
"""Get Redis statistics.""" """Get Redis statistics."""
try: try:
redis_manager = RedisManager() redis_manager = RedisManager()
redis_manager.initialize() # Initialize the Redis manager redis_manager.initialize()
# Get Redis info
info = redis_manager.get_info() info = redis_manager.get_info()
return dmc.Stack([ return html.Div([
dmc.Group([ dbc.Row([dbc.Col("Memory Used:"), dbc.Col(info.get('used_memory_human', 'N/A'), className="text-end")]),
dmc.Text("Memory Used:", fw=500), dbc.Row([dbc.Col("Connected Clients:"), dbc.Col(info.get('connected_clients', 'N/A'), className="text-end")]),
dmc.Text(f"{info.get('used_memory_human', 'Unknown')}", c="#2c3e50") dbc.Row([dbc.Col("Uptime (hours):"), dbc.Col(f"{info.get('uptime_in_seconds', 0) // 3600}", className="text-end")])
], 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")
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error loading Redis stats: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Statistics Unavailable",
color="red",
variant="light"
)
def _get_system_performance_metrics() -> html.Div: def _get_system_performance_metrics() -> html.Div:
"""Get system performance metrics.""" """Get system performance metrics."""
try: try:
# CPU usage
cpu_percent = psutil.cpu_percent(interval=0.1) cpu_percent = psutil.cpu_percent(interval=0.1)
cpu_count = psutil.cpu_count() cpu_count = psutil.cpu_count()
# Memory usage
memory = psutil.virtual_memory() memory = psutil.virtual_memory()
# Disk usage
disk = psutil.disk_usage('/') disk = psutil.disk_usage('/')
# Network I/O (if available) def get_color(percent):
try: if percent < 70: return "success"
network = psutil.net_io_counters() if percent < 85: return "warning"
network_sent = f"{network.bytes_sent / (1024**3):.2f} GB" return "danger"
network_recv = f"{network.bytes_recv / (1024**3):.2f} GB"
except:
network_sent = "N/A"
network_recv = "N/A"
# Color coding for metrics return html.Div([
cpu_color = "green" if cpu_percent < 70 else "yellow" if cpu_percent < 85 else "red" html.Div([
memory_color = "green" if memory.percent < 70 else "yellow" if memory.percent < 85 else "red" html.Strong("CPU Usage: "),
disk_color = "green" if disk.percent < 70 else "yellow" if disk.percent < 85 else "red" 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"),
return dmc.Stack([ html.Div([
dmc.Group([ html.Strong("Memory Usage: "),
dmc.Text("CPU Usage:", fw=500), dbc.Badge(f"{memory.percent:.1f}%", color=get_color(memory.percent)),
dmc.Badge(f"{cpu_percent:.1f}%", color=cpu_color, variant="light"), html.Span(f" ({memory.used / (1024**3):.1f} / {memory.total / (1024**3):.1f} GB)", className="text-muted ms-1")
dmc.Text(f"({cpu_count} cores)", size="xs", c="dimmed") ], className="mb-2"),
], justify="space-between"), dbc.Progress(value=memory.percent, color=get_color(memory.percent), style={"height": "10px"}, className="mb-3"),
dmc.Group([
dmc.Text("Memory:", fw=500), html.Div([
dmc.Badge(f"{memory.percent:.1f}%", color=memory_color, variant="light"), html.Strong("Disk Usage: "),
dmc.Text(f"{memory.used // (1024**3)} GB / {memory.total // (1024**3)} GB", dbc.Badge(f"{disk.percent:.1f}%", color=get_color(disk.percent)),
size="xs", c="dimmed") html.Span(f" ({disk.used / (1024**3):.1f} / {disk.total / (1024**3):.1f} GB)", className="text-muted ms-1")
], justify="space-between"), ], className="mb-2"),
dmc.Group([ dbc.Progress(value=disk.percent, color=get_color(disk.percent), style={"height": "10px"})
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")
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error loading performance metrics: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Performance Metrics Unavailable",
color="red",
variant="light"
)
def _get_collection_details_content() -> html.Div: def _get_collection_details_content() -> html.Div:
"""Get detailed collection information for modal.""" """Get detailed collection information for modal."""
try: try:
# Detailed service and collector information return html.Div([
return dmc.Stack([ html.H5("Data Collection Service Details"),
dmc.Title("📊 Data Collection Service Details", order=5), html.P("Comprehensive data collection service information would be displayed here."),
dmc.Text("Comprehensive data collection service information would be displayed here."), html.Hr(),
dmc.Divider(), html.H6("Configuration"),
dmc.Title("Configuration", order=6), html.P("Service configuration details..."),
dmc.Text("Service configuration details..."), html.H6("Performance Metrics"),
dmc.Title("Performance Metrics", order=6), html.P("Detailed performance analytics..."),
dmc.Text("Detailed performance analytics..."), html.H6("Health Status"),
dmc.Title("Health Status", order=6), html.P("Individual collector health information...")
dmc.Text("Individual collector health information...") ])
], gap="md")
except Exception as e: except Exception as e:
return dmc.Alert( return dbc.Alert(f"Error loading details: {e}", color="danger")
f"Error: {str(e)}",
title="🔴 Error Loading Details",
color="red",
variant="light"
)
def _get_collection_logs_content() -> str: def _get_collection_logs_content() -> str:

View File

@ -3,6 +3,7 @@ Chart control components for the market data layout.
""" """
from dash import html, dcc from dash import html, dcc
import dash_bootstrap_components as dbc
from utils.logger import get_logger from utils.logger import get_logger
logger = get_logger("default_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): def create_chart_config_panel(strategy_options, overlay_options, subplot_options):
"""Create the chart configuration panel with add/edit UI.""" """Create the chart configuration panel with add/edit UI."""
return html.Div([ return dbc.Card([
html.H5("🎯 Chart Configuration", style={'color': '#2c3e50', 'margin-bottom': '15px'}), dbc.CardHeader(html.H5("🎯 Chart Configuration")),
dbc.CardBody([
dbc.Button(" Add New Indicator", id="add-indicator-btn-visible", color="primary", className="mb-3"),
# 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
html.Div([ html.Div([
html.Label("Overlay Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}), html.Label("Strategy Template:", className="form-label"),
html.Div([ dcc.Dropdown(
# Hidden checklist for callback compatibility 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( dcc.Checklist(
id='overlay-indicators-checklist', id='overlay-indicators-checklist',
options=overlay_options, options=overlay_options,
value=[], # Start with no indicators selected value=[],
style={'display': 'none'} # Hide the basic checklist style={'display': 'none'}
), ),
# Custom indicator list with edit buttons html.Div(id='overlay-indicators-list')
html.Div(id='overlay-indicators-list', children=[ ], width=6),
# This will be populated dynamically
])
])
], style={'width': '48%', 'display': 'inline-block', 'margin-right': '4%', 'vertical-align': 'top'}),
# Subplot Indicators dbc.Col([
html.Div([ html.Label("Subplot Indicators:", className="form-label"),
html.Label("Subplot Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}),
html.Div([
# Hidden checklist for callback compatibility
dcc.Checklist( dcc.Checklist(
id='subplot-indicators-checklist', id='subplot-indicators-checklist',
options=subplot_options, options=subplot_options,
value=[], # Start with no indicators selected value=[],
style={'display': 'none'} # Hide the basic checklist style={'display': 'none'}
), ),
# Custom indicator list with edit buttons html.Div(id='subplot-indicators-list')
html.Div(id='subplot-indicators-list', children=[ ], width=6)
# This will be populated dynamically ])
])
])
], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'})
]) ])
], style={ ], className="mb-4")
'border': '1px solid #bdc3c7',
'border-radius': '8px',
'padding': '15px',
'background-color': '#f8f9fa',
'margin-bottom': '20px'
})
def create_auto_update_control(): def create_auto_update_control():
"""Create the auto-update control section.""" """Create the auto-update control section."""
return html.Div([ return html.Div([
dcc.Checklist( dbc.Checkbox(
id='auto-update-checkbox', id='auto-update-checkbox',
options=[{'label': ' Auto-update charts', 'value': 'auto'}], label='Auto-update charts',
value=['auto'], value=True,
style={'margin-bottom': '10px'}
), ),
html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'}) html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'})
]) ], className="mb-3")
def create_time_range_controls(): def create_time_range_controls():
"""Create the time range control panel.""" """Create the time range control panel."""
return html.Div([ return dbc.Card([
html.H5("⏰ Time Range Controls", style={'color': '#2c3e50', 'margin-bottom': '15px'}), dbc.CardHeader(html.H5("⏰ Time Range Controls")),
dbc.CardBody([
# 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'}),
html.Div([ html.Div([
dcc.DatePickerRange( html.Label("Quick Select:", className="form-label"),
id='custom-date-range', dcc.Dropdown(
display_format='YYYY-MM-DD', id='time-range-quick-select',
style={'display': 'inline-block', 'margin-right': '10px'} options=[
), {'label': '🕐 Last 1 Hour', 'value': '1h'},
html.Button( {'label': '🕐 Last 4 Hours', 'value': '4h'},
"Clear", {'label': '🕐 Last 6 Hours', 'value': '6h'},
id="clear-date-range-btn", {'label': '🕐 Last 12 Hours', 'value': '12h'},
className="btn btn-sm btn-outline-secondary", {'label': '📅 Last 1 Day', 'value': '1d'},
style={ {'label': '📅 Last 3 Days', 'value': '3d'},
'display': 'inline-block', {'label': '📅 Last 7 Days', 'value': '7d'},
'vertical-align': 'top', {'label': '📅 Last 30 Days', 'value': '30d'},
'margin-top': '7px', {'label': '📅 Custom Range', 'value': 'custom'},
'padding': '5px 10px', {'label': '🔴 Real-time', 'value': 'realtime'}
'font-size': '12px' ],
} value='7d',
placeholder="Select time range",
) )
], style={'margin-bottom': '15px'}) ], className="mb-3"),
]),
# Analysis Mode Toggle html.Div([
html.Div([ html.Label("Custom Date Range:", className="form-label"),
html.Label("Analysis Mode:", style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}), dbc.InputGroup([
dcc.RadioItems( dcc.DatePickerRange(
id='analysis-mode-toggle', id='custom-date-range',
options=[ display_format='YYYY-MM-DD',
{'label': '🔴 Real-time Updates', 'value': 'realtime'}, ),
{'label': '🔒 Analysis Mode (Locked)', 'value': 'locked'} dbc.Button("Clear", id="clear-date-range-btn", color="secondary", outline=True, size="sm")
], ])
value='realtime', ], className="mb-3"),
inline=True,
style={'margin-bottom': '10px'}
)
]),
# Time Range Status html.Div([
html.Div(id='time-range-status', html.Label("Analysis Mode:", className="form-label"),
style={'font-size': '12px', 'color': '#7f8c8d', 'font-style': 'italic'}) dbc.RadioItems(
id='analysis-mode-toggle',
options=[
{'label': '🔴 Real-time Updates', 'value': 'realtime'},
{'label': '🔒 Analysis Mode (Locked)', 'value': 'locked'}
],
value='realtime',
inline=True,
)
]),
], style={ html.Div(id='time-range-status', className="text-muted fst-italic mt-2")
'border': '1px solid #bdc3c7', ])
'border-radius': '8px', ], className="mb-4")
'padding': '15px',
'background-color': '#f0f8ff',
'margin-bottom': '20px'
})
def create_export_controls(): def create_export_controls():
"""Create the data export control panel.""" """Create the data export control panel."""
return html.Div([ return dbc.Card([
html.H5("💾 Data Export", style={'color': '#2c3e50', 'margin-bottom': '15px'}), dbc.CardHeader(html.H5("💾 Data Export")),
html.Button( dbc.CardBody([
"Export to CSV", dbc.ButtonGroup([
id="export-csv-btn", dbc.Button("Export to CSV", id="export-csv-btn", color="primary"),
className="btn btn-primary", dbc.Button("Export to JSON", id="export-json-btn", color="secondary"),
style={ ]),
'background-color': '#28a745', dcc.Download(id="download-chart-data")
'color': 'white', ])
'border': 'none', ], className="mb-4")
'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'
})

View File

@ -3,7 +3,7 @@ Data analysis components for comprehensive market data analysis.
""" """
from dash import html, dcc 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.graph_objects as go
import plotly.express as px import plotly.express as px
from plotly.subplots import make_subplots from plotly.subplots import make_subplots
@ -466,7 +466,7 @@ def create_data_analysis_panel():
]), ]),
dcc.Tab(label="Price Movement", value="price-movement", children=[ dcc.Tab(label="Price Movement", value="price-movement", children=[
html.Div(id='price-movement-content', 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: def create_volume_stats_display(stats: Dict[str, Any]) -> html.Div:
"""Create volume statistics display.""" """Create volume statistics display."""
if 'error' in stats: if 'error' in stats:
return dmc.Alert( return dbc.Alert(
"Error loading volume statistics", "Error loading volume statistics",
title="Volume Analysis Error", color="danger",
color="red" dismissable=True
) )
return dmc.SimpleGrid([ def create_stat_card(icon, title, value, color="primary"):
dmc.Paper([ return dbc.Col(dbc.Card(dbc.CardBody([
dmc.Group([ html.Div([
dmc.ThemeIcon("📊", size="lg", color="blue"), html.Div(icon, className="display-6"),
dmc.Stack([ html.Div([
dmc.Text("Total Volume", size="sm", c="dimmed"), html.P(title, className="card-title mb-1 text-muted"),
dmc.Text(format_number(stats['total_volume']), fw=700, size="lg") html.H4(value, className=f"card-text fw-bold text-{color}")
], gap="xs") ], className="ms-3")
]) ], className="d-flex align-items-center")
], p="md", shadow="sm"), ])), width=4, className="mb-3")
dmc.Paper([ return dbc.Row([
dmc.Group([ create_stat_card("📊", "Total Volume", format_number(stats['total_volume'])),
dmc.ThemeIcon("📈", size="lg", color="green"), create_stat_card("📈", "Average Volume", format_number(stats['avg_volume'])),
dmc.Stack([ create_stat_card("🎯", "Volume Trend", stats['volume_trend'],
dmc.Text("Average Volume", size="sm", c="dimmed"), "success" if stats['volume_trend'] == "Increasing" else "danger"),
dmc.Text(format_number(stats['avg_volume']), fw=700, size="lg") create_stat_card("", "High Volume Periods", str(stats['high_volume_periods'])),
], gap="xs") create_stat_card("🔗", "Volume-Price Correlation", f"{stats['volume_price_correlation']:.3f}"),
]) create_stat_card("💱", "Avg Trade Size", format_number(stats['avg_trade_size']))
], p="md", shadow="sm"), ], className="mt-3")
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_price_stats_display(stats: Dict[str, Any]) -> html.Div: def create_price_stats_display(stats: Dict[str, Any]) -> html.Div:
"""Create price movement statistics display.""" """Create price movement statistics display."""
if 'error' in stats: if 'error' in stats:
return dmc.Alert( return dbc.Alert(
"Error loading price statistics", "Error loading price statistics",
title="Price Analysis Error", color="danger",
color="red" dismissable=True
) )
return dmc.SimpleGrid([ def create_stat_card(icon, title, value, color="primary"):
dmc.Paper([ text_color = "text-dark"
dmc.Group([ if color == "success":
dmc.ThemeIcon("💰", size="lg", color="blue"), text_color = "text-success"
dmc.Stack([ elif color == "danger":
dmc.Text("Current Price", size="sm", c="dimmed"), text_color = "text-danger"
dmc.Text(f"${stats['current_price']:.2f}", fw=700, size="lg")
], gap="xs")
])
], p="md", shadow="sm"),
dmc.Paper([ return dbc.Col(dbc.Card(dbc.CardBody([
dmc.Group([ html.Div([
dmc.ThemeIcon("📈", size="lg", color="green" if stats['period_return'] >= 0 else "red"), html.Div(icon, className="display-6"),
dmc.Stack([ html.Div([
dmc.Text("Period Return", size="sm", c="dimmed"), html.P(title, className="card-title mb-1 text-muted"),
dmc.Text(f"{stats['period_return']:+.2f}%", fw=700, size="lg", html.H4(value, className=f"card-text fw-bold {text_color}")
c="green" if stats['period_return'] >= 0 else "red") ], className="ms-3")
], gap="xs") ], className="d-flex align-items-center")
]) ])), width=4, className="mb-3")
], p="md", shadow="sm"),
dmc.Paper([ return dbc.Row([
dmc.Group([ create_stat_card("💰", "Current Price", f"${stats['current_price']:.2f}"),
dmc.ThemeIcon("📊", size="lg", color="orange"), create_stat_card("📈", "Period Return", f"{stats['period_return']:+.2f}%",
dmc.Stack([ "success" if stats['period_return'] >= 0 else "danger"),
dmc.Text("Volatility", size="sm", c="dimmed"), create_stat_card("📊", "Volatility", f"{stats['volatility']:.2f}%", color="warning"),
dmc.Text(f"{stats['volatility']:.2f}%", fw=700, size="lg") create_stat_card("🎯", "Bullish Ratio", f"{stats['bullish_ratio']:.1f}%"),
], gap="xs") create_stat_card("", "Momentum", f"{stats['momentum']:+.2f}%",
]) "success" if stats['momentum'] >= 0 else "danger"),
], p="md", shadow="sm"), create_stat_card("📉", "Max Loss", f"{stats['max_loss']:.2f}%", "danger")
], className="mt-3")
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 get_market_statistics(df: pd.DataFrame, symbol: str, timeframe: str) -> html.Div: 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)" time_status = f"📅 Analysis Range: {start_date} to {end_date} (~{days_back} days)"
return html.Div([ return html.Div([
html.H3("📊 Enhanced Market Statistics"), html.H3("📊 Enhanced Market Statistics", className="mb-3"),
html.P( html.P(
time_status, 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_price_stats_display(price_stats),
create_volume_stats_display(volume_stats) create_volume_stats_display(volume_stats)
]) ])
except Exception as e: except Exception as e:
logger.error(f"Error in get_market_statistics: {e}", exc_info=True) logger.error(f"Error in get_market_statistics: {e}", exc_info=True)
return html.Div(f"Error generating statistics display: {e}", style={'color': 'red'}) return dbc.Alert(f"Error generating statistics display: {e}", color="danger")

View File

@ -3,281 +3,118 @@ Indicator modal component for creating and editing indicators.
""" """
from dash import html, dcc from dash import html, dcc
import dash_bootstrap_components as dbc
def create_indicator_modal(): def create_indicator_modal():
"""Create the indicator modal dialog for adding/editing indicators.""" """Create the indicator modal dialog for adding/editing indicators."""
return html.Div([ return html.Div([
dcc.Store(id='edit-indicator-store', data=None), # Store for edit mode - explicitly start with None 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(),
# Modal Background # Parameters Section
html.Div( html.H5("Parameters"),
id='indicator-modal-background', html.Div(
style={ id='indicator-parameters-message',
'display': 'none', children=[html.P("Select an indicator type to configure parameters", className="text-muted fst-italic")]
'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 # Parameter fields (SMA, EMA, etc.)
html.Div([ create_parameter_fields(),
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.Hr(),
html.Div([ # Styling Section
# Basic Settings html.H5("Styling"),
html.Div([ dbc.Row([
html.H5("Basic Settings", style={'color': '#2c3e50', 'margin-bottom': '15px'}), dbc.Col([
dbc.Label("Color:"),
# Indicator Name dcc.Input(id='indicator-color-input', type='text', value='#007bff', className="w-100")
html.Div([ ], width=6),
html.Label("Indicator Name:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), dbc.Col([
dcc.Input( dbc.Label("Line Width:"),
id='indicator-name-input', 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)})
type='text', ], width=6)
placeholder='e.g., "SMA 30 Custom"', ], className="mb-3"),
style={'width': '100%', 'padding': '8px', 'margin-bottom': '10px', 'border': '1px solid #ddd', 'border-radius': '4px'} ]),
) dbc.ModalFooter([
]), html.Div(id='save-indicator-feedback', className="me-auto"),
dbc.Button("Cancel", id="cancel-indicator-btn", color="secondary"),
# Indicator Type dbc.Button("Save Indicator", id="save-indicator-btn", color="primary")
html.Div([ ])
html.Label("Indicator Type:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), ], id='indicator-modal', size="lg", is_open=False),
dcc.Dropdown( ])
id='indicator-type-dropdown',
options=[ def create_parameter_fields():
{'label': 'Simple Moving Average (SMA)', 'value': 'sma'}, """Helper function to create parameter input fields for all indicator types."""
{'label': 'Exponential Moving Average (EMA)', 'value': 'ema'}, return html.Div([
{'label': 'Relative Strength Index (RSI)', 'value': 'rsi'}, # SMA Parameters
{'label': 'MACD', 'value': 'macd'}, html.Div([
{'label': 'Bollinger Bands', 'value': 'bollinger_bands'} dbc.Label("Period:"),
], dcc.Input(id='sma-period-input', type='number', value=20, min=1, max=200),
placeholder='Select indicator type', dbc.FormText("Number of periods for Simple Moving Average calculation")
style={'margin-bottom': '10px'} ], id='sma-parameters', style={'display': 'none'}, className="mb-3"),
)
]), # EMA Parameters
html.Div([
# Description dbc.Label("Period:"),
html.Div([ dcc.Input(id='ema-period-input', type='number', value=12, min=1, max=200),
html.Label("Description (Optional):", style={'font-weight': 'bold', 'margin-bottom': '5px'}), dbc.FormText("Number of periods for Exponential Moving Average calculation")
dcc.Textarea( ], id='ema-parameters', style={'display': 'none'}, className="mb-3"),
id='indicator-description-input',
placeholder='Brief description of this indicator configuration...', # RSI Parameters
style={'width': '100%', 'height': '60px', 'padding': '8px', 'margin-bottom': '15px', 'border': '1px solid #ddd', 'border-radius': '4px', 'resize': 'vertical'} html.Div([
) dbc.Label("Period:"),
]) dcc.Input(id='rsi-period-input', type='number', value=14, min=2, max=50),
], style={'margin-bottom': '20px'}), dbc.FormText("Number of periods for RSI calculation (typically 14)")
], id='rsi-parameters', style={'display': 'none'}, className="mb-3"),
# Parameters Section
html.Div([ # MACD Parameters
html.H5("Parameters", style={'color': '#2c3e50', 'margin-bottom': '15px'}), html.Div([
dbc.Row([
# Default message dbc.Col([dbc.Label("Fast Period:"), dcc.Input(id='macd-fast-period-input', type='number', value=12)], width=4),
html.Div( dbc.Col([dbc.Label("Slow Period:"), dcc.Input(id='macd-slow-period-input', type='number', value=26)], width=4),
id='indicator-parameters-message', dbc.Col([dbc.Label("Signal Period:"), dcc.Input(id='macd-signal-period-input', type='number', value=9)], width=4),
children=[html.P("Select an indicator type to configure parameters", style={'color': '#7f8c8d', 'font-style': 'italic'})], ]),
style={'display': 'block'} dbc.FormText("MACD periods: Fast EMA, Slow EMA, and Signal line")
), ], id='macd-parameters', style={'display': 'none'}, className="mb-3"),
# SMA Parameters (hidden by default) # Bollinger Bands Parameters
html.Div([ html.Div([
html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), dbc.Row([
dcc.Input( dbc.Col([dbc.Label("Period:"), dcc.Input(id='bb-period-input', type='number', value=20)], width=6),
id='sma-period-input', dbc.Col([dbc.Label("Standard Deviation:"), dcc.Input(id='bb-stddev-input', type='number', value=2.0, step=0.1)], width=6),
type='number', ]),
value=20, dbc.FormText("Period for middle line (SMA) and standard deviation multiplier")
min=1, max=200, ], id='bb-parameters', style={'display': 'none'}, className="mb-3")
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'
})
]) ])

View File

@ -2,211 +2,130 @@
System health monitoring layout for the dashboard. System health monitoring layout for the dashboard.
""" """
from dash import html, dcc from dash import html
import dash_mantine_components as dmc import dash_bootstrap_components as dbc
def get_system_health_layout(): 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([ return html.Div([
# Header section # Header section
dmc.Paper([ html.Div([
dmc.Title("⚙️ System Health & Data Monitoring", order=2, c="#2c3e50"), html.H2("⚙️ System Health & Data Monitoring"),
dmc.Text("Real-time monitoring of data collection services, database health, and system performance", html.P("Real-time monitoring of data collection services, database health, and system performance",
c="dimmed", size="sm") className="lead")
], p="lg", mb="xl"), ], className="p-5 mb-4 bg-light rounded-3"),
# Quick Status Overview Row # Quick Status Overview Row
dmc.Grid([ dbc.Row([
dmc.GridCol([ dbc.Col(create_quick_status_card("Data Collection", "data-collection-quick-status", "📊"), width=3),
dmc.Card([ dbc.Col(create_quick_status_card("Database", "database-quick-status", "🗄️"), width=3),
dmc.CardSection([ dbc.Col(create_quick_status_card("Redis", "redis-quick-status", "🔗"), width=3),
dmc.Group([ dbc.Col(create_quick_status_card("Performance", "performance-quick-status", "📈"), width=3),
dmc.Text("📊 Data Collection", fw=600, c="#2c3e50"), ], className="mb-4"),
], 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"),
# Detailed Monitoring Sections # Detailed Monitoring Sections
dmc.Grid([ dbc.Row([
# Left Column - Data Collection Service # Left Column - Data Collection Service
dmc.GridCol([ dbc.Col([
# Data Collection Service Status # Data Collection Service Status
dmc.Card([ dbc.Card([
dmc.CardSection([ dbc.CardHeader(html.H4("📡 Data Collection Service")),
dmc.Title("📡 Data Collection Service", order=4, c="#2c3e50") dbc.CardBody([
], inheritPadding=True, py="xs", withBorder=True), html.H5("Service Status", className="card-title"),
dmc.CardSection([ html.Div(id='data-collection-service-status', className="mb-4"),
# Service Status
dmc.Stack([
dmc.Title("Service Status", order=5, c="#34495e"),
html.Div(id='data-collection-service-status'),
], gap="sm"),
# Data Collection Metrics html.H5("Collection Metrics", className="card-title"),
dmc.Stack([ html.Div(id='data-collection-metrics', className="mb-4"),
dmc.Title("Collection Metrics", order=5, c="#34495e"),
html.Div(id='data-collection-metrics'),
], gap="sm"),
# Service Controls html.H5("Service Controls", className="card-title"),
dmc.Stack([ dbc.ButtonGroup([
dmc.Title("Service Controls", order=5, c="#34495e"), dbc.Button("🔄 Refresh Status", id="refresh-data-status-btn", color="primary", outline=True, size="sm"),
dmc.Group([ dbc.Button("📊 View Details", id="view-collection-details-btn", color="secondary", outline=True, size="sm"),
dmc.Button("🔄 Refresh Status", id="refresh-data-status-btn", dbc.Button("📋 View Logs", id="view-collection-logs-btn", color="info", outline=True, size="sm")
variant="light", color="blue", size="sm"), ])
dmc.Button("📊 View Details", id="view-collection-details-btn", ])
variant="outline", color="blue", size="sm"), ], className="mb-4"),
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"),
# Data Collector Health # Data Collector Health
dmc.Card([ dbc.Card([
dmc.CardSection([ dbc.CardHeader(html.H4("🔌 Individual Collectors")),
dmc.Title("🔌 Individual Collectors", order=4, c="#2c3e50") dbc.CardBody([
], inheritPadding=True, py="xs", withBorder=True),
dmc.CardSection([
html.Div(id='individual-collectors-status'), html.Div(id='individual-collectors-status'),
html.Div([ html.Div([
dmc.Alert( dbc.Alert(
"Collector health data will be displayed here when the data collection service is running.", "Collector health data will be displayed here when the data collection service is running.",
title="📊 Collector Health Monitoring", id="collectors-info-alert",
color="blue", color="info",
variant="light", is_open=True,
id="collectors-info-alert"
) )
], id='collectors-placeholder') ], id='collectors-placeholder')
], p="md") ])
], shadow="sm", radius="md", withBorder=True, mb="md") ], className="mb-4"),
], span=6), ], width=6),
# Right Column - System Health # Right Column - System Health
dmc.GridCol([ dbc.Col([
# Database Status # Database Status
dmc.Card([ dbc.Card([
dmc.CardSection([ dbc.CardHeader(html.H4("🗄️ Database Health")),
dmc.Title("🗄️ Database Health", order=4, c="#2c3e50") dbc.CardBody([
], inheritPadding=True, py="xs", withBorder=True), html.H5("Connection Status", className="card-title"),
dmc.CardSection([ html.Div(id='database-status', className="mb-3"),
dmc.Stack([ html.Hr(),
dmc.Title("Connection Status", order=5, c="#34495e"), html.H5("Database Statistics", className="card-title"),
html.Div(id='database-status') html.Div(id='database-stats')
], gap="sm"), ])
], className="mb-4"),
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"),
# Redis Status # Redis Status
dmc.Card([ dbc.Card([
dmc.CardSection([ dbc.CardHeader(html.H4("🔗 Redis Status")),
dmc.Title("🔗 Redis Status", order=4, c="#2c3e50") dbc.CardBody([
], inheritPadding=True, py="xs", withBorder=True), html.H5("Connection Status", className="card-title"),
dmc.CardSection([ html.Div(id='redis-status', className="mb-3"),
dmc.Stack([ html.Hr(),
dmc.Title("Connection Status", order=5, c="#34495e"), html.H5("Redis Statistics", className="card-title"),
html.Div(id='redis-status') html.Div(id='redis-stats')
], gap="sm"), ])
], className="mb-4"),
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"),
# System Performance # System Performance
dmc.Card([ dbc.Card([
dmc.CardSection([ dbc.CardHeader(html.H4("📈 System Performance")),
dmc.Title("📈 System Performance", order=4, c="#2c3e50") dbc.CardBody([
], inheritPadding=True, py="xs", withBorder=True),
dmc.CardSection([
html.Div(id='system-performance-metrics') html.Div(id='system-performance-metrics')
], p="md") ])
], shadow="sm", radius="md", withBorder=True, mb="md") ], className="mb-4"),
], span=6) ], width=6)
], gutter="md"), ]),
# Data Collection Details Modal # Data Collection Details Modal
dmc.Modal( dbc.Modal([
title="📊 Data Collection Details", dbc.ModalHeader(dbc.ModalTitle("📊 Data Collection Details")),
id="collection-details-modal", dbc.ModalBody(id="collection-details-content")
children=[ ], id="collection-details-modal", is_open=False, size="lg"),
html.Div(id="collection-details-content")
],
size="lg"
),
# Collection Logs Modal # Collection Logs Modal
dmc.Modal( dbc.Modal([
title="📋 Collection Service Logs", dbc.ModalHeader(dbc.ModalTitle("📋 Collection Service Logs")),
id="collection-logs-modal", dbc.ModalBody(
children=[ html.Div(
dmc.ScrollArea([ html.Pre(id="collection-logs-content", style={'max-height': '400px', 'overflow-y': 'auto'}),
dmc.Code( style={'white-space': 'pre-wrap', 'background-color': '#f8f9fa', 'padding': '15px', 'border-radius': '5px'}
id="collection-logs-content", )
block=True, ),
style={ dbc.ModalFooter([
'white-space': 'pre-wrap', dbc.Button("Refresh", id="refresh-logs-btn", color="primary"),
'background-color': '#f8f9fa', dbc.Button("Close", id="close-logs-modal", color="secondary", className="ms-auto")
'padding': '15px', ])
'border-radius': '5px', ], id="collection-logs-modal", is_open=False, size="xl")
'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"
)
]) ])

View File

@ -7,8 +7,10 @@ requires-python = ">=3.10"
dependencies = [ dependencies = [
# Core web framework # Core web framework
"dash>=2.14.0", "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", "plotly>=5.17.0",
"waitress>=3.0.0",
# Database # Database
"sqlalchemy>=2.0.0", "sqlalchemy>=2.0.0",
"psycopg2-binary>=2.9.0", "psycopg2-binary>=2.9.0",

40
uv.lock generated
View File

@ -389,15 +389,30 @@ wheels = [
] ]
[[package]] [[package]]
name = "dash-mantine-components" name = "dash-bootstrap-components"
version = "2.0.0" version = "2.0.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "dash" }, { 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 = [ 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]] [[package]]
@ -409,7 +424,8 @@ dependencies = [
{ name = "alembic" }, { name = "alembic" },
{ name = "click" }, { name = "click" },
{ name = "dash" }, { name = "dash" },
{ name = "dash-mantine-components" }, { name = "dash-bootstrap-components" },
{ name = "dash-bootstrap-templates" },
{ name = "numpy" }, { name = "numpy" },
{ name = "pandas" }, { name = "pandas" },
{ name = "plotly" }, { name = "plotly" },
@ -425,6 +441,7 @@ dependencies = [
{ name = "requests" }, { name = "requests" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "structlog" }, { name = "structlog" },
{ name = "waitress" },
{ name = "watchdog" }, { name = "watchdog" },
{ name = "websocket-client" }, { name = "websocket-client" },
{ name = "websockets" }, { name = "websockets" },
@ -455,7 +472,8 @@ requires-dist = [
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },
{ name = "click", specifier = ">=8.0.0" }, { name = "click", specifier = ">=8.0.0" },
{ name = "dash", specifier = ">=2.14.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 = "flake8", marker = "extra == 'dev'", specifier = ">=6.0.0" },
{ name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.0" },
@ -479,6 +497,7 @@ requires-dist = [
{ name = "requests", specifier = ">=2.31.0" }, { name = "requests", specifier = ">=2.31.0" },
{ name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.0" },
{ name = "structlog", specifier = ">=23.1.0" }, { name = "structlog", specifier = ">=23.1.0" },
{ name = "waitress", specifier = ">=3.0.0" },
{ name = "watchdog", specifier = ">=3.0.0" }, { name = "watchdog", specifier = ">=3.0.0" },
{ name = "websocket-client", specifier = ">=1.6.0" }, { name = "websocket-client", specifier = ">=1.6.0" },
{ name = "websockets", specifier = ">=11.0.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 }, { 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]] [[package]]
name = "watchdog" name = "watchdog"
version = "6.0.0" version = "6.0.0"