#!/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, State, 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, create_strategy_chart, create_chart_with_indicators ) from components.charts.config import ( get_available_strategy_names, get_all_example_strategies, get_overlay_indicators, get_subplot_indicators, get_all_default_indicators, get_indicators_by_category ) from components.charts.indicator_manager import get_indicator_manager from components.charts.indicator_defaults import ensure_default_indicators # Initialize logger logger = get_logger("dashboard_app") # Create the app instance at module level 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'), # Hidden button for callback compatibility (real button is in market data layout) html.Button(id='add-indicator-btn', style={'display': 'none'}), # Add Indicator Modal 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 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={'text-align': 'right', 'border-top': '1px solid #eee', 'padding-top': '15px'}) ], style={ 'background-color': 'white', 'margin': '5% auto', 'padding': '30px', 'border-radius': '8px', 'box-shadow': '0 4px 6px rgba(0, 0, 0, 0.1)', 'width': '600px', 'max-width': '90%', 'max-height': '80%', 'overflow-y': 'auto' }) ], id='indicator-modal', style={ 'display': 'none', 'position': 'fixed', 'z-index': '1001', 'left': '0', 'top': '0', 'width': '100%', 'height': '100%', 'visibility': 'hidden' }) ]) ]) def get_market_data_layout(): """Create the market data visualization layout with indicator controls.""" # 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] # Get available strategies and indicators try: strategy_names = get_available_strategy_names() strategy_options = [{'label': name.replace('_', ' ').title(), 'value': name} for name in strategy_names] # Get user indicators from the new indicator manager indicator_manager = get_indicator_manager() # Ensure default indicators exist ensure_default_indicators() # Get indicators by display type overlay_indicators = indicator_manager.get_indicators_by_type('overlay') subplot_indicators = indicator_manager.get_indicators_by_type('subplot') # Create checkbox options for overlay indicators overlay_options = [] for indicator in overlay_indicators: display_name = f"{indicator.name} ({indicator.type.upper()})" overlay_options.append({'label': display_name, 'value': indicator.id}) # Create checkbox options for subplot indicators subplot_options = [] for indicator in subplot_indicators: display_name = f"{indicator.name} ({indicator.type.upper()})" subplot_options.append({'label': display_name, 'value': indicator.id}) except Exception as e: logger.warning(f"Error loading indicator options: {e}") strategy_options = [{'label': 'Basic Chart', 'value': 'basic'}] overlay_options = [] subplot_options = [] # Chart Configuration Panel with Add/Edit UI chart_config_panel = 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 html.Div([ html.Label("Overlay Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}), html.Div([ # Hidden checklist for callback compatibility dcc.Checklist( id='overlay-indicators-checklist', options=overlay_options, value=[], # Start with no indicators selected style={'display': 'none'} # Hide the basic checklist ), # 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 dcc.Checklist( id='subplot-indicators-checklist', options=subplot_options, value=[], # Start with no indicators selected style={'display': 'none'} # Hide the basic checklist ), # 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'}) ]) ], style={ 'border': '1px solid #bdc3c7', 'border-radius': '8px', 'padding': '15px', 'background-color': '#f8f9fa', 'margin-bottom': '20px' }) # Parameter Controls Section parameter_controls = html.Div([ html.H5("📊 Indicator Parameters", style={'color': '#2c3e50', 'margin-bottom': '15px'}), # SMA/EMA Period Controls html.Div([ html.Label("Moving Average Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), dcc.Slider( id='ma-period-slider', min=5, max=200, step=5, value=20, marks={i: str(i) for i in [5, 20, 50, 100, 200]}, tooltip={'placement': 'bottom', 'always_visible': True} ) ], style={'margin-bottom': '20px'}), # RSI Period Control html.Div([ html.Label("RSI Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}), dcc.Slider( id='rsi-period-slider', min=7, max=30, step=1, value=14, marks={i: str(i) for i in [7, 14, 21, 30]}, tooltip={'placement': 'bottom', 'always_visible': True} ) ], style={'margin-bottom': '20px'}), # MACD Parameters html.Div([ html.Label("MACD Parameters:", style={'font-weight': 'bold', 'margin-bottom': '10px'}), html.Div([ html.Div([ html.Label("Fast:", style={'font-size': '12px'}), dcc.Input( id='macd-fast-input', type='number', value=12, min=5, max=50, style={'width': '60px', 'margin-left': '5px'} ) ], style={'display': 'inline-block', 'margin-right': '15px'}), html.Div([ html.Label("Slow:", style={'font-size': '12px'}), dcc.Input( id='macd-slow-input', type='number', value=26, min=10, max=100, style={'width': '60px', 'margin-left': '5px'} ) ], style={'display': 'inline-block', 'margin-right': '15px'}), html.Div([ html.Label("Signal:", style={'font-size': '12px'}), dcc.Input( id='macd-signal-input', type='number', value=9, min=3, max=20, style={'width': '60px', 'margin-left': '5px'} ) ], style={'display': 'inline-block'}) ]) ], style={'margin-bottom': '20px'}), # Bollinger Bands Parameters html.Div([ html.Label("Bollinger Bands:", style={'font-weight': 'bold', 'margin-bottom': '10px'}), html.Div([ html.Div([ html.Label("Period:", style={'font-size': '12px'}), dcc.Input( id='bb-period-input', type='number', value=20, min=5, max=50, style={'width': '60px', 'margin-left': '5px'} ) ], style={'display': 'inline-block', 'margin-right': '15px'}), html.Div([ html.Label("Std Dev:", style={'font-size': '12px'}), dcc.Input( id='bb-stddev-input', type='number', value=2.0, min=1.0, max=3.0, step=0.1, style={'width': '70px', 'margin-left': '5px'} ) ], style={'display': 'inline-block'}) ]) ]) ], style={ 'border': '1px solid #bdc3c7', 'border-radius': '8px', 'padding': '15px', 'background-color': '#f8f9fa', 'margin-bottom': '20px' }) # Auto-update control auto_update_control = html.Div([ dcc.Checklist( id='auto-update-checkbox', options=[{'label': ' Auto-update charts', 'value': 'auto'}], value=['auto'], style={'margin-bottom': '10px'} ), html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'}) ]) return html.Div([ # Title and basic controls html.H3("💹 Market Data Visualization", style={'color': '#2c3e50', 'margin-bottom': '20px'}), # Main chart controls html.Div([ html.Div([ html.Label("Symbol:", style={'font-weight': 'bold'}), dcc.Dropdown( id='symbol-dropdown', options=symbol_options, value=symbols[0] if symbols else 'BTC-USDT', clearable=False, style={'margin-bottom': '10px'} ) ], style={'width': '48%', 'display': 'inline-block'}), html.Div([ html.Label("Timeframe:", style={'font-weight': 'bold'}), dcc.Dropdown( id='timeframe-dropdown', options=timeframe_options, value='1h', clearable=False, style={'margin-bottom': '10px'} ) ], style={'width': '48%', 'float': 'right', 'display': 'inline-block'}) ], style={'margin-bottom': '20px'}), # Chart Configuration Panel chart_config_panel, # Parameter Controls Section parameter_controls, # Auto-update control auto_update_control, # Chart dcc.Graph(id='price-chart'), # Market statistics html.Div(id='market-stats', style={'margin-top': '20px'}) ]) 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'}) ]) # Tab switching callback @app.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 @app.callback( Output('price-chart', 'figure'), [Input('symbol-dropdown', 'value'), Input('timeframe-dropdown', 'value'), Input('overlay-indicators-checklist', 'value'), Input('subplot-indicators-checklist', 'value'), Input('strategy-dropdown', 'value'), Input('interval-component', 'n_intervals')] ) def update_price_chart(symbol, timeframe, overlay_indicators, subplot_indicators, selected_strategy, n_intervals): """Update the price chart with latest market data and selected indicators.""" try: # If a strategy is selected, use strategy chart if selected_strategy and selected_strategy != 'basic': fig = create_strategy_chart(symbol, timeframe, selected_strategy) logger.debug(f"Created strategy chart for {symbol} ({timeframe}) with strategy: {selected_strategy}") else: # Create chart with dynamically selected indicators fig = create_chart_with_indicators( symbol=symbol, timeframe=timeframe, overlay_indicators=overlay_indicators or [], subplot_indicators=subplot_indicators or [], days_back=7 ) indicator_count = len(overlay_indicators or []) + len(subplot_indicators or []) logger.debug(f"Created dynamic chart for {symbol} ({timeframe}) with {indicator_count} indicators") return fig except Exception as e: logger.error(f"Error updating price chart: {e}") return create_error_chart(f"Error loading chart: {str(e)}") # Strategy selection callback - automatically load strategy indicators @app.callback( [Output('overlay-indicators-checklist', 'value'), Output('subplot-indicators-checklist', 'value')], [Input('strategy-dropdown', 'value')] ) def update_indicators_from_strategy(selected_strategy): """Update indicator selections when a strategy is chosen.""" if not selected_strategy or selected_strategy == 'basic': return [], [] try: # Get strategy configuration all_strategies = get_all_example_strategies() if selected_strategy in all_strategies: strategy_example = all_strategies[selected_strategy] config = strategy_example.config # Extract overlay and subplot indicators from strategy overlay_indicators = config.overlay_indicators or [] # Extract subplot indicators from subplot configs subplot_indicators = [] for subplot_config in config.subplot_configs or []: subplot_indicators.extend(subplot_config.indicators or []) logger.debug(f"Loaded strategy {selected_strategy}: {len(overlay_indicators)} overlays, {len(subplot_indicators)} subplots") return overlay_indicators, subplot_indicators else: logger.warning(f"Strategy {selected_strategy} not found") return [], [] except Exception as e: logger.error(f"Error loading strategy indicators: {e}") return [], [] # Market statistics callback @app.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 @app.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'}) ]) @app.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'}) ]) ]) # 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'), 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'), prevent_initial_call=True ) def sync_add_button_clicks(visible_clicks): """Sync clicks from visible button to hidden button.""" return visible_clicks or 0 # Update parameter fields based on indicator type @app.callback( [Output('indicator-parameters-message', 'style'), Output('sma-parameters', 'style'), Output('ema-parameters', 'style'), Output('rsi-parameters', 'style'), Output('macd-parameters', 'style'), Output('bb-parameters', 'style')], Input('indicator-type-dropdown', 'value'), prevent_initial_call=True ) def update_parameter_fields(indicator_type): """Show/hide parameter input fields based on selected indicator type.""" # Default styles hidden_style = {'display': 'none', 'margin-bottom': '10px'} visible_style = {'display': 'block', 'margin-bottom': '10px'} # Default message visibility message_style = {'display': 'block'} if not indicator_type else {'display': 'none'} # Initialize all as hidden sma_style = hidden_style ema_style = hidden_style rsi_style = hidden_style macd_style = hidden_style bb_style = hidden_style # Show the relevant parameter section if indicator_type == 'sma': sma_style = visible_style elif indicator_type == 'ema': ema_style = visible_style elif indicator_type == 'rsi': rsi_style = visible_style elif indicator_type == 'macd': macd_style = visible_style elif indicator_type == 'bollinger_bands': bb_style = visible_style return message_style, sma_style, ema_style, rsi_style, macd_style, bb_style # Save indicator callback @app.callback( [Output('save-indicator-feedback', 'children'), Output('overlay-indicators-checklist', 'options'), Output('subplot-indicators-checklist', 'options')], Input('save-indicator-btn', 'n_clicks'), [State('indicator-name-input', 'value'), State('indicator-type-dropdown', 'value'), State('indicator-description-input', 'value'), State('indicator-color-input', 'value'), State('indicator-line-width-slider', 'value'), # SMA parameters State('sma-period-input', 'value'), # EMA parameters State('ema-period-input', 'value'), # RSI parameters State('rsi-period-input', 'value'), # MACD parameters State('macd-fast-period-input', 'value'), State('macd-slow-period-input', 'value'), State('macd-signal-period-input', 'value'), # Bollinger Bands parameters State('bb-period-input', 'value'), State('bb-stddev-input', 'value'), # Edit mode data State('edit-indicator-store', 'data')], prevent_initial_call=True ) def save_new_indicator(n_clicks, name, indicator_type, description, color, line_width, sma_period, ema_period, rsi_period, macd_fast, macd_slow, macd_signal, 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 try: # Get indicator manager from components.charts.indicator_manager import get_indicator_manager manager = get_indicator_manager() # Collect parameters based on indicator type and actual input values parameters = {} if indicator_type == 'sma': parameters = {'period': sma_period or 20} elif indicator_type == 'ema': parameters = {'period': ema_period or 12} elif indicator_type == 'rsi': parameters = {'period': rsi_period or 14} elif indicator_type == 'macd': parameters = { 'fast_period': macd_fast or 12, 'slow_period': macd_slow or 26, 'signal_period': macd_signal or 9 } elif indicator_type == 'bollinger_bands': parameters = { 'period': bb_period or 20, 'std_dev': bb_stddev or 2.0 } # Check if this is an edit operation is_edit = edit_data and edit_data.get('mode') == 'edit' if is_edit: # Update existing indicator indicator_id = edit_data.get('indicator_id') success = manager.update_indicator( indicator_id, name=name, description=description or "", parameters=parameters, styling={'color': color or "#007bff", 'line_width': line_width or 2} ) if success: success_msg = html.Div([ html.Span("✅ ", style={'color': '#28a745'}), html.Span(f"Indicator '{name}' updated successfully!", style={'color': '#28a745'}) ]) 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 else: # Create new indicator new_indicator = manager.create_indicator( name=name, indicator_type=indicator_type, parameters=parameters, description=description or "", color=color or "#007bff" ) 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 success_msg = html.Div([ html.Span("✅ ", style={'color': '#28a745'}), html.Span(f"Indicator '{name}' saved successfully!", style={'color': '#28a745'}) ]) # Refresh the indicator options overlay_indicators = manager.get_indicators_by_type('overlay') subplot_indicators = manager.get_indicators_by_type('subplot') overlay_options = [] for indicator in overlay_indicators: display_name = f"{indicator.name} ({indicator.type.upper()})" overlay_options.append({'label': display_name, 'value': indicator.id}) subplot_options = [] for indicator in subplot_indicators: display_name = f"{indicator.name} ({indicator.type.upper()})" subplot_options.append({'label': display_name, 'value': indicator.id}) return success_msg, overlay_options, subplot_options except Exception as e: logger.error(f"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 # Update custom indicator lists with edit/delete buttons @app.callback( [Output('overlay-indicators-list', 'children'), Output('subplot-indicators-list', 'children')], [Input('overlay-indicators-checklist', 'options'), Input('subplot-indicators-checklist', 'options'), Input('overlay-indicators-checklist', 'value'), Input('subplot-indicators-checklist', 'value')] ) def update_custom_indicator_lists(overlay_options, subplot_options, overlay_values, subplot_values): """Create custom indicator lists with edit and delete buttons.""" def create_indicator_item(option, is_checked): """Create a single indicator item with checkbox and buttons.""" indicator_id = option['value'] indicator_name = option['label'] return html.Div([ # Checkbox and name html.Div([ dcc.Checklist( options=[{'label': '', 'value': indicator_id}], value=[indicator_id] if is_checked else [], id={'type': 'indicator-checkbox', 'index': indicator_id}, style={'display': 'inline-block', 'margin-right': '8px'} ), html.Span(indicator_name, style={'display': 'inline-block', 'vertical-align': 'top'}) ], style={'display': 'inline-block', 'width': '70%'}), # Edit and Delete buttons html.Div([ html.Button( "✏️", 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' } ), 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' } ) ], style={'display': 'inline-block', 'width': '30%', 'text-align': 'right'}) ], style={ 'display': 'block', 'padding': '5px 0', 'border-bottom': '1px solid #f0f0f0', 'margin-bottom': '5px' }) # Create overlay indicators list overlay_list = [] for option in overlay_options: is_checked = option['value'] in (overlay_values or []) overlay_list.append(create_indicator_item(option, is_checked)) # Create subplot indicators list subplot_list = [] for option in subplot_options: is_checked = option['value'] in (subplot_values or []) subplot_list.append(create_indicator_item(option, is_checked)) return overlay_list, subplot_list # Sync individual indicator checkboxes with main checklist @app.callback( Output('overlay-indicators-checklist', 'value', allow_duplicate=True), [Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')], [State('overlay-indicators-checklist', 'options')], prevent_initial_call=True ) def sync_overlay_indicators(checkbox_values, overlay_options): """Sync individual indicator checkboxes with main overlay checklist.""" if not checkbox_values or not overlay_options: return [] selected_indicators = [] overlay_ids = [opt['value'] for opt in overlay_options] # Flatten the checkbox values and filter for overlay indicators for values in checkbox_values: if values: # values is a list, check if not empty for indicator_id in values: if indicator_id in overlay_ids: selected_indicators.append(indicator_id) # Remove duplicates return list(set(selected_indicators)) @app.callback( Output('subplot-indicators-checklist', 'value', allow_duplicate=True), [Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')], [State('subplot-indicators-checklist', 'options')], prevent_initial_call=True ) def sync_subplot_indicators(checkbox_values, subplot_options): """Sync individual indicator checkboxes with main subplot checklist.""" if not checkbox_values or not subplot_options: return [] selected_indicators = [] subplot_ids = [opt['value'] for opt in subplot_options] # Flatten the checkbox values and filter for subplot indicators for values in checkbox_values: if values: # values is a list, check if not empty for indicator_id in values: if indicator_id in subplot_ids: selected_indicators.append(indicator_id) # Remove duplicates return list(set(selected_indicators)) # Handle delete indicator @app.callback( [Output('save-indicator-feedback', 'children', allow_duplicate=True), Output('overlay-indicators-checklist', 'options', allow_duplicate=True), Output('subplot-indicators-checklist', 'options', allow_duplicate=True)], [Input({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'n_clicks')], [State({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'id')], prevent_initial_call=True ) def delete_indicator(delete_clicks, button_ids): """Delete an indicator when delete button is clicked.""" ctx = dash.callback_context if not ctx.triggered or not any(delete_clicks): return dash.no_update, dash.no_update, dash.no_update # Find which button was clicked triggered_id = ctx.triggered[0]['prop_id'] import json button_info = json.loads(triggered_id.split('.')[0]) indicator_id = button_info['index'] try: # Get indicator manager and delete the indicator from components.charts.indicator_manager import get_indicator_manager manager = get_indicator_manager() # Load indicator to get its name before deletion indicator = manager.load_indicator(indicator_id) indicator_name = indicator.name if indicator else indicator_id if manager.delete_indicator(indicator_id): # Refresh the indicator options overlay_indicators = manager.get_indicators_by_type('overlay') subplot_indicators = manager.get_indicators_by_type('subplot') overlay_options = [] for indicator in overlay_indicators: display_name = f"{indicator.name} ({indicator.type.upper()})" overlay_options.append({'label': display_name, 'value': indicator.id}) subplot_options = [] for indicator in subplot_indicators: 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'}) ]) 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 except Exception as e: logger.error(f"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 # Handle edit indicator - open modal with existing data @app.callback( [Output('modal-title', 'children'), Output('indicator-name-input', 'value'), Output('indicator-type-dropdown', 'value'), Output('indicator-description-input', 'value'), Output('indicator-color-input', 'value'), Output('edit-indicator-store', 'data'), # Add parameter field outputs Output('sma-period-input', 'value'), Output('ema-period-input', 'value'), Output('rsi-period-input', 'value'), Output('macd-fast-period-input', 'value'), Output('macd-slow-period-input', 'value'), Output('macd-signal-period-input', 'value'), Output('bb-period-input', 'value'), Output('bb-stddev-input', 'value')], [Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')], [State({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'id')], prevent_initial_call=True ) def edit_indicator(edit_clicks, button_ids): """Load indicator data for editing.""" ctx = dash.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 # Find which button was clicked triggered_id = ctx.triggered[0]['prop_id'] import json button_info = json.loads(triggered_id.split('.')[0]) indicator_id = button_info['index'] try: # Load the indicator data from components.charts.indicator_manager import get_indicator_manager manager = get_indicator_manager() indicator = manager.load_indicator(indicator_id) if indicator: # Store indicator ID for update edit_data = {'indicator_id': indicator_id, 'mode': 'edit', 'open_modal': True} # Extract parameter values based on indicator type params = indicator.parameters # Default parameter values sma_period = 20 ema_period = 12 rsi_period = 14 macd_fast = 12 macd_slow = 26 macd_signal = 9 bb_period = 20 bb_stddev = 2.0 # Update with actual saved values if indicator.type == 'sma': sma_period = params.get('period', 20) elif indicator.type == 'ema': ema_period = params.get('period', 12) elif indicator.type == 'rsi': rsi_period = params.get('period', 14) elif indicator.type == 'macd': macd_fast = params.get('fast_period', 12) macd_slow = params.get('slow_period', 26) macd_signal = params.get('signal_period', 9) elif indicator.type == 'bollinger_bands': bb_period = params.get('period', 20) bb_stddev = params.get('std_dev', 2.0) return ( "✏️ Edit Indicator", indicator.name, indicator.type, indicator.description, indicator.styling.color, edit_data, sma_period, ema_period, rsi_period, macd_fast, macd_slow, macd_signal, bb_period, 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 except Exception as e: logger.error(f"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 # Reset modal form when closed @app.callback( [Output('indicator-name-input', 'value', allow_duplicate=True), Output('indicator-type-dropdown', 'value', allow_duplicate=True), Output('indicator-description-input', 'value', allow_duplicate=True), Output('indicator-color-input', 'value', allow_duplicate=True), Output('indicator-line-width-slider', 'value'), Output('modal-title', 'children', allow_duplicate=True), Output('edit-indicator-store', 'data', allow_duplicate=True), # Add parameter field resets Output('sma-period-input', 'value', allow_duplicate=True), Output('ema-period-input', 'value', allow_duplicate=True), Output('rsi-period-input', 'value', allow_duplicate=True), Output('macd-fast-period-input', 'value', allow_duplicate=True), Output('macd-slow-period-input', 'value', allow_duplicate=True), Output('macd-signal-period-input', 'value', allow_duplicate=True), Output('bb-period-input', 'value', allow_duplicate=True), Output('bb-stddev-input', 'value', allow_duplicate=True)], [Input('close-modal-btn', 'n_clicks'), Input('cancel-indicator-btn', 'n_clicks')], 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: 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 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()