|
@@ -81,88 +81,15 @@ class PositionTracker:
|
|
|
previous_positions = self.current_positions.copy()
|
|
|
await self._update_current_positions()
|
|
|
|
|
|
-
|
|
|
+
|
|
|
await self._process_position_changes(previous_positions, self.current_positions)
|
|
|
|
|
|
-
|
|
|
- await self._reconcile_database_with_exchange()
|
|
|
-
|
|
|
|
|
|
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:
|
|
|
-
|
|
|
- if self.trading_stats is None:
|
|
|
- from ..stats.trading_stats import TradingStats
|
|
|
- self.trading_stats = TradingStats()
|
|
|
-
|
|
|
-
|
|
|
- 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
|
|
|
-
|
|
|
-
|
|
|
- token = symbol.split('/')[0] if '/' in symbol else symbol
|
|
|
-
|
|
|
-
|
|
|
- if token not in self.current_positions:
|
|
|
-
|
|
|
- logger.warning(f"🔍 Found missed position closure: {symbol} exists in database but not on exchange")
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- 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()
|
|
|
-
|
|
|
-
|
|
|
- if side == "long":
|
|
|
- pnl = (current_price - entry_price) * size
|
|
|
- else:
|
|
|
- pnl = (entry_price - current_price) * size
|
|
|
-
|
|
|
-
|
|
|
- await self._save_position_stats(symbol, side, size, entry_price, current_price, pnl)
|
|
|
-
|
|
|
-
|
|
|
- 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:
|
|
@@ -315,45 +242,67 @@ class PositionTracker:
|
|
|
logger.error(f"Error handling position opened for {symbol}: {e}")
|
|
|
|
|
|
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:
|
|
|
-
|
|
|
+
|
|
|
+ if self.trading_stats is None:
|
|
|
+ from ..stats.trading_stats import TradingStats
|
|
|
+ self.trading_stats = TradingStats()
|
|
|
+
|
|
|
+
|
|
|
full_symbol = f"{symbol}/USDC:USDC"
|
|
|
|
|
|
-
|
|
|
+
|
|
|
+ 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"
|
|
|
+
|
|
|
+
|
|
|
market_data = self.hl_client.get_market_data(full_symbol)
|
|
|
if not market_data:
|
|
|
logger.error(f"Could not get market data for {full_symbol}")
|
|
|
return
|
|
|
|
|
|
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"
|
|
|
|
|
|
-
|
|
|
+
|
|
|
if side == "Long":
|
|
|
- pnl = (current_price - entry_price) * size
|
|
|
+ realized_pnl = (current_price - entry_price) * size
|
|
|
else:
|
|
|
- pnl = (entry_price - current_price) * size
|
|
|
-
|
|
|
-
|
|
|
- await self._save_position_stats(full_symbol, side, size, entry_price, current_price, pnl)
|
|
|
-
|
|
|
-
|
|
|
- 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
|
|
|
+
|
|
|
+
|
|
|
+ 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:
|
|
|
+
|
|
|
+ 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:
|
|
|
logger.error(f"Error handling position closed for {symbol}: {e}")
|