|
@@ -207,22 +207,18 @@ class PositionMonitor:
|
|
|
pnl_emoji = "🟢" if realized_pnl and realized_pnl >= 0 else "🔴"
|
|
|
pnl_text = f"{await formatter.format_price_with_symbol(realized_pnl)}" if realized_pnl is not None else "N/A"
|
|
|
|
|
|
- # Get ROE directly from exchange data
|
|
|
- info_data = existing_lc.get('info', {})
|
|
|
- position_info = info_data.get('position', {})
|
|
|
- roe_raw = position_info.get('returnOnEquity') # Changed from 'percentage' to 'returnOnEquity'
|
|
|
-
|
|
|
- if roe_raw is not None:
|
|
|
+ # Use the last known ROE from heartbeat data (stored in lifecycle)
|
|
|
+ stored_roe = existing_lc.get('roe_percentage')
|
|
|
+ if stored_roe is not None:
|
|
|
try:
|
|
|
- # The exchange provides ROE as a decimal (e.g., -0.326 for -32.6%)
|
|
|
- # We need to multiply by 100 and keep the sign
|
|
|
- roe = float(roe_raw) * 100
|
|
|
+ roe = float(stored_roe)
|
|
|
roe_text = f" ({roe:+.2f}%)"
|
|
|
+ logger.debug(f"Using stored ROE from heartbeat for {full_symbol}: {roe:+.2f}%")
|
|
|
except (ValueError, TypeError):
|
|
|
- logger.warning(f"Could not parse ROE value: {roe_raw} for {full_symbol}")
|
|
|
+ logger.warning(f"Could not parse stored ROE value: {stored_roe} for {full_symbol}")
|
|
|
roe_text = ""
|
|
|
else:
|
|
|
- logger.warning(f"No ROE data available from exchange for {full_symbol}")
|
|
|
+ logger.debug(f"No stored ROE available for {full_symbol}")
|
|
|
roe_text = ""
|
|
|
|
|
|
message = f"""
|
|
@@ -970,19 +966,32 @@ class PositionMonitor:
|
|
|
logger.error(f"❌ Error handling position opened for {symbol}: {e}")
|
|
|
|
|
|
async def _handle_position_closed(self, symbol: str, db_pos: Dict, stats, timestamp: datetime):
|
|
|
- """Handle position closure detection."""
|
|
|
+ """Handle position closed during reconciliation."""
|
|
|
try:
|
|
|
lifecycle_id = db_pos['trade_lifecycle_id']
|
|
|
entry_price = db_pos.get('entry_price', 0)
|
|
|
position_side = db_pos.get('position_side')
|
|
|
size = db_pos.get('current_position_size', 0)
|
|
|
|
|
|
- # Estimate exit price (could be improved with recent fills)
|
|
|
- market_data = await self.trading_engine.get_market_data(symbol)
|
|
|
- exit_price = entry_price # Fallback
|
|
|
- if market_data and market_data.get('ticker'):
|
|
|
- exit_price = float(market_data['ticker'].get('last', exit_price))
|
|
|
+ # Get the latest lifecycle status to check if it was already closed
|
|
|
+ latest_lifecycle = stats.get_trade_by_lifecycle_id(lifecycle_id)
|
|
|
+ if latest_lifecycle and latest_lifecycle.get('status') == 'position_closed':
|
|
|
+ logger.info(f"ℹ️ Position for {symbol} already marked as closed in lifecycle {lifecycle_id[:8]}. Skipping duplicate close processing.")
|
|
|
+ return
|
|
|
|
|
|
+ # Estimate exit price from market data
|
|
|
+ exit_price = 0
|
|
|
+ try:
|
|
|
+ market_data = await self.trading_engine.get_symbol_data(symbol)
|
|
|
+ if market_data and 'markPrice' in market_data:
|
|
|
+ exit_price = float(market_data['markPrice'])
|
|
|
+ else:
|
|
|
+ logger.warning(f"⚠️ Could not get exit price for {symbol} - using entry price")
|
|
|
+ exit_price = entry_price
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning(f"⚠️ Error fetching market data for {symbol}: {e} - using entry price")
|
|
|
+ exit_price = entry_price
|
|
|
+
|
|
|
# Calculate realized PnL
|
|
|
realized_pnl = 0
|
|
|
if position_side == 'long':
|
|
@@ -1018,6 +1027,7 @@ class PositionMonitor:
|
|
|
'exit_price': exit_price,
|
|
|
'realized_pnl': realized_pnl,
|
|
|
'timestamp': timestamp,
|
|
|
+ 'lifecycle_id': lifecycle_id, # Pass lifecycle_id to get stored ROE
|
|
|
'info': exchange_pos.get('info', {}) if exchange_pos else {}
|
|
|
})
|
|
|
|
|
@@ -1176,22 +1186,26 @@ class PositionMonitor:
|
|
|
pnl = details['realized_pnl']
|
|
|
pnl_emoji = "🟢" if pnl >= 0 else "🔴"
|
|
|
|
|
|
- # Get ROE directly from exchange data
|
|
|
- info_data = details.get('info', {})
|
|
|
- position_info = info_data.get('position', {})
|
|
|
- roe_raw = position_info.get('returnOnEquity') # Changed from 'percentage' to 'returnOnEquity'
|
|
|
+ # Use the last known ROE from heartbeat data (stored in lifecycle)
|
|
|
+ stored_roe = None
|
|
|
+ lifecycle_id = details.get('lifecycle_id')
|
|
|
+ if lifecycle_id:
|
|
|
+ stats = self.trading_engine.get_stats()
|
|
|
+ if stats:
|
|
|
+ lifecycle = stats.get_trade_by_lifecycle_id(lifecycle_id)
|
|
|
+ if lifecycle:
|
|
|
+ stored_roe = lifecycle.get('roe_percentage')
|
|
|
|
|
|
- if roe_raw is not None:
|
|
|
+ if stored_roe is not None:
|
|
|
try:
|
|
|
- # The exchange provides ROE as a decimal (e.g., -0.326 for -32.6%)
|
|
|
- # We need to multiply by 100 and keep the sign
|
|
|
- roe = float(roe_raw) * 100
|
|
|
+ roe = float(stored_roe)
|
|
|
roe_text = f"({roe:+.2f}%)"
|
|
|
+ logger.debug(f"Using stored ROE from heartbeat for reconciled {symbol}: {roe:+.2f}%")
|
|
|
except (ValueError, TypeError):
|
|
|
- logger.warning(f"Could not parse ROE value: {roe_raw} for {symbol}")
|
|
|
+ logger.warning(f"Could not parse stored ROE value: {stored_roe} for {symbol}")
|
|
|
roe_text = ""
|
|
|
else:
|
|
|
- logger.warning(f"No ROE data available from exchange for {symbol}")
|
|
|
+ logger.debug(f"No stored ROE available for reconciled {symbol}")
|
|
|
roe_text = ""
|
|
|
|
|
|
message = f"""🎯 <b>Position Closed (Reconciled)</b>
|