3.1 - 3.3 Add main Dash application for Crypto Trading Bot Dashboard

- Introduced `app.py` as the main entry point for the dashboard, providing real-time visualization and bot management interface.
- Implemented layout components including header, navigation tabs, and content areas for market data, bot management, performance analytics, and system health.
- Added callbacks for dynamic updates of market data charts and statistics, ensuring real-time interaction.
- Created reusable UI components in `components` directory for modularity and maintainability.
- Enhanced database operations for fetching market data and checking data availability.
- Updated `main.py` to start the dashboard application with improved user instructions and error handling.
- Documented components and functions for clarity and future reference.
This commit is contained in:
Vasily.onl
2025-06-03 12:09:37 +08:00
parent 74d7e1ab2c
commit 720002a441
7 changed files with 1190 additions and 21 deletions

29
components/__init__.py Normal file
View File

@@ -0,0 +1,29 @@
"""
Dashboard UI Components Package
This package contains reusable UI components for the Crypto Trading Bot Dashboard.
Components are designed to be modular and can be composed to create complex layouts.
"""
from pathlib import Path
# Package metadata
__version__ = "0.1.0"
__package_name__ = "components"
# Make components directory available
COMPONENTS_DIR = Path(__file__).parent
# Component registry for future component discovery
AVAILABLE_COMPONENTS = [
"dashboard", # Main dashboard layout components
"charts", # Chart and visualization components
]
def get_component_path(component_name: str) -> Path:
"""Get the file path for a specific component."""
return COMPONENTS_DIR / f"{component_name}.py"
def list_components() -> list:
"""List all available components."""
return AVAILABLE_COMPONENTS.copy()

455
components/charts.py Normal file
View File

@@ -0,0 +1,455 @@
"""
Chart and Visualization Components
This module provides chart components for market data visualization,
including candlestick charts, technical indicators, and real-time updates.
"""
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pandas as pd
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Optional
from decimal import Decimal
from database.operations import get_database_operations, DatabaseOperationError
from utils.logger import get_logger
# Initialize logger
logger = get_logger("charts_component")
def fetch_market_data(symbol: str, timeframe: str,
days_back: int = 7, exchange: str = "okx") -> List[Dict[str, Any]]:
"""
Fetch market data from the database for chart display.
Args:
symbol: Trading pair (e.g., 'BTC-USDT')
timeframe: Timeframe (e.g., '1h', '1d')
days_back: Number of days to look back
exchange: Exchange name
Returns:
List of candle data dictionaries
"""
try:
db = get_database_operations(logger)
# Calculate time range
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(days=days_back)
# Fetch candles from database using the proper API
candles = db.market_data.get_candles(
symbol=symbol,
timeframe=timeframe,
start_time=start_time,
end_time=end_time,
exchange=exchange
)
logger.debug(f"Fetched {len(candles)} candles for {symbol} {timeframe}")
return candles
except DatabaseOperationError as e:
logger.error(f"Database error fetching market data: {e}")
return []
except Exception as e:
logger.error(f"Unexpected error fetching market data: {e}")
return []
def create_candlestick_chart(symbol: str, timeframe: str,
candles: Optional[List[Dict[str, Any]]] = None) -> go.Figure:
"""
Create a candlestick chart with real market data.
Args:
symbol: Trading pair
timeframe: Timeframe
candles: Optional pre-fetched candle data
Returns:
Plotly Figure object
"""
try:
# Fetch data if not provided
if candles is None:
candles = fetch_market_data(symbol, timeframe)
# Handle empty data
if not candles:
logger.warning(f"No data available for {symbol} {timeframe}")
return create_empty_chart(f"No data available for {symbol} {timeframe}")
# Convert to DataFrame for easier manipulation
df = pd.DataFrame(candles)
# Ensure timestamp column is datetime
df['timestamp'] = pd.to_datetime(df['timestamp'])
# Sort by timestamp
df = df.sort_values('timestamp')
# Create candlestick chart
fig = go.Figure(data=go.Candlestick(
x=df['timestamp'],
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name=symbol,
increasing_line_color='#26a69a',
decreasing_line_color='#ef5350'
))
# Update layout
fig.update_layout(
title=f"{symbol} - {timeframe} Chart",
xaxis_title="Time",
yaxis_title="Price (USDT)",
template="plotly_white",
showlegend=False,
height=600,
xaxis_rangeslider_visible=False,
hovermode='x unified'
)
# Add volume subplot if volume data exists
if 'volume' in df.columns and df['volume'].sum() > 0:
fig = create_candlestick_with_volume(df, symbol, timeframe)
logger.debug(f"Created candlestick chart for {symbol} {timeframe} with {len(df)} candles")
return fig
except Exception as e:
logger.error(f"Error creating candlestick chart for {symbol} {timeframe}: {e}")
return create_error_chart(f"Error loading chart: {str(e)}")
def create_candlestick_with_volume(df: pd.DataFrame, symbol: str, timeframe: str) -> go.Figure:
"""
Create a candlestick chart with volume subplot.
Args:
df: DataFrame with OHLCV data
symbol: Trading pair
timeframe: Timeframe
Returns:
Plotly Figure with candlestick and volume
"""
# Create subplots
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.03,
subplot_titles=(f'{symbol} Price', 'Volume'),
row_width=[0.7, 0.3]
)
# Add candlestick chart
fig.add_trace(
go.Candlestick(
x=df['timestamp'],
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name=symbol,
increasing_line_color='#26a69a',
decreasing_line_color='#ef5350'
),
row=1, col=1
)
# Add volume bars
colors = ['#26a69a' if close >= open else '#ef5350'
for close, open in zip(df['close'], df['open'])]
fig.add_trace(
go.Bar(
x=df['timestamp'],
y=df['volume'],
name='Volume',
marker_color=colors,
opacity=0.7
),
row=2, col=1
)
# Update layout
fig.update_layout(
title=f"{symbol} - {timeframe} Chart with Volume",
template="plotly_white",
showlegend=False,
height=700,
xaxis_rangeslider_visible=False,
hovermode='x unified'
)
# Update axes
fig.update_yaxes(title_text="Price (USDT)", row=1, col=1)
fig.update_yaxes(title_text="Volume", row=2, col=1)
fig.update_xaxes(title_text="Time", row=2, col=1)
return fig
def create_empty_chart(message: str = "No data available") -> go.Figure:
"""
Create an empty chart with a message.
Args:
message: Message to display
Returns:
Empty Plotly Figure
"""
fig = go.Figure()
fig.add_annotation(
text=message,
xref="paper", yref="paper",
x=0.5, y=0.5,
xanchor='center', yanchor='middle',
showarrow=False,
font=dict(size=16, color="#7f8c8d")
)
fig.update_layout(
template="plotly_white",
height=600,
showlegend=False,
xaxis=dict(visible=False),
yaxis=dict(visible=False)
)
return fig
def create_error_chart(error_message: str) -> go.Figure:
"""
Create an error chart with error message.
Args:
error_message: Error message to display
Returns:
Error Plotly Figure
"""
fig = go.Figure()
fig.add_annotation(
text=f"⚠️ {error_message}",
xref="paper", yref="paper",
x=0.5, y=0.5,
xanchor='center', yanchor='middle',
showarrow=False,
font=dict(size=16, color="#e74c3c")
)
fig.update_layout(
template="plotly_white",
height=600,
showlegend=False,
xaxis=dict(visible=False),
yaxis=dict(visible=False)
)
return fig
def get_market_statistics(symbol: str, timeframe: str = "1h") -> Dict[str, str]:
"""
Calculate market statistics from recent data.
Args:
symbol: Trading pair
timeframe: Timeframe for calculations
Returns:
Dictionary of market statistics
"""
try:
# Fetch recent data for statistics
candles = fetch_market_data(symbol, timeframe, days_back=1)
if not candles:
return {
'Price': 'N/A',
'24h Change': 'N/A',
'24h Volume': 'N/A',
'High 24h': 'N/A',
'Low 24h': 'N/A'
}
# Convert to DataFrame
df = pd.DataFrame(candles)
# Get latest and 24h ago prices
latest_candle = df.iloc[-1]
current_price = float(latest_candle['close'])
# Calculate 24h change
if len(df) > 1:
price_24h_ago = float(df.iloc[0]['open'])
change_24h = current_price - price_24h_ago
change_percent = (change_24h / price_24h_ago) * 100
else:
change_24h = 0
change_percent = 0
# Calculate volume and high/low
total_volume = df['volume'].sum()
high_24h = df['high'].max()
low_24h = df['low'].min()
# Format statistics
return {
'Price': f"${current_price:,.2f}",
'24h Change': f"{'+' if change_24h >= 0 else ''}{change_percent:.2f}%",
'24h Volume': f"{total_volume:,.2f}",
'High 24h': f"${float(high_24h):,.2f}",
'Low 24h': f"${float(low_24h):,.2f}"
}
except Exception as e:
logger.error(f"Error calculating market statistics for {symbol}: {e}")
return {
'Price': 'Error',
'24h Change': 'Error',
'24h Volume': 'Error',
'High 24h': 'Error',
'Low 24h': 'Error'
}
def check_data_availability(symbol: str, timeframe: str) -> Dict[str, Any]:
"""
Check data availability for a symbol and timeframe.
Args:
symbol: Trading pair
timeframe: Timeframe
Returns:
Dictionary with data availability information
"""
try:
db = get_database_operations(logger)
# Get latest candle using the proper API
latest_candle = db.market_data.get_latest_candle(symbol, timeframe)
if latest_candle:
latest_time = latest_candle['timestamp']
time_diff = datetime.now(timezone.utc) - latest_time.replace(tzinfo=timezone.utc)
return {
'has_data': True,
'latest_timestamp': latest_time,
'time_since_last': time_diff,
'is_recent': time_diff < timedelta(hours=1),
'message': f"Latest data: {latest_time.strftime('%Y-%m-%d %H:%M:%S UTC')}"
}
else:
return {
'has_data': False,
'latest_timestamp': None,
'time_since_last': None,
'is_recent': False,
'message': f"No data available for {symbol} {timeframe}"
}
except Exception as e:
logger.error(f"Error checking data availability for {symbol} {timeframe}: {e}")
return {
'has_data': False,
'latest_timestamp': None,
'time_since_last': None,
'is_recent': False,
'message': f"Error checking data: {str(e)}"
}
def create_data_status_indicator(symbol: str, timeframe: str) -> str:
"""
Create a data status indicator for the dashboard.
Args:
symbol: Trading pair
timeframe: Timeframe
Returns:
HTML string for status indicator
"""
status = check_data_availability(symbol, timeframe)
if status['has_data']:
if status['is_recent']:
icon = "🟢"
color = "#27ae60"
status_text = "Real-time Data"
else:
icon = "🟡"
color = "#f39c12"
status_text = "Delayed Data"
else:
icon = "🔴"
color = "#e74c3c"
status_text = "No Data"
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>'
def get_supported_symbols() -> List[str]:
"""
Get list of symbols that have data in the database.
Returns:
List of available trading pairs
"""
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
# Query distinct symbols from market_data table
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT symbol FROM market_data ORDER BY symbol"))
symbols = [row[0] for row in result]
logger.debug(f"Found {len(symbols)} symbols in database: {symbols}")
return symbols
except Exception as e:
logger.error(f"Error fetching supported symbols: {e}")
# Return default symbols if database query fails
return ['BTC-USDT', 'ETH-USDT', 'LTC-USDT']
def get_supported_timeframes() -> List[str]:
"""
Get list of timeframes that have data in the database.
Returns:
List of available timeframes
"""
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
# Query distinct timeframes from market_data table
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT timeframe FROM market_data ORDER BY timeframe"))
timeframes = [row[0] for row in result]
logger.debug(f"Found {len(timeframes)} timeframes in database: {timeframes}")
return timeframes
except Exception as e:
logger.error(f"Error fetching supported timeframes: {e}")
# Return default timeframes if database query fails
return ['1m', '5m', '15m', '1h', '4h', '1d']

