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