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:
parent
74d7e1ab2c
commit
720002a441
358
app.py
Normal file
358
app.py
Normal file
@ -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()
|
||||||
29
components/__init__.py
Normal file
29
components/__init__.py
Normal 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
455
components/charts.py
Normal 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
323
components/dashboard.py
Normal 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); }
|
||||||
|
}
|
||||||
|
"""
|
||||||
@ -169,7 +169,7 @@ class MarketDataRepository(BaseRepository):
|
|||||||
query = text("""
|
query = text("""
|
||||||
SELECT exchange, symbol, timeframe, timestamp,
|
SELECT exchange, symbol, timeframe, timestamp,
|
||||||
open, high, low, close, volume, trades_count,
|
open, high, low, close, volume, trades_count,
|
||||||
created_at, updated_at
|
created_at
|
||||||
FROM market_data
|
FROM market_data
|
||||||
WHERE exchange = :exchange
|
WHERE exchange = :exchange
|
||||||
AND symbol = :symbol
|
AND symbol = :symbol
|
||||||
@ -200,15 +200,14 @@ class MarketDataRepository(BaseRepository):
|
|||||||
'close': row.close,
|
'close': row.close,
|
||||||
'volume': row.volume,
|
'volume': row.volume,
|
||||||
'trades_count': row.trades_count,
|
'trades_count': row.trades_count,
|
||||||
'created_at': row.created_at,
|
'created_at': row.created_at
|
||||||
'updated_at': row.updated_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
|
return candles
|
||||||
|
|
||||||
except Exception as e:
|
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}")
|
raise DatabaseOperationError(f"Failed to retrieve candles: {e}")
|
||||||
|
|
||||||
def get_latest_candle(self, symbol: str, timeframe: str, exchange: str = "okx") -> Optional[Dict[str, Any]]:
|
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("""
|
query = text("""
|
||||||
SELECT exchange, symbol, timeframe, timestamp,
|
SELECT exchange, symbol, timeframe, timestamp,
|
||||||
open, high, low, close, volume, trades_count,
|
open, high, low, close, volume, trades_count,
|
||||||
created_at, updated_at
|
created_at
|
||||||
FROM market_data
|
FROM market_data
|
||||||
WHERE exchange = :exchange
|
WHERE exchange = :exchange
|
||||||
AND symbol = :symbol
|
AND symbol = :symbol
|
||||||
@ -256,8 +255,7 @@ class MarketDataRepository(BaseRepository):
|
|||||||
'close': row.close,
|
'close': row.close,
|
||||||
'volume': row.volume,
|
'volume': row.volume,
|
||||||
'trades_count': row.trades_count,
|
'trades_count': row.trades_count,
|
||||||
'created_at': row.created_at,
|
'created_at': row.created_at
|
||||||
'updated_at': row.updated_at
|
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
26
main.py
26
main.py
@ -23,23 +23,29 @@ def main():
|
|||||||
|
|
||||||
if app.environment == "development":
|
if app.environment == "development":
|
||||||
print("\n🔧 Running in development mode")
|
print("\n🔧 Running in development mode")
|
||||||
print("To start the full application:")
|
print("Dashboard features available:")
|
||||||
print("1. Run: python scripts/dev.py setup")
|
print("✅ Basic Dash application framework")
|
||||||
print("2. Run: python scripts/dev.py start")
|
print("✅ Real-time price charts (sample data)")
|
||||||
print("3. Update .env with your OKX API credentials")
|
print("✅ System health monitoring")
|
||||||
print("4. Run: uv run python tests/test_setup.py")
|
print("🚧 Real data connection (coming in task 3.7)")
|
||||||
|
|
||||||
# TODO: Start the Dash application when ready
|
# Start the Dash application
|
||||||
# from app import create_app
|
print(f"\n🌐 Starting dashboard at: http://{dashboard.host}:{dashboard.port}")
|
||||||
# app = create_app()
|
print("Press Ctrl+C to stop the application")
|
||||||
# app.run(host=dashboard.host, port=dashboard.port, debug=dashboard.debug)
|
|
||||||
|
|
||||||
print(f"\n📝 Next: Implement Phase 1.0 - Database Infrastructure Setup")
|
from app import main as app_main
|
||||||
|
app_main()
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ Failed to import modules: {e}")
|
print(f"❌ Failed to import modules: {e}")
|
||||||
print("Run: uv sync")
|
print("Run: uv sync")
|
||||||
sys.exit(1)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -77,9 +77,9 @@
|
|||||||
- [x] 2.9 Unit test data collection and aggregation logic
|
- [x] 2.9 Unit test data collection and aggregation logic
|
||||||
|
|
||||||
- [ ] 3.0 Basic Dashboard for Data Visualization and Analysis
|
- [ ] 3.0 Basic Dashboard for Data Visualization and Analysis
|
||||||
- [ ] 3.1 Setup Dash application framework with Mantine UI components
|
- [x] 3.1 Setup Dash application framework with Mantine UI components
|
||||||
- [ ] 3.2 Create basic layout and navigation structure
|
- [x] 3.2 Create basic layout and navigation structure
|
||||||
- [ ] 3.3 Implement real-time OHLCV price charts with Plotly (candlestick charts)
|
- [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.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.5 Create market data monitoring dashboard (real-time data feed status)
|
||||||
- [ ] 3.6 Build simple data analysis tools (volume analysis, price movement statistics)
|
- [ ] 3.6 Build simple data analysis tools (volume analysis, price movement statistics)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user