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.
This commit is contained in:
@@ -22,7 +22,7 @@ def create_app():
|
||||
# Initialize Dash app
|
||||
app = dash.Dash(__name__, suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.LUX])
|
||||
|
||||
# Define the main layout wrapped in MantineProvider
|
||||
# Define the main layout
|
||||
app.layout = html.Div([
|
||||
html.Div([
|
||||
# Page title
|
||||
|
||||
@@ -6,13 +6,14 @@ from dash import Output, Input, html, dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
from utils.logger import get_logger
|
||||
from dashboard.components.data_analysis import (
|
||||
VolumeAnalyzer,
|
||||
PriceMovementAnalyzer,
|
||||
create_volume_analysis_chart,
|
||||
create_price_movement_chart,
|
||||
create_volume_stats_display,
|
||||
create_price_stats_display
|
||||
create_price_stats_display,
|
||||
get_market_statistics,
|
||||
VolumeAnalyzer,
|
||||
PriceMovementAnalyzer
|
||||
)
|
||||
from database.operations import get_database_operations
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
logger = get_logger("data_analysis_callbacks")
|
||||
|
||||
@@ -24,26 +25,38 @@ def register_data_analysis_callbacks(app):
|
||||
|
||||
# Initial callback to populate charts on load
|
||||
@app.callback(
|
||||
[Output('analysis-chart-container', 'children'),
|
||||
Output('analysis-stats-container', 'children')],
|
||||
[Input('analysis-type-selector', 'value'),
|
||||
Input('analysis-period-selector', 'value')],
|
||||
[Output('volume-analysis-chart', 'figure'),
|
||||
Output('price-movement-chart', 'figure'),
|
||||
Output('volume-stats-output', 'children'),
|
||||
Output('price-stats-output', 'children'),
|
||||
Output('market-statistics-output', 'children')],
|
||||
[Input('data-analysis-symbol-dropdown', 'value'),
|
||||
Input('data-analysis-timeframe-dropdown', 'value'),
|
||||
Input('data-analysis-days-back-dropdown', 'value')],
|
||||
prevent_initial_call=False
|
||||
)
|
||||
def update_data_analysis(analysis_type, period):
|
||||
def update_data_analysis(symbol, timeframe, days_back):
|
||||
"""Update data analysis with statistical cards only (no duplicate charts)."""
|
||||
logger.info(f"🎯 DATA ANALYSIS CALLBACK TRIGGERED! Type: {analysis_type}, Period: {period}")
|
||||
logger.info(f"🎯 DATA ANALYSIS CALLBACK TRIGGERED! Symbol: {symbol}, Timeframe: {timeframe}, Days Back: {days_back}")
|
||||
|
||||
db_ops = get_database_operations(logger)
|
||||
end_time = datetime.now(timezone.utc)
|
||||
start_time = end_time - timedelta(days=days_back)
|
||||
|
||||
# Return placeholder message since we're moving to enhanced market stats
|
||||
info_msg = dbc.Alert([
|
||||
html.H4("📊 Statistical Analysis", className="alert-heading"),
|
||||
html.P("Data analysis has been integrated into the Market Statistics section above."),
|
||||
html.P("The enhanced statistics now include volume analysis, price movement analysis, and trend indicators."),
|
||||
html.P("Change the symbol and timeframe in the main chart to see updated analysis."),
|
||||
html.Hr(),
|
||||
html.P("This section will be updated with additional analytical tools in future versions.", className="mb-0")
|
||||
], color="info")
|
||||
df = db_ops.market_data.get_candles_df(symbol, timeframe, start_time, end_time)
|
||||
|
||||
volume_analyzer = VolumeAnalyzer()
|
||||
price_analyzer = PriceMovementAnalyzer()
|
||||
|
||||
return info_msg, html.Div()
|
||||
volume_stats = volume_analyzer.get_volume_statistics(df)
|
||||
price_stats = price_analyzer.get_price_movement_statistics(df)
|
||||
|
||||
volume_stats_display = create_volume_stats_display(volume_stats)
|
||||
price_stats_display = create_price_stats_display(price_stats)
|
||||
market_stats_display = get_market_statistics(df, symbol, timeframe)
|
||||
|
||||
# Return empty figures for charts, as they are no longer the primary display
|
||||
# And the stats displays
|
||||
return {}, {}, volume_stats_display, price_stats_display, market_stats_display
|
||||
|
||||
logger.info("✅ Data analysis callbacks registered successfully")
|
||||
@@ -5,10 +5,10 @@ Chart control components for the market data layout.
|
||||
from dash import html, dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
from utils.logger import get_logger
|
||||
from utils.time_range_utils import load_time_range_options
|
||||
|
||||
logger = get_logger("default_logger")
|
||||
|
||||
|
||||
def create_chart_config_panel(strategy_options, overlay_options, subplot_options):
|
||||
"""Create the chart configuration panel with add/edit UI."""
|
||||
return dbc.Card([
|
||||
@@ -74,18 +74,7 @@ def create_time_range_controls():
|
||||
html.Label("Quick Select:", className="form-label"),
|
||||
dcc.Dropdown(
|
||||
id='time-range-quick-select',
|
||||
options=[
|
||||
{'label': '🕐 Last 1 Hour', 'value': '1h'},
|
||||
{'label': '🕐 Last 4 Hours', 'value': '4h'},
|
||||
{'label': '🕐 Last 6 Hours', 'value': '6h'},
|
||||
{'label': '🕐 Last 12 Hours', 'value': '12h'},
|
||||
{'label': '📅 Last 1 Day', 'value': '1d'},
|
||||
{'label': '📅 Last 3 Days', 'value': '3d'},
|
||||
{'label': '📅 Last 7 Days', 'value': '7d'},
|
||||
{'label': '📅 Last 30 Days', 'value': '30d'},
|
||||
{'label': '📅 Custom Range', 'value': 'custom'},
|
||||
{'label': '🔴 Real-time', 'value': 'realtime'}
|
||||
],
|
||||
options=load_time_range_options(),
|
||||
value='7d',
|
||||
placeholder="Select time range",
|
||||
)
|
||||
|
||||
@@ -4,9 +4,6 @@ Data analysis components for comprehensive market data analysis.
|
||||
|
||||
from dash import html, dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
import plotly.graph_objects as go
|
||||
import plotly.express as px
|
||||
from plotly.subplots import make_subplots
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime, timezone, timedelta
|
||||
@@ -14,7 +11,8 @@ from typing import Dict, Any, List, Optional
|
||||
|
||||
from utils.logger import get_logger
|
||||
from database.connection import DatabaseManager
|
||||
from database.operations import DatabaseOperationError
|
||||
from database.operations import DatabaseOperationError, get_database_operations
|
||||
from config.constants.chart_constants import CHART_COLORS, UI_TEXT
|
||||
|
||||
logger = get_logger("data_analysis")
|
||||
|
||||
@@ -23,8 +21,7 @@ class VolumeAnalyzer:
|
||||
"""Analyze trading volume patterns and trends."""
|
||||
|
||||
def __init__(self):
|
||||
self.db_manager = DatabaseManager()
|
||||
self.db_manager.initialize()
|
||||
pass
|
||||
|
||||
def get_volume_statistics(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""Calculate comprehensive volume statistics from a DataFrame."""
|
||||
@@ -32,69 +29,81 @@ class VolumeAnalyzer:
|
||||
if df.empty or 'volume' not in df.columns:
|
||||
return {'error': 'DataFrame is empty or missing volume column'}
|
||||
|
||||
# Convert all relevant columns to float to avoid type errors with Decimal
|
||||
df = df.copy()
|
||||
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)
|
||||
df = self._ensure_numeric_cols(df)
|
||||
|
||||
# Calculate volume statistics
|
||||
total_volume = df['volume'].sum()
|
||||
avg_volume = df['volume'].mean()
|
||||
volume_std = df['volume'].std()
|
||||
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))
|
||||
|
||||
# Volume trend analysis
|
||||
recent_volume = df['volume'].tail(10).mean() # Last 10 periods
|
||||
older_volume = df['volume'].head(10).mean() # First 10 periods
|
||||
volume_trend = "Increasing" if recent_volume > older_volume else "Decreasing"
|
||||
|
||||
# High volume periods (above 2 standard deviations)
|
||||
high_volume_threshold = avg_volume + (2 * volume_std)
|
||||
high_volume_periods = len(df[df['volume'] > high_volume_threshold])
|
||||
|
||||
# Volume-Price correlation
|
||||
price_change = df['close'] - df['open']
|
||||
volume_price_corr = df['volume'].corr(price_change.abs())
|
||||
|
||||
# Average trade size (volume per trade)
|
||||
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 # Not available
|
||||
|
||||
return {
|
||||
'total_volume': total_volume,
|
||||
'avg_volume': avg_volume,
|
||||
'volume_std': volume_std,
|
||||
'volume_trend': volume_trend,
|
||||
'high_volume_periods': high_volume_periods,
|
||||
'volume_price_correlation': volume_price_corr,
|
||||
'avg_trade_size': avg_trade_size,
|
||||
'max_volume': df['volume'].max(),
|
||||
'min_volume': df['volume'].min(),
|
||||
'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)
|
||||
}
|
||||
}
|
||||
|
||||
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):
|
||||
self.db_manager = DatabaseManager()
|
||||
self.db_manager.initialize()
|
||||
pass
|
||||
|
||||
def get_price_movement_statistics(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""Calculate comprehensive price movement statistics from a DataFrame."""
|
||||
@@ -102,499 +111,317 @@ class PriceMovementAnalyzer:
|
||||
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'}
|
||||
|
||||
# Convert all relevant columns to float to avoid type errors with Decimal
|
||||
df = df.copy()
|
||||
numeric_cols = ['open', 'high', 'low', 'close', 'volume']
|
||||
for col in numeric_cols:
|
||||
if col in df.columns:
|
||||
df[col] = df[col].astype(float)
|
||||
|
||||
# Basic price statistics
|
||||
current_price = df['close'].iloc[-1]
|
||||
period_start_price = df['open'].iloc[0]
|
||||
period_return = ((current_price - period_start_price) / period_start_price) * 100
|
||||
df = self._ensure_numeric_cols(df)
|
||||
|
||||
# Daily returns (percentage changes)
|
||||
df['returns'] = df['close'].pct_change() * 100
|
||||
df['returns'] = df['returns'].fillna(0)
|
||||
|
||||
# Volatility metrics
|
||||
volatility = df['returns'].std()
|
||||
avg_return = df['returns'].mean()
|
||||
|
||||
# Price range analysis
|
||||
df['range'] = df['high'] - df['low']
|
||||
df['range_pct'] = (df['range'] / df['open']) * 100
|
||||
avg_range_pct = df['range_pct'].mean()
|
||||
|
||||
# Directional analysis
|
||||
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
|
||||
|
||||
# Price extremes
|
||||
period_high = df['high'].max()
|
||||
period_low = df['low'].min()
|
||||
|
||||
# Momentum indicators
|
||||
# Simple momentum (current vs N periods ago)
|
||||
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
|
||||
|
||||
# Trend strength (linear regression slope)
|
||||
if len(df) > 2:
|
||||
x = np.arange(len(df))
|
||||
slope, _ = np.polyfit(x, df['close'], 1)
|
||||
trend_strength = slope / df['close'].mean() * 100 # Normalize by average price
|
||||
else:
|
||||
trend_strength = 0
|
||||
|
||||
return {
|
||||
'current_price': current_price,
|
||||
'period_return': period_return,
|
||||
'volatility': volatility,
|
||||
'avg_return': avg_return,
|
||||
'avg_range_pct': avg_range_pct,
|
||||
'bullish_periods': bullish_periods,
|
||||
'bearish_periods': bearish_periods,
|
||||
'neutral_periods': neutral_periods,
|
||||
'bullish_ratio': bullish_ratio,
|
||||
'period_high': period_high,
|
||||
'period_low': period_low,
|
||||
'momentum': momentum,
|
||||
'trend_strength': trend_strength,
|
||||
'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(),
|
||||
'positive_returns': len(df[df['returns'] > 0]),
|
||||
'negative_returns': len(df[df['returns'] < 0])
|
||||
}
|
||||
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 create_volume_analysis_chart(symbol: str, timeframe: str = "1h", days_back: int = 7) -> go.Figure:
|
||||
"""Create a comprehensive volume analysis chart."""
|
||||
try:
|
||||
analyzer = VolumeAnalyzer()
|
||||
|
||||
# Fetch market data for chart
|
||||
db_manager = DatabaseManager()
|
||||
db_manager.initialize()
|
||||
|
||||
end_time = datetime.now(timezone.utc)
|
||||
start_time = end_time - timedelta(days=days_back)
|
||||
|
||||
with db_manager.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
query = text("""
|
||||
SELECT timestamp, open, high, low, close, volume, trades_count
|
||||
FROM market_data
|
||||
WHERE symbol = :symbol
|
||||
AND timeframe = :timeframe
|
||||
AND timestamp >= :start_time
|
||||
AND timestamp <= :end_time
|
||||
ORDER BY timestamp ASC
|
||||
""")
|
||||
|
||||
result = session.execute(query, {
|
||||
'symbol': symbol,
|
||||
'timeframe': timeframe,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time
|
||||
})
|
||||
|
||||
candles = []
|
||||
for row in result:
|
||||
candles.append({
|
||||
'timestamp': row.timestamp,
|
||||
'open': float(row.open),
|
||||
'high': float(row.high),
|
||||
'low': float(row.low),
|
||||
'close': float(row.close),
|
||||
'volume': float(row.volume),
|
||||
'trades_count': int(row.trades_count) if row.trades_count else 0
|
||||
})
|
||||
|
||||
if not candles:
|
||||
fig = go.Figure()
|
||||
fig.add_annotation(text="No data available", xref="paper", yref="paper", x=0.5, y=0.5)
|
||||
return fig
|
||||
|
||||
df = pd.DataFrame(candles)
|
||||
|
||||
# Calculate volume moving average
|
||||
df['volume_ma'] = df['volume'].rolling(window=20, min_periods=1).mean()
|
||||
|
||||
# Create subplots
|
||||
fig = make_subplots(
|
||||
rows=3, cols=1,
|
||||
subplot_titles=('Price Action', 'Volume Analysis', 'Volume vs Moving Average'),
|
||||
vertical_spacing=0.08,
|
||||
row_heights=[0.4, 0.3, 0.3]
|
||||
)
|
||||
|
||||
# Price candlestick
|
||||
fig.add_trace(
|
||||
go.Candlestick(
|
||||
x=df['timestamp'],
|
||||
open=df['open'],
|
||||
high=df['high'],
|
||||
low=df['low'],
|
||||
close=df['close'],
|
||||
name='Price',
|
||||
increasing_line_color='#26a69a',
|
||||
decreasing_line_color='#ef5350'
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Volume bars with color coding
|
||||
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
|
||||
)
|
||||
|
||||
# Volume vs moving average
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['timestamp'],
|
||||
y=df['volume'],
|
||||
mode='lines',
|
||||
name='Volume',
|
||||
line=dict(color='#2196f3', width=1)
|
||||
),
|
||||
row=3, col=1
|
||||
)
|
||||
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['timestamp'],
|
||||
y=df['volume_ma'],
|
||||
mode='lines',
|
||||
name='Volume MA(20)',
|
||||
line=dict(color='#ff9800', width=2)
|
||||
),
|
||||
row=3, col=1
|
||||
)
|
||||
|
||||
# Update layout
|
||||
fig.update_layout(
|
||||
title=f'{symbol} Volume Analysis ({timeframe})',
|
||||
xaxis_rangeslider_visible=False,
|
||||
height=800,
|
||||
showlegend=True,
|
||||
template='plotly_white'
|
||||
)
|
||||
|
||||
# Update y-axes
|
||||
fig.update_yaxes(title_text="Price", row=1, col=1)
|
||||
fig.update_yaxes(title_text="Volume", row=2, col=1)
|
||||
fig.update_yaxes(title_text="Volume", row=3, col=1)
|
||||
|
||||
return fig
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Volume chart creation error: {e}")
|
||||
fig = go.Figure()
|
||||
fig.add_annotation(text=f"Error: {str(e)}", xref="paper", yref="paper", x=0.5, y=0.5)
|
||||
return fig
|
||||
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 create_price_movement_chart(symbol: str, timeframe: str = "1h", days_back: int = 7) -> go.Figure:
|
||||
"""Create a comprehensive price movement analysis chart."""
|
||||
try:
|
||||
# Fetch market data for chart
|
||||
db_manager = DatabaseManager()
|
||||
db_manager.initialize()
|
||||
|
||||
end_time = datetime.now(timezone.utc)
|
||||
start_time = end_time - timedelta(days=days_back)
|
||||
|
||||
with db_manager.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
query = text("""
|
||||
SELECT timestamp, open, high, low, close, volume
|
||||
FROM market_data
|
||||
WHERE symbol = :symbol
|
||||
AND timeframe = :timeframe
|
||||
AND timestamp >= :start_time
|
||||
AND timestamp <= :end_time
|
||||
ORDER BY timestamp ASC
|
||||
""")
|
||||
|
||||
result = session.execute(query, {
|
||||
'symbol': symbol,
|
||||
'timeframe': timeframe,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time
|
||||
})
|
||||
|
||||
candles = []
|
||||
for row in result:
|
||||
candles.append({
|
||||
'timestamp': row.timestamp,
|
||||
'open': float(row.open),
|
||||
'high': float(row.high),
|
||||
'low': float(row.low),
|
||||
'close': float(row.close),
|
||||
'volume': float(row.volume)
|
||||
})
|
||||
|
||||
if not candles:
|
||||
fig = go.Figure()
|
||||
fig.add_annotation(text="No data available", xref="paper", yref="paper", x=0.5, y=0.5)
|
||||
return fig
|
||||
|
||||
df = pd.DataFrame(candles)
|
||||
|
||||
# Calculate returns and statistics
|
||||
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)
|
||||
df['range_pct'] = ((df['high'] - df['low']) / df['open']) * 100
|
||||
df['cumulative_return'] = (1 + df['returns'] / 100).cumprod()
|
||||
|
||||
# Create subplots
|
||||
fig = make_subplots(
|
||||
rows=3, cols=1,
|
||||
subplot_titles=('Cumulative Returns', 'Period Returns (%)', 'Price Range (%)'),
|
||||
vertical_spacing=0.08,
|
||||
row_heights=[0.4, 0.3, 0.3]
|
||||
)
|
||||
|
||||
# Cumulative returns
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['timestamp'],
|
||||
y=df['cumulative_return'],
|
||||
mode='lines',
|
||||
name='Cumulative Return',
|
||||
line=dict(color='#2196f3', width=2)
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Period returns with color coding
|
||||
colors = ['#26a69a' if ret >= 0 else '#ef5350' for ret in df['returns']]
|
||||
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=df['timestamp'],
|
||||
y=df['returns'],
|
||||
name='Returns (%)',
|
||||
marker_color=colors,
|
||||
opacity=0.7
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
# Price range percentage
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['timestamp'],
|
||||
y=df['range_pct'],
|
||||
mode='lines+markers',
|
||||
name='Range %',
|
||||
line=dict(color='#ff9800', width=1),
|
||||
marker=dict(size=4)
|
||||
),
|
||||
row=3, col=1
|
||||
)
|
||||
|
||||
# Add zero line for returns
|
||||
fig.add_hline(y=0, line_dash="dash", line_color="gray", row=2, col=1)
|
||||
|
||||
# Update layout
|
||||
fig.update_layout(
|
||||
title=f'{symbol} Price Movement Analysis ({timeframe})',
|
||||
height=800,
|
||||
showlegend=True,
|
||||
template='plotly_white'
|
||||
)
|
||||
|
||||
# Update y-axes
|
||||
fig.update_yaxes(title_text="Cumulative Return", row=1, col=1)
|
||||
fig.update_yaxes(title_text="Returns (%)", row=2, col=1)
|
||||
fig.update_yaxes(title_text="Range (%)", row=3, col=1)
|
||||
|
||||
return fig
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Price movement chart creation error: {e}")
|
||||
fig = go.Figure()
|
||||
fig.add_annotation(text=f"Error: {str(e)}", xref="paper", yref="paper", x=0.5, y=0.5)
|
||||
return fig
|
||||
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 create_data_analysis_panel():
|
||||
"""Create the main data analysis panel with tabs for different analyses."""
|
||||
return html.Div([
|
||||
dcc.Tabs(
|
||||
id="data-analysis-tabs",
|
||||
value="volume-analysis",
|
||||
children=[
|
||||
dcc.Tab(label="Volume Analysis", value="volume-analysis", children=[
|
||||
html.Div(id='volume-analysis-content', children=[
|
||||
html.P("Content for Volume Analysis")
|
||||
]),
|
||||
html.Div(id='volume-stats-container', children=[
|
||||
html.P("Stats container loaded - waiting for callback...")
|
||||
])
|
||||
]),
|
||||
dcc.Tab(label="Price Movement", value="price-movement", children=[
|
||||
html.Div(id='price-movement-content', children=[
|
||||
dbc.Alert("Select a symbol and timeframe to view price movement analysis.", color="primary")
|
||||
])
|
||||
]),
|
||||
],
|
||||
)
|
||||
], id='data-analysis-panel-wrapper')
|
||||
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:
|
||||
"""Format number with appropriate decimals and units."""
|
||||
if pd.isna(value):
|
||||
"""Formats a number to a string with specified decimals."""
|
||||
if value is None:
|
||||
return "N/A"
|
||||
|
||||
if abs(value) >= 1e9:
|
||||
return f"{value/1e9:.{decimals}f}B"
|
||||
elif abs(value) >= 1e6:
|
||||
return f"{value/1e6:.{decimals}f}M"
|
||||
elif abs(value) >= 1e3:
|
||||
return f"{value/1e3:.{decimals}f}K"
|
||||
else:
|
||||
return f"{value:.{decimals}f}"
|
||||
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:
|
||||
"""Create volume statistics display."""
|
||||
"""Creates a display for volume statistics."""
|
||||
if 'error' in stats:
|
||||
return dbc.Alert(
|
||||
"Error loading volume statistics",
|
||||
color="danger",
|
||||
dismissable=True
|
||||
)
|
||||
|
||||
def create_stat_card(icon, title, value, color="primary"):
|
||||
return dbc.Col(dbc.Card(dbc.CardBody([
|
||||
html.Div([
|
||||
html.Div(icon, className="display-6"),
|
||||
html.Div([
|
||||
html.P(title, className="card-title mb-1 text-muted"),
|
||||
html.H4(value, className=f"card-text fw-bold text-{color}")
|
||||
], className="ms-3")
|
||||
], className="d-flex align-items-center")
|
||||
])), width=4, className="mb-3")
|
||||
return html.Div(f"Error: {stats['error']}", className="alert alert-danger")
|
||||
|
||||
return dbc.Row([
|
||||
create_stat_card("📊", "Total Volume", format_number(stats['total_volume'])),
|
||||
create_stat_card("📈", "Average Volume", format_number(stats['avg_volume'])),
|
||||
create_stat_card("🎯", "Volume Trend", stats['volume_trend'],
|
||||
"success" if stats['volume_trend'] == "Increasing" else "danger"),
|
||||
create_stat_card("⚡", "High Volume Periods", str(stats['high_volume_periods'])),
|
||||
create_stat_card("🔗", "Volume-Price Correlation", f"{stats['volume_price_correlation']:.3f}"),
|
||||
create_stat_card("💱", "Avg Trade Size", format_number(stats['avg_trade_size']))
|
||||
], className="mt-3")
|
||||
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:
|
||||
"""Create price movement statistics display."""
|
||||
"""Creates a display for price movement statistics."""
|
||||
if 'error' in stats:
|
||||
return dbc.Alert(
|
||||
"Error loading price statistics",
|
||||
color="danger",
|
||||
dismissable=True
|
||||
)
|
||||
return html.Div(f"Error: {stats['error']}", className="alert alert-danger")
|
||||
|
||||
def create_stat_card(icon, title, value, color="primary"):
|
||||
text_color = "text-dark"
|
||||
if color == "success":
|
||||
text_color = "text-success"
|
||||
elif color == "danger":
|
||||
text_color = "text-danger"
|
||||
|
||||
return dbc.Col(dbc.Card(dbc.CardBody([
|
||||
html.Div([
|
||||
html.Div(icon, className="display-6"),
|
||||
html.Div([
|
||||
html.P(title, className="card-title mb-1 text-muted"),
|
||||
html.H4(value, className=f"card-text fw-bold {text_color}")
|
||||
], className="ms-3")
|
||||
], className="d-flex align-items-center")
|
||||
])), width=4, className="mb-3")
|
||||
|
||||
return dbc.Row([
|
||||
create_stat_card("💰", "Current Price", f"${stats['current_price']:.2f}"),
|
||||
create_stat_card("📈", "Period Return", f"{stats['period_return']:+.2f}%",
|
||||
"success" if stats['period_return'] >= 0 else "danger"),
|
||||
create_stat_card("📊", "Volatility", f"{stats['volatility']:.2f}%", color="warning"),
|
||||
create_stat_card("🎯", "Bullish Ratio", f"{stats['bullish_ratio']:.1f}%"),
|
||||
create_stat_card("⚡", "Momentum", f"{stats['momentum']:+.2f}%",
|
||||
"success" if stats['momentum'] >= 0 else "danger"),
|
||||
create_stat_card("📉", "Max Loss", f"{stats['max_loss']:.2f}%", "danger")
|
||||
], className="mt-3")
|
||||
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:
|
||||
"""
|
||||
Generate a comprehensive market statistics component from a DataFrame.
|
||||
Generates a display of key market statistics from the provided DataFrame.
|
||||
"""
|
||||
if df.empty:
|
||||
return html.Div("No data available for statistics.", className="text-center text-muted")
|
||||
return html.Div([html.P("No market data available for statistics.")], className="alert alert-info mt-4")
|
||||
|
||||
try:
|
||||
# Get statistics
|
||||
price_analyzer = PriceMovementAnalyzer()
|
||||
volume_analyzer = VolumeAnalyzer()
|
||||
|
||||
price_stats = price_analyzer.get_price_movement_statistics(df)
|
||||
volume_stats = volume_analyzer.get_volume_statistics(df)
|
||||
|
||||
# Format key statistics for display
|
||||
start_date = df.index.min().strftime('%Y-%m-%d %H:%M')
|
||||
end_date = df.index.max().strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
# Check for errors from analyzers
|
||||
if 'error' in price_stats or 'error' in volume_stats:
|
||||
error_msg = price_stats.get('error') or volume_stats.get('error')
|
||||
return html.Div(f"Error generating statistics: {error_msg}", style={'color': 'red'})
|
||||
|
||||
# Time range for display
|
||||
days_back = (df.index.max() - df.index.min()).days
|
||||
time_status = f"📅 Analysis Range: {start_date} to {end_date} (~{days_back} days)"
|
||||
|
||||
return html.Div([
|
||||
html.H3("📊 Enhanced Market Statistics", className="mb-3"),
|
||||
html.P(
|
||||
time_status,
|
||||
className="lead text-center text-muted mb-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"
|
||||
),
|
||||
create_price_stats_display(price_stats),
|
||||
create_volume_stats_display(volume_stats)
|
||||
])
|
||||
except Exception as e:
|
||||
logger.error(f"Error in get_market_statistics: {e}", exc_info=True)
|
||||
return dbc.Alert(f"Error generating statistics display: {e}", color="danger")
|
||||
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
|
||||
)
|
||||
])
|
||||
@@ -4,6 +4,7 @@ Indicator modal component for creating and editing indicators.
|
||||
|
||||
from dash import html, dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
from utils.timeframe_utils import load_timeframe_options
|
||||
|
||||
|
||||
def create_indicator_modal():
|
||||
@@ -37,19 +38,7 @@ def create_indicator_modal():
|
||||
dbc.Col(dbc.Label("Timeframe (Optional):"), width=12),
|
||||
dbc.Col(dcc.Dropdown(
|
||||
id='indicator-timeframe-dropdown',
|
||||
options=[
|
||||
{'label': 'Chart Timeframe', 'value': ''},
|
||||
{'label': "1 Second", 'value': '1s'},
|
||||
{'label': "5 Seconds", 'value': '5s'},
|
||||
{'label': "15 Seconds", 'value': '15s'},
|
||||
{'label': "30 Seconds", 'value': '30s'},
|
||||
{'label': '1 Minute', 'value': '1m'},
|
||||
{'label': '5 Minutes', 'value': '5m'},
|
||||
{'label': '15 Minutes', 'value': '15m'},
|
||||
{'label': '1 Hour', 'value': '1h'},
|
||||
{'label': '4 Hours', 'value': '4h'},
|
||||
{'label': '1 Day', 'value': '1d'},
|
||||
],
|
||||
options=[{'label': 'Chart Timeframe', 'value': ''}] + load_timeframe_options(),
|
||||
value='',
|
||||
placeholder='Defaults to chart timeframe'
|
||||
), width=12),
|
||||
|
||||
@@ -13,81 +13,68 @@ from dashboard.components.chart_controls import (
|
||||
create_time_range_controls,
|
||||
create_export_controls
|
||||
)
|
||||
from utils.timeframe_utils import load_timeframe_options
|
||||
|
||||
|
||||
logger = get_logger("default_logger")
|
||||
|
||||
|
||||
def get_market_data_layout():
|
||||
"""Create the market data visualization layout with indicator controls."""
|
||||
# Get available symbols and timeframes from database
|
||||
symbols = get_supported_symbols()
|
||||
timeframes = get_supported_timeframes()
|
||||
|
||||
# Create dropdown options
|
||||
def _create_dropdown_options(symbols, timeframes):
|
||||
"""Creates symbol and timeframe dropdown options."""
|
||||
symbol_options = [{'label': symbol, 'value': symbol} for symbol in symbols]
|
||||
timeframe_options = [
|
||||
{'label': "1 Second", 'value': '1s'},
|
||||
{'label': "5 Seconds", 'value': '5s'},
|
||||
{'label': "15 Seconds", 'value': '15s'},
|
||||
{'label': "30 Seconds", 'value': '30s'},
|
||||
{'label': '1 Minute', 'value': '1m'},
|
||||
{'label': '5 Minutes', 'value': '5m'},
|
||||
{'label': '15 Minutes', 'value': '15m'},
|
||||
{'label': '1 Hour', 'value': '1h'},
|
||||
{'label': '4 Hours', 'value': '4h'},
|
||||
{'label': '1 Day', 'value': '1d'},
|
||||
]
|
||||
all_timeframe_options = load_timeframe_options()
|
||||
|
||||
# Filter timeframe options to only show those available in database
|
||||
available_timeframes = [tf for tf in ['1s', '5s', '15s', '30s', '1m', '5m', '15m', '1h', '4h', '1d'] if tf in timeframes]
|
||||
if not available_timeframes:
|
||||
available_timeframes = ['5m'] # Default fallback
|
||||
available_timeframes_from_db = [tf for tf in [opt['value'] for opt in all_timeframe_options] if tf in timeframes]
|
||||
if not available_timeframes_from_db:
|
||||
available_timeframes_from_db = ['5m'] # Default fallback
|
||||
|
||||
timeframe_options = [opt for opt in timeframe_options if opt['value'] in available_timeframes]
|
||||
timeframe_options = [opt for opt in all_timeframe_options if opt['value'] in available_timeframes_from_db]
|
||||
|
||||
# Get available strategies and indicators
|
||||
return symbol_options, timeframe_options
|
||||
|
||||
def _load_strategy_and_indicator_options():
|
||||
"""Loads strategy and indicator options for chart configuration."""
|
||||
try:
|
||||
strategy_names = get_available_strategy_names()
|
||||
strategy_options = [{'label': name.replace('_', ' ').title(), 'value': name} for name in strategy_names]
|
||||
|
||||
# Get user indicators from the new indicator manager
|
||||
indicator_manager = get_indicator_manager()
|
||||
|
||||
# Ensure default indicators exist
|
||||
ensure_default_indicators()
|
||||
|
||||
# Get indicators by display type
|
||||
overlay_indicators = indicator_manager.get_indicators_by_type('overlay')
|
||||
subplot_indicators = indicator_manager.get_indicators_by_type('subplot')
|
||||
|
||||
# Create checkbox options for overlay indicators
|
||||
overlay_options = []
|
||||
for indicator in overlay_indicators:
|
||||
display_name = f"{indicator.name} ({indicator.type.upper()})"
|
||||
overlay_options.append({'label': display_name, 'value': indicator.id})
|
||||
|
||||
# Create checkbox options for subplot indicators
|
||||
subplot_options = []
|
||||
for indicator in subplot_indicators:
|
||||
display_name = f"{indicator.name} ({indicator.type.upper()})"
|
||||
subplot_options.append({'label': display_name, 'value': indicator.id})
|
||||
|
||||
return strategy_options, overlay_options, subplot_options
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Market data layout: Error loading indicator options: {e}")
|
||||
strategy_options = [{'label': 'Basic Chart', 'value': 'basic'}]
|
||||
overlay_options = []
|
||||
subplot_options = []
|
||||
return [{'label': 'Basic Chart', 'value': 'basic'}], [], []
|
||||
|
||||
|
||||
def get_market_data_layout():
|
||||
"""Create the market data visualization layout with indicator controls."""
|
||||
symbols = get_supported_symbols()
|
||||
timeframes = get_supported_timeframes()
|
||||
|
||||
# Create components using the new modular functions
|
||||
symbol_options, timeframe_options = _create_dropdown_options(symbols, timeframes)
|
||||
strategy_options, overlay_options, subplot_options = _load_strategy_and_indicator_options()
|
||||
|
||||
chart_config_panel = create_chart_config_panel(strategy_options, overlay_options, subplot_options)
|
||||
time_range_controls = create_time_range_controls()
|
||||
export_controls = create_export_controls()
|
||||
|
||||
return html.Div([
|
||||
# Title and basic controls
|
||||
html.H3("💹 Market Data Visualization", style={'color': '#2c3e50', 'margin-bottom': '20px'}),
|
||||
|
||||
# Main chart controls
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label("Symbol:", style={'font-weight': 'bold'}),
|
||||
@@ -111,21 +98,15 @@ def get_market_data_layout():
|
||||
], style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
|
||||
], style={'margin-bottom': '20px'}),
|
||||
|
||||
# Chart Configuration Panel
|
||||
chart_config_panel,
|
||||
|
||||
# Time Range Controls (positioned under indicators, next to chart)
|
||||
time_range_controls,
|
||||
|
||||
# Export Controls
|
||||
export_controls,
|
||||
|
||||
# Chart
|
||||
dcc.Graph(id='price-chart'),
|
||||
|
||||
# Hidden store for chart data
|
||||
dcc.Store(id='chart-data-store'),
|
||||
|
||||
# Enhanced Market statistics with integrated data analysis
|
||||
html.Div(id='market-stats', style={'margin-top': '20px'})
|
||||
])
|
||||
@@ -5,127 +5,146 @@ System health monitoring layout for the dashboard.
|
||||
from dash import html
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
def create_quick_status_card(title, component_id, icon):
|
||||
"""Helper to create a quick status card."""
|
||||
return dbc.Card(dbc.CardBody([
|
||||
html.H5(f"{icon} {title}", className="card-title"),
|
||||
html.Div(id=component_id, children=[
|
||||
dbc.Badge("Checking...", color="warning", className="me-1")
|
||||
])
|
||||
]), className="text-center")
|
||||
|
||||
def _create_header_section():
|
||||
"""Creates the header section for the system health layout."""
|
||||
return html.Div([
|
||||
html.H2("⚙️ System Health & Data Monitoring"),
|
||||
html.P("Real-time monitoring of data collection services, database health, and system performance",
|
||||
className="lead")
|
||||
], className="p-5 mb-4 bg-light rounded-3")
|
||||
|
||||
def _create_quick_status_row():
|
||||
"""Creates the quick status overview row."""
|
||||
return dbc.Row([
|
||||
dbc.Col(create_quick_status_card("Data Collection", "data-collection-quick-status", "📊"), width=3),
|
||||
dbc.Col(create_quick_status_card("Database", "database-quick-status", "🗄️"), width=3),
|
||||
dbc.Col(create_quick_status_card("Redis", "redis-quick-status", "🔗"), width=3),
|
||||
dbc.Col(create_quick_status_card("Performance", "performance-quick-status", "📈"), width=3),
|
||||
], className="mb-4")
|
||||
|
||||
def _create_data_collection_service_card():
|
||||
"""Creates the data collection service status card."""
|
||||
return dbc.Card([
|
||||
dbc.CardHeader(html.H4("📡 Data Collection Service")),
|
||||
dbc.CardBody([
|
||||
html.H5("Service Status", className="card-title"),
|
||||
html.Div(id='data-collection-service-status', className="mb-4"),
|
||||
|
||||
html.H5("Collection Metrics", className="card-title"),
|
||||
html.Div(id='data-collection-metrics', className="mb-4"),
|
||||
|
||||
html.H5("Service Controls", className="card-title"),
|
||||
dbc.ButtonGroup([
|
||||
dbc.Button("🔄 Refresh Status", id="refresh-data-status-btn", color="primary", outline=True, size="sm"),
|
||||
dbc.Button("📊 View Details", id="view-collection-details-btn", color="secondary", outline=True, size="sm"),
|
||||
dbc.Button("📋 View Logs", id="view-collection-logs-btn", color="info", outline=True, size="sm")
|
||||
])
|
||||
])
|
||||
], className="mb-4")
|
||||
|
||||
def _create_individual_collectors_card():
|
||||
"""Creates the individual collectors health card."""
|
||||
return dbc.Card([
|
||||
dbc.CardHeader(html.H4("🔌 Individual Collectors")),
|
||||
dbc.CardBody([
|
||||
html.Div(id='individual-collectors-status'),
|
||||
html.Div([
|
||||
dbc.Alert(
|
||||
"Collector health data will be displayed here when the data collection service is running.",
|
||||
id="collectors-info-alert",
|
||||
color="info",
|
||||
is_open=True,
|
||||
)
|
||||
], id='collectors-placeholder')
|
||||
])
|
||||
], className="mb-4")
|
||||
|
||||
def _create_database_status_card():
|
||||
"""Creates the database health status card."""
|
||||
return dbc.Card([
|
||||
dbc.CardHeader(html.H4("🗄️ Database Health")),
|
||||
dbc.CardBody([
|
||||
html.H5("Connection Status", className="card-title"),
|
||||
html.Div(id='database-status', className="mb-3"),
|
||||
html.Hr(),
|
||||
html.H5("Database Statistics", className="card-title"),
|
||||
html.Div(id='database-stats')
|
||||
])
|
||||
], className="mb-4")
|
||||
|
||||
def _create_redis_status_card():
|
||||
"""Creates the Redis health status card."""
|
||||
return dbc.Card([
|
||||
dbc.CardHeader(html.H4("🔗 Redis Status")),
|
||||
dbc.CardBody([
|
||||
html.H5("Connection Status", className="card-title"),
|
||||
html.Div(id='redis-status', className="mb-3"),
|
||||
html.Hr(),
|
||||
html.H5("Redis Statistics", className="card-title"),
|
||||
html.Div(id='redis-stats')
|
||||
])
|
||||
], className="mb-4")
|
||||
|
||||
def _create_system_performance_card():
|
||||
"""Creates the system performance metrics card."""
|
||||
return dbc.Card([
|
||||
dbc.CardHeader(html.H4("📈 System Performance")),
|
||||
dbc.CardBody([
|
||||
html.Div(id='system-performance-metrics')
|
||||
])
|
||||
], className="mb-4")
|
||||
|
||||
def _create_collection_details_modal():
|
||||
"""Creates the data collection details modal."""
|
||||
return dbc.Modal([
|
||||
dbc.ModalHeader(dbc.ModalTitle("📊 Data Collection Details")),
|
||||
dbc.ModalBody(id="collection-details-content")
|
||||
], id="collection-details-modal", is_open=False, size="lg")
|
||||
|
||||
def _create_collection_logs_modal():
|
||||
"""Creates the collection service logs modal."""
|
||||
return dbc.Modal([
|
||||
dbc.ModalHeader(dbc.ModalTitle("📋 Collection Service Logs")),
|
||||
dbc.ModalBody(
|
||||
html.Div(
|
||||
html.Pre(id="collection-logs-content", style={'max-height': '400px', 'overflow-y': 'auto'}),
|
||||
style={'white-space': 'pre-wrap', 'background-color': '#f8f9fa', 'padding': '15px', 'border-radius': '5px'}
|
||||
)
|
||||
),
|
||||
dbc.ModalFooter([
|
||||
dbc.Button("Refresh", id="refresh-logs-btn", color="primary"),
|
||||
dbc.Button("Close", id="close-logs-modal", color="secondary", className="ms-auto")
|
||||
])
|
||||
], id="collection-logs-modal", is_open=False, size="xl")
|
||||
|
||||
def get_system_health_layout():
|
||||
"""Create the enhanced system health monitoring layout with Bootstrap components."""
|
||||
|
||||
def create_quick_status_card(title, component_id, icon):
|
||||
return dbc.Card(dbc.CardBody([
|
||||
html.H5(f"{icon} {title}", className="card-title"),
|
||||
html.Div(id=component_id, children=[
|
||||
dbc.Badge("Checking...", color="warning", className="me-1")
|
||||
])
|
||||
]), className="text-center")
|
||||
|
||||
return html.Div([
|
||||
# Header section
|
||||
html.Div([
|
||||
html.H2("⚙️ System Health & Data Monitoring"),
|
||||
html.P("Real-time monitoring of data collection services, database health, and system performance",
|
||||
className="lead")
|
||||
], className="p-5 mb-4 bg-light rounded-3"),
|
||||
_create_header_section(),
|
||||
_create_quick_status_row(),
|
||||
|
||||
# Quick Status Overview Row
|
||||
dbc.Row([
|
||||
dbc.Col(create_quick_status_card("Data Collection", "data-collection-quick-status", "📊"), width=3),
|
||||
dbc.Col(create_quick_status_card("Database", "database-quick-status", "🗄️"), width=3),
|
||||
dbc.Col(create_quick_status_card("Redis", "redis-quick-status", "🔗"), width=3),
|
||||
dbc.Col(create_quick_status_card("Performance", "performance-quick-status", "📈"), width=3),
|
||||
], className="mb-4"),
|
||||
|
||||
# Detailed Monitoring Sections
|
||||
dbc.Row([
|
||||
# Left Column - Data Collection Service
|
||||
dbc.Col([
|
||||
# Data Collection Service Status
|
||||
dbc.Card([
|
||||
dbc.CardHeader(html.H4("📡 Data Collection Service")),
|
||||
dbc.CardBody([
|
||||
html.H5("Service Status", className="card-title"),
|
||||
html.Div(id='data-collection-service-status', className="mb-4"),
|
||||
|
||||
html.H5("Collection Metrics", className="card-title"),
|
||||
html.Div(id='data-collection-metrics', className="mb-4"),
|
||||
|
||||
html.H5("Service Controls", className="card-title"),
|
||||
dbc.ButtonGroup([
|
||||
dbc.Button("🔄 Refresh Status", id="refresh-data-status-btn", color="primary", outline=True, size="sm"),
|
||||
dbc.Button("📊 View Details", id="view-collection-details-btn", color="secondary", outline=True, size="sm"),
|
||||
dbc.Button("📋 View Logs", id="view-collection-logs-btn", color="info", outline=True, size="sm")
|
||||
])
|
||||
])
|
||||
], className="mb-4"),
|
||||
|
||||
# Data Collector Health
|
||||
dbc.Card([
|
||||
dbc.CardHeader(html.H4("🔌 Individual Collectors")),
|
||||
dbc.CardBody([
|
||||
html.Div(id='individual-collectors-status'),
|
||||
html.Div([
|
||||
dbc.Alert(
|
||||
"Collector health data will be displayed here when the data collection service is running.",
|
||||
id="collectors-info-alert",
|
||||
color="info",
|
||||
is_open=True,
|
||||
)
|
||||
], id='collectors-placeholder')
|
||||
])
|
||||
], className="mb-4"),
|
||||
_create_data_collection_service_card(),
|
||||
_create_individual_collectors_card(),
|
||||
], width=6),
|
||||
|
||||
# Right Column - System Health
|
||||
dbc.Col([
|
||||
# Database Status
|
||||
dbc.Card([
|
||||
dbc.CardHeader(html.H4("🗄️ Database Health")),
|
||||
dbc.CardBody([
|
||||
html.H5("Connection Status", className="card-title"),
|
||||
html.Div(id='database-status', className="mb-3"),
|
||||
html.Hr(),
|
||||
html.H5("Database Statistics", className="card-title"),
|
||||
html.Div(id='database-stats')
|
||||
])
|
||||
], className="mb-4"),
|
||||
|
||||
# Redis Status
|
||||
dbc.Card([
|
||||
dbc.CardHeader(html.H4("🔗 Redis Status")),
|
||||
dbc.CardBody([
|
||||
html.H5("Connection Status", className="card-title"),
|
||||
html.Div(id='redis-status', className="mb-3"),
|
||||
html.Hr(),
|
||||
html.H5("Redis Statistics", className="card-title"),
|
||||
html.Div(id='redis-stats')
|
||||
])
|
||||
], className="mb-4"),
|
||||
|
||||
# System Performance
|
||||
dbc.Card([
|
||||
dbc.CardHeader(html.H4("📈 System Performance")),
|
||||
dbc.CardBody([
|
||||
html.Div(id='system-performance-metrics')
|
||||
])
|
||||
], className="mb-4"),
|
||||
_create_database_status_card(),
|
||||
_create_redis_status_card(),
|
||||
_create_system_performance_card(),
|
||||
], width=6)
|
||||
]),
|
||||
|
||||
# Data Collection Details Modal
|
||||
dbc.Modal([
|
||||
dbc.ModalHeader(dbc.ModalTitle("📊 Data Collection Details")),
|
||||
dbc.ModalBody(id="collection-details-content")
|
||||
], id="collection-details-modal", is_open=False, size="lg"),
|
||||
|
||||
# Collection Logs Modal
|
||||
dbc.Modal([
|
||||
dbc.ModalHeader(dbc.ModalTitle("📋 Collection Service Logs")),
|
||||
dbc.ModalBody(
|
||||
html.Div(
|
||||
html.Pre(id="collection-logs-content", style={'max-height': '400px', 'overflow-y': 'auto'}),
|
||||
style={'white-space': 'pre-wrap', 'background-color': '#f8f9fa', 'padding': '15px', 'border-radius': '5px'}
|
||||
)
|
||||
),
|
||||
dbc.ModalFooter([
|
||||
dbc.Button("Refresh", id="refresh-logs-btn", color="primary"),
|
||||
dbc.Button("Close", id="close-logs-modal", color="secondary", className="ms-auto")
|
||||
])
|
||||
], id="collection-logs-modal", is_open=False, size="xl")
|
||||
_create_collection_details_modal(),
|
||||
_create_collection_logs_modal()
|
||||
])
|
||||
Reference in New Issue
Block a user