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:
Vasily.onl 2025-06-05 12:54:41 +08:00
parent 132710a9a7
commit 87843a1d35
7 changed files with 521 additions and 55 deletions

View File

@ -32,8 +32,8 @@ def main():
logger.info("Dashboard application initialized successfully") logger.info("Dashboard application initialized successfully")
# Run the app (updated for newer Dash version) # Run the app (debug=False for stability, manual restart required for changes)
app.run(debug=True, host='0.0.0.0', port=8050) app.run(debug=False, host='0.0.0.0', port=8050)
except Exception as e: except Exception as e:
logger.error(f"Failed to start dashboard application: {e}") logger.error(f"Failed to start dashboard application: {e}")

View File

@ -260,33 +260,50 @@ def get_supported_timeframes():
return ['5s', '1m', '15m', '1h'] # Fallback return ['5s', '1m', '15m', '1h'] # Fallback
def get_market_statistics(symbol: str, timeframe: str = "1h"): def get_market_statistics(symbol: str, timeframe: str = "1h", days_back: int = 1):
"""Calculate market statistics from recent data.""" """Calculate market statistics from recent data over a specified period."""
builder = ChartBuilder() 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: 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 import pandas as pd
df = pd.DataFrame(candles) df = pd.DataFrame(candles)
latest = df.iloc[-1] latest = df.iloc[-1]
current_price = float(latest['close']) current_price = float(latest['close'])
# Calculate 24h change # Calculate change over the period
if len(df) > 1: if len(df) > 1:
price_24h_ago = float(df.iloc[0]['open']) price_period_ago = float(df.iloc[0]['open'])
change_percent = ((current_price - price_24h_ago) / price_24h_ago) * 100 change_percent = ((current_price - price_period_ago) / price_period_ago) * 100
else: else:
change_percent = 0 change_percent = 0
from .utils import format_price, format_volume 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 { return {
'Price': format_price(current_price, decimals=2), 'Price': format_price(current_price, decimals=2),
'24h Change': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%", f'Change ({period_label})': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%",
'24h Volume': format_volume(df['volume'].sum()), f'Volume ({period_label})': format_volume(df['volume'].sum()),
'High 24h': format_price(df['high'].max(), decimals=2), f'High ({period_label})': format_price(df['high'].max(), decimals=2),
'Low 24h': format_price(df['low'].min(), decimals=2) f'Low ({period_label})': format_price(df['low'].min(), decimals=2)
} }
def check_data_availability(symbol: str, timeframe: str): def check_data_availability(symbol: str, timeframe: str):
@ -473,3 +490,7 @@ def create_chart_with_indicators(symbol: str, timeframe: str,
return builder.create_chart_with_indicators( return builder.create_chart_with_indicators(
symbol, timeframe, overlay_indicators, subplot_indicators, days_back, **kwargs symbol, timeframe, overlay_indicators, subplot_indicators, days_back, **kwargs
) )
def initialize_indicator_manager():
# Implementation of initialize_indicator_manager function
pass

View File

@ -2,8 +2,8 @@
Chart-related callbacks for the dashboard. Chart-related callbacks for the dashboard.
""" """
from dash import Output, Input from dash import Output, Input, State, Patch, ctx, html, no_update
from datetime import datetime from datetime import datetime, timedelta
from utils.logger import get_logger from utils.logger import get_logger
from components.charts import ( from components.charts import (
create_strategy_chart, create_strategy_chart,
@ -13,48 +13,200 @@ from components.charts import (
) )
from components.charts.config import get_all_example_strategies from components.charts.config import get_all_example_strategies
from database.connection import DatabaseManager 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") 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): def register_chart_callbacks(app):
"""Register chart-related callbacks.""" """Register chart-related callbacks."""
@app.callback( @app.callback(
Output('price-chart', 'figure'), [Output('price-chart', 'figure'),
Output('time-range-status', 'children')],
[Input('symbol-dropdown', 'value'), [Input('symbol-dropdown', 'value'),
Input('timeframe-dropdown', 'value'), Input('timeframe-dropdown', 'value'),
Input('overlay-indicators-checklist', 'value'), Input('overlay-indicators-checklist', 'value'),
Input('subplot-indicators-checklist', 'value'), Input('subplot-indicators-checklist', 'value'),
Input('strategy-dropdown', '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.""" """Update the price chart with latest market data and selected indicators."""
try: 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': if selected_strategy and selected_strategy != 'basic':
fig = create_strategy_chart(symbol, timeframe, 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}") logger.debug(f"Chart callback: Created strategy chart for {symbol} ({timeframe}) with strategy: {selected_strategy}, days_back: {days_back}")
else: else:
# Create chart with dynamically selected indicators
fig = create_chart_with_indicators( fig = create_chart_with_indicators(
symbol=symbol, symbol=symbol,
timeframe=timeframe, timeframe=timeframe,
overlay_indicators=overlay_indicators or [], overlay_indicators=overlay_indicators or [],
subplot_indicators=subplot_indicators or [], subplot_indicators=subplot_indicators or [],
days_back=7 days_back=days_back
) )
indicator_count = len(overlay_indicators or []) + len(subplot_indicators or []) 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: except Exception as e:
logger.error(f"Error updating price chart: {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 # Strategy selection callback - automatically load strategy indicators
@app.callback( @app.callback(
@ -97,28 +249,48 @@ def register_chart_callbacks(app):
Output('market-stats', 'children'), Output('market-stats', 'children'),
[Input('symbol-dropdown', 'value'), [Input('symbol-dropdown', 'value'),
Input('timeframe-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')] 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.""" """Update comprehensive market statistics with analysis."""
try: 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 # Import analysis classes
from dashboard.components.data_analysis import VolumeAnalyzer, PriceMovementAnalyzer from dashboard.components.data_analysis import VolumeAnalyzer, PriceMovementAnalyzer
# Get basic market statistics # Get basic market statistics for the selected time range
basic_stats = get_market_statistics(symbol, timeframe) basic_stats = get_market_statistics(symbol, timeframe, days_back=days_back)
# Create analyzers for comprehensive analysis # Create analyzers for comprehensive analysis
volume_analyzer = VolumeAnalyzer() volume_analyzer = VolumeAnalyzer()
price_analyzer = PriceMovementAnalyzer() price_analyzer = PriceMovementAnalyzer()
# Get analysis for 7 days # Get analysis for the selected time range
volume_analysis = volume_analyzer.get_volume_statistics(symbol, timeframe, 7) volume_analysis = volume_analyzer.get_volume_statistics(symbol, timeframe, days_back)
price_analysis = price_analyzer.get_price_movement_statistics(symbol, timeframe, 7) price_analysis = price_analyzer.get_price_movement_statistics(symbol, timeframe, days_back)
# Create enhanced statistics layout # Create enhanced statistics layout
return html.Div([ return html.Div([
html.H3("📊 Enhanced Market Statistics"), 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 # Basic Market Data
html.Div([ 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'}), ], style={'border': '1px solid #bdc3c7', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#f8f9fa'}),
# Volume Analysis Section # Volume Analysis Section
create_volume_analysis_section(volume_analysis), create_volume_analysis_section(volume_analysis, days_back),
# Price Movement Analysis Section # Price Movement Analysis Section
create_price_movement_section(price_analysis), create_price_movement_section(price_analysis, days_back),
# Additional Market Insights # Additional Market Insights
html.Div([ html.Div([
html.H4("🔍 Market Insights", style={'color': '#2c3e50', 'margin-bottom': '10px'}), html.H4("🔍 Market Insights", style={'color': '#2c3e50', 'margin-bottom': '10px'}),
html.Div([ 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(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'}) ], 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.""" """Create volume analysis section for market statistics."""
if not volume_stats or volume_stats.get('total_volume', 0) == 0: if not volume_stats or volume_stats.get('total_volume', 0) == 0:
return html.Div([ 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'}) 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'}) ], style={'border': '1px solid #e74c3c', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#fdeded'})
return html.Div([ 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.Div([ html.Div([
html.Strong("Total Volume: "), 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'}) ], 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.""" """Create price movement analysis section for market statistics."""
if not price_stats or price_stats.get('total_returns') is None: if not price_stats or price_stats.get('total_returns') is None:
return html.Div([ 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'}) 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'}) ], style={'border': '1px solid #e74c3c', 'padding': '15px', 'margin': '10px 0', 'border-radius': '5px', 'background-color': '#fdeded'})
return html.Div([ 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.Div([ html.Div([
html.Strong("Total Return: "), html.Strong("Total Return: "),
@ -231,6 +403,24 @@ def create_price_movement_section(price_stats):
) )
], style={'margin': '5px 0'}) ], 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") logger.info("Chart callback: Chart callbacks registered successfully")

View File

@ -102,3 +102,83 @@ def create_auto_update_control():
), ),
html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'}) 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'
})

View File

@ -10,7 +10,7 @@ from components.charts.indicator_manager import get_indicator_manager
from components.charts.indicator_defaults import ensure_default_indicators from components.charts.indicator_defaults import ensure_default_indicators
from dashboard.components.chart_controls import ( from dashboard.components.chart_controls import (
create_chart_config_panel, create_chart_config_panel,
create_auto_update_control create_time_range_controls
) )
logger = get_logger("default_logger") logger = get_logger("default_logger")
@ -79,7 +79,7 @@ def get_market_data_layout():
# Create components using the new modular functions # Create components using the new modular functions
chart_config_panel = create_chart_config_panel(strategy_options, overlay_options, subplot_options) 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([ return html.Div([
# Title and basic controls # Title and basic controls
@ -112,8 +112,8 @@ def get_market_data_layout():
# Chart Configuration Panel # Chart Configuration Panel
chart_config_panel, chart_config_panel,
# Auto-update control # Time Range Controls (positioned under indicators, next to chart)
auto_update_control, time_range_controls,
# Chart # Chart
dcc.Graph(id='price-chart'), dcc.Graph(id='price-chart'),

View 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

View File

@ -84,10 +84,11 @@
- [x] 3.3 Implement real-time OHLCV price charts with Plotly (candlestick charts) - [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.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) - [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) - [x] 3.6 Build simple data analysis tools (volume analysis, price movement statistics)
- [ ] 3.7 Setup real-time dashboard updates using Redis callbacks - [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 Add data export functionality for analysis (CSV/JSON export) - [ ] 3.8 Setup real-time dashboard updates using Redis callbacks
- [ ] 3.9 Unit test basic dashboard components and data visualization - [ ] 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.0 Strategy Engine and Bot Management Framework
- [ ] 4.1 Design and implement base strategy interface class - [ ] 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.9 Add gap detection and automatic data recovery during reconnections
- [ ] 13.10 Implement data integrity validation and conflict resolution for recovered data - [ ] 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 ### Notes