浏览代码

Increment BOT_VERSION to 2.2.135 and enhance MarketMonitor with improved fill processing logic.

- Updated BOT_VERSION for the upcoming release.
- Refined logic in MarketMonitor to better link fills to bot orders, including handling for bot exits, stop losses, and take profits.
- Improved logging for lifecycle closures to enhance traceability and debugging capabilities.
Carles Sentis 2 天之前
父节点
当前提交
5712974e83
共有 2 个文件被更改,包括 81 次插入64 次删除
  1. 80 63
      src/monitoring/market_monitor.py
  2. 1 1
      trading_bot.py

+ 80 - 63
src/monitoring/market_monitor.py

@@ -730,77 +730,94 @@ class MarketMonitor:
                                     stats.update_order_status(order_db_id=order_in_db_for_entry['id'], new_status='filled', amount_filled_increment=amount_from_fill)
                             fill_processed_this_iteration = True
                     
-                    # 2. Check if fill matches an OPENED trade lifecycle (SL/TP fill or external close)
+                    # 2. Check if fill matches an OPENED trade lifecycle (Bot Exit, SL/TP by OID, or External Close)
                     if not fill_processed_this_iteration:
                         active_lc = None
-                        is_direct_sl_tp_fill = False
+                        closure_reason_action_type = None # e.g., "bot_exit_long_close", "sl_long_close", "external_long_close"
+                        bot_order_db_id_to_update = None # DB ID of the bot's order (exit, SL, TP) that this fill corresponds to
 
                         if exchange_order_id_from_fill:
-                            lc_by_sl = stats.get_lifecycle_by_sl_order_id(exchange_order_id_from_fill, status='position_opened')
-                            if lc_by_sl and lc_by_sl.get('symbol') == full_symbol:
-                                active_lc = lc_by_sl
-                                is_direct_sl_tp_fill = True
-                            
-                            if not active_lc: # Only check TP if SL didn't match
-                                lc_by_tp = stats.get_lifecycle_by_tp_order_id(exchange_order_id_from_fill, status='position_opened')
-                                if lc_by_tp and lc_by_tp.get('symbol') == full_symbol:
-                                    active_lc = lc_by_tp
-                                    is_direct_sl_tp_fill = True
-                        
-                        # If not a direct SL/TP fill by OID, check for external close of any active position for the symbol
+                            # Attempt to link fill to a specific bot order (exit, SL, TP) first
+                            bot_order_for_fill = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
+
+                            if bot_order_for_fill and bot_order_for_fill.get('symbol') == full_symbol:
+                                order_type = bot_order_for_fill.get('type')
+                                order_side = bot_order_for_fill.get('side') # Side of the bot's order
+
+                                # A. Check if this fill corresponds to a bot-initiated EXIT order
+                                if order_type == 'market': # Common for /exit, add other explicit exit types if any
+                                    potential_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
+                                    if potential_lc:
+                                        lc_pos_side = potential_lc.get('position_side')
+                                        # Ensure bot's market order & this fill are closing the position
+                                        if (lc_pos_side == 'long' and order_side == 'sell' and side_from_fill == 'sell') or \
+                                           (lc_pos_side == 'short' and order_side == 'buy' and side_from_fill == 'buy'):
+                                            active_lc = potential_lc
+                                            closure_reason_action_type = f"bot_exit_{lc_pos_side}_close"
+                                            bot_order_db_id_to_update = bot_order_for_fill.get('id')
+                                            logger.info(f"ℹ️ Lifecycle BOT EXIT: Fill {trade_id} (OID {exchange_order_id_from_fill}) for {full_symbol} matches bot exit for lifecycle {active_lc['trade_lifecycle_id']}.")
+                                
+                                # B. If not a bot exit, check if it's an SL or TP order linked to a lifecycle
+                                if not active_lc: # Check only if not already matched as bot_exit
+                                    # Check SL by OID (exchange_order_id_from_fill should be the SL's actual exchange OID)
+                                    lc_by_sl = stats.get_lifecycle_by_sl_order_id(exchange_order_id_from_fill, status='position_opened')
+                                    if lc_by_sl and lc_by_sl.get('symbol') == full_symbol:
+                                        active_lc = lc_by_sl
+                                        closure_reason_action_type = f"sl_{active_lc.get('position_side')}_close"
+                                        # bot_order_for_fill is the SL order itself in this context
+                                        bot_order_db_id_to_update = bot_order_for_fill.get('id') 
+                                        logger.info(f"ℹ️ Lifecycle SL: Fill {trade_id} for OID {exchange_order_id_from_fill} matches SL for lifecycle {active_lc['trade_lifecycle_id']}.")
+                                    
+                                    if not active_lc: # Check TP only if not SL (and not bot exit)
+                                        lc_by_tp = stats.get_lifecycle_by_tp_order_id(exchange_order_id_from_fill, status='position_opened')
+                                        if lc_by_tp and lc_by_tp.get('symbol') == full_symbol:
+                                            active_lc = lc_by_tp
+                                            closure_reason_action_type = f"tp_{active_lc.get('position_side')}_close"
+                                            # bot_order_for_fill is the TP order itself here
+                                            bot_order_db_id_to_update = bot_order_for_fill.get('id') 
+                                            logger.info(f"ℹ️ Lifecycle TP: Fill {trade_id} for OID {exchange_order_id_from_fill} matches TP for lifecycle {active_lc['trade_lifecycle_id']}.")
+
+                        # C. If fill was not matched to a specific bot order by OID, check for generic external closure
                         if not active_lc:
