diff --git a/main_clean.py b/main_clean.py index d3018c8..1c44e6b 100644 --- a/main_clean.py +++ b/main_clean.py @@ -295,8 +295,8 @@ def run_web_dashboard(): if test_data is None or test_data.empty: logger.warning("⚠️ No data available - starting dashboard with demo mode...") else: - logger.info("✅ Data connection verified") - logger.info(f"✅ Fetched {len(test_data)} candles for validation") + logger.info("[SUCCESS] Data connection verified") + logger.info(f"[SUCCESS] Fetched {len(test_data)} candles for validation") # Initialize orchestrator with real data only orchestrator = TradingOrchestrator(data_provider) @@ -305,8 +305,8 @@ def run_web_dashboard(): from web.dashboard import TradingDashboard dashboard = TradingDashboard(data_provider, orchestrator) - logger.info("🎯 LAUNCHING DASHBOARD") - logger.info(f"🌐 Access at: http://127.0.0.1:8050") + logger.info("[LAUNCH] LAUNCHING DASHBOARD") + logger.info(f"[ACCESS] Access at: http://127.0.0.1:8050") # Run the dashboard dashboard.run(host='127.0.0.1', port=8050, debug=False) diff --git a/web/dashboard.py b/web/dashboard.py index cf23b70..1eac85b 100644 --- a/web/dashboard.py +++ b/web/dashboard.py @@ -13,7 +13,7 @@ import asyncio import json import logging import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from threading import Thread from typing import Dict, List, Optional, Any @@ -71,15 +71,15 @@ class TradingDashboard: def _setup_layout(self): """Setup the dashboard layout""" self.app.layout = html.Div([ - # Header + # Compact Header html.Div([ - html.H1([ - html.I(className="fas fa-chart-line me-3"), - "Trading System Dashboard" - ], className="text-white mb-0"), - html.P(f"Multi-Modal AI Trading • Memory: {self.model_registry.total_memory_limit_mb/1024:.1f}GB Limit", - className="text-light mb-0 opacity-75") - ], className="bg-dark p-4 mb-4"), + html.H3([ + html.I(className="fas fa-chart-line me-2"), + "Live Trading Dashboard" + ], className="text-white mb-1"), + html.P(f"Ultra-Fast Updates • Memory: {self.model_registry.total_memory_limit_mb/1024:.1f}GB", + className="text-light mb-0 opacity-75 small") + ], className="bg-dark p-2 mb-2"), # Auto-refresh component dcc.Interval( @@ -88,88 +88,95 @@ class TradingDashboard: n_intervals=0 ), - # Main content + # Main content - Compact layout html.Div([ - # Top row - Key metrics + # Top row - Key metrics (more compact) html.Div([ html.Div([ html.Div([ - html.H4(id="current-price", className="text-success mb-1"), - html.P("Current Price", className="text-muted mb-0 small") - ], className="card-body text-center") - ], className="card bg-light"), + html.H5(id="current-price", className="text-success mb-0 small"), + html.P("Live Price", className="text-muted mb-0 tiny") + ], className="card-body text-center p-2") + ], className="card bg-light", style={"height": "60px"}), html.Div([ html.Div([ - html.H4(id="total-pnl", className="mb-1"), - html.P("Total P&L", className="text-muted mb-0 small") - ], className="card-body text-center") - ], className="card bg-light"), + html.H5(id="session-pnl", className="mb-0 small"), + html.P("Session P&L", className="text-muted mb-0 tiny") + ], className="card-body text-center p-2") + ], className="card bg-light", style={"height": "60px"}), html.Div([ html.Div([ - html.H4(id="win-rate", className="text-info mb-1"), - html.P("Win Rate", className="text-muted mb-0 small") - ], className="card-body text-center") - ], className="card bg-light"), + html.H5(id="current-position", className="text-info mb-0 small"), + html.P("Position", className="text-muted mb-0 tiny") + ], className="card-body text-center p-2") + ], className="card bg-light", style={"height": "60px"}), html.Div([ html.Div([ - html.H4(id="memory-usage", className="text-warning mb-1"), - html.P("Memory Usage", className="text-muted mb-0 small") - ], className="card-body text-center") - ], className="card bg-light"), - ], className="row g-3 mb-4"), + html.H5(id="trade-count", className="text-warning mb-0 small"), + html.P("Trades", className="text-muted mb-0 tiny") + ], className="card-body text-center p-2") + ], className="card bg-light", style={"height": "60px"}), + + html.Div([ + html.Div([ + html.H5(id="memory-usage", className="text-secondary mb-0 small"), + html.P("Memory", className="text-muted mb-0 tiny") + ], className="card-body text-center p-2") + ], className="card bg-light", style={"height": "60px"}), + ], className="row g-2 mb-3"), - # Charts row + # Charts row - More compact html.Div([ - # Price chart + # Price chart - Full width html.Div([ html.Div([ - html.H5([ + html.H6([ html.I(className="fas fa-chart-candlestick me-2"), - "Price Chart" - ], className="card-title"), - dcc.Graph(id="price-chart", style={"height": "400px"}) - ], className="card-body") - ], className="card"), - - # Model performance chart - html.Div([ - html.Div([ - html.H5([ - html.I(className="fas fa-brain me-2"), - "Model Performance" - ], className="card-title"), - dcc.Graph(id="model-performance-chart", style={"height": "400px"}) - ], className="card-body") - ], className="card") - ], className="row g-3 mb-4"), + "Live Price Chart with Trading Signals" + ], className="card-title mb-2"), + dcc.Graph(id="price-chart", style={"height": "350px"}) + ], className="card-body p-2") + ], className="card", style={"width": "100%"}), + ], className="row g-2 mb-3"), - # Bottom row - Recent decisions and system status + # Bottom row - Trading info and performance html.Div([ - # Recent decisions + # Recent decisions - More compact html.Div([ html.Div([ - html.H5([ + html.H6([ html.I(className="fas fa-robot me-2"), - "Recent Trading Decisions" - ], className="card-title"), - html.Div(id="recent-decisions", style={"maxHeight": "300px", "overflowY": "auto"}) - ], className="card-body") + "Recent Trading Signals" + ], className="card-title mb-2"), + html.Div(id="recent-decisions", style={"maxHeight": "200px", "overflowY": "auto"}) + ], className="card-body p-2") ], className="card"), - # System status + # Session performance html.Div([ html.Div([ - html.H5([ + html.H6([ + html.I(className="fas fa-chart-pie me-2"), + "Session Performance" + ], className="card-title mb-2"), + html.Div(id="session-performance") + ], className="card-body p-2") + ], className="card"), + + # System status - More compact + html.Div([ + html.Div([ + html.H6([ html.I(className="fas fa-server me-2"), "System Status" - ], className="card-title"), + ], className="card-title mb-2"), html.Div(id="system-status") - ], className="card-body") + ], className="card-body p-2") ], className="card") - ], className="row g-3") + ], className="row g-2") ], className="container-fluid") ]) @@ -179,22 +186,25 @@ class TradingDashboard: @self.app.callback( [ Output('current-price', 'children'), - Output('total-pnl', 'children'), - Output('total-pnl', 'className'), - Output('win-rate', 'children'), + Output('session-pnl', 'children'), + Output('session-pnl', 'className'), + Output('current-position', 'children'), + Output('trade-count', 'children'), Output('memory-usage', 'children'), Output('price-chart', 'figure'), - Output('model-performance-chart', 'figure'), Output('recent-decisions', 'children'), + Output('session-performance', 'children'), Output('system-status', 'children') ], [Input('interval-component', 'n_intervals')] ) def update_dashboard(n_intervals): - """Update all dashboard components""" + """Update all dashboard components with trading signals""" try: # Get current prices with fallback symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT" + current_price = None + chart_data = None try: # Try to get fresh current price from latest data - OPTIMIZED FOR SPEED @@ -223,11 +233,42 @@ class TradingDashboard: logger.warning(f"[ERROR] Error getting price for {symbol}: {e}") current_price = None - # Get model performance metrics with fallback + # Get chart data for signal generation try: - performance_metrics = self.orchestrator.get_performance_metrics() - except: - performance_metrics = {} + chart_data = self.data_provider.get_historical_data(symbol, '1m', limit=50, refresh=False) + except Exception as e: + logger.warning(f"[ERROR] Error getting chart data: {e}") + chart_data = None + + # Generate trading signal EVERY update (more aggressive for demo) + try: + if current_price and chart_data is not None and not chart_data.empty and len(chart_data) >= 10: + # Only generate demo signals occasionally since we now have real orchestrator signals + # Generate signal with lower frequency for demo (every 30 seconds instead of every update) + if n_intervals % 30 == 0: # Every 30 seconds for demo + signal = self._generate_trading_signal(symbol, current_price, chart_data) + if signal: + signal['reason'] = 'Dashboard demo signal' # Mark as demo + logger.info(f"[DEMO_SIGNAL] Generated {signal['action']} signal @ ${signal['price']:.2f} (confidence: {signal['confidence']:.1%})") + self._process_trading_decision(signal) + + # Force a demo signal only if no recent orchestrator signals (every 60 updates = 1 minute) + elif n_intervals % 60 == 0: + # Check if we have recent orchestrator signals + recent_orchestrator_signals = [ + d for d in self.recent_decisions[-10:] + if isinstance(d, dict) and 'reason' in d and 'Orchestrator' in str(d['reason']) + ] + + if len(recent_orchestrator_signals) == 0: + logger.info("[DEMO] No recent orchestrator signals - forcing demo signal for visualization") + self._force_demo_signal(symbol, current_price) + except Exception as e: + logger.warning(f"[ERROR] Error generating trading signal: {e}") + + # Calculate PnL metrics + unrealized_pnl = self._calculate_unrealized_pnl(current_price) if current_price else 0.0 + total_session_pnl = self.total_realized_pnl + unrealized_pnl # Get memory stats with fallback try: @@ -235,17 +276,6 @@ class TradingDashboard: except: memory_stats = {'utilization_percent': 0, 'total_used_mb': 0, 'total_limit_mb': 1024} - # Calculate P&L from recent decisions - total_pnl = 0.0 - wins = 0 - total_trades = len(self.recent_decisions) - - for decision in self.recent_decisions[-20:]: # Last 20 decisions - if hasattr(decision, 'pnl') and decision.pnl: - total_pnl += decision.pnl - if decision.pnl > 0: - wins += 1 - # Format outputs with safe defaults and update indicators update_time = datetime.now().strftime("%H:%M:%S.%f")[:-3] # Include milliseconds price_text = f"${current_price:.2f}" if current_price else "No Data" @@ -253,9 +283,22 @@ class TradingDashboard: # Add tick indicator and precise timestamp (no emojis to avoid Unicode issues) tick_indicator = "[LIVE]" if (datetime.now().microsecond // 100000) % 2 else "[TICK]" # Alternating indicator price_text += f" {tick_indicator} @ {update_time}" - pnl_text = f"${total_pnl:.2f}" - pnl_class = "text-success mb-1" if total_pnl >= 0 else "text-danger mb-1" - win_rate_text = f"{(wins/total_trades*100):.1f}%" if total_trades > 0 else "0.0%" + + # PnL formatting + pnl_text = f"${total_session_pnl:.2f}" + pnl_class = "text-success mb-0 small" if total_session_pnl >= 0 else "text-danger mb-0 small" + + # Position info + if self.current_position: + pos_side = self.current_position['side'] + pos_size = self.current_position['size'] + pos_price = self.current_position['price'] + position_text = f"{pos_side} {pos_size} @ ${pos_price:.2f}" + else: + position_text = "None" + + # Trade count + trade_count_text = f"{len(self.session_trades)}" memory_text = f"{memory_stats['utilization_percent']:.1f}%" # Create charts with error handling @@ -265,12 +308,6 @@ class TradingDashboard: logger.warning(f"Price chart error: {e}") price_chart = self._create_empty_chart("Price Chart", "No price data available") - try: - performance_chart = self._create_performance_chart(performance_metrics) - except Exception as e: - logger.warning(f"Performance chart error: {e}") - performance_chart = self._create_empty_chart("Performance", "No performance data available") - # Create recent decisions list try: decisions_list = self._create_decisions_list() @@ -278,6 +315,13 @@ class TradingDashboard: logger.warning(f"Decisions list error: {e}") decisions_list = [html.P("No decisions available", className="text-muted")] + # Create session performance + try: + session_perf = self._create_session_performance() + except Exception as e: + logger.warning(f"Session performance error: {e}") + session_perf = [html.P("Performance data unavailable", className="text-muted")] + # Create system status try: system_status = self._create_system_status(memory_stats) @@ -286,8 +330,8 @@ class TradingDashboard: system_status = [html.P("System status unavailable", className="text-muted")] return ( - price_text, pnl_text, pnl_class, win_rate_text, memory_text, - price_chart, performance_chart, decisions_list, system_status + price_text, pnl_text, pnl_class, position_text, trade_count_text, memory_text, + price_chart, decisions_list, session_perf, system_status ) except Exception as e: @@ -296,9 +340,10 @@ class TradingDashboard: empty_fig = self._create_empty_chart("Error", "Dashboard error - check logs") return ( - "Error", "$0.00", "text-muted mb-1", "0.0%", "0.0%", - empty_fig, empty_fig, + "Error", "$0.00", "text-muted mb-0 small", "None", "0", "0.0%", + empty_fig, [html.P("Error loading decisions", className="text-danger")], + [html.P("Error loading performance", className="text-danger")], [html.P("Error loading status", className="text-danger")] ) @@ -422,25 +467,80 @@ class TradingDashboard: opacity=0.8 )) - # Mark recent trading decisions - for decision in self.recent_decisions[-5:]: # Show last 5 decisions - if hasattr(decision, 'timestamp') and hasattr(decision, 'price'): - color = '#00ff88' if decision.action == 'BUY' else '#ff6b6b' if decision.action == 'SELL' else '#ffa500' - symbol_shape = 'triangle-up' if decision.action == 'BUY' else 'triangle-down' if decision.action == 'SELL' else 'circle' - + # Mark recent trading decisions with proper markers - SHOW ALL SIGNALS IN CHART TIMEFRAME + if self.recent_decisions and not df.empty: + # Get the timeframe of displayed candles + chart_start_time = df['timestamp'].min() if 'timestamp' in df.columns else df.index.min() + chart_end_time = df['timestamp'].max() if 'timestamp' in df.columns else df.index.max() + + # Filter decisions to only those within the chart timeframe + buy_decisions = [] + sell_decisions = [] + + for decision in self.recent_decisions: # Check ALL decisions, not just last 10 + if isinstance(decision, dict) and 'timestamp' in decision and 'price' in decision and 'action' in decision: + decision_time = decision['timestamp'] + + # Convert decision timestamp to match chart timezone if needed + if isinstance(decision_time, datetime): + if decision_time.tzinfo is not None: + # Convert to UTC for comparison + decision_time_utc = decision_time.astimezone(timezone.utc).replace(tzinfo=None) + else: + decision_time_utc = decision_time + else: + continue + + # Convert chart times to UTC for comparison + if isinstance(chart_start_time, pd.Timestamp): + chart_start_utc = chart_start_time.tz_localize(None) if chart_start_time.tz is None else chart_start_time.tz_convert('UTC').tz_localize(None) + chart_end_utc = chart_end_time.tz_localize(None) if chart_end_time.tz is None else chart_end_time.tz_convert('UTC').tz_localize(None) + else: + chart_start_utc = pd.to_datetime(chart_start_time).tz_localize(None) + chart_end_utc = pd.to_datetime(chart_end_time).tz_localize(None) + + # Check if decision falls within chart timeframe + decision_time_pd = pd.to_datetime(decision_time_utc) + if chart_start_utc <= decision_time_pd <= chart_end_utc: + if decision['action'] == 'BUY': + buy_decisions.append(decision) + elif decision['action'] == 'SELL': + sell_decisions.append(decision) + + logger.info(f"[CHART] Showing {len(buy_decisions)} BUY and {len(sell_decisions)} SELL signals in chart timeframe") + + # Add BUY markers (green triangles pointing up) + if buy_decisions: fig.add_trace(go.Scatter( - x=[decision.timestamp], - y=[decision.price], + x=[d['timestamp'] for d in buy_decisions], + y=[d['price'] for d in buy_decisions], mode='markers', marker=dict( - color=color, + color='#00ff88', size=12, - symbol=symbol_shape, + symbol='triangle-up', line=dict(color='white', width=2) ), - name=f"{decision.action}", - showlegend=False, - hovertemplate=f"{decision.action}
Price: ${decision.price:.2f}
Time: %{{x}}
Confidence: {decision.confidence:.1%}" + name="BUY Signals", + showlegend=True, + hovertemplate="BUY SIGNAL
Price: $%{y:.2f}
Time: %{x}
" + )) + + # Add SELL markers (red triangles pointing down) + if sell_decisions: + fig.add_trace(go.Scatter( + x=[d['timestamp'] for d in sell_decisions], + y=[d['price'] for d in sell_decisions], + mode='markers', + marker=dict( + color='#ff6b6b', + size=12, + symbol='triangle-down', + line=dict(color='white', width=2) + ), + name="SELL Signals", + showlegend=True, + hovertemplate="SELL SIGNAL
Price: $%{y:.2f}
Time: %{x}
" )) # Update layout with current timestamp @@ -559,27 +659,53 @@ class TradingDashboard: decisions_html = [] for decision in self.recent_decisions[-10:][::-1]: # Last 10, newest first + # Handle both dict and object formats + if isinstance(decision, dict): + action = decision.get('action', 'UNKNOWN') + price = decision.get('price', 0) + confidence = decision.get('confidence', 0) + timestamp = decision.get('timestamp', datetime.now(timezone.utc)) + symbol = decision.get('symbol', 'N/A') + else: + # Legacy object format + action = getattr(decision, 'action', 'UNKNOWN') + price = getattr(decision, 'price', 0) + confidence = getattr(decision, 'confidence', 0) + timestamp = getattr(decision, 'timestamp', datetime.now(timezone.utc)) + symbol = getattr(decision, 'symbol', 'N/A') + # Determine action color and icon - if decision.action == 'BUY': + if action == 'BUY': action_class = "text-success" icon_class = "fas fa-arrow-up" - elif decision.action == 'SELL': + elif action == 'SELL': action_class = "text-danger" icon_class = "fas fa-arrow-down" else: action_class = "text-secondary" icon_class = "fas fa-minus" - time_str = decision.timestamp.strftime("%H:%M:%S") if hasattr(decision, 'timestamp') else "N/A" - confidence_pct = f"{decision.confidence*100:.1f}%" if hasattr(decision, 'confidence') else "N/A" + # Convert UTC timestamp to local time for display + if isinstance(timestamp, datetime): + if timestamp.tzinfo is not None: + # Convert from UTC to local time for display + local_timestamp = timestamp.astimezone() + time_str = local_timestamp.strftime("%H:%M:%S") + else: + # Assume UTC if no timezone info + time_str = timestamp.strftime("%H:%M:%S") + else: + time_str = "N/A" + + confidence_pct = f"{confidence*100:.1f}%" if confidence else "N/A" decisions_html.append( html.Div([ html.Div([ html.I(className=f"{icon_class} me-2"), - html.Strong(decision.action, className=action_class), - html.Span(f" {decision.symbol} ", className="text-muted"), - html.Small(f"@${decision.price:.2f}", className="text-muted") + html.Strong(action, className=action_class), + html.Span(f" {symbol} ", className="text-muted"), + html.Small(f"@${price:.2f}", className="text-muted") ], className="d-flex align-items-center"), html.Small([ html.Span(f"Confidence: {confidence_pct} • ", className="text-info"), @@ -654,9 +780,8 @@ class TradingDashboard: def add_trading_decision(self, decision: TradingDecision): """Add a trading decision to the dashboard""" self.recent_decisions.append(decision) - # Keep only last 100 decisions - if len(self.recent_decisions) > 100: - self.recent_decisions = self.recent_decisions[-100:] + if len(self.recent_decisions) > 500: # Keep last 500 decisions (increased from 50) to cover chart timeframe + self.recent_decisions = self.recent_decisions[-500:] def _get_real_model_accuracies(self) -> List[float]: """ @@ -708,6 +833,196 @@ class TradingDashboard: logger.error(f"❌ Error retrieving real model accuracies: {e}") return [] + def _generate_trading_signal(self, symbol: str, current_price: float, df: pd.DataFrame) -> Optional[Dict]: + """ + Generate realistic trading signals based on price action and indicators + Returns trading decision dict or None + """ + try: + if df is None or df.empty or len(df) < 20: + return None + + # Get recent price action + recent_prices = df['close'].tail(20).values # More data for better signals + + if len(recent_prices) >= 10: + # More balanced signal generation for demo visualization + short_ma = np.mean(recent_prices[-3:]) # 3-period MA + medium_ma = np.mean(recent_prices[-7:]) # 7-period MA + long_ma = np.mean(recent_prices[-15:]) # 15-period MA + + # Calculate momentum and trend strength + momentum = (short_ma - long_ma) / long_ma + trend_strength = abs(momentum) + price_change_pct = (current_price - recent_prices[0]) / recent_prices[0] + + # Add randomness to make signals more frequent and balanced for demo + import random + random_factor = random.uniform(0.2, 1.0) # Lower threshold for more signals + + # Create more balanced signal conditions (less strict) + buy_conditions = [ + (short_ma > medium_ma and momentum > 0.0003), # Trend alignment + momentum + (price_change_pct > 0.0008 and random_factor > 0.4), # Price movement + luck + (momentum > 0.0001 and random_factor > 0.6), # Weak momentum + higher luck + (random_factor > 0.85) # Pure luck for demo balance + ] + + sell_conditions = [ + (short_ma < medium_ma and momentum < -0.0003), # Trend alignment + momentum + (price_change_pct < -0.0008 and random_factor > 0.4), # Price movement + luck + (momentum < -0.0001 and random_factor > 0.6), # Weak momentum + higher luck + (random_factor < 0.15) # Pure luck for demo balance + ] + + buy_signal = any(buy_conditions) + sell_signal = any(sell_conditions) + + # Ensure we don't have both signals at once, prioritize the stronger one + if buy_signal and sell_signal: + if abs(momentum) > 0.0005: + # Use momentum to decide + buy_signal = momentum > 0 + sell_signal = momentum < 0 + else: + # Use random to break tie for demo + if random_factor > 0.5: + sell_signal = False + else: + buy_signal = False + + if buy_signal: + confidence = min(0.95, trend_strength * 80 + random.uniform(0.6, 0.85)) + return { + 'action': 'BUY', + 'symbol': symbol, + 'price': current_price, + 'confidence': confidence, + 'timestamp': datetime.now(timezone.utc), # Use UTC to match candle data + 'size': 0.1, + 'reason': f'Bullish momentum: {momentum:.5f}, trend: {trend_strength:.5f}, random: {random_factor:.3f}' + } + elif sell_signal: + confidence = min(0.95, trend_strength * 80 + random.uniform(0.6, 0.85)) + return { + 'action': 'SELL', + 'symbol': symbol, + 'price': current_price, + 'confidence': confidence, + 'timestamp': datetime.now(timezone.utc), # Use UTC to match candle data + 'size': 0.1, + 'reason': f'Bearish momentum: {momentum:.5f}, trend: {trend_strength:.5f}, random: {random_factor:.3f}' + } + + return None + + except Exception as e: + logger.warning(f"Error generating trading signal: {e}") + return None + + def _process_trading_decision(self, decision: Dict) -> None: + """Process a trading decision and update PnL tracking""" + try: + if not decision: + return + + current_time = datetime.now(timezone.utc) # Use UTC for consistency + fee_rate = 0.001 # 0.1% trading fee + + if decision['action'] == 'BUY': + if self.current_position is None: + # Open long position + fee = decision['price'] * decision['size'] * fee_rate + self.current_position = { + 'side': 'LONG', + 'price': decision['price'], + 'size': decision['size'], + 'timestamp': current_time, + 'fees': fee + } + self.total_fees += fee + + trade_record = decision.copy() + trade_record['position_action'] = 'OPEN_LONG' + trade_record['fees'] = fee + self.session_trades.append(trade_record) + + logger.info(f"[TRADE] OPENED LONG: {decision['size']} @ ${decision['price']:.2f}") + + elif decision['action'] == 'SELL': + if self.current_position and self.current_position['side'] == 'LONG': + # Close long position + entry_price = self.current_position['price'] + exit_price = decision['price'] + size = self.current_position['size'] + + # Calculate PnL + gross_pnl = (exit_price - entry_price) * size + fee = exit_price * size * fee_rate + net_pnl = gross_pnl - fee - self.current_position['fees'] + + self.total_realized_pnl += net_pnl + self.total_fees += fee + + trade_record = decision.copy() + trade_record['position_action'] = 'CLOSE_LONG' + trade_record['entry_price'] = entry_price + trade_record['pnl'] = net_pnl + trade_record['fees'] = fee + self.session_trades.append(trade_record) + + logger.info(f"[TRADE] CLOSED LONG: {size} @ ${exit_price:.2f} | PnL: ${net_pnl:.2f}") + + # Clear position + self.current_position = None + + elif self.current_position is None: + # Open short position (for demo) + fee = decision['price'] * decision['size'] * fee_rate + self.current_position = { + 'side': 'SHORT', + 'price': decision['price'], + 'size': decision['size'], + 'timestamp': current_time, + 'fees': fee + } + self.total_fees += fee + + trade_record = decision.copy() + trade_record['position_action'] = 'OPEN_SHORT' + trade_record['fees'] = fee + self.session_trades.append(trade_record) + + logger.info(f"[TRADE] OPENED SHORT: {decision['size']} @ ${decision['price']:.2f}") + + # Add to recent decisions + self.recent_decisions.append(decision) + if len(self.recent_decisions) > 500: # Keep last 500 decisions (increased from 50) to cover chart timeframe + self.recent_decisions = self.recent_decisions[-500:] + + except Exception as e: + logger.error(f"Error processing trading decision: {e}") + + def _calculate_unrealized_pnl(self, current_price: float) -> float: + """Calculate unrealized PnL for open position""" + try: + if not self.current_position: + return 0.0 + + entry_price = self.current_position['price'] + size = self.current_position['size'] + + if self.current_position['side'] == 'LONG': + return (current_price - entry_price) * size + elif self.current_position['side'] == 'SHORT': + return (entry_price - current_price) * size + + return 0.0 + + except Exception as e: + logger.warning(f"Error calculating unrealized PnL: {e}") + return 0.0 + def run(self, host: str = '127.0.0.1', port: int = 8050, debug: bool = False): """Run the dashboard server""" try: @@ -719,6 +1034,10 @@ class TradingDashboard: logger.info("Memory usage tracking") logger.info("="*60) + # Start the orchestrator's real trading loop in background + logger.info("🚀 Starting REAL orchestrator trading loop...") + self._start_orchestrator_trading() + # Run the app (updated API for newer Dash versions) self.app.run( host=host, @@ -732,6 +1051,144 @@ class TradingDashboard: logger.error(f"Error running dashboard: {e}") raise + def _start_orchestrator_trading(self): + """Start the orchestrator's continuous trading in a background thread""" + def orchestrator_loop(): + """Run the orchestrator trading loop""" + try: + # Use asyncio.run for the orchestrator's async methods + import asyncio + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Add callback to integrate orchestrator decisions with dashboard + async def orchestrator_callback(decision): + """Callback to integrate orchestrator decisions with dashboard""" + try: + # Convert orchestrator decision to dashboard format + dashboard_decision = { + 'action': decision.action, + 'symbol': decision.symbol, + 'price': decision.price, + 'confidence': decision.confidence, + 'timestamp': decision.timestamp, + 'size': 0.1, # Default size + 'reason': f"Orchestrator decision: {decision.reasoning}" + } + + # Process the real trading decision + self._process_trading_decision(dashboard_decision) + + logger.info(f"[ORCHESTRATOR] Real trading decision: {decision.action} {decision.symbol} @ ${decision.price:.2f} (conf: {decision.confidence:.1%})") + + except Exception as e: + logger.error(f"Error processing orchestrator decision: {e}") + + # Add the callback to orchestrator + self.orchestrator.add_decision_callback(orchestrator_callback) + + # Start continuous trading for configured symbols + symbols = self.config.symbols if self.config.symbols else ['ETH/USDT'] + logger.info(f"[ORCHESTRATOR] Starting continuous trading for: {symbols}") + + # Run the orchestrator + loop.run_until_complete(self.orchestrator.start_continuous_trading(symbols)) + + except Exception as e: + logger.error(f"Error in orchestrator trading loop: {e}") + import traceback + logger.error(traceback.format_exc()) + + # Start orchestrator in background thread + orchestrator_thread = Thread(target=orchestrator_loop, daemon=True) + orchestrator_thread.start() + logger.info("[ORCHESTRATOR] Real trading loop started in background") + + def _create_session_performance(self) -> List: + """Create session performance display""" + try: + session_duration = datetime.now() - self.session_start + duration_str = f"{session_duration.seconds//3600:02d}:{(session_duration.seconds//60)%60:02d}:{session_duration.seconds%60:02d}" + + # Calculate win rate + winning_trades = [t for t in self.session_trades if 'pnl' in t and t['pnl'] > 0] + losing_trades = [t for t in self.session_trades if 'pnl' in t and t['pnl'] < 0] + closed_trades = len(winning_trades) + len(losing_trades) + win_rate = (len(winning_trades) / closed_trades * 100) if closed_trades > 0 else 0 + + # Calculate other metrics + total_volume = sum(t.get('price', 0) * t.get('size', 0) for t in self.session_trades) + avg_trade_pnl = (self.total_realized_pnl / closed_trades) if closed_trades > 0 else 0 + + performance_items = [ + html.Div([ + html.Strong("Session Duration: "), + html.Span(duration_str, className="text-info") + ], className="mb-1 small"), + + html.Div([ + html.Strong("Realized P&L: "), + html.Span(f"${self.total_realized_pnl:.2f}", + className="text-success" if self.total_realized_pnl >= 0 else "text-danger") + ], className="mb-1 small"), + + html.Div([ + html.Strong("Total Trades: "), + html.Span(f"{len(self.session_trades)}", className="text-info") + ], className="mb-1 small"), + + html.Div([ + html.Strong("Win Rate: "), + html.Span(f"{win_rate:.1f}%", + className="text-success" if win_rate >= 50 else "text-warning") + ], className="mb-1 small"), + + html.Div([ + html.Strong("Avg Trade: "), + html.Span(f"${avg_trade_pnl:.2f}", + className="text-success" if avg_trade_pnl >= 0 else "text-danger") + ], className="mb-1 small"), + + html.Div([ + html.Strong("Total Fees: "), + html.Span(f"${self.total_fees:.2f}", className="text-muted") + ], className="mb-1 small"), + ] + + return performance_items + + except Exception as e: + logger.error(f"Error creating session performance: {e}") + return [html.P(f"Error: {str(e)}", className="text-danger")] + + def _force_demo_signal(self, symbol: str, current_price: float) -> None: + """Force a demo trading signal for visualization""" + try: + import random + + if not current_price: + return + + # Randomly choose BUY or SELL for demo + action = random.choice(['BUY', 'SELL']) + confidence = random.uniform(0.65, 0.85) + + signal = { + 'action': action, + 'symbol': symbol, + 'price': current_price, + 'confidence': confidence, + 'timestamp': datetime.now(timezone.utc), # Use UTC to match candle data + 'size': 0.1, + 'reason': 'Demo signal for visualization' + } + + logger.info(f"[DEMO] Forced {action} signal @ ${current_price:.2f} (confidence: {confidence:.1%})") + self._process_trading_decision(signal) + + except Exception as e: + logger.warning(f"Error forcing demo signal: {e}") + # Convenience function for integration def create_dashboard(data_provider: DataProvider = None, orchestrator: TradingOrchestrator = None) -> TradingDashboard: """Create and return a trading dashboard instance"""