323
components/dashboard.py Normal file
View File

@@ -0,0 +1,323 @@
"""
Dashboard Layout Components
This module contains reusable layout components for the main dashboard interface.
These components handle the overall structure and navigation of the dashboard.
"""
from dash import html, dcc
from typing import List, Dict, Any, Optional
from datetime import datetime
def create_header(title: str = "Crypto Trading Bot Dashboard",
subtitle: str = "Real-time monitoring and bot management") -> html.Div:
"""
Create the main dashboard header component.
Args:
title: Main title text
subtitle: Subtitle text
Returns:
Dash HTML component for the header
"""
return html.Div([
html.H1(f"🚀 {title}",
style={'margin': '0', 'color': '#2c3e50', 'font-size': '28px'}),
html.P(subtitle,
style={'margin': '5px 0 0 0', 'color': '#7f8c8d', 'font-size': '14px'})
], style={
'padding': '20px',
'background-color': '#ecf0f1',
'border-bottom': '2px solid #bdc3c7',
'box-shadow': '0 2px 4px rgba(0,0,0,0.1)'
})
def create_navigation_tabs(active_tab: str = 'market-data') -> dcc.Tabs:
"""
Create the main navigation tabs component.
Args:
active_tab: Default active tab
Returns:
Dash Tabs component
"""
tab_style = {
'borderBottom': '1px solid #d6d6d6',
'padding': '6px',
'fontWeight': 'bold'
}
tab_selected_style = {
'borderTop': '1px solid #d6d6d6',
'borderBottom': '1px solid #d6d6d6',
'backgroundColor': '#119DFF',
'color': 'white',
'padding': '6px'
}
return dcc.Tabs(
id="main-tabs",
value=active_tab,
children=[
dcc.Tab(
label='📊 Market Data',
value='market-data',
style=tab_style,
selected_style=tab_selected_style
),
dcc.Tab(
label='🤖 Bot Management',
value='bot-management',
style=tab_style,
selected_style=tab_selected_style
),
dcc.Tab(
label='📈 Performance',
value='performance',
style=tab_style,
selected_style=tab_selected_style
),
dcc.Tab(
label='⚙️ System Health',
value='system-health',
style=tab_style,
selected_style=tab_selected_style
),
],
style={'margin': '10px 20px'}
)
def create_content_container(content_id: str = 'tab-content') -> html.Div:
"""
Create the main content container.
Args:
content_id: HTML element ID for the content area
Returns:
Dash HTML component for content container
"""
return html.Div(
id=content_id,
style={
'padding': '20px',
'min-height': '600px',
'background-color': '#ffffff'
}
)
def create_status_indicator(status: str, message: str,
timestamp: Optional[datetime] = None) -> html.Div:
"""
Create a status indicator component.
Args:
status: Status type ('connected', 'error', 'warning', 'info')
message: Status message
timestamp: Optional timestamp for the status
Returns:
Dash HTML component for status indicator
"""
status_colors = {
'connected': '#27ae60',
'error': '#e74c3c',
'warning': '#f39c12',
'info': '#3498db'
}
status_icons = {
'connected': '🟢',
'error': '🔴',
'warning': '🟡',
'info': '🔵'
}
color = status_colors.get(status, '#7f8c8d')
icon = status_icons.get(status, '')
components = [
html.Span(f"{icon} {message}",
style={'color': color, 'font-weight': 'bold'})
]
if timestamp:
components.append(
html.P(f"Last updated: {timestamp.strftime('%H:%M:%S')}",
style={'margin': '5px 0', 'color': '#7f8c8d', 'font-size': '12px'})
)
return html.Div(components)
def create_card(title: str, content: Any,
card_id: Optional[str] = None) -> html.Div:
"""
Create a card component for organizing content.
Args:
title: Card title
content: Card content (can be any Dash component)
card_id: Optional HTML element ID
Returns:
Dash HTML component for the card
"""
return html.Div([
html.H3(title, style={
'margin': '0 0 15px 0',
'color': '#2c3e50',
'border-bottom': '2px solid #ecf0f1',
'padding-bottom': '10px'
}),
content
], style={
'border': '1px solid #ddd',
'border-radius': '8px',
'padding': '20px',
'margin': '10px 0',
'background-color': '#ffffff',
'box-shadow': '0 2px 4px rgba(0,0,0,0.1)'
}, id=card_id)
def create_metric_display(metrics: Dict[str, str]) -> html.Div:
"""
Create a metrics display component.
Args:
metrics: Dictionary of metric names and values
Returns:
Dash HTML component for metrics display
"""
metric_components = []
for key, value in metrics.items():
# Color coding for percentage changes
color = '#27ae60' if '+' in str(value) else '#e74c3c' if '-' in str(value) else '#2c3e50'
metric_components.append(
html.Div([
html.Strong(f"{key}: ", style={'color': '#2c3e50'}),
html.Span(str(value), style={'color': color})
], style={
'margin': '8px 0',
'padding': '5px',
'background-color': '#f8f9fa',
'border-radius': '4px'
})
)
return html.Div(metric_components, style={
'display': 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(200px, 1fr))',
'gap': '10px'
})
def create_selector_group(selectors: List[Dict[str, Any]]) -> html.Div:
"""
Create a group of selector components (dropdowns, etc.).
Args:
selectors: List of selector configurations
Returns:
Dash HTML component for selector group
"""
selector_components = []
for selector in selectors:
selector_div = html.Div([
html.Label(
selector.get('label', ''),
style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}
),
dcc.Dropdown(
id=selector.get('id'),
options=selector.get('options', []),
value=selector.get('value'),
style={'margin-bottom': '15px'}
)
], style={'width': '250px', 'margin': '10px 20px 10px 0', 'display': 'inline-block'})
selector_components.append(selector_div)
return html.Div(selector_components, style={'margin': '20px 0'})
def create_loading_component(component_id: str, message: str = "Loading...") -> html.Div:
"""
Create a loading component for async operations.
Args:
component_id: ID for the component that will replace this loading screen
message: Loading message
Returns:
Dash HTML component for loading screen
"""
return html.Div([
html.Div([
html.Div(className="loading-spinner", style={
'border': '4px solid #f3f3f3',
'border-top': '4px solid #3498db',
'border-radius': '50%',
'width': '40px',
'height': '40px',
'animation': 'spin 2s linear infinite',
'margin': '0 auto 20px auto'
}),
html.P(message, style={'text-align': 'center', 'color': '#7f8c8d'})
], style={
'display': 'flex',
'flex-direction': 'column',
'align-items': 'center',
'justify-content': 'center',
'height': '200px'
})
], id=component_id)
def create_placeholder_content(title: str, description: str,
phase: str = "future implementation") -> html.Div:
"""
Create placeholder content for features not yet implemented.
Args:
title: Section title
description: Description of what will be implemented
phase: Implementation phase information
Returns:
Dash HTML component for placeholder content
"""
return html.Div([
html.H2(title, style={'color': '#2c3e50'}),
html.Div([
html.P(description, style={'color': '#7f8c8d', 'font-size': '16px'}),
html.P(f"🚧 Planned for {phase}",
style={'color': '#f39c12', 'font-weight': 'bold', 'font-style': 'italic'})
], style={
'background-color': '#f8f9fa',
'padding': '20px',
'border-radius': '8px',
'border-left': '4px solid #f39c12'
})
])
# CSS Styles for animation (to be included in assets or inline styles)
LOADING_CSS = """
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
"""