455 lines
13 KiB
Python
Raw Normal View History

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