diff --git a/web/clean_dashboard.py b/web/clean_dashboard.py index 3fd7e1e..e903f48 100644 --- a/web/clean_dashboard.py +++ b/web/clean_dashboard.py @@ -397,17 +397,26 @@ class CleanTradingDashboard: @self.app.callback( Output('price-chart', 'figure'), - [Input('interval-component', 'n_intervals')] + [Input('interval-component', 'n_intervals')], + [State('price-chart', 'relayoutData')] ) - def update_price_chart(n): - """Update price chart every second (1000ms interval)""" + def update_price_chart(n, relayout_data): + """Update price chart every second, persisting user zoom/pan""" try: - return self._create_price_chart('ETH/USDT') + fig = self._create_price_chart('ETH/USDT') + + if relayout_data: + if 'xaxis.range[0]' in relayout_data and 'xaxis.range[1]' in relayout_data: + fig.update_xaxes(range=[relayout_data['xaxis.range[0]'], relayout_data['xaxis.range[1]']]) + if 'yaxis.range[0]' in relayout_data and 'yaxis.range[1]' in relayout_data: + fig.update_yaxes(range=[relayout_data['yaxis.range[0]'], relayout_data['yaxis.range[1]']]) + + return fig except Exception as e: logger.error(f"Error updating chart: {e}") return go.Figure().add_annotation(text=f"Chart Error: {str(e)}", - xref="paper", yref="paper", - x=0.5, y=0.5, showarrow=False) + xref="paper", yref="paper", + x=0.5, y=0.5, showarrow=False) @self.app.callback( Output('closed-trades-table', 'children'), @@ -1059,7 +1068,7 @@ class CleanTradingDashboard: mode='markers', marker=dict( symbol='diamond', - size=[15 + p['confidence'] * 20 for p in up_predictions], + size=[2 + p['confidence'] * 12 for p in up_predictions], color=[f'rgba(0, 150, 255, {0.4 + p["confidence"] * 0.6})' for p in up_predictions], line=dict(width=2, color='darkblue') ), @@ -1084,7 +1093,7 @@ class CleanTradingDashboard: mode='markers', marker=dict( symbol='diamond', - size=[15 + p['confidence'] * 20 for p in down_predictions], + size=[2 + p['confidence'] * 12 for p in down_predictions], color=[f'rgba(255, 140, 0, {0.4 + p["confidence"] * 0.6})' for p in down_predictions], line=dict(width=2, color='darkorange') ), @@ -1109,7 +1118,7 @@ class CleanTradingDashboard: mode='markers', marker=dict( symbol='diamond', - size=[12 + p['confidence'] * 15 for p in sideways_predictions], + size=[6 + p['confidence'] * 10 for p in sideways_predictions], color=[f'rgba(128, 128, 128, {0.3 + p["confidence"] * 0.5})' for p in sideways_predictions], line=dict(width=1, color='gray') ), @@ -2674,6 +2683,21 @@ class CleanTradingDashboard: # Sync current position from trading executor first self._sync_position_from_executor(symbol) + # DEBUG: Log current position state before trade + if self.current_position: + logger.info(f"MANUAL TRADE DEBUG: Current position before {action}: " + f"{self.current_position['side']} {self.current_position['size']:.3f} @ ${self.current_position['price']:.2f}") + else: + logger.info(f"MANUAL TRADE DEBUG: No current position before {action}") + + # Log the trading executor's position state + if hasattr(self.trading_executor, 'get_current_position'): + executor_pos = self.trading_executor.get_current_position(symbol) + if executor_pos: + logger.info(f"MANUAL TRADE DEBUG: Executor position: {executor_pos}") + else: + logger.info(f"MANUAL TRADE DEBUG: No position in executor") + # CAPTURE ALL MODEL INPUTS INCLUDING COB DATA FOR RETROSPECTIVE TRAINING try: from core.trade_data_manager import TradeDataManager @@ -2727,7 +2751,10 @@ class CleanTradingDashboard: # Execute through trading executor try: + logger.info(f"MANUAL TRADE DEBUG: Attempting to execute {action} trade via executor...") result = self.trading_executor.execute_trade(symbol, action, 0.01) # Small size for testing + logger.info(f"MANUAL TRADE DEBUG: Execute trade result: {result}") + if result: decision['executed'] = True decision['execution_time'] = datetime.now() # Track execution time @@ -2736,12 +2763,28 @@ class CleanTradingDashboard: # Sync position from trading executor after execution self._sync_position_from_executor(symbol) + # DEBUG: Log position state after trade + if self.current_position: + logger.info(f"MANUAL TRADE DEBUG: Position after {action}: " + f"{self.current_position['side']} {self.current_position['size']:.3f} @ ${self.current_position['price']:.2f}") + else: + logger.info(f"MANUAL TRADE DEBUG: No position after {action} - position was closed") + + # Check trading executor's position after execution + if hasattr(self.trading_executor, 'get_current_position'): + executor_pos_after = self.trading_executor.get_current_position(symbol) + if executor_pos_after: + logger.info(f"MANUAL TRADE DEBUG: Executor position after trade: {executor_pos_after}") + else: + logger.info(f"MANUAL TRADE DEBUG: No position in executor after trade") + # Get trade history from executor for completed trades executor_trades = self.trading_executor.get_trade_history() if hasattr(self.trading_executor, 'get_trade_history') else [] # Only add completed trades to closed_trades (not position opens) if executor_trades: latest_trade = executor_trades[-1] + logger.info(f"MANUAL TRADE DEBUG: Latest trade from executor: {latest_trade}") # Check if this is a completed trade (has exit price/time) if hasattr(latest_trade, 'exit_time') and latest_trade.exit_time: trade_record = { @@ -2864,43 +2907,21 @@ class CleanTradingDashboard: logger.warning(f"Failed to store opening trade as base case: {e}") else: - decision['executed'] = False decision['blocked'] = True - decision['block_reason'] = "Trading executor returned False" - logger.warning(f"Manual {action} failed - executor returned False") - + decision['block_reason'] = "Trading executor failed" + logger.warning(f"BLOCKED manual {action}: executor returned False") except Exception as e: - decision['executed'] = False decision['blocked'] = True decision['block_reason'] = str(e) - logger.error(f"Manual {action} failed with error: {e}") + logger.error(f"Error executing manual {action}: {e}") - # ENHANCED: Add to recent decisions with PRIORITY INSERTION for better persistence + # Add to recent decisions for dashboard display self.recent_decisions.append(decision) - - # CONSERVATIVE: Keep MORE decisions for longer history - extend to 300 decisions - if len(self.recent_decisions) > 300: - # When trimming, PRESERVE MANUAL TRADES at higher priority - manual_decisions = [d for d in self.recent_decisions if self._get_signal_attribute(d, 'manual', False)] - other_decisions = [d for d in self.recent_decisions if not self._get_signal_attribute(d, 'manual', False)] - - # Keep all manual decisions + most recent other decisions - max_other_decisions = 300 - len(manual_decisions) - if max_other_decisions > 0: - trimmed_decisions = manual_decisions + other_decisions[-max_other_decisions:] - else: - # If too many manual decisions, keep most recent ones - trimmed_decisions = manual_decisions[-300:] - - self.recent_decisions = trimmed_decisions - logger.debug(f"Trimmed decisions: kept {len(manual_decisions)} manual + {len(trimmed_decisions) - len(manual_decisions)} other") - - # LOG the manual trade execution with enhanced details - status = "EXECUTED" if decision['executed'] else ("BLOCKED" if decision['blocked'] else "PENDING") - logger.info(f"[MANUAL-{status}] {action} trade at ${current_price:.2f} - Decision stored with enhanced persistence") + if len(self.recent_decisions) > 200: + self.recent_decisions = self.recent_decisions[-200:] except Exception as e: - logger.error(f"Error executing manual {action}: {e}") + logger.error(f"Error in manual trade execution: {e}") # Model input capture moved to core.trade_data_manager.TradeDataManager @@ -3599,6 +3620,7 @@ class CleanTradingDashboard: if hasattr(self.orchestrator, '_on_cob_dashboard_data'): try: self.orchestrator._on_cob_dashboard_data(symbol, history_data) + logger.debug(f"COB data fed to orchestrator for {symbol}") except Exception as e: logger.debug(f"Error feeding COB data to orchestrator: {e}") @@ -3676,7 +3698,7 @@ class CleanTradingDashboard: 'training_steps': len(model.losses), 'last_update': datetime.now().isoformat() }) - + except Exception as e: logger.debug(f"Error updating training progress: {e}") @@ -3801,7 +3823,7 @@ class CleanTradingDashboard: stats[name] = 0.0 return stats - + def _connect_to_orchestrator(self): """Connect to orchestrator for real trading signals""" try: @@ -3820,7 +3842,7 @@ class CleanTradingDashboard: logger.warning("Orchestrator not available or doesn't support callbacks") except Exception as e: logger.error(f"Error initiating orchestrator connection: {e}") - + async def _on_trading_decision(self, decision): """Handle trading decision from orchestrator.""" try: @@ -3839,7 +3861,7 @@ class CleanTradingDashboard: logger.info(f"[ORCHESTRATOR SIGNAL] Received: {action} for {symbol}") except Exception as e: logger.error(f"Error handling trading decision: {e}") - + def _initialize_streaming(self): """Initialize data streaming""" try: @@ -3848,7 +3870,7 @@ class CleanTradingDashboard: logger.info("Data streaming initialized") except Exception as e: logger.error(f"Error initializing streaming: {e}") - + def _start_websocket_streaming(self): """Start WebSocket streaming for real-time data.""" ws_thread = threading.Thread(target=self._ws_worker, daemon=True) @@ -3894,7 +3916,7 @@ class CleanTradingDashboard: except Exception as e: logger.error(f"WebSocket worker error: {e}") self.is_streaming = False - + def _start_data_collection(self): """Start background data collection""" data_thread = threading.Thread(target=self._data_worker, daemon=True) @@ -3935,7 +3957,7 @@ class CleanTradingDashboard: self._start_real_training_system() except Exception as e: logger.error(f"Error starting comprehensive training system: {e}") - + def _start_real_training_system(self): """Start real training system with data collection and actual model training""" try: @@ -3969,7 +3991,7 @@ class CleanTradingDashboard: logger.info("TRAINING: Real training system started successfully") except Exception as e: logger.error(f"Error starting real training system: {e}") - + def _collect_training_data(self) -> List[Dict]: """Collect real market data for training""" try: @@ -4001,7 +4023,7 @@ class CleanTradingDashboard: except Exception as e: logger.error(f"Error collecting training data: {e}") return [] - + def _perform_real_dqn_training(self, market_data: List[Dict]): """Perform actual DQN training with real market experiences""" try: @@ -4044,7 +4066,7 @@ class CleanTradingDashboard: logger.info(f"DQN TRAINING: Added {training_samples} experiences, memory size: {len(agent.memory)}") except Exception as e: logger.error(f"Error in real DQN training: {e}") - + def _perform_real_cnn_training(self, market_data: List[Dict]): """Perform actual CNN training with real price prediction""" try: @@ -4088,7 +4110,7 @@ class CleanTradingDashboard: logger.info(f"CNN TRAINING: Processed {training_samples} price prediction samples") except Exception as e: logger.error(f"Error in real CNN training: {e}") - + def _update_training_progress(self, iteration: int): """Update training progress and metrics""" try: diff --git a/web/component_manager.py b/web/component_manager.py index 71eeff5..7b468b6 100644 --- a/web/component_manager.py +++ b/web/component_manager.py @@ -273,13 +273,13 @@ class DashboardComponentManager: overview_panel = self._create_cob_overview_panel(symbol, stats, cumulative_imbalance_stats) # --- Right Panel: Compact Ladder --- - ladder_panel = self._create_cob_ladder_panel(bids, asks, mid_price) + ladder_panel = self._create_cob_ladder_panel(bids, asks, mid_price, symbol) return dbc.Row([ dbc.Col(overview_panel, width=5, className="pe-1"), dbc.Col(ladder_panel, width=7, className="ps-1") ], className="g-0") # g-0 removes gutters - + except Exception as e: logger.error(f"Error formatting split COB data: {e}") return html.P(f"Error: {str(e)}", className="text-danger small") @@ -347,7 +347,7 @@ class DashboardComponentManager: html.Div(value, className="fw-bold") ], className="text-center") - def _create_cob_ladder_panel(self, bids, asks, mid_price): + def _create_cob_ladder_panel(self, bids, asks, mid_price, symbol=""): """Creates the right panel with the compact COB ladder.""" bucket_size = 10 num_levels = 5 @@ -356,52 +356,77 @@ class DashboardComponentManager: buckets = {} for order in orders: price = order.get('price', 0) - size = order.get('size', 0) + # Handle both old format (size) and new format (total_size) + size = order.get('total_size', order.get('size', 0)) + volume_usd = order.get('total_volume_usd', size * price) if price > 0: bucket_key = round(price / bucket_size) * bucket_size if bucket_key not in buckets: - buckets[bucket_key] = 0 - buckets[bucket_key] += size * price + buckets[bucket_key] = {'usd_volume': 0, 'crypto_volume': 0} + buckets[bucket_key]['usd_volume'] += volume_usd + buckets[bucket_key]['crypto_volume'] += size return buckets bid_buckets = aggregate_buckets(bids) ask_buckets = aggregate_buckets(asks) - all_volumes = list(bid_buckets.values()) + list(ask_buckets.values()) - max_volume = max(all_volumes) if all_volumes else 1 + all_usd_volumes = [b['usd_volume'] for b in bid_buckets.values()] + [a['usd_volume'] for a in ask_buckets.values()] + max_volume = max(all_usd_volumes) if all_usd_volumes else 1 center_bucket = round(mid_price / bucket_size) * bucket_size ask_levels = [center_bucket + i * bucket_size for i in range(1, num_levels + 1)] bid_levels = [center_bucket - i * bucket_size for i in range(num_levels)] - def create_ladder_row(price, volume, max_vol, row_type): - progress = (volume / max_vol) * 100 if max_vol > 0 else 0 + def create_ladder_row(price, bucket_data, max_vol, row_type): + usd_volume = bucket_data.get('usd_volume', 0) + crypto_volume = bucket_data.get('crypto_volume', 0) + + progress = (usd_volume / max_vol) * 100 if max_vol > 0 else 0 color = "danger" if row_type == 'ask' else "success" text_color = "text-danger" if row_type == 'ask' else "text-success" - vol_str = f"${volume/1e3:.0f}K" if volume > 1e3 else f"${volume:,.0f}" + # Format USD volume (no $ symbol) + if usd_volume > 1e6: + usd_str = f"{usd_volume/1e6:.1f}M" + elif usd_volume > 1e3: + usd_str = f"{usd_volume/1e3:.0f}K" + else: + usd_str = f"{usd_volume:,.0f}" + + # Format crypto volume (no unit symbol) + if crypto_volume > 1000: + crypto_str = f"{crypto_volume/1000:.1f}K" + elif crypto_volume > 1: + crypto_str = f"{crypto_volume:.1f}" + else: + crypto_str = f"{crypto_volume:.3f}" return html.Tr([ - html.Td(f"${price:,.2f}", className=f"{text_color} price-level"), + html.Td(f"${price:,.0f}", className=f"{text_color} price-level small"), html.Td( dbc.Progress(value=progress, color=color, className="vh-25 compact-progress"), - className="progress-cell" + className="progress-cell p-0" ), - html.Td(vol_str, className="volume-level text-end") - ], className="compact-ladder-row") + html.Td(usd_str, className="volume-level text-end fw-bold small p-0 pe-1"), + html.Td(crypto_str, className="volume-level text-start small text-muted p-0 ps-1") + ], className="compact-ladder-row p-0") - ask_rows = [create_ladder_row(p, ask_buckets.get(p, 0), max_volume, 'ask') for p in sorted(ask_levels, reverse=True)] - bid_rows = [create_ladder_row(p, bid_buckets.get(p, 0), max_volume, 'bid') for p in sorted(bid_levels, reverse=True)] + def get_bucket_data(buckets, price): + return buckets.get(price, {'usd_volume': 0, 'crypto_volume': 0}) + + ask_rows = [create_ladder_row(p, get_bucket_data(ask_buckets, p), max_volume, 'ask') for p in sorted(ask_levels, reverse=True)] + bid_rows = [create_ladder_row(p, get_bucket_data(bid_buckets, p), max_volume, 'bid') for p in sorted(bid_levels, reverse=True)] mid_row = html.Tr([ - html.Td(f"${mid_price:,.2f}", colSpan=3, className="text-center fw-bold small mid-price-row") + html.Td(f"${mid_price:,.0f}", colSpan=4, className="text-center fw-bold small mid-price-row p-0") ]) ladder_table = html.Table([ html.Thead(html.Tr([ - html.Th("Price", className="small"), - html.Th("Volume", className="small"), - html.Th("Total", className="small text-end") + html.Th("Price", className="small p-0"), + html.Th("Volume", className="small p-0"), + html.Th("USD", className="small text-end p-0 pe-1"), + html.Th("Crypto", className="small text-start p-0 ps-1") ])), html.Tbody(ask_rows + [mid_row] + bid_rows) ], className="table table-sm table-borderless cob-ladder-table-compact m-0 p-0") # Compact classes @@ -477,7 +502,10 @@ class DashboardComponentManager: bid_pct = bucket['bid_pct'] ask_pct = bucket['ask_pct'] - # Format volume + # Get crypto volume if available (some bucket formats include crypto_volume) + crypto_vol = bucket.get('crypto_volume', bucket.get('size', 0)) + + # Format USD volume if total_vol > 1000000: vol_str = f"${total_vol/1000000:.1f}M" elif total_vol > 1000: @@ -485,6 +513,17 @@ class DashboardComponentManager: else: vol_str = f"${total_vol:.0f}" + # Format crypto volume based on symbol + crypto_unit = "BTC" if "BTC" in symbol else "ETH" if "ETH" in symbol else "CRYPTO" + if crypto_vol > 1000: + crypto_str = f"{crypto_vol/1000:.1f}K {crypto_unit}" + elif crypto_vol > 1: + crypto_str = f"{crypto_vol:.1f} {crypto_unit}" + elif crypto_vol > 0: + crypto_str = f"{crypto_vol:.3f} {crypto_unit}" + else: + crypto_str = "" + # Color based on bid/ask dominance if bid_pct > 60: row_class = "border-success" @@ -503,8 +542,9 @@ class DashboardComponentManager: html.Div([ html.Span(f"${price:.0f}", className="fw-bold me-2"), html.Span(vol_str, className="text-info me-2"), + html.Span(crypto_str, className="small text-muted me-2") if crypto_str else "", html.Span(f"{dominance}", className=f"small {dominance_class}") - ], className="d-flex justify-content-between"), + ], className="d-flex justify-content-between align-items-center"), html.Div([ # Bid bar html.Div(