- 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.
455 lines
13 KiB
Python
455 lines
13 KiB
Python
"""
|
|
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'] |