""" 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']