Selaa lähdekoodia

- Improved handling of external trades by introducing lifecycle tracking for new positions detected on the exchange.
- Enhanced logging for unmatched fills and potential contradictions in position tracking, providing better diagnostics.
- Refactored logic to ensure accurate trade recording and notifications for both matched and unmatched external trades.

Carles Sentis 1 päivä sitten
vanhempi
sitoutus
dfbab65496
2 muutettua tiedostoa jossa 101 lisäystä ja 51 poistoa
  1. 100 50
      src/monitoring/external_event_monitor.py
  2. 1 1
      trading_bot.py

+ 100 - 50
src/monitoring/external_event_monitor.py

@@ -335,64 +335,114 @@ class ExternalEventMonitor:
                                     logger.warning(f"⚠️ External fill {trade_id} for {full_symbol} (Lifecycle: {lc_id}, Amount: {amount_from_fill}) did NOT fully close position. Exchange size now: {current_size_on_exchange}. Lifecycle remains open. Fill will be recorded as 'external_unmatched'.")
 
                     if not fill_processed_this_iteration:
-                        # Access cached_positions via self.market_monitor_cache for diagnostics
+                        # Check if this external trade opens a new position that should be tracked
                         current_positions_from_cache_map = {
                             pos.get('symbol'): pos for pos in (self.market_monitor_cache.cached_positions or [])
                             if pos.get('symbol') and abs(float(pos.get('contracts', 0))) > 1e-9
                         }
-                        all_open_positions_in_db = stats.get_open_positions()
-                        db_open_symbols = {pos_db.get('symbol') for pos_db in all_open_positions_in_db}
-
-                        if full_symbol in db_open_symbols:
-                            logger.error(f"🚨 DIAGNOSTIC: Contradiction for {full_symbol}! get_open_positions() includes it, but get_trade_by_symbol_and_status('{full_symbol}', 'position_opened') failed to find it within _check_external_trades context for fill {trade_id}. This needs investigation into TradingStats symbol querying.")
                         
-                        potential_match_failure_logged = False
-                        if not stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened'):
-                            logger.warning(f"⚠️ DIAGNOSTIC for UNMATCHED FILL {trade_id} ({full_symbol}):")
-                            logger.warning(f"   Fill details: Side={side_from_fill}, Amount={amount_from_fill}, Price={price_from_fill}")
-                            logger.warning(f"   Attempted lookup with full_symbol='{full_symbol}' and status='position_opened' found NO active lifecycle.")
-                            if all_open_positions_in_db:
-                                logger.warning(f"   However, DB currently has these 'position_opened' lifecycles (symbol - lifecycle_id):")
-                                for db_pos in all_open_positions_in_db:
-                                    logger.warning(f"     - '{db_pos.get('symbol')}' - ID: {db_pos.get('trade_lifecycle_id')}")
-                                base_token_fill = full_symbol.split('/')[0].split(':')[0]
-                                near_matches = [db_s for db_s in db_open_symbols if base_token_fill in db_s]
-                                if near_matches:
-                                    logger.warning(f"   Possible near matches in DB for base token '{base_token_fill}': {near_matches}")
-                                else:
-                                    logger.warning(f"   No near matches found in DB for base token '{base_token_fill}'.")
-                            else:
-                                logger.warning("   DB has NO 'position_opened' lifecycles at all right now.")
-                            potential_match_failure_logged = True
+                        # Check if there's a position on the exchange for this symbol
+                        exchange_position = current_positions_from_cache_map.get(full_symbol)
+                        should_create_lifecycle = False
                         
-                        linked_order_db_id = None
-                        if exchange_order_id_from_fill:
-                            order_in_db = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
-                            if order_in_db:
-                                linked_order_db_id = order_in_db.get('id')
-                                logger.info(f"🔗 Fallback: Fill {trade_id} for OID {exchange_order_id_from_fill} (DB ID {linked_order_db_id}) not tied to active lifecycle step.")
-                                current_status = order_in_db.get('status', '')
-                                if current_status in ['open', 'partially_filled', 'pending_submission']:
-                                    amt_req = float(order_in_db.get('amount_requested', 0))
-                                    amt_filled_so_far = float(order_in_db.get('amount_filled',0))
-                                    new_status = 'partially_filled'
-                                    if (amt_filled_so_far + amount_from_fill) >= amt_req - 1e-9:
-                                        new_status = 'filled'
-                                    stats.update_order_status(
-                                        order_db_id=linked_order_db_id, new_status=new_status,
-                                        amount_filled_increment=amount_from_fill
-                                    )
-                                    logger.info(f"📊 Updated bot order {linked_order_db_id} (fallback): {current_status} → {new_status}")
+                        if exchange_position:
+                            # There's a position on exchange but no lifecycle tracking it
+                            # This indicates a new external position that should be tracked
+                            should_create_lifecycle = True
+                            logger.info(f"🚀 Detected new external position for {full_symbol}: {side_from_fill} {amount_from_fill} @ {price_from_fill}")
                         
