TCPDashboard/dashboard/components/data_analysis.py
Vasily.onl dbe58e5cef Remove deprecated app_new.py and consolidate main application logic into main.py
- 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.
2025-06-11 18:36:34 +08:00

427 lines
17 KiB
Python

"""
Data analysis components for comprehensive market data analysis.
"""
from dash import html, dcc
import dash_bootstrap_components as dbc
import pandas as pd
import numpy as np
from datetime import datetime, timezone, timedelta
from typing import Dict, Any, List, Optional
from utils.logger import get_logger
from database.connection import DatabaseManager
from database.operations import DatabaseOperationError, get_database_operations
from config.constants.chart_constants import CHART_COLORS, UI_TEXT
logger = get_logger("data_analysis")
class VolumeAnalyzer:
"""Analyze trading volume patterns and trends."""
def __init__(self):
pass
def get_volume_statistics(self, df: pd.DataFrame) -> Dict[str, Any]:
"""Calculate comprehensive volume statistics from a DataFrame."""
try:
if df.empty or 'volume' not in df.columns:
return {'error': 'DataFrame is empty or missing volume column'}
df = df.copy()
df = self._ensure_numeric_cols(df)
stats = {}
stats.update(self._calculate_basic_volume_stats(df))
stats.update(self._analyze_volume_trend(df))
stats.update(self._identify_high_volume_periods(df, stats['avg_volume'], stats['volume_std']))
stats.update(self._calculate_volume_price_correlation(df))
stats.update(self._calculate_avg_trade_size(df))
stats.update(self._calculate_volume_percentiles(df))
return stats
except Exception as e:
logger.error(f"Volume analysis error: {e}")
return {'error': str(e)}
def _ensure_numeric_cols(self, df: pd.DataFrame) -> pd.DataFrame:
numeric_cols = ['open', 'high', 'low', 'close', 'volume']
for col in numeric_cols:
if col in df.columns:
df[col] = df[col].astype(float)
if 'trades_count' in df.columns:
df['trades_count'] = df['trades_count'].astype(float)
return df
def _calculate_basic_volume_stats(self, df: pd.DataFrame) -> Dict[str, Any]:
return {
'total_volume': df['volume'].sum(),
'avg_volume': df['volume'].mean(),
'volume_std': df['volume'].std(),
'max_volume': df['volume'].max(),
'min_volume': df['volume'].min()
}
def _analyze_volume_trend(self, df: pd.DataFrame) -> Dict[str, Any]:
recent_volume = df['volume'].tail(10).mean()
older_volume = df['volume'].head(10).mean()
volume_trend = "Increasing" if recent_volume > older_volume else "Decreasing"
return {'volume_trend': volume_trend}
def _identify_high_volume_periods(self, df: pd.DataFrame, avg_volume: float, volume_std: float) -> Dict[str, Any]:
high_volume_threshold = avg_volume + (2 * volume_std)
high_volume_periods = len(df[df['volume'] > high_volume_threshold])
return {'high_volume_periods': high_volume_periods}
def _calculate_volume_price_correlation(self, df: pd.DataFrame) -> Dict[str, Any]:
price_change = df['close'] - df['open']
volume_price_corr = df['volume'].corr(price_change.abs())
return {'volume_price_correlation': volume_price_corr}
def _calculate_avg_trade_size(self, df: pd.DataFrame) -> Dict[str, Any]:
if 'trades_count' in df.columns:
df['avg_trade_size'] = df['volume'] / df['trades_count'].replace(0, 1)
avg_trade_size = df['avg_trade_size'].mean()
else:
avg_trade_size = None
return {'avg_trade_size': avg_trade_size}
def _calculate_volume_percentiles(self, df: pd.DataFrame) -> Dict[str, Any]:
return {
'volume_percentiles': {
'25th': df['volume'].quantile(0.25),
'50th': df['volume'].quantile(0.50),
'75th': df['volume'].quantile(0.75),
'95th': df['volume'].quantile(0.95)
}
}
class PriceMovementAnalyzer:
"""Analyze price movement patterns and statistics."""
def __init__(self):
pass
def get_price_movement_statistics(self, df: pd.DataFrame) -> Dict[str, Any]:
"""Calculate comprehensive price movement statistics from a DataFrame."""
try:
if df.empty or not all(col in df.columns for col in ['open', 'high', 'low', 'close']):
return {'error': 'DataFrame is empty or missing required price columns'}
df = df.copy()
df = self._ensure_numeric_cols(df)
stats = {}
stats.update(self._calculate_basic_price_stats(df))
stats.update(self._calculate_returns_and_volatility(df))
stats.update(self._analyze_price_range(df))
stats.update(self._analyze_directional_movement(df))
stats.update(self._calculate_price_extremes(df))
stats.update(self._calculate_momentum(df))
stats.update(self._calculate_trend_strength(df))
stats.update(self._calculate_return_percentiles(df))
return stats
except Exception as e:
logger.error(f"Price movement analysis error: {e}")
return {'error': str(e)}
def _ensure_numeric_cols(self, df: pd.DataFrame) -> pd.DataFrame:
numeric_cols = ['open', 'high', 'low', 'close', 'volume']
for col in numeric_cols:
if col in df.columns:
df[col] = df[col].astype(float)
return df
def _calculate_basic_price_stats(self, df: pd.DataFrame) -> Dict[str, Any]:
current_price = df['close'].iloc[-1]
period_start_price = df['open'].iloc[0]
period_return = ((current_price - period_start_price) / period_start_price) * 100
return {'current_price': current_price, 'period_return': period_return}
def _calculate_returns_and_volatility(self, df: pd.DataFrame) -> Dict[str, Any]:
df['returns'] = df['close'].pct_change() * 100
df['returns'] = df['returns'].fillna(0)
volatility = df['returns'].std()
avg_return = df['returns'].mean()
return {'volatility': volatility, 'avg_return': avg_return, 'returns': df['returns']}
def _analyze_price_range(self, df: pd.DataFrame) -> Dict[str, Any]:
df['range'] = df['high'] - df['low']
df['range_pct'] = (df['range'] / df['open']) * 100
avg_range_pct = df['range_pct'].mean()
return {'avg_range_pct': avg_range_pct}
def _analyze_directional_movement(self, df: pd.DataFrame) -> Dict[str, Any]:
bullish_periods = len(df[df['close'] > df['open']])
bearish_periods = len(df[df['close'] < df['open']])
neutral_periods = len(df[df['close'] == df['open']])
total_periods = len(df)
bullish_ratio = (bullish_periods / total_periods) * 100 if total_periods > 0 else 0
return {
'bullish_periods': bullish_periods,
'bearish_periods': bearish_periods,
'neutral_periods': neutral_periods,
'bullish_ratio': bullish_ratio,
'positive_returns': len(df[df['returns'] > 0]),
'negative_returns': len(df[df['returns'] < 0])
}
def _calculate_price_extremes(self, df: pd.DataFrame) -> Dict[str, Any]:
period_high = df['high'].max()
period_low = df['low'].min()
return {'period_high': period_high, 'period_low': period_low}
def _calculate_momentum(self, df: pd.DataFrame) -> Dict[str, Any]:
current_price = df['close'].iloc[-1]
momentum_periods = min(10, len(df) - 1)
if momentum_periods > 0:
momentum = ((current_price - df['close'].iloc[-momentum_periods-1]) / df['close'].iloc[-momentum_periods-1]) * 100
else:
momentum = 0
return {'momentum': momentum}
def _calculate_trend_strength(self, df: pd.DataFrame) -> Dict[str, Any]:
if len(df) > 2:
x = np.arange(len(df))
slope, _ = np.polyfit(x, df['close'], 1)
trend_strength = slope / df['close'].mean() * 100
else:
trend_strength = 0
return {'trend_strength': trend_strength}
def _calculate_return_percentiles(self, df: pd.DataFrame) -> Dict[str, Any]:
return {
'return_percentiles': {
'5th': df['returns'].quantile(0.05),
'25th': df['returns'].quantile(0.25),
'75th': df['returns'].quantile(0.75),
'95th': df['returns'].quantile(0.95)
},
'max_gain': df['returns'].max(),
'max_loss': df['returns'].min()
}
def format_number(value: float, decimals: int = 2) -> str:
"""Formats a number to a string with specified decimals."""
if value is None:
return "N/A"
return f"{value:,.{decimals}f}"
def _create_stat_card(icon, title, value, color="primary") -> dbc.Card: # Extracted helper
return dbc.Card(
dbc.CardBody(
[
html.H4(title, className="card-title"),
html.P(value, className="card-text"),
html.I(className=f"fas fa-{icon} text-{color}"),
]
),
className=f"text-center m-1 bg-light border-{color}"
)
def create_volume_stats_display(stats: Dict[str, Any]) -> html.Div:
"""Creates a display for volume statistics."""
if 'error' in stats:
return html.Div(f"Error: {stats['error']}", className="alert alert-danger")
return html.Div(
[
html.H3("Volume Statistics", className="mb-3 text-primary"),
dbc.Row([
dbc.Col(_create_stat_card("chart-bar", "Total Volume", format_number(stats.get('total_volume')), "success"), md=6),
dbc.Col(_create_stat_card("calculator", "Avg. Volume", format_number(stats.get('avg_volume')), "info"), md=6),
]),
dbc.Row([
dbc.Col(_create_stat_card("arrow-trend-up", "Volume Trend", stats.get('volume_trend'), "warning"), md=6),
dbc.Col(_create_stat_card("hand-holding-usd", "Avg. Trade Size", format_number(stats.get('avg_trade_size')), "secondary"), md=6),
]),
dbc.Row([
dbc.Col(_create_stat_card("ranking-star", "High Vol. Periods", stats.get('high_volume_periods')), md=6),
dbc.Col(_create_stat_card("arrows-left-right", "Vol-Price Corr.", format_number(stats.get('volume_price_correlation'), 4), "primary"), md=6),
]),
]
)
def create_price_stats_display(stats: Dict[str, Any]) -> html.Div:
"""Creates a display for price movement statistics."""
if 'error' in stats:
return html.Div(f"Error: {stats['error']}", className="alert alert-danger")
return html.Div(
[
html.H3("Price Movement Statistics", className="mb-3 text-success"),
dbc.Row([
dbc.Col(_create_stat_card("dollar-sign", "Current Price", format_number(stats.get('current_price')), "success"), md=6),
dbc.Col(_create_stat_card("percent", "Period Return", f"{format_number(stats.get('period_return'))}%"), md=6),
]),
dbc.Row([
dbc.Col(_create_stat_card("wave-square", "Volatility", f"{format_number(stats.get('volatility'))}%"), md=6),
dbc.Col(_create_stat_card("chart-line", "Avg. Daily Return", f"{format_number(stats.get('avg_return'))}%"), md=6),
]),
dbc.Row([
dbc.Col(_create_stat_card("arrows-up-down-left-right", "Avg. Range %", f"{format_number(stats.get('avg_range_pct'))}%"), md=6),
dbc.Col(_create_stat_card("arrow-up", "Bullish Ratio", f"{format_number(stats.get('bullish_ratio'))}%"), md=6),
]),
]
)
def get_market_statistics(df: pd.DataFrame, symbol: str, timeframe: str) -> html.Div:
"""
Generates a display of key market statistics from the provided DataFrame.
"""
if df.empty:
return html.Div([html.P("No market data available for statistics.")], className="alert alert-info mt-4")
# Basic Market Overview
first_timestamp = df.index.min()
last_timestamp = df.index.max()
num_candles = len(df)
# Price Changes
first_close = df['close'].iloc[0]
last_close = df['close'].iloc[-1]
price_change_abs = last_close - first_close
price_change_pct = (price_change_abs / first_close) * 100 if first_close != 0 else 0
# Highs and Lows
period_high = df['high'].max()
period_low = df['low'].min()
# Average True Range (ATR) - A measure of volatility
# Requires TA-Lib or manual calculation. For simplicity, we'll use a basic range for now.
# Ideally, integrate a proper TA library.
df['tr'] = np.maximum(df['high'] - df['low'],
np.maximum(abs(df['high'] - df['close'].shift()),
abs(df['low'] - df['close'].shift())))
atr = df['tr'].mean() if not df['tr'].empty else 0
# Trading Volume Analysis
total_volume = df['volume'].sum()
average_volume = df['volume'].mean()
# Market Cap (placeholder - requires external data)
market_cap_info = "N/A (requires external API)"
# Order Book Depth (placeholder - requires real-time order book data)
order_book_depth = "N/A (requires real-time data)"
stats_content = html.Div([
html.H3(f"Market Statistics for {symbol} ({timeframe})", className="mb-3 text-info"),
_create_basic_market_overview(
first_timestamp, last_timestamp, num_candles,
first_close, last_close, price_change_abs, price_change_pct,
total_volume, average_volume, atr
),
html.Hr(className="my-4"),
_create_advanced_market_stats(
period_high, period_low, market_cap_info, order_book_depth
)
], className="mb-4")
return stats_content
def _create_basic_market_overview(
first_timestamp: datetime, last_timestamp: datetime, num_candles: int,
first_close: float, last_close: float, price_change_abs: float, price_change_pct: float,
total_volume: float, average_volume: float, atr: float
) -> dbc.Row:
return dbc.Row([
dbc.Col(
dbc.Card(
dbc.CardBody(
[
html.H4("Time Period", className="card-title"),
html.P(f"From: {first_timestamp.strftime('%Y-%m-%d %H:%M')}"),
html.P(f"To: {last_timestamp.strftime('%Y-%m-%d %H:%M')}"),
html.P(f"Candles: {num_candles}"),
]
),
className="text-center m-1 bg-light border-info"
),
md=4
),
dbc.Col(
dbc.Card(
dbc.CardBody(
[
html.H4("Price Movement", className="card-title"),
html.P(f"Initial Price: {format_number(first_close)}"),
html.P(f"Final Price: {format_number(last_close)}"),
html.P(f"Change: {format_number(price_change_abs)} ({format_number(price_change_pct)}%)",
style={'color': 'green' if price_change_pct >= 0 else 'red'}),
]
),
className="text-center m-1 bg-light border-info"
),
md=4
),
dbc.Col(
dbc.Card(
dbc.CardBody(
[
html.H4("Volume & Volatility", className="card-title"),
html.P(f"Total Volume: {format_number(total_volume)}"),
html.P(f"Average Volume: {format_number(average_volume)}"),
html.P(f"Average True Range: {format_number(atr, 4)}"),
]
),
className="text-center m-1 bg-light border-info"
),
md=4
),
])
def _create_advanced_market_stats(
period_high: float, period_low: float, market_cap_info: str, order_book_depth: str
) -> dbc.Row:
return dbc.Row([
dbc.Col(
dbc.Card(
dbc.CardBody(
[
html.H4("Period Extremes", className="card-title"),
html.P(f"Period High: {format_number(period_high)}"),
html.P(f"Period Low: {format_number(period_low)}"),
]
),
className="text-center m-1 bg-light border-warning"
),
md=4
),
dbc.Col(
dbc.Card(
dbc.CardBody(
[
html.H4("Liquidity/Depth", className="card-title"),
html.P(f"Market Cap: {market_cap_info}"),
html.P(f"Order Book Depth: {order_book_depth}"),
]
),
className="text-center m-1 bg-light border-warning"
),
md=4
),
dbc.Col(
dbc.Card(
dbc.CardBody(
[
html.H4("Custom Indicators", className="card-title"),
html.P("RSI: N/A"), # Placeholder
html.P("MACD: N/A"), # Placeholder
]
),
className="text-center m-1 bg-light border-warning"
),
md=4
)
])