diff --git a/NN/models/saved/checkpoint_metadata.json b/NN/models/saved/checkpoint_metadata.json index 3cb26aa..9d5b504 100644 --- a/NN/models/saved/checkpoint_metadata.json +++ b/NN/models/saved/checkpoint_metadata.json @@ -122,5 +122,67 @@ "wandb_run_id": null, "wandb_artifact_name": null } + ], + "extrema_trainer": [ + { + "checkpoint_id": "extrema_trainer_20250624_221645", + "model_name": "extrema_trainer", + "model_type": "extrema_trainer", + "file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_221645.pt", + "created_at": "2025-06-24T22:16:45.728299", + "file_size_mb": 0.0013427734375, + "performance_score": 0.1, + "accuracy": 0.0, + "loss": null, + "val_accuracy": null, + "val_loss": null, + "reward": null, + "pnl": null, + "epoch": null, + "training_time_hours": null, + "total_parameters": null, + "wandb_run_id": null, + "wandb_artifact_name": null + }, + { + "checkpoint_id": "extrema_trainer_20250624_221915", + "model_name": "extrema_trainer", + "model_type": "extrema_trainer", + "file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_221915.pt", + "created_at": "2025-06-24T22:19:15.325368", + "file_size_mb": 0.0013427734375, + "performance_score": 0.1, + "accuracy": 0.0, + "loss": null, + "val_accuracy": null, + "val_loss": null, + "reward": null, + "pnl": null, + "epoch": null, + "training_time_hours": null, + "total_parameters": null, + "wandb_run_id": null, + "wandb_artifact_name": null + }, + { + "checkpoint_id": "extrema_trainer_20250624_222303", + "model_name": "extrema_trainer", + "model_type": "extrema_trainer", + "file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_222303.pt", + "created_at": "2025-06-24T22:23:03.283194", + "file_size_mb": 0.0013427734375, + "performance_score": 0.1, + "accuracy": 0.0, + "loss": null, + "val_accuracy": null, + "val_loss": null, + "reward": null, + "pnl": null, + "epoch": null, + "training_time_hours": null, + "total_parameters": null, + "wandb_run_id": null, + "wandb_artifact_name": null + } ] } \ No newline at end of file diff --git a/_dev/notes.md b/_dev/notes.md index 30f5853..d3c91d1 100644 --- a/_dev/notes.md +++ b/_dev/notes.md @@ -16,6 +16,14 @@ do we load the best model for each model type? or we do a cold start each time? we stopped showing executed trades on the chart. let's add them back . update chart every second as well. +the list with closed trades is not updated. clear session button does not clear all data. + +add buttons for quick manual buy/sell (max 1 lot. sell closes long, buy closes short if already open position exists) + + + + + >> Training diff --git a/add_current_trade.py b/add_current_trade.py new file mode 100644 index 0000000..1ce0eb9 --- /dev/null +++ b/add_current_trade.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import json +from datetime import datetime +import time + +def add_current_trade(): + """Add a trade with current timestamp for immediate visibility""" + now = datetime.now() + + # Create a trade that just happened + current_trade = { + 'trade_id': 999, + 'symbol': 'ETHUSDT', + 'side': 'LONG', + 'entry_time': (now - timedelta(seconds=30)).isoformat(), # 30 seconds ago + 'exit_time': now.isoformat(), # Just now + 'entry_price': 2434.50, + 'exit_price': 2434.70, + 'size': 0.001, + 'fees': 0.05, + 'net_pnl': 0.15, # Small profit + 'mexc_executed': True, + 'duration_seconds': 30, + 'leverage': 50.0, + 'gross_pnl': 0.20, + 'fee_type': 'TAKER', + 'fee_rate': 0.0005 + } + + # Load existing trades + try: + with open('closed_trades_history.json', 'r') as f: + trades = json.load(f) + except: + trades = [] + + # Add the current trade + trades.append(current_trade) + + # Save back + with open('closed_trades_history.json', 'w') as f: + json.dump(trades, f, indent=2) + + print(f"āœ… Added current trade: LONG @ {current_trade['entry_time']} -> {current_trade['exit_time']}") + print(f" Entry: ${current_trade['entry_price']} | Exit: ${current_trade['exit_price']} | P&L: ${current_trade['net_pnl']}") + +if __name__ == "__main__": + from datetime import timedelta + add_current_trade() \ No newline at end of file diff --git a/test_manual_trading.py b/test_manual_trading.py new file mode 100644 index 0000000..433c15a --- /dev/null +++ b/test_manual_trading.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +""" +Test script for manual trading buttons functionality +""" + +import requests +import json +import time +from datetime import datetime + +def test_manual_trading(): + """Test the manual trading buttons functionality""" + print("Testing manual trading buttons...") + + # Check if dashboard is running + try: + response = requests.get("http://127.0.0.1:8050", timeout=5) + if response.status_code == 200: + print("āœ… Dashboard is running on port 8050") + else: + print(f"āŒ Dashboard returned status code: {response.status_code}") + return + except Exception as e: + print(f"āŒ Dashboard not accessible: {e}") + return + + # Check if trades file exists + try: + with open('closed_trades_history.json', 'r') as f: + trades = json.load(f) + print(f"šŸ“Š Current trades in history: {len(trades)}") + if trades: + latest_trade = trades[-1] + print(f" Latest trade: {latest_trade.get('side')} at ${latest_trade.get('exit_price', latest_trade.get('entry_price'))}") + except FileNotFoundError: + print("šŸ“Š No trades history file found (this is normal for fresh start)") + except Exception as e: + print(f"āŒ Error reading trades file: {e}") + + print("\nšŸŽÆ Manual Trading Test Instructions:") + print("1. Open dashboard at http://127.0.0.1:8050") + print("2. Look for the 'MANUAL BUY' and 'MANUAL SELL' buttons") + print("3. Click 'MANUAL BUY' to create a test long position") + print("4. Wait a few seconds, then click 'MANUAL SELL' to close and create short") + print("5. Check the chart for green triangles showing trade entry/exit points") + print("6. Check the 'Closed Trades' table for trade records") + + print("\nšŸ“ˆ Expected Results:") + print("- Green triangles should appear on the price chart at trade execution times") + print("- Dashed lines should connect entry and exit points") + print("- Trade records should appear in the closed trades table") + print("- Session P&L should update with trade profits/losses") + + print("\nšŸ” Monitoring trades file...") + initial_count = 0 + try: + with open('closed_trades_history.json', 'r') as f: + initial_count = len(json.load(f)) + except: + pass + + print(f"Initial trade count: {initial_count}") + print("Watching for new trades... (Press Ctrl+C to stop)") + + try: + while True: + time.sleep(2) + try: + with open('closed_trades_history.json', 'r') as f: + current_trades = json.load(f) + current_count = len(current_trades) + + if current_count > initial_count: + new_trades = current_trades[initial_count:] + for trade in new_trades: + print(f"šŸ†• NEW TRADE: {trade.get('side')} | Entry: ${trade.get('entry_price'):.2f} | Exit: ${trade.get('exit_price'):.2f} | P&L: ${trade.get('net_pnl'):.2f}") + initial_count = current_count + + except FileNotFoundError: + pass + except Exception as e: + print(f"Error monitoring trades: {e}") + + except KeyboardInterrupt: + print("\nāœ… Test monitoring stopped") + +if __name__ == "__main__": + test_manual_trading() \ No newline at end of file diff --git a/web/dashboard.py b/web/dashboard.py index 1300a8b..5169216 100644 --- a/web/dashboard.py +++ b/web/dashboard.py @@ -309,7 +309,9 @@ class TradingDashboard: self.closed_trades = [] # List of all closed trades with full details # Load existing closed trades from file + logger.info("DASHBOARD: Loading closed trades from file...") self._load_closed_trades_from_file() + logger.info(f"DASHBOARD: Loaded {len(self.closed_trades)} closed trades") # Signal execution settings for scalping - REMOVED FREQUENCY LIMITS self.min_confidence_threshold = 0.30 # Start lower to allow learning @@ -840,6 +842,7 @@ class TradingDashboard: ], className="card-body text-center p-2") ], className="card bg-light", style={"height": "60px"}), ], style={"display": "grid", "gridTemplateColumns": "repeat(4, 1fr)", "gap": "8px", "width": "60%"}), + # Right side - Merged: Recent Signals & Model Training - 2 columns html.Div([ @@ -869,13 +872,28 @@ class TradingDashboard: # Charts row - Now full width since training moved up html.Div([ - # Price chart - Full width + # Price chart - Full width with manual trading buttons html.Div([ html.Div([ - html.H6([ - html.I(className="fas fa-chart-candlestick me-2"), - "Live 1s Price & Volume Chart (WebSocket Stream)" - ], className="card-title mb-2"), + # Chart header with manual trading buttons + html.Div([ + html.H6([ + html.I(className="fas fa-chart-candlestick me-2"), + "Live 1s Price & Volume Chart (WebSocket Stream)" + ], className="card-title mb-0"), + html.Div([ + html.Button([ + html.I(className="fas fa-arrow-up me-1"), + "BUY" + ], id="manual-buy-btn", className="btn btn-success btn-sm me-2", + style={"fontSize": "10px", "padding": "2px 8px"}), + html.Button([ + html.I(className="fas fa-arrow-down me-1"), + "SELL" + ], id="manual-sell-btn", className="btn btn-danger btn-sm", + style={"fontSize": "10px", "padding": "2px 8px"}) + ], className="d-flex") + ], className="d-flex justify-content-between align-items-center mb-2"), dcc.Graph(id="price-chart", style={"height": "400px"}) ], className="card-body p-2") ], className="card", style={"width": "100%"}), @@ -1172,25 +1190,30 @@ class TradingDashboard: ] position_class = "fw-bold mb-0 small" else: - position_text = "No Position" - position_class = "text-muted mb-0 small" + # Show HOLD when no position is open + from dash import html + position_text = [ + html.Span("[HOLD] ", className="text-warning fw-bold"), + html.Span("No Position - Waiting for Signal", className="text-muted") + ] + position_class = "fw-bold mb-0 small" # MEXC status (simple) mexc_status = "LIVE" if (self.trading_executor and self.trading_executor.trading_enabled and not self.trading_executor.simulation_mode) else "SIM" - # CHART OPTIMIZATION - Real-time chart updates every 1 second + # OPTIMIZED CHART - Using new optimized version with trade caching if is_chart_update: try: if hasattr(self, '_cached_chart_data_time'): cache_time = self._cached_chart_data_time - if time.time() - cache_time < 5: # Use cached chart if < 5s old for faster updates + if time.time() - cache_time < 3: # Use cached chart if < 3s old for faster updates price_chart = getattr(self, '_cached_price_chart', None) else: - price_chart = self._create_price_chart_optimized(symbol, current_price) + price_chart = self._create_price_chart_optimized_v2(symbol) self._cached_price_chart = price_chart self._cached_chart_data_time = time.time() else: - price_chart = self._create_price_chart_optimized(symbol, current_price) + price_chart = self._create_price_chart_optimized_v2(symbol) self._cached_price_chart = price_chart self._cached_chart_data_time = time.time() except Exception as e: @@ -1382,6 +1405,83 @@ class TradingDashboard: except Exception as e: logger.error(f"Error updating leverage: {e}") return f"{self.leverage_multiplier:.0f}x", "Error", "badge bg-secondary" + + # Manual Buy button callback + @self.app.callback( + Output('recent-decisions', 'children', allow_duplicate=True), + [Input('manual-buy-btn', 'n_clicks')], + prevent_initial_call=True + ) + def manual_buy(n_clicks): + """Execute manual buy order""" + if n_clicks and n_clicks > 0: + try: + symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT" + current_price = self.get_realtime_price(symbol) or 2434.0 + + # Create manual trading decision + manual_decision = { + 'action': 'BUY', + 'symbol': symbol, + 'price': current_price, + 'size': 0.001, # Small test size (max 1 lot) + 'confidence': 1.0, # Manual trades have 100% confidence + 'timestamp': datetime.now(), + 'source': 'MANUAL_BUY', + 'mexc_executed': False, # Mark as manual/test trade + 'usd_size': current_price * 0.001 + } + + # Process the trading decision + self._process_trading_decision(manual_decision) + + logger.info(f"MANUAL: BUY executed at ${current_price:.2f}") + return dash.no_update + + except Exception as e: + logger.error(f"Error executing manual buy: {e}") + return dash.no_update + + return dash.no_update + + # Manual Sell button callback + @self.app.callback( + Output('recent-decisions', 'children', allow_duplicate=True), + [Input('manual-sell-btn', 'n_clicks')], + prevent_initial_call=True + ) + def manual_sell(n_clicks): + """Execute manual sell order""" + if n_clicks and n_clicks > 0: + try: + symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT" + current_price = self.get_realtime_price(symbol) or 2434.0 + + # Create manual trading decision + manual_decision = { + 'action': 'SELL', + 'symbol': symbol, + 'price': current_price, + 'size': 0.001, # Small test size (max 1 lot) + 'confidence': 1.0, # Manual trades have 100% confidence + 'timestamp': datetime.now(), + 'source': 'MANUAL_SELL', + 'mexc_executed': False, # Mark as manual/test trade + 'usd_size': current_price * 0.001 + } + + # Process the trading decision + self._process_trading_decision(manual_decision) + + logger.info(f"MANUAL: SELL executed at ${current_price:.2f}") + return dash.no_update + + except Exception as e: + logger.error(f"Error executing manual sell: {e}") + return dash.no_update + + return dash.no_update + def _simulate_price_update(self, symbol: str, base_price: float) -> float: """ @@ -1439,19 +1539,18 @@ class TradingDashboard: """Create price chart with volume and Williams pivot points from cached data""" try: # For Williams Market Structure, we need 1s data for proper recursive analysis - # Get 5 minutes (300 seconds) of 1s data for accurate pivot calculation + # Get 4 hours (240 minutes) of 1m data for better trade visibility df_1s = None df_1m = None - # Try to get 1s data first for Williams analysis + # Try to get 1s data first for Williams analysis (reduced to 10 minutes for performance) try: - df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=300, refresh=False) + df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=600, refresh=False) if df_1s is None or df_1s.empty: logger.warning("[CHART] No 1s cached data available, trying fresh 1s data") df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=300, refresh=True) if df_1s is not None and not df_1s.empty: - logger.debug(f"[CHART] Using {len(df_1s)} 1s bars for Williams analysis") # Aggregate 1s data to 1m for chart display (cleaner visualization) df = self._aggregate_1s_to_1m(df_1s) actual_timeframe = '1s→1m' @@ -1461,14 +1560,14 @@ class TradingDashboard: logger.warning(f"[CHART] Error getting 1s data: {e}") df_1s = None - # Fallback to 1m data if 1s not available + # Fallback to 1m data if 1s not available (4 hours for historical trades) if df_1s is None: - df = self.data_provider.get_historical_data(symbol, '1m', limit=30, refresh=False) + df = self.data_provider.get_historical_data(symbol, '1m', limit=240, refresh=False) if df is None or df.empty: logger.warning("[CHART] No cached 1m data available, trying fresh 1m data") try: - df = self.data_provider.get_historical_data(symbol, '1m', limit=30, refresh=True) + df = self.data_provider.get_historical_data(symbol, '1m', limit=240, refresh=True) if df is not None and not df.empty: # Ensure timezone consistency for fresh data df = self._ensure_timezone_consistency(df) @@ -1491,7 +1590,6 @@ class TradingDashboard: # Ensure timezone consistency for cached data df = self._ensure_timezone_consistency(df) actual_timeframe = '1m' - logger.debug(f"[CHART] Using {len(df)} 1m bars from cached data in {self.timezone}") # Final check: ensure we have valid data with proper index if df is None or df.empty: @@ -1542,9 +1640,7 @@ class TradingDashboard: pivot_points = self._get_williams_pivot_points_for_chart(williams_data, chart_df=df) if pivot_points: self._add_williams_pivot_points_to_chart(fig, pivot_points, row=1) - logger.info(f"[CHART] Added Williams pivot points using {actual_timeframe} data") - else: - logger.debug("[CHART] No Williams pivot points calculated") + logger.debug(f"[CHART] Added Williams pivot points using {actual_timeframe} data") except Exception as e: logger.debug(f"Error adding Williams pivot points to chart: {e}") @@ -1632,7 +1728,7 @@ class TradingDashboard: elif decision['action'] == 'SELL': sell_decisions.append((decision, signal_type)) - logger.debug(f"[CHART] Showing {len(buy_decisions)} BUY and {len(sell_decisions)} SELL signals in chart timeframe") + # Add BUY markers with different styles for executed vs ignored executed_buys = [d[0] for d in buy_decisions if d[1] == 'EXECUTED'] @@ -1766,7 +1862,9 @@ class TradingDashboard: if (chart_start_utc <= entry_time_pd <= chart_end_utc) or (chart_start_utc <= exit_time_pd <= chart_end_utc): chart_trades.append(trade) - logger.debug(f"[CHART] Showing {len(chart_trades)} closed trades on chart") + # Minimal logging - only show count + if len(chart_trades) > 0: + logger.debug(f"[CHART] Showing {len(chart_trades)} trades on chart") # Plot closed trades with profit/loss styling profitable_entries_x = [] @@ -2926,9 +3024,12 @@ class TradingDashboard: import json from pathlib import Path + logger.info("LOAD_TRADES: Checking for closed_trades_history.json...") if Path('closed_trades_history.json').exists(): + logger.info("LOAD_TRADES: File exists, loading...") with open('closed_trades_history.json', 'r') as f: trades_data = json.load(f) + logger.info(f"LOAD_TRADES: Raw data loaded: {len(trades_data)} trades") # Convert string dates back to datetime objects for trade in trades_data: @@ -6035,6 +6136,188 @@ class TradingDashboard: except Exception as e: logger.debug(f"Signal processing error: {e}") + def _create_price_chart_optimized_v2(self, symbol: str) -> go.Figure: + """OPTIMIZED: Create price chart with cached trade filtering and minimal logging""" + try: + chart_start = time.time() + + # STEP 1: Get chart data with minimal API calls + df = None + actual_timeframe = '1m' + + # Try cached 1m data first (fastest) + df = self.data_provider.get_historical_data(symbol, '1m', limit=120, refresh=False) + if df is None or df.empty: + # Fallback to fresh data only if needed + df = self.data_provider.get_historical_data(symbol, '1m', limit=120, refresh=True) + if df is None or df.empty: + return self._create_empty_chart(f"{symbol} Chart", "No data available") + + # STEP 2: Ensure proper timezone (cached result) + if not hasattr(self, '_tz_cache_time') or time.time() - self._tz_cache_time > 300: # 5min cache + df = self._ensure_timezone_consistency(df) + self._tz_cache_time = time.time() + + # STEP 3: Create base chart quickly + fig = make_subplots( + rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1, + subplot_titles=(f'{symbol} Price ({actual_timeframe.upper()})', 'Volume'), + row_heights=[0.7, 0.3] + ) + + # STEP 4: Add price line (main trace) + fig.add_trace( + go.Scatter( + x=df.index, y=df['close'], mode='lines', name=f"{symbol} Price", + line=dict(color='#00ff88', width=2), + hovertemplate='$%{y:.2f}
%{x}' + ), row=1, col=1 + ) + + # STEP 5: Add volume (if available) + if 'volume' in df.columns: + fig.add_trace( + go.Bar(x=df.index, y=df['volume'], name='Volume', + marker_color='rgba(158, 158, 158, 0.6)'), row=2, col=1 + ) + + # STEP 6: OPTIMIZED TRADE VISUALIZATION - with caching + if self.closed_trades: + # Cache trade filtering results for 30 seconds + cache_key = f"trades_{len(self.closed_trades)}_{df.index.min()}_{df.index.max()}" + if (not hasattr(self, '_trade_cache') or + self._trade_cache.get('key') != cache_key or + time.time() - self._trade_cache.get('time', 0) > 30): + + # Filter trades to chart timeframe (expensive operation) + chart_start_utc = df.index.min().tz_localize(None) if df.index.min().tz else df.index.min() + chart_end_utc = df.index.max().tz_localize(None) if df.index.max().tz else df.index.max() + + chart_trades = [] + for trade in self.closed_trades: + if not isinstance(trade, dict): + continue + + entry_time = trade.get('entry_time') + exit_time = trade.get('exit_time') + if not entry_time or not exit_time: + continue + + # Quick timezone conversion + try: + if isinstance(entry_time, datetime): + entry_utc = entry_time.replace(tzinfo=None) if not entry_time.tzinfo else entry_time.astimezone(timezone.utc).replace(tzinfo=None) + else: + continue + + if isinstance(exit_time, datetime): + exit_utc = exit_time.replace(tzinfo=None) if not exit_time.tzinfo else exit_time.astimezone(timezone.utc).replace(tzinfo=None) + else: + continue + + # Check if trade overlaps with chart + entry_pd = pd.to_datetime(entry_utc) + exit_pd = pd.to_datetime(exit_utc) + + if (chart_start_utc <= entry_pd <= chart_end_utc) or (chart_start_utc <= exit_pd <= chart_end_utc): + chart_trades.append(trade) + except: + continue # Skip problematic trades + + # Cache the result + self._trade_cache = { + 'key': cache_key, + 'time': time.time(), + 'trades': chart_trades + } + else: + # Use cached trades + chart_trades = self._trade_cache['trades'] + + # STEP 7: Render trade markers (optimized) + if chart_trades: + profitable_entries_x, profitable_entries_y = [], [] + profitable_exits_x, profitable_exits_y = [], [] + + for trade in chart_trades: + entry_price = trade.get('entry_price', 0) + exit_price = trade.get('exit_price', 0) + entry_time = trade.get('entry_time') + exit_time = trade.get('exit_time') + net_pnl = trade.get('net_pnl', 0) + + if not all([entry_price, exit_price, entry_time, exit_time]): + continue + + # Convert to local time for display + entry_local = self._to_local_timezone(entry_time) + exit_local = self._to_local_timezone(exit_time) + + # Only show profitable trades as filled markers (cleaner UI) + if net_pnl > 0: + profitable_entries_x.append(entry_local) + profitable_entries_y.append(entry_price) + profitable_exits_x.append(exit_local) + profitable_exits_y.append(exit_price) + + # Add connecting line for all trades + line_color = '#00ff88' if net_pnl > 0 else '#ff6b6b' + fig.add_trace( + go.Scatter( + x=[entry_local, exit_local], y=[entry_price, exit_price], + mode='lines', line=dict(color=line_color, width=2, dash='dash'), + name="Trade", showlegend=False, hoverinfo='skip' + ), row=1, col=1 + ) + + # Add profitable trade markers + if profitable_entries_x: + fig.add_trace( + go.Scatter( + x=profitable_entries_x, y=profitable_entries_y, mode='markers', + marker=dict(color='#00ff88', size=12, symbol='triangle-up', + line=dict(color='white', width=1)), + name="Profitable Entry", showlegend=True, + hovertemplate="ENTRY
$%{y:.2f}
%{x}" + ), row=1, col=1 + ) + + if profitable_exits_x: + fig.add_trace( + go.Scatter( + x=profitable_exits_x, y=profitable_exits_y, mode='markers', + marker=dict(color='#00ff88', size=12, symbol='triangle-down', + line=dict(color='white', width=1)), + name="Profitable Exit", showlegend=True, + hovertemplate="EXIT
$%{y:.2f}
%{x}" + ), row=1, col=1 + ) + + # STEP 8: Update layout efficiently + latest_price = df['close'].iloc[-1] if not df.empty else 0 + current_time = datetime.now().strftime("%H:%M:%S") + + fig.update_layout( + title=f"{symbol} | ${latest_price:.2f} | {current_time}", + template="plotly_dark", height=400, xaxis_rangeslider_visible=False, + margin=dict(l=20, r=20, t=50, b=20), + legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) + ) + + fig.update_yaxes(title_text="Price ($)", row=1, col=1) + fig.update_yaxes(title_text="Volume", row=2, col=1) + + # Performance logging (minimal) + chart_time = (time.time() - chart_start) * 1000 + if chart_time > 200: # Only log slow charts + logger.warning(f"[CHART] Slow chart render: {chart_time:.0f}ms") + + return fig + + except Exception as e: + logger.error(f"Optimized chart error: {e}") + return self._create_empty_chart(f"{symbol} Chart", f"Chart Error: {str(e)}") + def _create_price_chart_optimized(self, symbol, current_price): """Optimized chart creation with minimal data fetching""" try: