3. 7 Enhance chart functionality with time range controls and stability improvements
- Updated `app_new.py` to run the application in debug mode for stability. - Introduced a new time range control panel in `dashboard/components/chart_controls.py`, allowing users to select predefined time ranges and custom date ranges. - Enhanced chart callbacks in `dashboard/callbacks/charts.py` to handle time range inputs, ensuring accurate market statistics and analysis based on user selections. - Implemented logic to preserve chart state during updates, preventing resets of zoom/pan settings. - Updated market statistics display to reflect the selected time range, improving user experience and data relevance. - Added a clear button for custom date ranges to reset selections easily. - Enhanced documentation to reflect the new time range features and usage guidelines.
This commit is contained in:
parent
132710a9a7
commit
87843a1d35
@ -32,8 +32,8 @@ def main():
|
||||
|
||||
logger.info("Dashboard application initialized successfully")
|
||||
|
||||
# Run the app (updated for newer Dash version)
|
||||
app.run(debug=True, host='0.0.0.0', port=8050)
|
||||
# Run the app (debug=False for stability, manual restart required for changes)
|
||||
app.run(debug=False, host='0.0.0.0', port=8050)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start dashboard application: {e}")
|
||||
|
||||
@ -260,33 +260,50 @@ def get_supported_timeframes():
|
||||
|
||||
return ['5s', '1m', '15m', '1h'] # Fallback
|
||||
|
||||
def get_market_statistics(symbol: str, timeframe: str = "1h"):
|
||||
"""Calculate market statistics from recent data."""
|
||||
def get_market_statistics(symbol: str, timeframe: str = "1h", days_back: int = 1):
|
||||
"""Calculate market statistics from recent data over a specified period."""
|
||||
builder = ChartBuilder()
|
||||
candles = builder.fetch_market_data(symbol, timeframe, days_back=1)
|
||||
candles = builder.fetch_market_data(symbol, timeframe, days_back=days_back)
|
||||
|
||||
if not candles:
|
||||
return {'Price': 'N/A', '24h Change': 'N/A', '24h Volume': 'N/A', 'High 24h': 'N/A', 'Low 24h': 'N/A'}
|
||||
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'}
|
||||
|
||||
import pandas as pd
|
||||
df = pd.DataFrame(candles)
|
||||
latest = df.iloc[-1]
|
||||
current_price = float(latest['close'])
|
||||
|
||||
# Calculate 24h change
|
||||
# Calculate change over the period
|
||||
if len(df) > 1:
|
||||
price_24h_ago = float(df.iloc[0]['open'])
|
||||
change_percent = ((current_price - price_24h_ago) / price_24h_ago) * 100
|
||||
price_period_ago = float(df.iloc[0]['open'])
|
||||
change_percent = ((current_price - price_period_ago) / price_period_ago) * 100
|
||||
else:
|
||||
change_percent = 0
|
||||
|
||||
from .utils import format_price, format_volume
|
||||
|
||||
# Determine label for period (e.g., "24h", "7d", "1h")
|
||||
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),
|
||||
'24h Change': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%",
|
||||
'24h Volume': format_volume(df['volume'].sum()),
|
||||
'High 24h': format_price(df['high'].max(), decimals=2),
|
||||
'Low 24h': format_price(df['low'].min(), 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):
|
||||
@ -472,4 +489,8 @@ def create_chart_with_indicators(symbol: str, timeframe: str,
|
||||
builder = ChartBuilder()
|
||||
return builder.create_chart_with_indicators(
|
||||
symbol, timeframe, overlay_indicators, subplot_indicators, days_back, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def initialize_indicator_manager():
|
||||
# Implementation of initialize_indicator_manager function
|
||||
pass
|
||||
@ -2,8 +2,8 @@
|
||||
Chart-related callbacks for the dashboard.
|
||||
"""
|
||||
|
||||
from dash import Output, Input
|
||||
from datetime import datetime
|
||||
from dash import Output, Input, State, Patch, ctx, html, no_update
|
||||
from datetime import datetime, timedelta
|
||||
from utils.logger import get_logger
|
||||
from components.charts import (
|
||||
create_strategy_chart,
|
||||
@ -13,48 +13,200 @@ from components.charts import (
|
||||
)
|
||||
from components.charts.config import get_all_example_strategies
|
||||
from database.connection import DatabaseManager
|
||||
from dash import html
|
||||
from components.charts.builder import ChartBuilder
|
||||
from components.charts.utils import prepare_chart_data
|
||||
|
||||
logger = get_logger("default_logger")
|
||||
|
||||
|
||||
def calculate_time_range(time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals):
|
||||
"""Calculate days_back and status message based on time range controls."""
|
||||
try:
|
||||
# Define predefined quick select options (excluding 'custom' and 'realtime')
|
||||
predefined_ranges = ['1h', '4h', '6h', '12h', '1d', '3d', '7d', '30d']
|
||||
|
||||
# PRIORITY 1: Explicit Predefined Dropdown Selection
|
||||
if time_range_quick in predefined_ranges:
|
||||
time_map = {
|
||||
'1h': (1/24, '🕐 Last 1 Hour'),
|
||||
'4h': (4/24, '🕐 Last 4 Hours'),
|
||||
'6h': (6/24, '🕐 Last 6 Hours'),
|
||||
'12h': (12/24, '🕐 Last 12 Hours'),
|
||||
'1d': (1, '📅 Last 1 Day'),
|
||||
'3d': (3, '📅 Last 3 Days'),
|
||||
'7d': (7, '📅 Last 7 Days'),
|
||||
'30d': (30, '📅 Last 30 Days')
|
||||
}
|
||||
days_back_fractional, label = time_map[time_range_quick]
|
||||
mode_text = "🔒 Locked" if analysis_mode == 'locked' else "🔴 Live"
|
||||
status = f"{label} | {mode_text}"
|
||||
days_back = days_back_fractional if days_back_fractional < 1 else int(days_back_fractional)
|
||||
logger.debug(f"Using predefined dropdown selection: {time_range_quick} -> {days_back} days. Custom dates ignored.")
|
||||
return days_back, status
|
||||
|
||||
# PRIORITY 2: Custom Date Range (if dropdown is 'custom' and dates are set)
|
||||
if time_range_quick == 'custom' and custom_start_date and custom_end_date:
|
||||
start_date = datetime.fromisoformat(custom_start_date.split('T')[0])
|
||||
end_date = datetime.fromisoformat(custom_end_date.split('T')[0])
|
||||
days_diff = (end_date - start_date).days
|
||||
status = f"📅 Custom Range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')} ({days_diff} days)"
|
||||
logger.debug(f"Using custom date range: {days_diff} days as dropdown is 'custom'.")
|
||||
return max(1, days_diff), status
|
||||
|
||||
# PRIORITY 3: Real-time (uses default lookback, typically 7 days for context)
|
||||
if time_range_quick == 'realtime':
|
||||
mode_text = "🔒 Analysis Mode" if analysis_mode == 'locked' else "🔴 Real-time Updates"
|
||||
status = f"📈 Real-time Mode | {mode_text} (Default: Last 7 Days)"
|
||||
logger.debug("Using real-time mode with default 7 days lookback.")
|
||||
return 7, status
|
||||
|
||||
# Fallback / Default (e.g., if time_range_quick is None or an unexpected value, or 'custom' without dates)
|
||||
# This also covers the case where 'custom' is selected but dates are not yet picked.
|
||||
mode_text = "🔒 Analysis Mode" if analysis_mode == 'locked' else "🔴 Live"
|
||||
default_label = "📅 Default (Last 7 Days)"
|
||||
if time_range_quick == 'custom' and not (custom_start_date and custom_end_date):
|
||||
default_label = "⏳ Select Custom Dates" # Prompt user if 'custom' is chosen but dates aren't set
|
||||
|
||||
status = f"{default_label} | {mode_text}"
|
||||
logger.debug(f"Fallback to default time range (7 days). time_range_quick: {time_range_quick}")
|
||||
return 7, status
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error calculating time range: {e}. Defaulting to 7 days.")
|
||||
return 7, f"⚠️ Error in time range. Defaulting to 7 days."
|
||||
|
||||
|
||||
def register_chart_callbacks(app):
|
||||
"""Register chart-related callbacks."""
|
||||
|
||||
@app.callback(
|
||||
Output('price-chart', 'figure'),
|
||||
[Output('price-chart', 'figure'),
|
||||
Output('time-range-status', 'children')],
|
||||
[Input('symbol-dropdown', 'value'),
|
||||
Input('timeframe-dropdown', 'value'),
|
||||
Input('overlay-indicators-checklist', 'value'),
|
||||
Input('subplot-indicators-checklist', 'value'),
|
||||
Input('strategy-dropdown', 'value'),
|
||||
Input('interval-component', 'n_intervals')]
|
||||
Input('time-range-quick-select', 'value'),
|
||||
Input('custom-date-range', 'start_date'),
|
||||
Input('custom-date-range', 'end_date'),
|
||||
Input('analysis-mode-toggle', 'value'),
|
||||
Input('interval-component', 'n_intervals')],
|
||||
[State('price-chart', 'relayoutData'),
|
||||
State('price-chart', 'figure')]
|
||||
)
|
||||
def update_price_chart(symbol, timeframe, overlay_indicators, subplot_indicators, selected_strategy, n_intervals):
|
||||
def update_price_chart(symbol, timeframe, overlay_indicators, subplot_indicators, selected_strategy,
|
||||
time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals,
|
||||
relayout_data, current_figure):
|
||||
"""Update the price chart with latest market data and selected indicators."""
|
||||
try:
|
||||
# If a strategy is selected, use strategy chart
|
||||
triggered_id = ctx.triggered_id
|
||||
logger.debug(f"Update_price_chart triggered by: {triggered_id}")
|
||||
|
||||
days_back, status_message = calculate_time_range(
|
||||
time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals
|
||||
)
|
||||
|
||||
# Condition for attempting to use Patch()
|
||||
can_patch = (
|
||||
triggered_id == 'interval-component' and
|
||||
analysis_mode == 'realtime' and
|
||||
(not selected_strategy or selected_strategy == 'basic') and
|
||||
not (overlay_indicators or []) and # Ensure lists are treated as empty if None
|
||||
not (subplot_indicators or [])
|
||||
)
|
||||
|
||||
if can_patch:
|
||||
logger.info(f"Attempting to PATCH chart for {symbol} {timeframe}")
|
||||
|
||||
try:
|
||||
# Find trace indices from current_figure
|
||||
candlestick_trace_idx = -1
|
||||
volume_trace_idx = -1
|
||||
if current_figure and 'data' in current_figure:
|
||||
for i, trace in enumerate(current_figure['data']):
|
||||
if trace.get('type') == 'candlestick':
|
||||
candlestick_trace_idx = i
|
||||
elif trace.get('type') == 'bar' and trace.get('name', '').lower() == 'volume': # Basic volume trace often named 'Volume'
|
||||
volume_trace_idx = i
|
||||
logger.debug(f"Found candlestick trace at index {candlestick_trace_idx}, volume trace at index {volume_trace_idx}")
|
||||
|
||||
if candlestick_trace_idx == -1:
|
||||
logger.warning(f"Could not find candlestick trace in current figure for patch. Falling back to full draw.")
|
||||
# Fall through to full draw by re-setting can_patch or just letting logic proceed
|
||||
else:
|
||||
chart_builder = ChartBuilder(logger_instance=logger)
|
||||
candles = chart_builder.fetch_market_data_enhanced(symbol, timeframe, days_back)
|
||||
|
||||
if not candles:
|
||||
logger.warning(f"Patch update: No candles fetched for {symbol} {timeframe}. No update.")
|
||||
return ctx.no_update, status_message
|
||||
|
||||
df = prepare_chart_data(candles)
|
||||
if df.empty:
|
||||
logger.warning(f"Patch update: DataFrame empty after preparing chart data for {symbol} {timeframe}. No update.")
|
||||
return ctx.no_update, status_message
|
||||
|
||||
patched_figure = Patch()
|
||||
|
||||
# Patch Candlestick Data using found index
|
||||
patched_figure['data'][candlestick_trace_idx]['x'] = df['timestamp']
|
||||
patched_figure['data'][candlestick_trace_idx]['open'] = df['open']
|
||||
patched_figure['data'][candlestick_trace_idx]['high'] = df['high']
|
||||
patched_figure['data'][candlestick_trace_idx]['low'] = df['low']
|
||||
patched_figure['data'][candlestick_trace_idx]['close'] = df['close']
|
||||
logger.debug(f"Patched candlestick data (trace {candlestick_trace_idx}) for {symbol} {timeframe} with {len(df)} points.")
|
||||
|
||||
# Patch Volume Data using found index (if volume trace exists)
|
||||
if volume_trace_idx != -1:
|
||||
if 'volume' in df.columns and df['volume'].sum() > 0:
|
||||
patched_figure['data'][volume_trace_idx]['x'] = df['timestamp']
|
||||
patched_figure['data'][volume_trace_idx]['y'] = df['volume']
|
||||
logger.debug(f"Patched volume data (trace {volume_trace_idx}) for {symbol} {timeframe}.")
|
||||
else:
|
||||
logger.debug(f"No significant volume data in new fetch for {symbol} {timeframe}. Clearing data for volume trace {volume_trace_idx}.")
|
||||
patched_figure['data'][volume_trace_idx]['x'] = []
|
||||
patched_figure['data'][volume_trace_idx]['y'] = []
|
||||
elif 'volume' in df.columns and df['volume'].sum() > 0:
|
||||
logger.warning(f"New volume data present, but no existing volume trace found to patch in current figure.")
|
||||
|
||||
logger.info(f"Successfully prepared patch for {symbol} {timeframe}.")
|
||||
return patched_figure, status_message
|
||||
|
||||
except Exception as patch_exception:
|
||||
logger.error(f"Error during chart PATCH attempt for {symbol} {timeframe}: {patch_exception}. Falling back to full draw.")
|
||||
# Fall through to full chart creation if patching fails
|
||||
|
||||
# Full figure creation (default or if not patching or if patch failed)
|
||||
logger.debug(f"Performing full chart draw for {symbol} {timeframe}. Can_patch: {can_patch}")
|
||||
if selected_strategy and selected_strategy != 'basic':
|
||||
fig = create_strategy_chart(symbol, timeframe, selected_strategy)
|
||||
logger.debug(f"Chart callback: Created strategy chart for {symbol} ({timeframe}) with strategy: {selected_strategy}")
|
||||
fig = create_strategy_chart(symbol, timeframe, selected_strategy, days_back=days_back)
|
||||
logger.debug(f"Chart callback: Created strategy chart for {symbol} ({timeframe}) with strategy: {selected_strategy}, days_back: {days_back}")
|
||||
else:
|
||||
# Create chart with dynamically selected indicators
|
||||
fig = create_chart_with_indicators(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
overlay_indicators=overlay_indicators or [],
|
||||
subplot_indicators=subplot_indicators or [],
|
||||
days_back=7
|
||||
days_back=days_back
|
||||
)
|
||||
|
||||
indicator_count = len(overlay_indicators or []) + len(subplot_indicators or [])
|
||||
logger.debug(f"Chart callback: Created dynamic chart for {symbol} ({timeframe}) with {indicator_count} indicators")
|
||||
logger.debug(f"Chart callback: Created dynamic chart for {symbol} ({timeframe}) with {indicator_count} indicators, days_back: {days_back}")
|
||||
|
||||
return fig
|
||||
if relayout_data and 'xaxis.range' in relayout_data:
|
||||
fig.update_layout(
|
||||
xaxis=dict(range=relayout_data['xaxis.range']),
|
||||
yaxis=dict(range=relayout_data.get('yaxis.range'))
|
||||
)
|
||||
logger.debug("Chart callback: Preserved chart zoom/pan state")
|
||||
|
||||
return fig, status_message
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating price chart: {e}")
|
||||
return create_error_chart(f"Error loading chart: {str(e)}")
|
||||
error_fig = create_error_chart(f"Error loading chart: {str(e)}")
|
||||
error_status = f"❌ Error: {str(e)}"
|
||||
return error_fig, error_status
|
||||
|
||||
# Strategy selection callback - automatically load strategy indicators
|
||||
@app.callback(
|
||||
@ -97,28 +249,48 @@ def register_chart_callbacks(app):
|
||||
Output('market-stats', 'children'),
|
||||
[Input('symbol-dropdown', 'value'),
|
||||
Input('timeframe-dropdown', 'value'),
|
||||
Input('time-range-quick-select', 'value'),
|
||||
Input('custom-date-range', 'start_date'),
|
||||
Input('custom-date-range', 'end_date'),
|
||||
Input('analysis-mode-toggle', 'value'),
|
||||
Input('interval-component', 'n_intervals')]
|
||||
)
|
||||
def update_market_stats(symbol, timeframe, n_intervals):
|
||||
def update_market_stats(symbol, timeframe, time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals):
|
||||
"""Update comprehensive market statistics with analysis."""
|
||||
try:
|
||||
triggered_id = ctx.triggered_id
|
||||
logger.debug(f"update_market_stats triggered by: {triggered_id}, analysis_mode: {analysis_mode}")
|
||||
|
||||
if analysis_mode == 'locked' and triggered_id == 'interval-component':
|
||||
logger.info("Stats: Analysis mode is locked and triggered by interval; skipping stats update.")
|
||||
return no_update
|
||||
|
||||
# Calculate time range for analysis
|
||||
days_back, time_status = calculate_time_range(
|
||||
time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals
|
||||
)
|
||||
|
||||
# Import analysis classes
|
||||
from dashboard.components.data_analysis import VolumeAnalyzer, PriceMovementAnalyzer
|
||||
|
||||
# Get basic market statistics
|
||||
basic_stats = get_market_statistics(symbol, timeframe)
|
||||
# Get basic market statistics for the selected time range
|
||||
basic_stats = get_market_statistics(symbol, timeframe, days_back=days_back)
|
||||
|
||||
# Create analyzers for comprehensive analysis
|
||||
volume_analyzer = VolumeAnalyzer()
|
||||
price_analyzer = PriceMovementAnalyzer()
|
||||
|
||||
# Get analysis for 7 days
|
||||
volume_analysis = volume_analyzer.get_volume_statistics(symbol, timeframe, 7)
|
||||
price_analysis = price_analyzer.get_price_movement_statistics(symbol, timeframe, 7)
|
||||
# Get analysis for the selected time range
|
||||
volume_analysis = volume_analyzer.get_volume_statistics(symbol, timeframe, days_back)
|
||||
price_analysis = price_analyzer.get_price_movement_statistics(symbol, timeframe, days_back)
|
||||
|
||||
# Create enhanced statistics layout
|
||||
return html.Div([
|
||||
html.H3("📊 Enhanced Market Statistics"),
|
||||
html.P(
|
||||
f"{time_status}",
|
||||
style={'font-weight': 'bold', 'margin-bottom': '15px', 'color': '#4A4A4A', 'text-align': 'center', 'font-size': '1.1em'}
|
||||
),
|
||||
|
||||
# Basic Market Data
|
||||
html.Div([
|
||||
@ -135,18 +307,18 @@ def register_chart_callbacks(app):
|
||||
], style={'border': '1px solid #bdc3c7', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#f8f9fa'}),
|
||||
|
||||
# Volume Analysis Section
|
||||
create_volume_analysis_section(volume_analysis),
|
||||
create_volume_analysis_section(volume_analysis, days_back),
|
||||
|
||||
# Price Movement Analysis Section
|
||||
create_price_movement_section(price_analysis),
|
||||
create_price_movement_section(price_analysis, days_back),
|
||||
|
||||
# Additional Market Insights
|
||||
html.Div([
|
||||
html.H4("🔍 Market Insights", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.Div([
|
||||
html.P(f"📈 Analysis Period: 7 days | Timeframe: {timeframe}", style={'margin': '5px 0'}),
|
||||
html.P(f"📈 Analysis Period: {days_back} days | Timeframe: {timeframe}", style={'margin': '5px 0'}),
|
||||
html.P(f"🎯 Symbol: {symbol}", style={'margin': '5px 0'}),
|
||||
html.P("💡 Statistics update automatically with chart changes", style={'margin': '5px 0', 'font-style': 'italic'})
|
||||
html.P("💡 Statistics are calculated for the selected time range.", style={'margin': '5px 0', 'font-style': 'italic', 'font-size': '14px'})
|
||||
])
|
||||
], style={'border': '1px solid #3498db', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#ebf3fd'})
|
||||
])
|
||||
@ -159,16 +331,16 @@ def register_chart_callbacks(app):
|
||||
])
|
||||
|
||||
|
||||
def create_volume_analysis_section(volume_stats):
|
||||
def create_volume_analysis_section(volume_stats, days_back=7):
|
||||
"""Create volume analysis section for market statistics."""
|
||||
if not volume_stats or volume_stats.get('total_volume', 0) == 0:
|
||||
return html.Div([
|
||||
html.H4("📊 Volume Analysis", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.H4(f"📊 Volume Analysis ({days_back} days)", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.P("No volume data available for analysis", style={'color': '#e74c3c'})
|
||||
], style={'border': '1px solid #e74c3c', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#fdeded'})
|
||||
|
||||
return html.Div([
|
||||
html.H4("📊 Volume Analysis (7 days)", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.H4(f"📊 Volume Analysis ({days_back} days)", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Strong("Total Volume: "),
|
||||
@ -193,16 +365,16 @@ def create_volume_analysis_section(volume_stats):
|
||||
], style={'border': '1px solid #27ae60', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#eafaf1'})
|
||||
|
||||
|
||||
def create_price_movement_section(price_stats):
|
||||
def create_price_movement_section(price_stats, days_back=7):
|
||||
"""Create price movement analysis section for market statistics."""
|
||||
if not price_stats or price_stats.get('total_returns') is None:
|
||||
return html.Div([
|
||||
html.H4("📈 Price Movement Analysis", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.H4(f"📈 Price Movement Analysis ({days_back} days)", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.P("No price movement data available for analysis", style={'color': '#e74c3c'})
|
||||
], style={'border': '1px solid #e74c3c', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#fdeded'})
|
||||
|
||||
return html.Div([
|
||||
html.H4("📈 Price Movement Analysis (7 days)", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.H4(f"📈 Price Movement Analysis ({days_back} days)", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Strong("Total Return: "),
|
||||
@ -231,6 +403,24 @@ def create_price_movement_section(price_stats):
|
||||
)
|
||||
], style={'margin': '5px 0'})
|
||||
])
|
||||
], style={'border': '1px solid #3498db', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#ebf3fd'})
|
||||
], style={'border': '1px solid #3498db', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#ebf3fd'})
|
||||
|
||||
# Clear date range button callback
|
||||
@app.callback(
|
||||
[Output('custom-date-range', 'start_date'),
|
||||
Output('custom-date-range', 'end_date'),
|
||||
Output('time-range-quick-select', 'value')],
|
||||
[Input('clear-date-range-btn', 'n_clicks')],
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def clear_custom_date_range(n_clicks):
|
||||
"""Clear the custom date range and reset dropdown to force update."""
|
||||
if n_clicks and n_clicks > 0:
|
||||
logger.debug("Clear button clicked: Clearing custom dates and setting dropdown to 7d.")
|
||||
return None, None, '7d' # Clear dates AND set dropdown to default '7d'
|
||||
# Should not happen with prevent_initial_call=True and n_clicks > 0 check, but as a fallback:
|
||||
return ctx.no_update, ctx.no_update, ctx.no_update
|
||||
|
||||
|
||||
|
||||
logger.info("Chart callback: Chart callbacks registered successfully")
|
||||
@ -101,4 +101,84 @@ def create_auto_update_control():
|
||||
style={'margin-bottom': '10px'}
|
||||
),
|
||||
html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'})
|
||||
])
|
||||
])
|
||||
|
||||
|
||||
def create_time_range_controls():
|
||||
"""Create the time range control panel."""
|
||||
return html.Div([
|
||||
html.H5("⏰ Time Range Controls", style={'color': '#2c3e50', 'margin-bottom': '15px'}),
|
||||
|
||||
# Quick Select Dropdown
|
||||
html.Div([
|
||||
html.Label("Quick Select:", style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}),
|
||||
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'}
|
||||
],
|
||||
value='7d',
|
||||
placeholder="Select time range",
|
||||
style={'margin-bottom': '15px'}
|
||||
)
|
||||
]),
|
||||
|
||||
# Custom Date Range Picker
|
||||
html.Div([
|
||||
html.Label("Custom Date Range:", style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}),
|
||||
html.Div([
|
||||
dcc.DatePickerRange(
|
||||
id='custom-date-range',
|
||||
display_format='YYYY-MM-DD',
|
||||
style={'display': 'inline-block', 'margin-right': '10px'}
|
||||
),
|
||||
html.Button(
|
||||
"Clear",
|
||||
id="clear-date-range-btn",
|
||||
className="btn btn-sm btn-outline-secondary",
|
||||
style={
|
||||
'display': 'inline-block',
|
||||
'vertical-align': 'top',
|
||||
'margin-top': '7px',
|
||||
'padding': '5px 10px',
|
||||
'font-size': '12px'
|
||||
}
|
||||
)
|
||||
], style={'margin-bottom': '15px'})
|
||||
]),
|
||||
|
||||
# Analysis Mode Toggle
|
||||
html.Div([
|
||||
html.Label("Analysis Mode:", style={'font-weight': 'bold', 'margin-bottom': '5px', 'display': 'block'}),
|
||||
dcc.RadioItems(
|
||||
id='analysis-mode-toggle',
|
||||
options=[
|
||||
{'label': '🔴 Real-time Updates', 'value': 'realtime'},
|
||||
{'label': '🔒 Analysis Mode (Locked)', 'value': 'locked'}
|
||||
],
|
||||
value='realtime',
|
||||
inline=True,
|
||||
style={'margin-bottom': '10px'}
|
||||
)
|
||||
]),
|
||||
|
||||
# Time Range Status
|
||||
html.Div(id='time-range-status',
|
||||
style={'font-size': '12px', 'color': '#7f8c8d', 'font-style': 'italic'})
|
||||
|
||||
], style={
|
||||
'border': '1px solid #bdc3c7',
|
||||
'border-radius': '8px',
|
||||
'padding': '15px',
|
||||
'background-color': '#f0f8ff',
|
||||
'margin-bottom': '20px'
|
||||
})
|
||||
@ -10,7 +10,7 @@ from components.charts.indicator_manager import get_indicator_manager
|
||||
from components.charts.indicator_defaults import ensure_default_indicators
|
||||
from dashboard.components.chart_controls import (
|
||||
create_chart_config_panel,
|
||||
create_auto_update_control
|
||||
create_time_range_controls
|
||||
)
|
||||
|
||||
logger = get_logger("default_logger")
|
||||
@ -79,7 +79,7 @@ def get_market_data_layout():
|
||||
|
||||
# Create components using the new modular functions
|
||||
chart_config_panel = create_chart_config_panel(strategy_options, overlay_options, subplot_options)
|
||||
auto_update_control = create_auto_update_control()
|
||||
time_range_controls = create_time_range_controls()
|
||||
|
||||
return html.Div([
|
||||
# Title and basic controls
|
||||
@ -112,8 +112,8 @@ def get_market_data_layout():
|
||||
# Chart Configuration Panel
|
||||
chart_config_panel,
|
||||
|
||||
# Auto-update control
|
||||
auto_update_control,
|
||||
# Time Range Controls (positioned under indicators, next to chart)
|
||||
time_range_controls,
|
||||
|
||||
# Chart
|
||||
dcc.Graph(id='price-chart'),
|
||||
|
||||
157
tasks/chart-improvements-immediate.md
Normal file
157
tasks/chart-improvements-immediate.md
Normal file
@ -0,0 +1,157 @@
|
||||
# Chart Improvements - Immediate Tasks
|
||||
|
||||
## Overview
|
||||
This document outlines immediate improvements for chart functionality, time range selection, and performance optimization to address current issues with page refreshing and chart state preservation.
|
||||
|
||||
## Current Issues Identified
|
||||
- Frequent page refreshing due to debug mode hot-reload (every 2-3 minutes)
|
||||
- Chart zoom/pan state resets when callbacks trigger
|
||||
- No time range control for historical data analysis
|
||||
- Statistics reset when changing parameters
|
||||
- No way to "lock" time range for analysis without real-time updates
|
||||
|
||||
## Immediate Tasks (Priority Order)
|
||||
|
||||
- [x] **Task 1: Fix Page Refresh Issues** (Priority: HIGH - 5 minutes)
|
||||
- [x] 1.1 Choose debug mode option (Option A: debug=False OR Option B: debug=True, use_reloader=False)
|
||||
- [x] 1.2 Update app_new.py with selected debug settings
|
||||
- [x] 1.3 Test app stability (no frequent restarts)
|
||||
|
||||
- [x] **Task 2: Add Time Range Selector** (Priority: HIGH - 45 minutes) ✅ COMPLETED + ENHANCED
|
||||
- [x] 2.1 Create time range control components
|
||||
- [x] 2.1.1 Add quick select dropdown (1h, 4h, 6h, 12h, 1d, 3d, 7d, 30d, real-time)
|
||||
- [x] 2.1.2 Add custom date picker component
|
||||
- [x] 2.1.3 Add analysis mode toggle (real-time vs locked)
|
||||
- [x] 2.2 Update dashboard layout with time range controls
|
||||
- [x] 2.3 Modify chart callbacks to handle time range inputs
|
||||
- [x] 2.4 Test time range functionality
|
||||
- [x] 2.5 **ENHANCEMENT**: Fixed sub-day time period precision (1h, 4h working correctly)
|
||||
- [x] 2.6 **ENHANCEMENT**: Added 6h and 12h options per user request
|
||||
- [x] 2.7 **ENHANCEMENT**: Fixed custom date range and dropdown interaction logic with Clear button and explicit "Custom Range" dropdown option.
|
||||
|
||||
- [ ] **Task 3: Prevent Chart State Reset** (Priority: MEDIUM - 45 minutes)
|
||||
- [x] 3.1 Add relayoutData state preservation to chart callbacks (Completed as part of Task 2)
|
||||
- [x] 3.2 Implement smart partial updates using Patch() (Initial implementation for basic charts completed)
|
||||
- [x] 3.3 Preserve zoom/pan during data updates (Completed as part of Task 2 & 3.1)
|
||||
- [x] 3.4 Test chart state preservation (Visual testing by user indicates OK)
|
||||
- [x] 3.5 Refine Patching: More robust trace identification (New sub-task) (Completed)
|
||||
|
||||
- [x] **Task 4: Enhanced Statistics Integration** (Priority: MEDIUM - 30 minutes)
|
||||
- [x] 4.1 Make statistics respect selected time range
|
||||
- [x] 4.2 Add time range context to statistics display
|
||||
- [x] 4.3 Implement real-time vs historical analysis modes
|
||||
- [x] 4.4 Test statistics integration with time controls
|
||||
|
||||
- [ ] **Task 5: Advanced Chart Controls** (Priority: LOW - Future)
|
||||
- [ ] 5.1 Chart annotation tools
|
||||
- [ ] 5.2 Export functionality (PNG, SVG, data)
|
||||
- [-] 3.6 Refine Patching: Optimize data fetching for patches (fetch only new data) (New sub-task)
|
||||
- [-] 3.7 Refine Patching: Enable for simple overlay indicators (New sub-task)
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Immediate Fixes (Day 1)
|
||||
1. **Fix refresh issues** (5 minutes)
|
||||
2. **Add basic time range dropdown** (30 minutes)
|
||||
3. **Test and validate** (15 minutes)
|
||||
|
||||
### Phase 2: Enhanced Time Controls (Day 1-2)
|
||||
1. **Add date picker component** (30 minutes)
|
||||
2. **Implement analysis mode toggle** (30 minutes)
|
||||
3. **Integrate with statistics** (30 minutes)
|
||||
|
||||
### Phase 3: Chart State Preservation (Day 2)
|
||||
1. **Implement zoom/pan preservation** (45 minutes)
|
||||
2. **Add smart partial updates** (30 minutes)
|
||||
3. **Testing and optimization** (30 minutes)
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Time Range Selector UI
|
||||
```python
|
||||
# Quick Select Dropdown
|
||||
dcc.Dropdown(
|
||||
id='time-range-quick-select',
|
||||
options=[
|
||||
{'label': '🕐 Last 1 Hour', 'value': '1h'},
|
||||
{'label': '🕐 Last 4 Hours', 'value': '4h'},
|
||||
{'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': '🔴 Real-time', 'value': 'realtime'}
|
||||
],
|
||||
value='7d'
|
||||
)
|
||||
|
||||
# Custom Date Range Picker
|
||||
dcc.DatePickerRange(
|
||||
id='custom-date-range',
|
||||
display_format='YYYY-MM-DD',
|
||||
style={'margin': '10px 0'}
|
||||
)
|
||||
|
||||
# Analysis Mode Toggle
|
||||
dcc.RadioItems(
|
||||
id='analysis-mode-toggle',
|
||||
options=[
|
||||
{'label': '🔴 Real-time Updates', 'value': 'realtime'},
|
||||
{'label': '🔒 Analysis Mode (Locked)', 'value': 'locked'}
|
||||
],
|
||||
value='realtime',
|
||||
inline=True
|
||||
)
|
||||
```
|
||||
|
||||
### Enhanced Callback Structure
|
||||
```python
|
||||
@app.callback(
|
||||
[Output('price-chart', 'figure'),
|
||||
Output('market-stats', 'children')],
|
||||
[Input('symbol-dropdown', 'value'),
|
||||
Input('timeframe-dropdown', 'value'),
|
||||
Input('time-range-quick-select', 'value'),
|
||||
Input('custom-date-range', 'start_date'),
|
||||
Input('custom-date-range', 'end_date'),
|
||||
Input('analysis-mode-toggle', 'value'),
|
||||
Input('interval-component', 'n_intervals')],
|
||||
[State('price-chart', 'relayoutData')],
|
||||
prevent_initial_call=False
|
||||
)
|
||||
def update_chart_and_stats_with_time_control(...):
|
||||
# Smart update logic with state preservation
|
||||
# Conditional real-time updates based on analysis mode
|
||||
# Time range validation and data fetching
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
- ✅ No more frequent page refreshes (app runs stable)
|
||||
- ✅ Chart zoom/pan preserved during updates
|
||||
- ✅ Time range selection works for both quick select and custom dates
|
||||
- ✅ Analysis mode prevents unwanted real-time resets
|
||||
- ✅ Statistics update correctly for selected time ranges
|
||||
- ✅ Smooth user experience without interruptions
|
||||
|
||||
## Files to Modify
|
||||
- `app_new.py` - Debug mode settings
|
||||
- `dashboard/layouts/market_data.py` - Add time range UI
|
||||
- `dashboard/callbacks/charts.py` - Enhanced callbacks with state preservation
|
||||
- `dashboard/components/chart_controls.py` - New time range control components
|
||||
- `components/charts/__init__.py` - Enhanced data fetching with time ranges
|
||||
|
||||
## Testing Checklist
|
||||
- [ ] App runs without frequent refreshes
|
||||
- [ ] Quick time range selection works
|
||||
- [ ] Custom date picker functions correctly
|
||||
- [ ] Analysis mode prevents real-time updates
|
||||
- [ ] Chart zoom/pan preserved during data updates
|
||||
- [ ] Statistics reflect selected time range
|
||||
- [ ] Symbol changes work with custom time ranges
|
||||
- [ ] Timeframe changes work with custom time ranges
|
||||
- [ ] Real-time mode resumes correctly after analysis mode
|
||||
|
||||
## Notes
|
||||
- Prioritize stability and user experience over advanced features
|
||||
- Keep implementation simple and focused on immediate user needs
|
||||
- Consider performance impact of frequent data queries
|
||||
- Ensure backward compatibility with existing functionality
|
||||
@ -84,10 +84,11 @@
|
||||
- [x] 3.3 Implement real-time OHLCV price charts with Plotly (candlestick charts)
|
||||
- [x] 3.4 Add technical indicators overlay on price charts (SMA, EMA, RSI, MACD)
|
||||
- [x] 3.5 Create market data monitoring dashboard (real-time data feed status)
|
||||
- [ ] 3.6 Build simple data analysis tools (volume analysis, price movement statistics)
|
||||
- [ ] 3.7 Setup real-time dashboard updates using Redis callbacks
|
||||
- [ ] 3.8 Add data export functionality for analysis (CSV/JSON export)
|
||||
- [ ] 3.9 Unit test basic dashboard components and data visualization
|
||||
- [x] 3.6 Build simple data analysis tools (volume analysis, price movement statistics)
|
||||
- [x] 3.7 Add the chart time range selector and trigger for realtime data or historical data (when i analyze specified time range i do not want it to reset with realtime data triggers and callbacks)
|
||||
- [ ] 3.8 Setup real-time dashboard updates using Redis callbacks
|
||||
- [ ] 3.9 Add data export functionality for analysis (CSV/JSON export)
|
||||
- [ ] 3.10 Unit test basic dashboard components and data visualization
|
||||
|
||||
- [ ] 4.0 Strategy Engine and Bot Management Framework
|
||||
- [ ] 4.1 Design and implement base strategy interface class
|
||||
@ -188,6 +189,23 @@
|
||||
- [ ] 13.9 Add gap detection and automatic data recovery during reconnections
|
||||
- [ ] 13.10 Implement data integrity validation and conflict resolution for recovered data
|
||||
|
||||
- [ ] 14.0 Advanced Dashboard Performance and User Experience (Future Enhancement)
|
||||
- [ ] 14.1 Implement dashboard state management with browser localStorage persistence
|
||||
- [ ] 14.2 Add client-side chart caching to reduce server load and improve responsiveness
|
||||
- [ ] 14.3 Implement lazy loading for dashboard components and data-heavy sections
|
||||
- [ ] 14.4 Add WebSocket connections for real-time dashboard updates instead of polling
|
||||
- [ ] 14.5 Implement dashboard layout customization (draggable panels, custom arrangements)
|
||||
- [ ] 14.6 Add multi-threading for callback processing to prevent UI blocking
|
||||
- [ ] 14.7 Implement progressive data loading (load recent data first, historical on demand)
|
||||
- [ ] 14.8 Add dashboard performance monitoring and bottleneck identification
|
||||
- [ ] 14.9 Implement chart virtualization for handling large datasets efficiently
|
||||
- [ ] 14.10 Add offline mode capabilities with local data caching
|
||||
- [ ] 14.11 Implement smart callback debouncing to reduce unnecessary updates
|
||||
- [ ] 14.12 Add dashboard preloading and background data prefetching
|
||||
- [ ] 14.13 Implement memory usage optimization for long-running dashboard sessions
|
||||
- [ ] 14.14 Add chart export capabilities (PNG, SVG, PDF) with high-quality rendering
|
||||
- [ ] 14.15 Implement dashboard mobile responsiveness and touch optimizations
|
||||
|
||||
|
||||
|
||||
### Notes
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user