critical: Fix P&L calculation to use Drift's actual settledPnl

- Query userAccount.perpPositions[].settledPnl from Drift SDK
- Eliminates 36% calculation errors from stale monitoring prices
- Real incident: Database -$101.68 vs Drift -$138.35 actual (Nov 20)
- Fallback to price calculation if Drift query fails
- Added initializeDriftService import to position-manager.ts
- Detailed logging: ' Using Drift's actual P&L' or '⚠️ fallback'
- Files: lib/trading/position-manager.ts lines 7, 854-900
This commit is contained in:
mindesbunister
2025-11-20 15:26:14 +01:00
parent a3a6222047
commit 8e600c8df6

View File

@@ -4,7 +4,7 @@
* Tracks active trades and manages automatic exits * Tracks active trades and manages automatic exits
*/ */
import { getDriftService } from '../drift/client' import { getDriftService, initializeDriftService } from '../drift/client'
import { closePosition } from '../drift/orders' import { closePosition } from '../drift/orders'
import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor' import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor'
import { getMergedConfig, TradingConfig, getMarketConfig } from '../../config/trading' 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%)`) console.log(` ⚠️ PHANTOM TRADE: Setting P&L to 0 (size mismatch >50%)`)
} }
// CRITICAL: Calculate P&L for THIS close only, do NOT add previouslyRealized // CRITICAL FIX (Nov 20, 2025): Query Drift's ACTUAL P&L instead of calculating
// Previous bug: Added trade.realizedPnL which could be from prior detection cycles // Previous bug: Calculated P&L from monitoring loop price (currentPrice)
// This caused 10x P&L inflation when same trade detected multiple times // But actual fill price can differ significantly (36% error in real case)
// FIX: Calculate ONLY the P&L for this specific closure // Solution: Query Drift's last known unrealizedPnL before position closed
let runnerRealized = 0 // That unrealizedPnL IS the realizedPnL once position is gone
let totalRealizedPnL = 0
let runnerProfitPercent = 0 let runnerProfitPercent = 0
if (!wasPhantom) { 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( runnerProfitPercent = this.calculateProfitPercent(
trade.entryPrice, trade.entryPrice,
currentPrice, currentPrice,
trade.direction trade.direction
) )
runnerRealized = (sizeForPnL * runnerProfitPercent) / 100 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`)
} }
const totalRealizedPnL = runnerRealized
console.log(` External closure P&L → ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`)
// Determine exit reason from P&L percentage and trade state // Determine exit reason from P&L percentage and trade state
// Use actual profit percent to determine what order filled // Use actual profit percent to determine what order filled