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
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
/**
|
|
* 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 })
|
|
}
|
|
}
|