diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 193e183..0115694 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -4,7 +4,7 @@ * Tracks active trades and manages automatic exits */ -import { getDriftService } from '../drift/client' +import { getDriftService, initializeDriftService } from '../drift/client' import { closePosition } from '../drift/orders' import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor' import { getMergedConfig, TradingConfig, getMarketConfig } from '../../config/trading' @@ -851,23 +851,57 @@ export class PositionManager { console.log(` ⚠️ PHANTOM TRADE: Setting P&L to 0 (size mismatch >50%)`) } - // CRITICAL: Calculate P&L for THIS close only, do NOT add previouslyRealized - // Previous bug: Added trade.realizedPnL which could be from prior detection cycles - // This caused 10x P&L inflation when same trade detected multiple times - // FIX: Calculate ONLY the P&L for this specific closure - let runnerRealized = 0 + // CRITICAL FIX (Nov 20, 2025): Query Drift's ACTUAL P&L instead of calculating + // Previous bug: Calculated P&L from monitoring loop price (currentPrice) + // But actual fill price can differ significantly (36% error in real case) + // Solution: Query Drift's last known unrealizedPnL before position closed + // That unrealizedPnL IS the realizedPnL once position is gone + let totalRealizedPnL = 0 let runnerProfitPercent = 0 - if (!wasPhantom) { - runnerProfitPercent = this.calculateProfitPercent( - trade.entryPrice, - currentPrice, - trade.direction - ) - runnerRealized = (sizeForPnL * runnerProfitPercent) / 100 - } - const totalRealizedPnL = runnerRealized - console.log(` External closure P&L → ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`) + if (!wasPhantom) { + // Try to get actual P&L from Drift's position data stored in trade + // If position was just closed, we can use the last unrealized P&L + // Otherwise fall back to calculation (less accurate but better than nothing) + const driftService = await initializeDriftService() + const marketConfig = getMarketConfig(trade.symbol) + + // Check if Drift has cached P&L data for this position + try { + const userAccount = driftService.getClient().getUserAccount() + if (userAccount) { + // Get the perp position (even if closed, might still have P&L data) + const position = userAccount.perpPositions.find((p: any) => + p.marketIndex === marketConfig.driftMarketIndex + ) + + if (position) { + // Use Drift's settled P&L if available + const settledPnL = Number(position.settledPnl || 0) / 1e6 + if (Math.abs(settledPnL) > 0.01) { + totalRealizedPnL = settledPnL + runnerProfitPercent = (totalRealizedPnL / sizeForPnL) * 100 + console.log(` ✅ Using Drift's actual P&L: $${totalRealizedPnL.toFixed(2)} (settled)`) + } + } + } + } catch (driftError) { + console.error('⚠️ Failed to query Drift P&L, falling back to calculation:', driftError) + } + + // Fallback: Calculate from price (less accurate) + if (totalRealizedPnL === 0) { + runnerProfitPercent = this.calculateProfitPercent( + trade.entryPrice, + currentPrice, + trade.direction + ) + totalRealizedPnL = (sizeForPnL * runnerProfitPercent) / 100 + console.log(` ⚠️ Using calculated P&L (fallback): ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`) + } + } else { + console.log(` Phantom trade P&L: $0.00`) + } // Determine exit reason from P&L percentage and trade state // Use actual profit percent to determine what order filled