- Deleted `app_new.py`, which was previously the main entry point for the dashboard application, to streamline the codebase. - Consolidated the application initialization and callback registration logic into `main.py`, enhancing modularity and maintainability. - Updated the logging and error handling practices in `main.py` to ensure consistent application behavior and improved debugging capabilities. These changes simplify the application structure, aligning with project standards for modularity and maintainability.
141 lines
5.7 KiB
Python
141 lines
5.7 KiB
Python
import pandas as pd
|
|
from datetime import datetime, timezone, timedelta
|
|
import plotly.graph_objects as go
|
|
|
|
from database.operations import get_database_operations
|
|
from utils.logger import get_logger
|
|
from utils.timeframe_utils import load_timeframe_options
|
|
|
|
from .builder import ChartBuilder
|
|
from .utils import format_price, format_volume
|
|
|
|
logger = get_logger("charts_data")
|
|
|
|
def get_supported_symbols():
|
|
"""Get list of symbols that have data in the database."""
|
|
builder = ChartBuilder()
|
|
# Test query - consider optimizing or removing if not critical for initial check
|
|
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1)
|
|
if candles:
|
|
try:
|
|
db = get_database_operations(logger)
|
|
with db.market_data.get_session() as session:
|
|
from sqlalchemy import text
|
|
result = session.execute(text("SELECT DISTINCT symbol FROM market_data ORDER BY symbol"))
|
|
return [row[0] for row in result]
|
|
except Exception as e:
|
|
logger.error(f"Error fetching supported symbols from DB: {e}")
|
|
pass
|
|
|
|
return ['BTC-USDT', 'ETH-USDT'] # Fallback
|
|
|
|
def get_supported_timeframes():
|
|
"""Get list of timeframes that have data in the database."""
|
|
builder = ChartBuilder()
|
|
# Test query - consider optimizing or removing if not critical for initial check
|
|
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1)
|
|
if candles:
|
|
try:
|
|
db = get_database_operations(logger)
|
|
with db.market_data.get_session() as session:
|
|
from sqlalchemy import text
|
|
result = session.execute(text("SELECT DISTINCT timeframe FROM market_data ORDER BY timeframe"))
|
|
return [row[0] for row in result]
|
|
except Exception as e:
|
|
logger.error(f"Error fetching supported timeframes from DB: {e}")
|
|
pass
|
|
|
|
# Fallback uses values from timeframe_options.json for consistency
|
|
return [item['value'] for item in load_timeframe_options() if item['value'] in ['5s', '1m', '15m', '1h']]
|
|
|
|
def get_market_statistics(symbol: str, timeframe: str = "1h", days_back: int = 1): # Changed from days_back: Union[int, float] to int
|
|
"""Calculate market statistics from recent data over a specified period."""
|
|
builder = ChartBuilder()
|
|
candles = builder.fetch_market_data(symbol, timeframe, days_back=days_back)
|
|
|
|
if not candles:
|
|
return {'Price': 'N/A', f'Change ({days_back}d)': 'N/A', f'Volume ({days_back}d)': 'N/A', f'High ({days_back}d)': 'N/A', f'Low ({days_back}d)': 'N/A'}
|
|
|
|
df = pd.DataFrame(candles)
|
|
latest = df.iloc[-1]
|
|
current_price = float(latest['close'])
|
|
|
|
# Calculate change over the period
|
|
if len(df) > 1:
|
|
price_period_ago = float(df.iloc[0]['open'])
|
|
change_percent = ((current_price - price_period_ago) / price_period_ago) * 100
|
|
else:
|
|
change_percent = 0
|
|
|
|
# Determine label for period (e.g., "24h", "7d", "1h")
|
|
# This part should be updated if `days_back` can be fractional again.
|
|
if days_back == 1/24:
|
|
period_label = "1h"
|
|
elif days_back == 4/24:
|
|
period_label = "4h"
|
|
elif days_back == 6/24:
|
|
period_label = "6h"
|
|
elif days_back == 12/24:
|
|
period_label = "12h"
|
|
elif days_back < 1: # For other fractional days, show as hours
|
|
period_label = f"{int(days_back * 24)}h"
|
|
elif days_back == 1:
|
|
period_label = "24h" # Keep 24h for 1 day for clarity
|
|
else:
|
|
period_label = f"{days_back}d"
|
|
|
|
return {
|
|
'Price': format_price(current_price, decimals=2),
|
|
f'Change ({period_label})': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%",
|
|
f'Volume ({period_label})': format_volume(df['volume'].sum()),
|
|
f'High ({period_label})': format_price(df['high'].max(), decimals=2),
|
|
f'Low ({period_label})': format_price(df['low'].min(), decimals=2)
|
|
}
|
|
|
|
def check_data_availability(symbol: str, timeframe: str):
|
|
"""Check data availability for a symbol and timeframe."""
|
|
try:
|
|
db = get_database_operations(logger)
|
|
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:
|
|
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):
|
|
"""Create a data status indicator for the dashboard."""
|
|
status = check_data_availability(symbol, timeframe)
|
|
|
|
if status['has_data']:
|
|
if status['is_recent']:
|
|
icon, color, status_text = "🟢", "#27ae60", "Real-time Data"
|
|
else:
|
|
icon, color, status_text = "🟡", "#f39c12", "Delayed Data"
|
|
else:
|
|
icon, color, status_text = "🔴", "#e74c3c", "No Data"
|
|
|
|
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>' |