|
@@ -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
|
|
|
|
|
|
-
|
|
|
+
|
|
|
if not fill_processed_this_iteration:
|
|
|
active_lc = None
|
|
|
- is_direct_sl_tp_fill = False
|
|
|
+ closure_reason_action_type = None
|
|
|
+ bot_order_db_id_to_update = None
|
|
|
|
|
|
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:
|
|
|
- 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
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+ 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')
|
|
|
+
|
|
|
+
|
|
|
+ if order_type == 'market':
|
|
|
+ potential_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
|
|
|
+ if potential_lc:
|
|
|
+ lc_pos_side = potential_lc.get('position_side')
|
|
|
+
|
|
|
+ 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']}.")
|
|
|
+
|
|
|
+
|
|
|
+ if not active_lc:
|
|
|
+
|
|
|
+ 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_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:
|
|
|
+ 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_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']}.")
|
|
|
+
|
|
|
+
|
|
|
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
|
|
|
+ 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')
|
|
|
+
|
|
|
+ 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 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')
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- is_external_closing_fill = False
|
|
|
- if not is_direct_sl_tp_fill and lc_position_side:
|
|
|
- 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 "🔴"
|
|
|
-
|
|
|
- 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()
|
|
|
- )
|
|
|
-
|
|
|
- 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')
|
|
|
+
|
|
|
+ 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
|
|
|
|
|
|
|