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:
@@ -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) {
|
|
||||||
runnerProfitPercent = this.calculateProfitPercent(
|
|
||||||
trade.entryPrice,
|
|
||||||
currentPrice,
|
|
||||||
trade.direction
|
|
||||||
)
|
|
||||||
runnerRealized = (sizeForPnL * runnerProfitPercent) / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalRealizedPnL = runnerRealized
|
if (!wasPhantom) {
|
||||||
console.log(` External closure P&L → ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`)
|
// 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
|
// 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
|
||||||
|
|||||||
Reference in New Issue
Block a user