#!/usr/bin/env python3 """ Main Dash application for the Crypto Trading Bot Dashboard. Provides real-time visualization and bot management interface. """ import sys from pathlib import Path # Add project root to path project_root = Path(__file__).parent sys.path.insert(0, str(project_root)) # Suppress SQLAlchemy logging to reduce verbosity import logging logging.getLogger('sqlalchemy').setLevel(logging.WARNING) logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING) logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING) logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING) logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING) import dash from dash import dcc, html, Input, Output, callback import plotly.graph_objects as go from datetime import datetime, timedelta import pandas as pd # Import project modules from config.settings import app as app_settings, dashboard as dashboard_settings from utils.logger import get_logger from database.connection import DatabaseManager from components.charts import ( create_candlestick_chart, get_market_statistics, get_supported_symbols, get_supported_timeframes, create_data_status_indicator, check_data_availability, create_error_chart ) # Initialize logger logger = get_logger("dashboard_app") def create_app(): """Create and configure the Dash application.""" # Initialize Dash app app = dash.Dash( __name__, title="Crypto Trading Bot Dashboard", update_title="Loading...", suppress_callback_exceptions=True ) # Configure app app.server.secret_key = "crypto-bot-dashboard-secret-key-2024" logger.info("Initializing Crypto Trading Bot Dashboard") # Define basic layout app.layout = html.Div([ # Header html.Div([ html.H1("🚀 Crypto Trading Bot Dashboard", style={'margin': '0', 'color': '#2c3e50'}), html.P("Real-time monitoring and bot management", style={'margin': '5px 0 0 0', 'color': '#7f8c8d'}) ], style={ 'padding': '20px', 'background-color': '#ecf0f1', 'border-bottom': '2px solid #bdc3c7' }), # Navigation tabs dcc.Tabs(id="main-tabs", value='market-data', children=[ dcc.Tab(label='📊 Market Data', value='market-data'), dcc.Tab(label='🤖 Bot Management', value='bot-management'), dcc.Tab(label='📈 Performance', value='performance'), dcc.Tab(label='⚙️ System Health', value='system-health'), ], style={'margin': '10px 20px'}), # Main content area html.Div(id='tab-content', style={'padding': '20px'}), # Auto-refresh interval for real-time updates dcc.Interval( id='interval-component', interval=5000, # Update every 5 seconds n_intervals=0 ), # Store components for data sharing between callbacks dcc.Store(id='market-data-store'), dcc.Store(id='bot-status-store'), ]) return app def get_market_data_layout(): """Create the market data visualization layout.""" # Get available symbols and timeframes from database symbols = get_supported_symbols() timeframes = get_supported_timeframes() # Create dropdown options symbol_options = [{'label': symbol, 'value': symbol} for symbol in symbols] timeframe_options = [ {'label': '1 Minute', 'value': '1m'}, {'label': '5 Minutes', 'value': '5m'}, {'label': '15 Minutes', 'value': '15m'}, {'label': '1 Hour', 'value': '1h'}, {'label': '4 Hours', 'value': '4h'}, {'label': '1 Day', 'value': '1d'}, ] # Filter timeframe options to only show those available in database available_timeframes = [tf for tf in ['1m', '5m', '15m', '1h', '4h', '1d'] if tf in timeframes] if not available_timeframes: available_timeframes = ['1h'] # Default fallback timeframe_options = [opt for opt in timeframe_options if opt['value'] in available_timeframes] return html.Div([ html.H2("📊 Real-time Market Data", style={'color': '#2c3e50'}), # Symbol selector html.Div([ html.Label("Select Trading Pair:", style={'font-weight': 'bold'}), dcc.Dropdown( id='symbol-dropdown', options=symbol_options, value=symbols[0] if symbols else 'BTC-USDT', style={'margin': '10px 0'} ) ], style={'width': '300px', 'margin': '20px 0'}), # Timeframe selector html.Div([ html.Label("Timeframe:", style={'font-weight': 'bold'}), dcc.Dropdown( id='timeframe-dropdown', options=timeframe_options, value=available_timeframes[0] if available_timeframes else '1h', style={'margin': '10px 0'} ) ], style={'width': '300px', 'margin': '20px 0'}), # Price chart dcc.Graph( id='price-chart', style={'height': '600px', 'margin': '20px 0'}, config={'displayModeBar': True, 'displaylogo': False} ), # Market statistics html.Div(id='market-stats', style={'margin': '20px 0'}), # Data status indicator html.Div(id='data-status', style={'margin': '20px 0'}) ]) def get_bot_management_layout(): """Create the bot management layout.""" return html.Div([ html.H2("🤖 Bot Management", style={'color': '#2c3e50'}), html.P("Bot management interface will be implemented in Phase 4.0"), # Placeholder for bot list html.Div([ html.H3("Active Bots"), html.Div(id='bot-list', children=[ html.P("No bots currently running", style={'color': '#7f8c8d'}) ]) ], style={'margin': '20px 0'}) ]) def get_performance_layout(): """Create the performance monitoring layout.""" return html.Div([ html.H2("📈 Performance Analytics", style={'color': '#2c3e50'}), html.P("Performance analytics will be implemented in Phase 6.0"), # Placeholder for performance metrics html.Div([ html.H3("Portfolio Performance"), html.P("Portfolio tracking coming soon", style={'color': '#7f8c8d'}) ], style={'margin': '20px 0'}) ]) def get_system_health_layout(): """Create the system health monitoring layout.""" return html.Div([ html.H2("⚙️ System Health", style={'color': '#2c3e50'}), # Database status html.Div([ html.H3("Database Status"), html.Div(id='database-status') ], style={'margin': '20px 0'}), # Data collection status html.Div([ html.H3("Data Collection Status"), html.Div(id='collection-status') ], style={'margin': '20px 0'}), # Redis status html.Div([ html.H3("Redis Status"), html.Div(id='redis-status') ], style={'margin': '20px 0'}) ]) # Create the app instance app = create_app() # Tab switching callback @callback( Output('tab-content', 'children'), Input('main-tabs', 'value') ) def render_tab_content(active_tab): """Render content based on selected tab.""" if active_tab == 'market-data': return get_market_data_layout() elif active_tab == 'bot-management': return get_bot_management_layout() elif active_tab == 'performance': return get_performance_layout() elif active_tab == 'system-health': return get_system_health_layout() else: return html.Div("Tab not found") # Market data chart callback @callback( Output('price-chart', 'figure'), [Input('symbol-dropdown', 'value'), Input('timeframe-dropdown', 'value'), Input('interval-component', 'n_intervals')] ) def update_price_chart(symbol, timeframe, n_intervals): """Update the price chart with latest market data.""" try: # Use the real chart component instead of sample data fig = create_candlestick_chart(symbol, timeframe) logger.debug(f"Updated chart for {symbol} ({timeframe}) - interval {n_intervals}") return fig except Exception as e: logger.error(f"Error updating price chart: {e}") # Return error chart on failure return create_error_chart(f"Error loading chart: {str(e)}") # Market statistics callback @callback( Output('market-stats', 'children'), [Input('symbol-dropdown', 'value'), Input('interval-component', 'n_intervals')] ) def update_market_stats(symbol, n_intervals): """Update market statistics.""" try: # Get real market statistics from database stats = get_market_statistics(symbol) return html.Div([ html.H3("Market Statistics"), html.Div([ html.Div([ html.Strong(f"{key}: "), html.Span(value, style={'color': '#27ae60' if '+' in str(value) else '#e74c3c' if '-' in str(value) else '#2c3e50'}) ], style={'margin': '5px 0'}) for key, value in stats.items() ]) ]) except Exception as e: logger.error(f"Error updating market stats: {e}") return html.Div("Error loading market statistics") # System health callbacks @callback( Output('database-status', 'children'), Input('interval-component', 'n_intervals') ) def update_database_status(n_intervals): """Update database connection status.""" try: db_manager = DatabaseManager() # Test database connection with db_manager.get_session() as session: # Simple query to test connection result = session.execute("SELECT 1").fetchone() if result: return html.Div([ html.Span("🟢 Connected", style={'color': '#27ae60', 'font-weight': 'bold'}), html.P(f"Last checked: {datetime.now().strftime('%H:%M:%S')}", style={'margin': '5px 0', 'color': '#7f8c8d'}) ]) else: return html.Div([ html.Span("🔴 Connection Error", style={'color': '#e74c3c', 'font-weight': 'bold'}) ]) except Exception as e: logger.error(f"Database status check failed: {e}") return html.Div([ html.Span("🔴 Connection Failed", style={'color': '#e74c3c', 'font-weight': 'bold'}), html.P(f"Error: {str(e)}", style={'color': '#7f8c8d', 'font-size': '12px'}) ]) @callback( Output('data-status', 'children'), [Input('symbol-dropdown', 'value'), Input('timeframe-dropdown', 'value'), Input('interval-component', 'n_intervals')] ) def update_data_status(symbol, timeframe, n_intervals): """Update data collection status.""" try: # Check real data availability status = check_data_availability(symbol, timeframe) return html.Div([ html.H3("Data Collection Status"), html.Div([ html.Div( create_data_status_indicator(symbol, timeframe), style={'margin': '10px 0'} ), html.P(f"Checking data for {symbol} {timeframe}", style={'color': '#7f8c8d', 'margin': '5px 0', 'font-style': 'italic'}) ], style={'background-color': '#f8f9fa', 'padding': '15px', 'border-radius': '5px'}) ]) except Exception as e: logger.error(f"Error updating data status: {e}") return html.Div([ html.H3("Data Collection Status"), html.Div([ html.Span("🔴 Status Check Failed", style={'color': '#e74c3c', 'font-weight': 'bold'}), html.P(f"Error: {str(e)}", style={'color': '#7f8c8d', 'margin': '5px 0'}) ]) ]) def main(): """Main function to run the dashboard.""" try: logger.info("Starting Crypto Trading Bot Dashboard") logger.info(f"Dashboard will be available at: http://{dashboard_settings.host}:{dashboard_settings.port}") # Run the app app.run( host=dashboard_settings.host, port=dashboard_settings.port, debug=dashboard_settings.debug ) except Exception as e: logger.error(f"Failed to start dashboard: {e}") sys.exit(1) if __name__ == '__main__': main()