Fix: Signal flip race condition - properly coordinate Position Manager during opposite signal closure

- Remove trade from Position Manager BEFORE closing Drift position (prevents race condition)
- Explicitly save closure to database with proper P&L calculation
- Mark flipped positions as 'manual' exit reason
- Increase delay from 1s to 2s for better on-chain confirmation
- Preserve MAE/MFE data in closure records

Fixes issue where SHORT signal would close LONG but not properly track the new SHORT position.
Database now correctly records both old position closure and new position opening.
This commit is contained in:
mindesbunister
2025-11-03 20:23:42 +01:00
parent 1426a9ec2f
commit f682b93a1e
2 changed files with 202 additions and 6 deletions

View File

@@ -11,7 +11,7 @@ import { openPosition, placeExitOrders } from '@/lib/drift/orders'
import { normalizeTradingViewSymbol } from '@/config/trading'
import { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
import { createTrade } from '@/lib/database/trades'
import { createTrade, updateTradeExit } from '@/lib/database/trades'
/**
* Calculate signal quality score (same logic as check-risk endpoint)
@@ -294,7 +294,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
if (oppositePosition) {
console.log(`🔄 Signal flip detected! Closing ${oppositePosition.direction} to open ${body.direction}`)
// Close opposite position
// CRITICAL: Remove from Position Manager FIRST to prevent race condition
// where Position Manager detects "external closure" while we're deliberately closing it
console.log(`🗑️ Removing ${oppositePosition.direction} position from Position Manager before flip...`)
await positionManager.removeTrade(oppositePosition.id)
console.log(`✅ Removed from Position Manager`)
// Close opposite position on Drift
const { closePosition } = await import('@/lib/drift/orders')
const closeResult = await closePosition({
symbol: driftSymbol,
@@ -308,12 +314,35 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
} else {
console.log(`✅ Closed ${oppositePosition.direction} position at $${closeResult.closePrice?.toFixed(4)} (P&L: $${closeResult.realizedPnL?.toFixed(2)})`)
// Position Manager will handle cleanup (including order cancellation)
// The executeExit method already removes the trade and updates database
// Save the closure to database
try {
const holdTimeSeconds = Math.floor((Date.now() - oppositePosition.entryTime) / 1000)
const profitPercent = ((closeResult.closePrice! - oppositePosition.entryPrice) / oppositePosition.entryPrice) * 100
const accountPnL = profitPercent * oppositePosition.leverage * (oppositePosition.direction === 'long' ? 1 : -1)
const realizedPnL = (oppositePosition.currentSize * accountPnL) / 100
await updateTradeExit({
positionId: oppositePosition.positionId,
exitPrice: closeResult.closePrice!,
exitReason: 'manual', // Manually closed for flip
realizedPnL: realizedPnL,
exitOrderTx: closeResult.transactionSignature || 'FLIP_CLOSE',
holdTimeSeconds,
maxDrawdown: Math.abs(Math.min(0, oppositePosition.maxAdverseExcursion)),
maxGain: Math.max(0, oppositePosition.maxFavorableExcursion),
maxFavorableExcursion: oppositePosition.maxFavorableExcursion,
maxAdverseExcursion: oppositePosition.maxAdverseExcursion,
maxFavorablePrice: oppositePosition.maxFavorablePrice,
maxAdversePrice: oppositePosition.maxAdversePrice,
})
console.log(`💾 Saved opposite position closure to database`)
} catch (dbError) {
console.error('❌ Failed to save opposite position closure:', dbError)
}
}
// Small delay to ensure position is fully closed
await new Promise(resolve => setTimeout(resolve, 1000))
// Small delay to ensure position is fully closed on-chain
await new Promise(resolve => setTimeout(resolve, 2000))
}
// Calculate position size with leverage