|
@@ -294,6 +294,83 @@ class ExternalEventMonitor:
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error sending position change notification: {e}")
|
|
|
|
|
|
+ async def _auto_sync_single_position(self, symbol: str, exchange_position: Dict[str, Any], stats) -> bool:
|
|
|
+ """Auto-sync a single orphaned position to create a lifecycle record."""
|
|
|
+ try:
|
|
|
+ import uuid
|
|
|
+ from src.utils.token_display_formatter import get_formatter
|
|
|
+
|
|
|
+ formatter = get_formatter()
|
|
|
+ contracts_abs = abs(float(exchange_position.get('contracts', 0)))
|
|
|
+
|
|
|
+ if contracts_abs <= 1e-9:
|
|
|
+ return False
|
|
|
+
|
|
|
+ entry_price_from_exchange = float(exchange_position.get('entryPrice', 0)) or float(exchange_position.get('entryPx', 0))
|
|
|
+
|
|
|
+
|
|
|
+ position_side, order_side = '', ''
|
|
|
+ ccxt_side = exchange_position.get('side', '').lower()
|
|
|
+ if ccxt_side == 'long':
|
|
|
+ position_side, order_side = 'long', 'buy'
|
|
|
+ elif ccxt_side == 'short':
|
|
|
+ position_side, order_side = 'short', 'sell'
|
|
|
+
|
|
|
+ if not position_side:
|
|
|
+ contracts_val = float(exchange_position.get('contracts', 0))
|
|
|
+ if contracts_val > 1e-9:
|
|
|
+ position_side, order_side = 'long', 'buy'
|
|
|
+ elif contracts_val < -1e-9:
|
|
|
+ position_side, order_side = 'short', 'sell'
|
|
|
+ else:
|
|
|
+ return False
|
|
|
+
|
|
|
+ if not position_side:
|
|
|
+ logger.error(f"AUTO-SYNC: Could not determine position side for {symbol}.")
|
|
|
+ return False
|
|
|
+
|
|
|
+ final_entry_price = entry_price_from_exchange
|
|
|
+ if not final_entry_price or final_entry_price <= 0:
|
|
|
+
|
|
|
+ mark_price = float(exchange_position.get('markPrice', 0)) or float(exchange_position.get('markPx', 0))
|
|
|
+ if mark_price > 0:
|
|
|
+ final_entry_price = mark_price
|
|
|
+ else:
|
|
|
+ logger.error(f"AUTO-SYNC: Could not determine entry price for {symbol}.")
|
|
|
+ return False
|
|
|
+
|
|
|
+ logger.info(f"🔄 AUTO-SYNC: Creating lifecycle for {symbol} {position_side.upper()} {contracts_abs} @ {formatter.format_price_with_symbol(final_entry_price, symbol)}")
|
|
|
+
|
|
|
+ unique_sync_id = str(uuid.uuid4())[:8]
|
|
|
+ lifecycle_id = stats.create_trade_lifecycle(
|
|
|
+ symbol=symbol,
|
|
|
+ side=order_side,
|
|
|
+ entry_order_id=f"external_sync_{unique_sync_id}",
|
|
|
+ trade_type='external_sync'
|
|
|
+ )
|
|
|
+
|
|
|
+ if lifecycle_id:
|
|
|
+ success = stats.update_trade_position_opened(
|
|
|
+ lifecycle_id,
|
|
|
+ final_entry_price,
|
|
|
+ contracts_abs,
|
|
|
+ f"external_fill_sync_{unique_sync_id}"
|
|
|
+ )
|
|
|
+
|
|
|
+ if success:
|
|
|
+ logger.info(f"✅ AUTO-SYNC: Successfully synced position for {symbol} (Lifecycle: {lifecycle_id[:8]})")
|
|
|
+ return True
|
|
|
+ else:
|
|
|
+ logger.error(f"❌ AUTO-SYNC: Failed to update lifecycle to 'position_opened' for {symbol}")
|
|
|
+ else:
|
|
|
+ logger.error(f"❌ AUTO-SYNC: Failed to create lifecycle for {symbol}")
|
|
|
+
|
|
|
+ return False
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"❌ AUTO-SYNC: Error syncing position for {symbol}: {e}")
|
|
|
+ return False
|
|
|
+
|
|
|
async def _check_external_trades(self):
|
|
|
"""Check for trades made outside the Telegram bot and update stats."""
|
|
|
try:
|
|
@@ -481,6 +558,23 @@ class ExternalEventMonitor:
|
|
|
|
|
|
if not fill_processed_this_iteration:
|
|
|
existing_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
|
|
|
+
|
|
|
+
|
|
|
+ if not existing_lc:
|
|
|
+ current_positions = self.trading_engine.get_positions() or []
|
|
|
+ exchange_position = None
|
|
|
+ for pos in current_positions:
|
|
|
+ if pos.get('symbol') == full_symbol:
|
|
|
+ exchange_position = pos
|
|
|
+ break
|
|
|
+
|
|
|
+ if exchange_position and abs(float(exchange_position.get('contracts', 0))) > 1e-9:
|
|
|
+ logger.info(f"🔄 AUTO-SYNC: Position exists on exchange for {full_symbol} but no lifecycle found. Auto-syncing before processing fill.")
|
|
|
+ success = await self._auto_sync_single_position(full_symbol, exchange_position, stats)
|
|
|
+ if success:
|
|
|
+
|
|
|
+ existing_lc = stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
|
|
|
+
|
|
|
action_type = await self._determine_position_action_type(
|
|
|
full_symbol, side_from_fill, amount_from_fill, existing_lc
|
|
|
)
|
|
@@ -576,7 +670,7 @@ class ExternalEventMonitor:
|
|
|
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.")
|
|
|
+ logger.debug(f"Position {full_symbol} found in open positions but no active lifecycle - likely auto-sync failed or timing issue for fill {trade_id}")
|
|
|
|
|
|
|
|
|
linked_order_db_id = None
|