|
@@ -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'.")
|
|
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:
|
|
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 = {
|
|
current_positions_from_cache_map = {
|
|
pos.get('symbol'): pos for pos in (self.market_monitor_cache.cached_positions or [])
|
|
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
|
|
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:
|
|
if fill_processed_this_iteration:
|
|
external_trades_processed += 1
|
|
external_trades_processed += 1
|