-                            active_lc_for_external_check = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
-                            if active_lc_for_external_check:
-                                active_lc = active_lc_for_external_check # This is the lifecycle to check for external closure
+                            potential_lc_external = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
+                            if potential_lc_external:
+                                lc_pos_side = potential_lc_external.get('position_side')
+                                # Ensure fill side is opposite to position side
+                                if (lc_pos_side == 'long' and side_from_fill == 'sell') or \
+                                   (lc_pos_side == 'short' and side_from_fill == 'buy'):
+                                    active_lc = potential_lc_external
+                                    closure_reason_action_type = f"external_{lc_pos_side}_close"
+                                    logger.info(f"ℹ️ Lifecycle EXTERNAL CLOSE: Fill {trade_id} for {full_symbol} (no matching bot OID) for lifecycle {active_lc['trade_lifecycle_id']}.")
                         
-                        if active_lc:
+                        # If a lifecycle was identified for closure by any of the above means
+                        if active_lc and closure_reason_action_type:
                             lc_id = active_lc['trade_lifecycle_id']
                             lc_entry_price = active_lc.get('entry_price', 0)
-                            lc_position_side = active_lc.get('position_side')
-
-                            # Determine if it's an SL/TP fill (if is_direct_sl_tp_fill is true) 
-                            # OR an external closing fill (if active_lc was found by symbol and sides match)
-                            is_external_closing_fill = False
-                            if not is_direct_sl_tp_fill and lc_position_side: # Only check for external close if not already matched as SL/TP
-                                is_external_closing_fill = (lc_position_side == 'long' and side_from_fill == 'sell') or \
-                                                       (lc_position_side == 'short' and side_from_fill == 'buy')
-
-                            if is_direct_sl_tp_fill or is_external_closing_fill:
-                                action_type = "unknown_closure"
-                                if is_direct_sl_tp_fill:
-                                    action_type = f"{lc_position_side}_sl_tp_close"
-                                    logger.info(f"ℹ️ Lifecycle SL/TP: Fill {trade_id} for {full_symbol} matches SL/TP for lifecycle {lc_id}.")
-                                elif is_external_closing_fill:
-                                    action_type = f"external_{lc_position_side}_close"
-                                    logger.info(f"ℹ️ Lifecycle EXTERNAL CLOSE: Fill {trade_id} for {full_symbol} for lifecycle {lc_id}.")
-
-                                realized_pnl = 0
-                                if lc_position_side == 'long':
-                                    realized_pnl = amount_from_fill * (price_from_fill - lc_entry_price)
-                                elif lc_position_side == 'short':
-                                    realized_pnl = amount_from_fill * (lc_entry_price - price_from_fill)
-                                
-                                success = stats.update_trade_position_closed(
-                                    lifecycle_id=lc_id, exit_price=price_from_fill,
-                                    realized_pnl=realized_pnl, exchange_fill_id=trade_id
-                                )
-                                if success:
-                                    pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
-                                    # Get formatter for this log line
-                                    formatter = get_formatter()
-                                    logger.info(f"{pnl_emoji} Lifecycle CLOSED: {lc_id} ({action_type}). PNL for fill: {formatter.format_price_with_symbol(realized_pnl)}")
-                                    symbols_with_fills.add(token)
-                                    if self.notification_manager:
-                                        await self.notification_manager.send_external_trade_notification(
-                                            full_symbol, side_from_fill, amount_from_fill, price_from_fill,
-                                            action_type, timestamp_dt.isoformat()
-                                        )
-                                    # MIGRATE STATS
-                                    stats._migrate_trade_to_aggregated_stats(lc_id)
-                                    if is_direct_sl_tp_fill and exchange_order_id_from_fill:
-                                        order_db = stats.get_order_by_exchange_id(exchange_order_id_from_fill)
-                                        if order_db:
-                                            stats.update_order_status(order_db_id=order_db['id'], new_status='filled', amount_filled_increment=amount_from_fill)
+                            lc_position_side = active_lc.get('position_side') # From the identified active_lc
+
+                            realized_pnl = 0
+                            if lc_position_side == 'long':
+                                realized_pnl = amount_from_fill * (price_from_fill - lc_entry_price)
+                            elif lc_position_side == 'short':
+                                realized_pnl = amount_from_fill * (lc_entry_price - price_from_fill)
+                            
+                            success = stats.update_trade_position_closed(
+                                lifecycle_id=lc_id, exit_price=price_from_fill,
+                                realized_pnl=realized_pnl, exchange_fill_id=trade_id
+                            )
+                            if success:
+                                pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
+                                formatter = get_formatter()
+                                logger.info(f"{pnl_emoji} Lifecycle CLOSED: {lc_id} ({closure_reason_action_type}). PNL for fill: {formatter.format_price_with_symbol(realized_pnl)}")
+                                symbols_with_fills.add(token)
+                                if self.notification_manager:
+                                    await self.notification_manager.send_external_trade_notification(
+                                        full_symbol, side_from_fill, amount_from_fill, price_from_fill, 
+                                        closure_reason_action_type, timestamp_dt.isoformat()
+                                    )
+                                stats._migrate_trade_to_aggregated_stats(lc_id)
+                                if bot_order_db_id_to_update:
+                                    stats.update_order_status(order_db_id=bot_order_db_id_to_update, new_status='filled', amount_filled_increment=amount_from_fill)
                                 fill_processed_this_iteration = True
 
                     # 3. Handle external stop loss executions (MarketMonitor's separate tracking)

+ 1 - 1
trading_bot.py

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