feat: ATR-based trailing stop + rate limit monitoring

MAJOR FIXES:
- ATR-based trailing stop for runners (was fixed 0.3%, now adapts to volatility)
- Fixes runners with +7-9% MFE exiting for losses
- Typical improvement: 2.24x more room (0.3% → 0.67% at 0.45% ATR)
- Enhanced rate limit logging with database tracking
- New /api/analytics/rate-limits endpoint for monitoring

DETAILS:
- Position Manager: Calculate trailing as (atrAtEntry / price × 100) × multiplier
- Config: TRAILING_STOP_ATR_MULTIPLIER=1.5, MIN=0.25%, MAX=0.9%
- Settings UI: Added ATR multiplier controls
- Rate limits: Log hits/recoveries/exhaustions to SystemEvent table
- Documentation: ATR_TRAILING_STOP_FIX.md + RATE_LIMIT_MONITORING.md

IMPACT:
- Runners can now capture big moves (like morning's $172→$162 SOL drop)
- Rate limit visibility prevents silent failures
- Data-driven optimization for RPC endpoint health
This commit is contained in:
mindesbunister
2025-11-11 14:51:41 +01:00
parent 0700daf8ff
commit 03e91fc18d
9 changed files with 577 additions and 7 deletions

View File

@@ -632,19 +632,76 @@ async function retryWithBackoff<T>(
maxRetries: number = 3,
baseDelay: number = 2000
): Promise<T> {
const startTime = Date.now()
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
const result = await fn()
// Log successful execution time for rate limit monitoring
if (attempt > 0) {
const totalTime = Date.now() - startTime
console.log(`✅ Retry successful after ${totalTime}ms (${attempt} retries)`)
// Log to database for analytics
try {
const { logSystemEvent } = await import('../database/trades')
await logSystemEvent('rate_limit_recovered', 'Drift RPC rate limit recovered after retries', {
retriesNeeded: attempt,
totalTimeMs: totalTime,
recoveredAt: new Date().toISOString(),
})
} catch (dbError) {
console.error('Failed to log rate limit recovery:', dbError)
}
}
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const isRateLimit = errorMessage.includes('429') || errorMessage.includes('rate limit')
if (!isRateLimit || attempt === maxRetries) {
// Log final failure with full context
if (isRateLimit && attempt === maxRetries) {
const totalTime = Date.now() - startTime
console.error(`❌ RATE LIMIT EXHAUSTED: Failed after ${maxRetries} retries and ${totalTime}ms`)
console.error(` Error: ${errorMessage}`)
// Log to database for analytics
try {
const { logSystemEvent } = await import('../database/trades')
await logSystemEvent('rate_limit_exhausted', 'Drift RPC rate limit exceeded max retries', {
maxRetries,
totalTimeMs: totalTime,
errorMessage: errorMessage.substring(0, 500),
failedAt: new Date().toISOString(),
})
} catch (dbError) {
console.error('Failed to log rate limit exhaustion:', dbError)
}
}
throw error
}
const delay = baseDelay * Math.pow(2, attempt)
console.log(`⏳ Rate limited, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${maxRetries})`)
console.log(`⏳ Rate limited (429), retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${maxRetries})`)
console.log(` Error context: ${errorMessage.substring(0, 100)}`)
// Log rate limit hit to database
try {
const { logSystemEvent } = await import('../database/trades')
await logSystemEvent('rate_limit_hit', 'Drift RPC rate limit encountered', {
attempt: attempt + 1,
maxRetries,
delayMs: delay,
errorSnippet: errorMessage.substring(0, 200),
hitAt: new Date().toISOString(),
})
} catch (dbError) {
console.error('Failed to log rate limit hit:', dbError)
}
await new Promise(resolve => setTimeout(resolve, delay))
}
}