|
@@ -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)
|