Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
This commit is contained in:
@@ -31,6 +31,9 @@ class DriftStateVerifier {
|
||||
private isRunning: boolean = false
|
||||
private checkIntervalMs: number = 10 * 60 * 1000 // 10 minutes
|
||||
private intervalId: NodeJS.Timeout | null = null
|
||||
// BUG #80 FIX: Track close attempts per symbol to enforce cooldown
|
||||
private recentCloseAttempts: Map<string, number> = new Map()
|
||||
private readonly COOLDOWN_MS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
/**
|
||||
* Start the periodic verification service
|
||||
@@ -215,14 +218,30 @@ class DriftStateVerifier {
|
||||
|
||||
/**
|
||||
* Retry closing a position that should be closed but isn't
|
||||
* CRITICAL FIX (Dec 9, 2025): Stop retry loop if close transaction confirms
|
||||
* BUG #80 FIX: Enhanced cooldown enforcement to prevent retry loops
|
||||
*/
|
||||
private async retryClose(mismatch: DriftStateMismatch): Promise<void> {
|
||||
console.log(`🔄 Retrying close for ${mismatch.symbol}...`)
|
||||
|
||||
try {
|
||||
// CRITICAL: Check if this trade already has a close attempt in progress
|
||||
// If we recently tried to close (within 5 minutes), SKIP to avoid retry loop
|
||||
// BUG #80 FIX: Check in-memory cooldown map first (faster than DB query)
|
||||
const lastAttemptTime = this.recentCloseAttempts.get(mismatch.symbol)
|
||||
|
||||
if (lastAttemptTime) {
|
||||
const timeSinceAttempt = Date.now() - lastAttemptTime
|
||||
|
||||
if (timeSinceAttempt < this.COOLDOWN_MS) {
|
||||
const remainingCooldown = Math.ceil((this.COOLDOWN_MS - timeSinceAttempt) / 1000)
|
||||
console.log(` ⏸️ COOLDOWN ACTIVE: Last attempt ${(timeSinceAttempt / 1000).toFixed(0)}s ago`)
|
||||
console.log(` ⏳ Must wait ${remainingCooldown}s more before retry (5min cooldown)`)
|
||||
console.log(` 📊 Cooldown map state: ${Array.from(this.recentCloseAttempts.entries()).map(([s, t]) => `${s}:${Date.now()-t}ms`).join(', ')}`)
|
||||
return
|
||||
} else {
|
||||
console.log(` ✅ Cooldown expired (${(timeSinceAttempt / 1000).toFixed(0)}s since last attempt)`)
|
||||
}
|
||||
}
|
||||
|
||||
// ALSO check database for persistent cooldown tracking (survives restarts)
|
||||
const prisma = getPrismaClient()
|
||||
const trade = await prisma.trade.findUnique({
|
||||
where: { id: mismatch.tradeId },
|
||||
@@ -241,13 +260,23 @@ class DriftStateVerifier {
|
||||
const timeSinceRetry = Date.now() - lastRetryTime.getTime()
|
||||
|
||||
// If we retried within last 5 minutes, SKIP (Drift propagation delay)
|
||||
if (timeSinceRetry < 5 * 60 * 1000) {
|
||||
console.log(` ⏳ Skipping retry - last attempt ${(timeSinceRetry / 1000).toFixed(0)}s ago (Drift propagation delay)`)
|
||||
if (timeSinceRetry < this.COOLDOWN_MS) {
|
||||
console.log(` ⏸️ DATABASE COOLDOWN: Last DB retry ${(timeSinceRetry / 1000).toFixed(0)}s ago`)
|
||||
console.log(` ⏳ Drift propagation delay - skipping retry`)
|
||||
|
||||
// Update in-memory map to match DB state
|
||||
this.recentCloseAttempts.set(mismatch.symbol, lastRetryTime.getTime())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` 🚀 Proceeding with close attempt...`)
|
||||
|
||||
// Record attempt time BEFORE calling closePosition
|
||||
const attemptTime = Date.now()
|
||||
this.recentCloseAttempts.set(mismatch.symbol, attemptTime)
|
||||
|
||||
const result = await closePosition({
|
||||
symbol: mismatch.symbol,
|
||||
percentToClose: 100,
|
||||
@@ -268,15 +297,20 @@ class DriftStateVerifier {
|
||||
configSnapshot: {
|
||||
...trade?.configSnapshot as any,
|
||||
retryCloseAttempted: true,
|
||||
retryCloseTime: new Date().toISOString(),
|
||||
retryCloseTime: new Date(attemptTime).toISOString(),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log(` 📝 Cooldown recorded: ${mismatch.symbol} → ${new Date(attemptTime).toISOString()}`)
|
||||
} else {
|
||||
console.error(` ❌ Failed to close ${mismatch.symbol}: ${result.error}`)
|
||||
// Keep cooldown even on failure to prevent spam
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error retrying close for ${mismatch.symbol}:`, error)
|
||||
// On error, still record attempt time to prevent rapid retries
|
||||
this.recentCloseAttempts.set(mismatch.symbol, Date.now())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user