Implement modular chart layers and error handling for Crypto Trading Bot Dashboard - Introduced a comprehensive chart layer system in `components/charts/layers/` to support various technical indicators and subplots. - Added base layer components including `BaseLayer`, `CandlestickLayer`, and `VolumeLayer` for flexible chart rendering. - Implemented overlay indicators such as `SMALayer`, `EMALayer`, and `BollingerBandsLayer` with robust error handling. - Created subplot layers for indicators like `RSILayer` and `MACDLayer`, enhancing visualization capabilities. - Developed a `MarketDataIntegrator` for seamless data fetching and validation, improving data quality assurance. - Enhanced error handling utilities in `components/charts/error_handling.py` to manage insufficient data scenarios effectively. - Updated documentation to reflect the new chart layer architecture and usage guidelines. - Added unit tests for all chart layer components to ensure functionality and reliability.
358 lines
12 KiB
Python
358 lines
12 KiB
Python
#!/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")
|
|
|
|
# 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'),
|
|
])
|
|
|
|
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'})
|
|
])
|
|
|
|
# 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('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
|
|
@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'})
|
|
])
|
|
])
|
|
|
|
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() |