Removed Mantine for UI, and used bootstrap for simplicity

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

View File

@ -6,7 +6,7 @@
"display_type": "overlay",
"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"
}

View File

@ -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",

View File

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

View File

@ -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()

View File

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

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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