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

@@ -0,0 +1,99 @@
/**
* Rate Limit Analytics Endpoint
* GET /api/analytics/rate-limits
*
* View Drift RPC rate limit occurrences for monitoring and optimization
*/
import { NextResponse } from 'next/server'
import { getPrismaClient } from '@/lib/database/trades'
export async function GET() {
try {
const prisma = getPrismaClient()
// Get rate limit events from last 7 days
const sevenDaysAgo = new Date()
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
const rateLimitEvents = await prisma.systemEvent.findMany({
where: {
eventType: {
in: ['rate_limit_hit', 'rate_limit_recovered', 'rate_limit_exhausted']
},
createdAt: {
gte: sevenDaysAgo
}
},
orderBy: {
createdAt: 'desc'
},
take: 100
})
// Calculate statistics
const stats = {
total_hits: rateLimitEvents.filter(e => e.eventType === 'rate_limit_hit').length,
total_recovered: rateLimitEvents.filter(e => e.eventType === 'rate_limit_recovered').length,
total_exhausted: rateLimitEvents.filter(e => e.eventType === 'rate_limit_exhausted').length,
// Group by hour to see patterns
by_hour: {} as Record<number, number>,
// Average recovery time
avg_recovery_time_ms: 0,
max_recovery_time_ms: 0,
}
// Process recovery times
const recoveredEvents = rateLimitEvents.filter(e => e.eventType === 'rate_limit_recovered')
if (recoveredEvents.length > 0) {
const recoveryTimes = recoveredEvents
.map(e => (e.details as any)?.totalTimeMs)
.filter(t => typeof t === 'number')
if (recoveryTimes.length > 0) {
stats.avg_recovery_time_ms = recoveryTimes.reduce((a, b) => a + b, 0) / recoveryTimes.length
stats.max_recovery_time_ms = Math.max(...recoveryTimes)
}
}
// Group by hour
rateLimitEvents.forEach(event => {
const hour = event.createdAt.getHours()
stats.by_hour[hour] = (stats.by_hour[hour] || 0) + 1
})
return NextResponse.json({
success: true,
stats,
recent_events: rateLimitEvents.slice(0, 20).map(e => ({
type: e.eventType,
message: e.message,
details: e.details,
timestamp: e.createdAt.toISOString(),
})),
analysis: {
recovery_rate: stats.total_hits > 0
? `${((stats.total_recovered / stats.total_hits) * 100).toFixed(1)}%`
: 'N/A',
failure_rate: stats.total_hits > 0
? `${((stats.total_exhausted / stats.total_hits) * 100).toFixed(1)}%`
: 'N/A',
avg_recovery_time: stats.avg_recovery_time_ms > 0
? `${(stats.avg_recovery_time_ms / 1000).toFixed(1)}s`
: 'N/A',
max_recovery_time: stats.max_recovery_time_ms > 0
? `${(stats.max_recovery_time_ms / 1000).toFixed(1)}s`
: 'N/A',
}
})
} catch (error) {
console.error('❌ Rate limit analytics error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 500 })
}
}