|
@@ -81,15 +81,88 @@ class PositionTracker:
|
|
|
previous_positions = self.current_positions.copy()
|
|
|
await self._update_current_positions()
|
|
|
|
|
|
- # Compare with previous positions
|
|
|
+ # Compare with previous positions (normal tracking)
|
|
|
await self._process_position_changes(previous_positions, self.current_positions)
|
|
|
|
|
|
+ # IMPORTANT: Also reconcile with database state to catch missed positions
|
|
|
+ await self._reconcile_database_with_exchange()
|
|
|
+
|
|
|
# Update database with current market data for open positions
|
|
|
await self._update_database_market_data()
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error checking position changes: {e}")
|
|
|
|
|
|
+ async def _reconcile_database_with_exchange(self):
|
|
|
+ """Reconcile database state with exchange state to catch missed position closures"""
|
|
|
+ try:
|
|
|
+ # Lazy load TradingStats if needed
|
|
|
+ if self.trading_stats is None:
|
|
|
+ from ..stats.trading_stats import TradingStats
|
|
|
+ self.trading_stats = TradingStats()
|
|
|
+
|
|
|
+ # Get open trades from database
|
|
|
+ open_trades = self.trading_stats.get_open_positions()
|
|
|
+ logger.debug(f"🔍 Reconciling: Database shows {len(open_trades)} open positions, Exchange shows {len(self.current_positions)}")
|
|
|
+
|
|
|
+ for trade in open_trades:
|
|
|
+ try:
|
|
|
+ symbol = trade.get('symbol', '')
|
|
|
+ if not symbol:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Extract token from symbol (e.g., "BTC/USDC:USDC" -> "BTC")
|
|
|
+ token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
+
|
|
|
+ # Check if this database position exists on the exchange
|
|
|
+ if token not in self.current_positions:
|
|
|
+ # Position exists in database but NOT on exchange = it was closed and we missed it!
|
|
|
+ logger.warning(f"🔍 Found missed position closure: {symbol} exists in database but not on exchange")
|
|
|
+
|
|
|
+ # We need to simulate the position closure
|
|
|
+ # Get current market price to calculate exit PnL
|
|
|
+ market_data = self.hl_client.get_market_data(symbol)
|
|
|
+ if not market_data:
|
|
|
+ logger.error(f"Could not get market data for {symbol} to process missed closure")
|
|
|
+ continue
|
|
|
+
|
|
|
+ current_price = float(market_data.get('ticker', {}).get('last', 0))
|
|
|
+ entry_price = float(trade.get('entry_price', 0))
|
|
|
+ size = float(trade.get('amount', 0))
|
|
|
+ side = trade.get('side', '').lower()
|
|
|
+
|
|
|
+ # Calculate PnL
|
|
|
+ if side == "long":
|
|
|
+ pnl = (current_price - entry_price) * size
|
|
|
+ else:
|
|
|
+ pnl = (entry_price - current_price) * size
|
|
|
+
|
|
|
+ # Close the position in database
|
|
|
+ await self._save_position_stats(symbol, side, size, entry_price, current_price, pnl)
|
|
|
+
|
|
|
+ # Send notification about missed closure
|
|
|
+ pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
+ message = (
|
|
|
+ f"{pnl_emoji} Position Closed (Missed)\n"
|
|
|
+ f"Token: {token}\n"
|
|
|
+ f"Side: {side.title()}\n"
|
|
|
+ f"Size: {size:.4f}\n"
|
|
|
+ f"Entry: ${entry_price:.4f}\n"
|
|
|
+ f"Exit: ${current_price:.4f}\n"
|
|
|
+ f"PnL: ${pnl:.3f}\n"
|
|
|
+ f"⚠️ Closed between monitoring cycles"
|
|
|
+ )
|
|
|
+
|
|
|
+ await self.notification_manager.send_generic_notification(message)
|
|
|
+ logger.info(f"📊 Processed missed position closure: {symbol} side={side} PnL=${pnl:.3f}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"Error processing missed closure for trade {trade.get('trade_lifecycle_id', 'unknown')}: {e}")
|
|
|
+ continue
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error reconciling database with exchange: {e}")
|
|
|
+
|
|
|
async def _update_database_market_data(self):
|
|
|
"""Update database with current market data for open positions"""
|
|
|
try:
|