fix: Handle multiple DB trades for single Drift position in validator

PROBLEM (User identified):
- Analytics showed 3 open trades when Drift UI showed only 1 position
- Database had 3 separate trade records all marked as 'open'
- Root cause: Drift has 1 POSITION + 3 ORDERS (TP/SL exit orders)
- Validator was incorrectly treating each as separate position

SOLUTION:
- Group database trades by symbol before validation
- If multiple DB trades exist for one Drift position, keep only MOST RECENT
- Close older duplicate trades with exitReason='DUPLICATE_CLEANUP'
- Properly handles: 1 Drift position → 1 DB trade (correct state)

RESULT:
- Database: 1 open trade (matches Drift reality)
- Analytics: Shows accurate position count
- Runs automatically every 10 minutes + manual trigger available
This commit is contained in:
mindesbunister
2025-11-16 21:36:21 +01:00
parent 66c3c42547
commit b813a38ae9

View File

@@ -112,7 +112,7 @@ export async function validateAllOpenTrades(): Promise<ValidationResult> {
let driftPositions let driftPositions
try { try {
driftPositions = await driftService.getAllPositions() driftPositions = await driftService.getAllPositions()
console.log(`📊 Found ${driftPositions.length} positions on Drift`) console.log(`📊 Found ${driftPositions.length} actual positions on Drift`)
} catch (error) { } catch (error) {
const errorMsg = `Failed to fetch Drift positions: ${error}` const errorMsg = `Failed to fetch Drift positions: ${error}`
console.error(`${errorMsg}`) console.error(`${errorMsg}`)
@@ -121,12 +121,20 @@ export async function validateAllOpenTrades(): Promise<ValidationResult> {
return result return result
} }
// Validate each database trade // Group database trades by symbol to handle multiple DB entries for same position
const tradesBySymbol = new Map<string, any[]>()
for (const trade of openTrades) { for (const trade of openTrades) {
const existing = tradesBySymbol.get(trade.symbol) || []
existing.push(trade)
tradesBySymbol.set(trade.symbol, existing)
}
// Validate each symbol's trades against Drift position
for (const [symbol, trades] of tradesBySymbol) {
try { try {
await validateSingleTrade(trade, driftPositions, result) await validateSymbolTrades(symbol, trades, driftPositions, result)
} catch (error) { } catch (error) {
const errorMsg = `Error validating ${trade.symbol}: ${error}` const errorMsg = `Error validating ${symbol}: ${error}`
console.error(`${errorMsg}`) console.error(`${errorMsg}`)
result.errors.push(errorMsg) result.errors.push(errorMsg)
} }
@@ -155,60 +163,97 @@ export async function validateAllOpenTrades(): Promise<ValidationResult> {
} }
/** /**
* Validate a single trade against Drift positions * Validate all trades for a symbol against the Drift position
*
* CRITICAL: Multiple DB trades can exist for one Drift position
* This happens because old trades get re-opened by startup validator
* Only the MOST RECENT trade should be kept open
*/ */
async function validateSingleTrade( async function validateSymbolTrades(
trade: any, symbol: string,
trades: any[],
driftPositions: any[], driftPositions: any[],
result: ValidationResult result: ValidationResult
): Promise<void> { ): Promise<void> {
const prisma = getPrismaClient() const prisma = getPrismaClient()
// Find matching Drift position // Find matching Drift position
const driftPosition = driftPositions.find(p => p.symbol === trade.symbol) const driftPosition = driftPositions.find(p => p.symbol === symbol)
if (!driftPosition || Math.abs(driftPosition.size) < 0.01) { if (!driftPosition || Math.abs(driftPosition.size) < 0.01) {
// GHOST DETECTED: Database says open, but Drift says closed/missing // NO POSITION ON DRIFT - all DB trades for this symbol are ghosts
console.log(`👻 GHOST DETECTED: ${trade.symbol} ${trade.direction}`) console.log(`👻 GHOST DETECTED: ${symbol} - ${trades.length} DB trades but no Drift position`)
console.log(` DB: Open since ${trade.createdAt.toISOString()}`)
console.log(` Drift: Position not found or size = 0`)
console.log(` P&L: ${trade.realizedPnL ? `$${trade.realizedPnL.toFixed(2)}` : 'null'}`)
// Check if this is a closed position with P&L but missing exitReason for (const trade of trades) {
const hasRealizedPnL = trade.realizedPnL !== null && trade.realizedPnL !== undefined const hasRealizedPnL = trade.realizedPnL !== null && trade.realizedPnL !== undefined
const exitReason = hasRealizedPnL ? 'manual' : 'GHOST_CLEANUP' const exitReason = hasRealizedPnL ? 'manual' : 'GHOST_CLEANUP'
const exitPrice = trade.exitPrice || trade.entryPrice const exitPrice = trade.exitPrice || trade.entryPrice
const realizedPnL = trade.realizedPnL || 0 const realizedPnL = trade.realizedPnL || 0
// Mark as closed in database await prisma.trade.update({
await prisma.trade.update({ where: { id: trade.id },
where: { id: trade.id }, data: {
data: { exitReason: exitReason,
exitReason: exitReason, exitTime: new Date(),
exitTime: new Date(), exitPrice: exitPrice,
exitPrice: exitPrice, realizedPnL: realizedPnL,
realizedPnL: realizedPnL, status: 'closed'
status: 'closed' }
} })
})
console.log(`Marked as closed (reason: ${exitReason})`) console.log(`Closed ghost trade ${trade.id} (${exitReason})`)
result.ghosts++ result.ghosts++
}
return return
} }
// Position exists on Drift - validate it matches // POSITION EXISTS ON DRIFT
const driftDirection = driftPosition.side.toLowerCase() const driftDirection = driftPosition.side.toLowerCase()
if (driftDirection !== trade.direction) { // Check if any trade has wrong direction
console.log(`⚠️ DIRECTION MISMATCH: ${trade.symbol}`) const wrongDirection = trades.find(t => t.direction !== driftDirection)
console.log(` DB: ${trade.direction} | Drift: ${driftDirection}`) if (wrongDirection) {
result.errors.push(`${trade.symbol}: Direction mismatch`) console.log(`⚠️ DIRECTION MISMATCH: ${symbol}`)
console.log(` DB: ${wrongDirection.direction} | Drift: ${driftDirection}`)
result.errors.push(`${symbol}: Direction mismatch`)
return return
} }
// Valid: DB and Drift both show position open with matching direction // If multiple trades exist for same symbol, keep only the MOST RECENT
result.valid++ if (trades.length > 1) {
console.log(`🔄 DUPLICATE TRADES: ${symbol} has ${trades.length} open DB entries for 1 Drift position`)
// Sort by creation time, keep newest
trades.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
const keepTrade = trades[0]
const closeTrades = trades.slice(1)
console.log(` ✅ Keeping: ${keepTrade.id} (${keepTrade.createdAt.toISOString()})`)
for (const trade of closeTrades) {
const hasRealizedPnL = trade.realizedPnL !== null && trade.realizedPnL !== undefined
const exitReason = hasRealizedPnL ? 'manual' : 'DUPLICATE_CLEANUP'
await prisma.trade.update({
where: { id: trade.id },
data: {
exitReason: exitReason,
exitTime: new Date(),
exitPrice: trade.exitPrice || trade.entryPrice,
realizedPnL: trade.realizedPnL || 0,
status: 'closed'
}
})
console.log(` 🗑️ Closed duplicate: ${trade.id} (${trade.createdAt.toISOString()})`)
result.ghosts++
}
result.valid++ // Count the one we kept
} else {
// Single trade matches single Drift position - all good
result.valid++
}
} }
/** /**