-                        if not (hasattr(stats, 'get_trade_by_exchange_fill_id') and stats.get_trade_by_exchange_fill_id(trade_id)):
-                            stats.record_trade(
-                                full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
-                                exchange_fill_id=trade_id, trade_type="external_unmatched",
-                                timestamp=timestamp_dt.isoformat(),
-                                linked_order_table_id_to_link=linked_order_db_id 
+                        if should_create_lifecycle:
+                            # Create a new trade lifecycle for this external position
+                            lifecycle_id = stats.create_trade_lifecycle(
+                                symbol=full_symbol,
+                                side=side_from_fill,
+                                entry_order_id=exchange_order_id_from_fill,
+                                trade_type="external"
                             )
-                            logger.info(f"📋 Recorded trade via FALLBACK: {trade_id} (Unmatched External Fill)")
-                        fill_processed_this_iteration = True
+                            
+                            if lifecycle_id:
+                                # Update the lifecycle to position_opened status
+                                success = stats.update_trade_position_opened(
+                                    lifecycle_id=lifecycle_id,
+                                    entry_price=price_from_fill,
+                                    entry_amount=amount_from_fill,
+                                    exchange_fill_id=trade_id
+                                )
+                                
+                                if success:
+                                    logger.info(f"📈 Created and opened new external lifecycle: {lifecycle_id[:8]} for {full_symbol}")
+                                    symbols_with_fills.add(token)
+                                    
+                                    # Send notification for new external position
+                                    if self.notification_manager:
+                                        await self.notification_manager.send_external_trade_notification(
+                                            full_symbol, side_from_fill, amount_from_fill, price_from_fill,
+                                            "position_opened", timestamp_dt.isoformat()
+                                        )
+                                    fill_processed_this_iteration = True
+                        
+                        # If we didn't create a lifecycle, fall back to the old behavior
+                        if not fill_processed_this_iteration:
+                            all_open_positions_in_db = stats.get_open_positions()
+                            db_open_symbols = {pos_db.get('symbol') for pos_db in all_open_positions_in_db}
+
+                            if full_symbol in db_open_symbols:
+                                logger.error(f"🚨 DIAGNOSTIC: Contradiction for {full_symbol}! get_open_positions() includes it, but get_trade_by_symbol_and_status('{full_symbol}', 'position_opened') failed to find it within _check_external_trades context for fill {trade_id}. This needs investigation into TradingStats symbol querying.")
+                            
+                            potential_match_failure_logged = False
+                            if not stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened'):
+                                logger.warning(f"⚠️ DIAGNOSTIC for UNMATCHED FILL {trade_id} ({full_symbol}):")
+                                logger.warning(f"   Fill details: Side={side_from_fill}, Amount={amount_from_fill}, Price={price_from_fill}")
+                                logger.warning(f"   Attempted lookup with full_symbol='{full_symbol}' and status='position_opened' found NO active lifecycle.")
+                                if all_open_positions_in_db:
+                                    logger.warning(f"   However, DB currently has these 'position_opened' lifecycles (symbol - lifecycle_id):")
+                                    for db_pos in all_open_positions_in_db:
+                                        logger.warning(f"     - '{db_pos.get('symbol')}' - ID: {db_pos.get('trade_lifecycle_id')}")
+                                    base_token_fill = full_symbol.split('/')[0].split(':')[0]
+                                    near_matches = [db_s for db_s in db_open_symbols if base_token_fill in db_s]
+                                    if near_matches:
+                                        logger.warning(f"   Possible near matches in DB for base token '{base_token_fill}': {near_matches}")
+                                    else:
+                                        logger.warning(f"   No near matches found in DB for base token '{base_token_fill}'.")
+                                else:
+                                    logger.warning("   DB has NO 'position_opened' lifecycles at all right now.")
+                                potential_match_failure_logged = True
+                            
+                            linked_order_db_id = None
+                            if exchange_order_id_from_fill:
+                                order_in_db = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
+                                if order_in_db:
+                                    linked_order_db_id = order_in_db.get('id')
+                                    logger.info(f"🔗 Fallback: Fill {trade_id} for OID {exchange_order_id_from_fill} (DB ID {linked_order_db_id}) not tied to active lifecycle step.")
+                                    current_status = order_in_db.get('status', '')
+                                    if current_status in ['open', 'partially_filled', 'pending_submission']:
+                                        amt_req = float(order_in_db.get('amount_requested', 0))
+                                        amt_filled_so_far = float(order_in_db.get('amount_filled',0))
+                                        new_status = 'partially_filled'
+                                        if (amt_filled_so_far + amount_from_fill) >= amt_req - 1e-9:
+                                            new_status = 'filled'
+                                        stats.update_order_status(
+                                            order_db_id=linked_order_db_id, new_status=new_status,
+                                            amount_filled_increment=amount_from_fill
+                                        )
+                                        logger.info(f"📊 Updated bot order {linked_order_db_id} (fallback): {current_status} → {new_status}")
+                            
+                            if not (hasattr(stats, 'get_trade_by_exchange_fill_id') and stats.get_trade_by_exchange_fill_id(trade_id)):
+                                stats.record_trade(
+                                    full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
+                                    exchange_fill_id=trade_id, trade_type="external_unmatched",
+                                    timestamp=timestamp_dt.isoformat(),
+                                    linked_order_table_id_to_link=linked_order_db_id 
+                                )
+                                logger.info(f"📋 Recorded trade via FALLBACK: {trade_id} (Unmatched External Fill)")
+                                
+                                # Send notification for unmatched external trade
+                                if self.notification_manager:
+                                    await self.notification_manager.send_external_trade_notification(
+                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill,
+                                        "external_unmatched", timestamp_dt.isoformat()
+                                    )
+                            fill_processed_this_iteration = True
 
                     if fill_processed_this_iteration:
                         external_trades_processed += 1

+ 1 - 1
trading_bot.py

@@ -14,7 +14,7 @@ from datetime import datetime
 from pathlib import Path
 
 # Bot version
-BOT_VERSION = "2.3.154"
+BOT_VERSION = "2.3.155"
 
 # Add src directory to Python path
 sys.path.insert(0, str(Path(__file__).parent / "src"))