diff --git a/app.py b/app.py
new file mode 100644
index 0000000..aa3229b
--- /dev/null
+++ b/app.py
@@ -0,0 +1,358 @@
+#!/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))
+
+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()
\ No newline at end of file
diff --git a/components/__init__.py b/components/__init__.py
new file mode 100644
index 0000000..b7a405f
--- /dev/null
+++ b/components/__init__.py
@@ -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()
\ No newline at end of file
diff --git a/components/charts.py b/components/charts.py
new file mode 100644
index 0000000..10cbfe0
--- /dev/null
+++ b/components/charts.py
@@ -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'{icon} {status_text}
{status["message"]}'
+
+
+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']
\ No newline at end of file
diff --git a/components/dashboard.py b/components/dashboard.py
new file mode 100644
index 0000000..1b5da99
--- /dev/null
+++ b/components/dashboard.py
@@ -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); }
+}
+"""
\ No newline at end of file
diff --git a/database/operations.py b/database/operations.py
index 6b57775..8aae165 100644
--- a/database/operations.py
+++ b/database/operations.py
@@ -169,7 +169,7 @@ class MarketDataRepository(BaseRepository):
query = text("""
SELECT exchange, symbol, timeframe, timestamp,
open, high, low, close, volume, trades_count,
- created_at, updated_at
+ created_at
FROM market_data
WHERE exchange = :exchange
AND symbol = :symbol
@@ -200,15 +200,14 @@ class MarketDataRepository(BaseRepository):
'close': row.close,
'volume': row.volume,
'trades_count': row.trades_count,
- 'created_at': row.created_at,
- 'updated_at': row.updated_at
+ 'created_at': row.created_at
})
- self.log_info(f"Retrieved {len(candles)} candles for {symbol} {timeframe}")
+ self.log_debug(f"Retrieved {len(candles)} candles for {symbol} {timeframe}")
return candles
except Exception as e:
- self.log_error(f"Error retrieving candles for {symbol} {timeframe}: {e}")
+ self.log_error(f"Error retrieving candles: {e}")
raise DatabaseOperationError(f"Failed to retrieve candles: {e}")
def get_latest_candle(self, symbol: str, timeframe: str, exchange: str = "okx") -> Optional[Dict[str, Any]]:
@@ -228,7 +227,7 @@ class MarketDataRepository(BaseRepository):
query = text("""
SELECT exchange, symbol, timeframe, timestamp,
open, high, low, close, volume, trades_count,
- created_at, updated_at
+ created_at
FROM market_data
WHERE exchange = :exchange
AND symbol = :symbol
@@ -256,8 +255,7 @@ class MarketDataRepository(BaseRepository):
'close': row.close,
'volume': row.volume,
'trades_count': row.trades_count,
- 'created_at': row.created_at,
- 'updated_at': row.updated_at
+ 'created_at': row.created_at
}
return None
diff --git a/main.py b/main.py
index 33e73c4..e49d050 100644
--- a/main.py
+++ b/main.py
@@ -23,23 +23,29 @@ def main():
if app.environment == "development":
print("\nš§ Running in development mode")
- print("To start the full application:")
- print("1. Run: python scripts/dev.py setup")
- print("2. Run: python scripts/dev.py start")
- print("3. Update .env with your OKX API credentials")
- print("4. Run: uv run python tests/test_setup.py")
+ print("Dashboard features available:")
+ print("ā
Basic Dash application framework")
+ print("ā
Real-time price charts (sample data)")
+ print("ā
System health monitoring")
+ print("š§ Real data connection (coming in task 3.7)")
- # TODO: Start the Dash application when ready
- # from app import create_app
- # app = create_app()
- # app.run(host=dashboard.host, port=dashboard.port, debug=dashboard.debug)
+ # Start the Dash application
+ print(f"\nš Starting dashboard at: http://{dashboard.host}:{dashboard.port}")
+ print("Press Ctrl+C to stop the application")
- print(f"\nš Next: Implement Phase 1.0 - Database Infrastructure Setup")
+ from app import main as app_main
+ app_main()
except ImportError as e:
print(f"ā Failed to import modules: {e}")
print("Run: uv sync")
sys.exit(1)
+ except KeyboardInterrupt:
+ print("\n\nš Dashboard stopped by user")
+ sys.exit(0)
+ except Exception as e:
+ print(f"ā Failed to start dashboard: {e}")
+ sys.exit(1)
if __name__ == "__main__":
diff --git a/tasks/tasks-crypto-bot-prd.md b/tasks/tasks-crypto-bot-prd.md
index acfa86d..96dc713 100644
--- a/tasks/tasks-crypto-bot-prd.md
+++ b/tasks/tasks-crypto-bot-prd.md
@@ -77,9 +77,9 @@
- [x] 2.9 Unit test data collection and aggregation logic
- [ ] 3.0 Basic Dashboard for Data Visualization and Analysis
- - [ ] 3.1 Setup Dash application framework with Mantine UI components
- - [ ] 3.2 Create basic layout and navigation structure
- - [ ] 3.3 Implement real-time OHLCV price charts with Plotly (candlestick charts)
+ - [x] 3.1 Setup Dash application framework with Mantine UI components
+ - [x] 3.2 Create basic layout and navigation structure
+ - [x] 3.3 Implement real-time OHLCV price charts with Plotly (candlestick charts)
- [ ] 3.4 Add technical indicators overlay on price charts (SMA, EMA, RSI, MACD)
- [ ] 3.5 Create market data monitoring dashboard (real-time data feed status)
- [ ] 3.6 Build simple data analysis tools (volume analysis, price movement statistics)