From db23ad10da670ba385708cf622d55e90e31393e1 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 28 Jul 2025 16:42:11 +0300 Subject: [PATCH] trading risk management --- .../bybit/debug/Order_history_sample.md | 0 core/trading_executor.py | 223 +++++++++++++++++- web/clean_dashboard.py | 172 +++++++------- 3 files changed, 305 insertions(+), 90 deletions(-) create mode 100644 core/exchanges/bybit/debug/Order_history_sample.md diff --git a/core/exchanges/bybit/debug/Order_history_sample.md b/core/exchanges/bybit/debug/Order_history_sample.md new file mode 100644 index 0000000..e69de29 diff --git a/core/trading_executor.py b/core/trading_executor.py index 9d7a166..bb20c71 100644 --- a/core/trading_executor.py +++ b/core/trading_executor.py @@ -260,8 +260,113 @@ class TradingExecutor: elif self.trading_enabled and self.exchange: logger.info(f"TRADING EXECUTOR: Using {self.primary_name.upper()} exchange - fee sync not available") + # Sync positions from exchange on startup if in live mode + if not self.simulation_mode and self.exchange and self.trading_enabled: + self._sync_positions_on_startup() + logger.info(f"Trading Executor initialized - Exchange: {self.primary_name.upper()}, Mode: {self.trading_mode}, Enabled: {self.trading_enabled}") + def _sync_positions_on_startup(self): + """Sync positions from exchange on startup""" + try: + logger.info("TRADING EXECUTOR: Syncing positions from exchange on startup...") + + # Get all open positions from exchange + if hasattr(self.exchange, 'get_positions'): + exchange_positions = self.exchange.get_positions() + if exchange_positions: + for position in exchange_positions: + symbol = position.get('symbol', '').replace('USDT', '/USDT') + size = float(position.get('size', 0)) + side = position.get('side', '').upper() + entry_price = float(position.get('entry_price', 0)) + + if size > 0 and symbol and side in ['LONG', 'SHORT']: + # Create position object + pos_obj = Position( + symbol=symbol, + side=side, + quantity=size, + entry_price=entry_price, + entry_time=datetime.now() + ) + self.positions[symbol] = pos_obj + logger.info(f"POSITION SYNC: Found {side} position for {symbol}: {size} @ ${entry_price:.2f}") + + logger.info(f"POSITION SYNC: Synced {len(self.positions)} positions from exchange") + else: + logger.warning("Exchange does not support position retrieval") + + except Exception as e: + logger.error(f"POSITION SYNC: Error syncing positions on startup: {e}") + + def _sync_single_position_from_exchange(self, symbol: str, exchange_position: dict): + """Sync a single position from exchange to local state""" + try: + size = float(exchange_position.get('size', 0)) + side = exchange_position.get('side', '').upper() + entry_price = float(exchange_position.get('entry_price', 0)) + + if size > 0 and side in ['LONG', 'SHORT']: + pos_obj = Position( + symbol=symbol, + side=side, + quantity=size, + entry_price=entry_price, + entry_time=datetime.now() + ) + self.positions[symbol] = pos_obj + logger.info(f"POSITION SYNC: Added {side} position for {symbol}: {size} @ ${entry_price:.2f}") + return True + except Exception as e: + logger.error(f"Error syncing single position for {symbol}: {e}") + return False + + def close_all_positions(self): + """Emergency close all positions - both local and exchange""" + logger.warning("CLOSE ALL POSITIONS: Starting emergency position closure") + positions_closed = 0 + + # Get all positions to close (local + exchange) + positions_to_close = set() + + # Add local positions + for symbol in self.positions.keys(): + positions_to_close.add(symbol) + + # Add exchange positions if not in simulation mode + if not self.simulation_mode and self.exchange: + try: + exchange_positions = self.exchange.get_positions() + if exchange_positions: + for pos in exchange_positions: + symbol = pos.get('symbol', '').replace('USDT', '/USDT') + size = float(pos.get('size', 0)) + if size > 0: + positions_to_close.add(symbol) + except Exception as e: + logger.error(f"Error getting exchange positions for closure: {e}") + + # Close all positions + for symbol in positions_to_close: + try: + if symbol in self.positions: + position = self.positions[symbol] + if position.side == 'LONG': + if self._close_long_position(symbol, 1.0, position.entry_price): + positions_closed += 1 + elif position.side == 'SHORT': + if self._close_short_position(symbol, 1.0, position.entry_price): + positions_closed += 1 + else: + logger.warning(f"Position {symbol} found on exchange but not locally - manual intervention needed") + + except Exception as e: + logger.error(f"Error closing position {symbol}: {e}") + + logger.warning(f"CLOSE ALL POSITIONS: Closed {positions_closed} positions") + return positions_closed + def _safe_exchange_call(self, method_name: str, *args, **kwargs): """Safely call exchange methods with null checking""" if not self.exchange: @@ -374,6 +479,27 @@ class TradingExecutor: if action == 'HOLD': return True + # PERIODIC POSITION SYNC: Every 10th signal execution, sync positions from exchange to prevent desync + if not hasattr(self, '_signal_count'): + self._signal_count = 0 + self._signal_count += 1 + + if self._signal_count % 10 == 0 and not self.simulation_mode and self.exchange: + logger.debug(f"PERIODIC SYNC: Checking position sync for {symbol} (signal #{self._signal_count})") + try: + exchange_positions = self.exchange.get_positions(symbol) + if exchange_positions: + for pos in exchange_positions: + size = float(pos.get('size', 0)) + if size > 0 and symbol not in self.positions: + logger.warning(f"DESYNC DETECTED: Found position on exchange but not locally for {symbol}") + self._sync_single_position_from_exchange(symbol, pos) + elif symbol in self.positions: + logger.warning(f"DESYNC DETECTED: Have local position but none on exchange for {symbol}") + # Consider removing local position or investigating further + except Exception as e: + logger.debug(f"Error in periodic position sync: {e}") + # Check safety conditions if not self._check_safety_conditions(symbol, action): return False @@ -866,17 +992,33 @@ class TradingExecutor: return True def _execute_buy(self, symbol: str, confidence: float, current_price: float) -> bool: - """Execute a buy order""" - # Check if we have a short position to close + """Execute a buy order with enhanced position management""" + # CRITICAL: Check for existing positions (both local and exchange) if symbol in self.positions: position = self.positions[symbol] if position.side == 'SHORT': logger.info(f"Closing SHORT position in {symbol}") return self._close_short_position(symbol, confidence, current_price) else: - logger.info(f"Already have LONG position in {symbol}") + logger.warning(f"POSITION SAFETY: Already have LONG position in {symbol} - blocking duplicate trade") return False + # ADDITIONAL SAFETY: Double-check with exchange if not in simulation mode + if not self.simulation_mode and self.exchange: + try: + exchange_positions = self.exchange.get_positions(symbol) + if exchange_positions: + for pos in exchange_positions: + if float(pos.get('size', 0)) > 0: + logger.warning(f"POSITION SAFETY: Found existing position on exchange for {symbol} - blocking duplicate trade") + logger.warning(f"Position details: {pos}") + # Sync this position to local state + self._sync_single_position_from_exchange(symbol, pos) + return False + except Exception as e: + logger.debug(f"Error checking exchange positions for {symbol}: {e}") + # Don't block trade if we can't check - but log it + # Cancel any existing open orders before placing new order if not self.simulation_mode: self._cancel_open_orders(symbol) @@ -902,6 +1044,12 @@ class TradingExecutor: else: # Place real order with enhanced error handling result = self._place_order_with_retry(symbol, 'BUY', 'MARKET', quantity, current_price) + + # Check for position check error + if result and 'error' in result and result['error'] == 'existing_position': + logger.error(f"BUY order blocked: {result['message']}") + return False + if result and 'orderId' in result: # Use actual fill information if available, otherwise fall back to order parameters filled_quantity = result.get('executedQty', quantity) @@ -943,7 +1091,27 @@ class TradingExecutor: return self._execute_short(symbol, confidence, current_price) def _execute_short(self, symbol: str, confidence: float, current_price: float) -> bool: - """Execute a short order (sell without holding the asset)""" + """Execute a short order (sell without holding the asset) with enhanced position management""" + # CRITICAL: Check for any existing positions before opening SHORT + if symbol in self.positions: + logger.warning(f"POSITION SAFETY: Already have position in {symbol} - blocking SHORT trade") + return False + + # ADDITIONAL SAFETY: Double-check with exchange if not in simulation mode + if not self.simulation_mode and self.exchange: + try: + exchange_positions = self.exchange.get_positions(symbol) + if exchange_positions: + for pos in exchange_positions: + if float(pos.get('size', 0)) > 0: + logger.warning(f"POSITION SAFETY: Found existing position on exchange for {symbol} - blocking SHORT trade") + logger.warning(f"Position details: {pos}") + # Sync this position to local state + self._sync_single_position_from_exchange(symbol, pos) + return False + except Exception as e: + logger.debug(f"Error checking exchange positions for SHORT {symbol}: {e}") + # Cancel any existing open orders before placing new order if not self.simulation_mode: self._cancel_open_orders(symbol) @@ -969,6 +1137,12 @@ class TradingExecutor: else: # Place real short order with enhanced error handling result = self._place_order_with_retry(symbol, 'SELL', 'MARKET', quantity, current_price) + + # Check for position check error + if result and 'error' in result and result['error'] == 'existing_position': + logger.error(f"SHORT order blocked: {result['message']}") + return False + if result and 'orderId' in result: # Use actual fill information if available, otherwise fall back to order parameters filled_quantity = result.get('executedQty', quantity) @@ -996,6 +1170,25 @@ class TradingExecutor: def _place_order_with_retry(self, symbol: str, side: str, order_type: str, quantity: float, current_price: float, max_retries: int = 3) -> Dict[str, Any]: """Place order with retry logic for MEXC error handling""" + + # FINAL POSITION CHECK: Verify no existing position before placing order + if not self.simulation_mode and self.exchange: + try: + exchange_positions = self.exchange.get_positions(symbol) + if exchange_positions: + for pos in exchange_positions: + size = float(pos.get('size', 0)) + if size > 0: + logger.error(f"FINAL POSITION CHECK FAILED: Found existing position for {symbol} before placing order") + logger.error(f"Position details: {pos}") + logger.error(f"Order details: {side} {quantity} @ ${current_price}") + # Sync the position to local state + self._sync_single_position_from_exchange(symbol, pos) + return {'error': 'existing_position', 'message': f'Position already exists for {symbol}'} + except Exception as e: + logger.warning(f"Error in final position check for {symbol}: {e}") + # Continue with order placement if we can't check positions + order_start_time = time.time() max_order_time = 8.0 # Maximum 8 seconds for order placement (leaves 2s buffer for lock timeout) @@ -1808,7 +2001,27 @@ class TradingExecutor: # Calculate total current position value total_position_value = 0.0 - # Add existing positions + # ENHANCED: Also check exchange positions to ensure we don't miss any + if not self.simulation_mode and self.exchange: + try: + exchange_positions = self.exchange.get_positions() + if exchange_positions: + for pos in exchange_positions: + symbol = pos.get('symbol', '').replace('USDT', '/USDT') + size = float(pos.get('size', 0)) + entry_price = float(pos.get('entry_price', 0)) + if size > 0 and symbol: + # Check if this position is also in our local state + if symbol not in self.positions: + logger.warning(f"POSITION LIMIT: Found untracked exchange position for {symbol}: {size} @ ${entry_price:.2f}") + # Add to total even if not in local state + position_value = size * entry_price + total_position_value += position_value + logger.debug(f"Exchange position {symbol}: {size:.6f} @ ${entry_price:.2f} = ${position_value:.2f}") + except Exception as e: + logger.debug(f"Error checking exchange positions for limit: {e}") + + # Add existing local positions for symbol, position in self.positions.items(): # Get current price for the symbol try: diff --git a/web/clean_dashboard.py b/web/clean_dashboard.py index 28f0b0e..780f075 100644 --- a/web/clean_dashboard.py +++ b/web/clean_dashboard.py @@ -1245,92 +1245,94 @@ class CleanTradingDashboard: return [html.I(className="fas fa-exclamation-triangle me-1"), "Store Failed"] return [html.I(className="fas fa-save me-1"), "Store All Models"] - # Trading Mode Toggle - @self.app.callback( - Output('trading-mode-display', 'children'), - Output('trading-mode-display', 'className'), - [Input('trading-mode-switch', 'value')] - ) - def update_trading_mode(switch_value): - """Update trading mode display and apply changes""" - try: - is_live = 'live' in (switch_value or []) - self.trading_mode_live = is_live - - # Update trading executor mode if available - if hasattr(self, 'trading_executor') and self.trading_executor: - if hasattr(self.trading_executor, 'set_trading_mode'): - # Use the new set_trading_mode method - success = self.trading_executor.set_trading_mode('live' if is_live else 'simulation') - if success: - logger.info(f"TRADING MODE: {'LIVE' if is_live else 'SIMULATION'} - Mode updated successfully") - else: - logger.error(f"Failed to update trading mode to {'LIVE' if is_live else 'SIMULATION'}") - else: - # Fallback to direct property setting - if is_live: - self.trading_executor.trading_mode = 'live' - self.trading_executor.simulation_mode = False - logger.info("TRADING MODE: LIVE - Real orders will be executed!") - else: - self.trading_executor.trading_mode = 'simulation' - self.trading_executor.simulation_mode = True - logger.info("TRADING MODE: SIMULATION - Orders are simulated") - - # Return display text and styling - if is_live: - return "LIVE", "fw-bold text-danger" - else: - return "SIM", "fw-bold text-warning" - - except Exception as e: - logger.error(f"Error updating trading mode: {e}") - return "ERROR", "fw-bold text-danger" + # Trading Mode Toggle - TEMPORARILY DISABLED TO FIX UI ERROR + # @self.app.callback( + # Output('trading-mode-display', 'children'), + # Output('trading-mode-display', 'className'), + # [Input('trading-mode-switch', 'value')] + # ) + # def update_trading_mode(switch_value): + # """Update trading mode display and apply changes""" + # logger.debug(f"Trading mode callback triggered with value: {switch_value}") + # try: + # is_live = 'live' in (switch_value or []) + # self.trading_mode_live = is_live + # + # # Update trading executor mode if available + # if hasattr(self, 'trading_executor') and self.trading_executor: + # if hasattr(self.trading_executor, 'set_trading_mode'): + # # Use the new set_trading_mode method + # success = self.trading_executor.set_trading_mode('live' if is_live else 'simulation') + # if success: + # logger.info(f"TRADING MODE: {'LIVE' if is_live else 'SIMULATION'} - Mode updated successfully") + # else: + # logger.error(f"Failed to update trading mode to {'LIVE' if is_live else 'SIMULATION'}") + # else: + # # Fallback to direct property setting + # if is_live: + # self.trading_executor.trading_mode = 'live' + # self.trading_executor.simulation_mode = False + # logger.info("TRADING MODE: LIVE - Real orders will be executed!") + # else: + # self.trading_executor.trading_mode = 'simulation' + # self.trading_executor.simulation_mode = True + # logger.info("TRADING MODE: SIMULATION - Orders are simulated") + # + # # Return display text and styling + # if is_live: + # return "LIVE", "fw-bold text-danger" + # else: + # return "SIM", "fw-bold text-warning" + # + # except Exception as e: + # logger.error(f"Error updating trading mode: {e}") + # return "ERROR", "fw-bold text-danger" - # Cold Start Toggle - @self.app.callback( - Output('cold-start-display', 'children'), - Output('cold-start-display', 'className'), - [Input('cold-start-switch', 'value')] - ) - def update_cold_start(switch_value): - """Update cold start training mode""" - try: - is_enabled = 'enabled' in (switch_value or []) - self.cold_start_enabled = is_enabled - - # Update orchestrator cold start mode if available - if hasattr(self, 'orchestrator') and self.orchestrator: - if hasattr(self.orchestrator, 'set_cold_start_training_enabled'): - # Use the new set_cold_start_training_enabled method - success = self.orchestrator.set_cold_start_training_enabled(is_enabled) - if success: - logger.info(f"COLD START: {'ON' if is_enabled else 'OFF'} - Training mode updated successfully") - else: - logger.error(f"Failed to update cold start training to {'ON' if is_enabled else 'OFF'}") - else: - # Fallback to direct property setting - if hasattr(self.orchestrator, 'cold_start_enabled'): - self.orchestrator.cold_start_enabled = is_enabled - - # Update training frequency based on cold start mode - if hasattr(self.orchestrator, 'training_frequency'): - if is_enabled: - self.orchestrator.training_frequency = 'high' # Train on every signal - logger.info("COLD START: ON - Excessive training enabled") - else: - self.orchestrator.training_frequency = 'normal' # Normal training - logger.info("COLD START: OFF - Normal training frequency") - - # Return display text and styling - if is_enabled: - return "ON", "fw-bold text-success" - else: - return "OFF", "fw-bold text-secondary" - - except Exception as e: - logger.error(f"Error updating cold start mode: {e}") - return "ERROR", "fw-bold text-danger" + # Cold Start Toggle - TEMPORARILY DISABLED TO FIX UI ERROR + # @self.app.callback( + # Output('cold-start-display', 'children'), + # Output('cold-start-display', 'className'), + # [Input('cold-start-switch', 'value')] + # ) + # def update_cold_start(switch_value): + # """Update cold start training mode""" + # logger.debug(f"Cold start callback triggered with value: {switch_value}") + # try: + # is_enabled = 'enabled' in (switch_value or []) + # self.cold_start_enabled = is_enabled + # + # # Update orchestrator cold start mode if available + # if hasattr(self, 'orchestrator') and self.orchestrator: + # if hasattr(self.orchestrator, 'set_cold_start_training_enabled'): + # # Use the new set_cold_start_training_enabled method + # success = self.orchestrator.set_cold_start_training_enabled(is_enabled) + # if success: + # logger.info(f"COLD START: {'ON' if is_enabled else 'OFF'} - Training mode updated successfully") + # else: + # logger.error(f"Failed to update cold start training to {'ON' if is_enabled else 'OFF'}") + # else: + # # Fallback to direct property setting + # if hasattr(self.orchestrator, 'cold_start_enabled'): + # self.orchestrator.cold_start_enabled = is_enabled + # + # # Update training frequency based on cold start mode + # if hasattr(self.orchestrator, 'training_frequency'): + # if is_enabled: + # self.orchestrator.training_frequency = 'high' # Train on every signal + # logger.info("COLD START: ON - Excessive training enabled") + # else: + # self.orchestrator.training_frequency = 'normal' # Normal training + # logger.info("COLD START: OFF - Normal training frequency") + # + # # Return display text and styling + # if is_enabled: + # return "ON", "fw-bold text-success" + # else: + # return "OFF", "fw-bold text-secondary" + # + # except Exception as e: + # logger.error(f"Error updating cold start mode: {e}") + # return "ERROR", "fw-bold text-danger" def _get_current_price(self, symbol: str) -> Optional[float]: """Get current price for symbol - ONLY using our data providers"""