|
@@ -81,88 +81,15 @@ class PositionTracker:
|
|
previous_positions = self.current_positions.copy()
|
|
previous_positions = self.current_positions.copy()
|
|
await self._update_current_positions()
|
|
await self._update_current_positions()
|
|
|
|
|
|
- # Compare with previous positions (normal tracking)
|
|
|
|
|
|
+ # Compare with previous positions (simple exchange state tracking)
|
|
await self._process_position_changes(previous_positions, self.current_positions)
|
|
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
|
|
# Update database with current market data for open positions
|
|
await self._update_database_market_data()
|
|
await self._update_database_market_data()
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
logger.error(f"Error checking position changes: {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):
|
|
async def _update_database_market_data(self):
|
|
"""Update database with current market data for open positions"""
|
|
"""Update database with current market data for open positions"""
|
|
try:
|
|
try:
|
|
@@ -315,45 +242,67 @@ class PositionTracker:
|
|
logger.error(f"Error handling position opened for {symbol}: {e}")
|
|
logger.error(f"Error handling position opened for {symbol}: {e}")
|
|
|
|
|
|
async def _handle_position_closed(self, symbol: str, position: Dict):
|
|
async def _handle_position_closed(self, symbol: str, position: Dict):
|
|
- """Handle position closed - save stats to database"""
|
|
|
|
|
|
+ """Handle position closed - find and close the corresponding database trade"""
|
|
try:
|
|
try:
|
|
- # Construct full symbol format for market data (symbol here is just token name like "BTC")
|
|
|
|
|
|
+ # Lazy load TradingStats if needed
|
|
|
|
+ if self.trading_stats is None:
|
|
|
|
+ from ..stats.trading_stats import TradingStats
|
|
|
|
+ self.trading_stats = TradingStats()
|
|
|
|
+
|
|
|
|
+ # Construct full symbol format (symbol here is just token name like "BTC")
|
|
full_symbol = f"{symbol}/USDC:USDC"
|
|
full_symbol = f"{symbol}/USDC:USDC"
|
|
|
|
|
|
- # Get current market price for PnL calculation
|
|
|
|
|
|
+ # Find the open trade in database for this symbol
|
|
|
|
+ open_trade = self.trading_stats.get_trade_by_symbol_and_status(full_symbol, 'position_opened')
|
|
|
|
+
|
|
|
|
+ if not open_trade:
|
|
|
|
+ logger.warning(f"No open trade found in database for {full_symbol} - position was closed on exchange but no database record")
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ lifecycle_id = open_trade['trade_lifecycle_id']
|
|
|
|
+ entry_price = position['entry_px']
|
|
|
|
+ size = abs(position['size'])
|
|
|
|
+ side = "Long" if position['size'] > 0 else "Short"
|
|
|
|
+
|
|
|
|
+ # Get current market price for exit calculation
|
|
market_data = self.hl_client.get_market_data(full_symbol)
|
|
market_data = self.hl_client.get_market_data(full_symbol)
|
|
if not market_data:
|
|
if not market_data:
|
|
logger.error(f"Could not get market data for {full_symbol}")
|
|
logger.error(f"Could not get market data for {full_symbol}")
|
|
return
|
|
return
|
|
|
|
|
|
current_price = float(market_data.get('ticker', {}).get('last', 0))
|
|
current_price = float(market_data.get('ticker', {}).get('last', 0))
|
|
- entry_price = position['entry_px']
|
|
|
|
- size = abs(position['size'])
|
|
|
|
- side = "Long" if position['size'] > 0 else "Short"
|
|
|
|
|
|
|
|
- # Calculate PnL
|
|
|
|
|
|
+ # Calculate realized PnL
|
|
if side == "Long":
|
|
if side == "Long":
|
|
- pnl = (current_price - entry_price) * size
|
|
|
|
|
|
+ realized_pnl = (current_price - entry_price) * size
|
|
else:
|
|
else:
|
|
- pnl = (entry_price - current_price) * size
|
|
|
|
-
|
|
|
|
- # Save to database with full symbol format
|
|
|
|
- await self._save_position_stats(full_symbol, side, size, entry_price, current_price, pnl)
|
|
|
|
-
|
|
|
|
- # Send notification
|
|
|
|
- pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
|
- message = (
|
|
|
|
- f"{pnl_emoji} Position Closed\n"
|
|
|
|
- f"Token: {symbol}\n"
|
|
|
|
- f"Side: {side}\n"
|
|
|
|
- f"Size: {size:.4f}\n"
|
|
|
|
- f"Entry: ${entry_price:.4f}\n"
|
|
|
|
- f"Exit: ${current_price:.4f}\n"
|
|
|
|
- f"PnL: ${pnl:.3f}"
|
|
|
|
|
|
+ realized_pnl = (entry_price - current_price) * size
|
|
|
|
+
|
|
|
|
+ # Close the trade in database
|
|
|
|
+ success = await self.trading_stats.update_trade_position_closed(
|
|
|
|
+ lifecycle_id=lifecycle_id,
|
|
|
|
+ exit_price=current_price,
|
|
|
|
+ realized_pnl=realized_pnl,
|
|
|
|
+ exchange_fill_id="position_tracker_detected_closure"
|
|
)
|
|
)
|
|
|
|
|
|
- await self.notification_manager.send_generic_notification(message)
|
|
|
|
- logger.info(f"Position closed: {symbol} {side} PnL: ${pnl:.3f}")
|
|
|
|
|
|
+ if success:
|
|
|
|
+ # Send clean notification
|
|
|
|
+ pnl_emoji = "🟢" if realized_pnl >= 0 else "🔴"
|
|
|
|
+ message = (
|
|
|
|
+ f"{pnl_emoji} Position Closed\n"
|
|
|
|
+ f"Token: {symbol}\n"
|
|
|
|
+ f"Side: {side}\n"
|
|
|
|
+ f"Size: {size:.4f}\n"
|
|
|
|
+ f"Entry: ${entry_price:.4f}\n"
|
|
|
|
+ f"Exit: ${current_price:.4f}\n"
|
|
|
|
+ f"PnL: ${realized_pnl:.3f}"
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ await self.notification_manager.send_generic_notification(message)
|
|
|
|
+ logger.info(f"Position closed: {symbol} {side} PnL: ${realized_pnl:.3f}")
|
|
|
|
+ else:
|
|
|
|
+ logger.error(f"Failed to close trade {lifecycle_id} for {symbol}")
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
logger.error(f"Error handling position closed for {symbol}: {e}")
|
|
logger.error(f"Error handling position closed for {symbol}: {e}")
|