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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user