fix: Implement critical risk management fixes for bugs #76, #77, #78, #80

Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-09 22:23:43 +00:00
parent 2b0673636f
commit 63b94016fe
4 changed files with 326 additions and 131 deletions

View File

@@ -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())
}
}