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

View File

@@ -21,6 +21,7 @@ export interface ActiveTrade {
entryTime: number
positionSize: number
leverage: number
atrAtEntry?: number // ATR value at entry for ATR-based trailing stop
// Targets
stopLossPrice: number
@@ -761,6 +762,22 @@ export class PositionManager {
// Calculate how much to close based on TP2 size percent
const percentToClose = this.config.takeProfit2SizePercent
// CRITICAL FIX: If percentToClose is 0, don't call executeExit (would close 100% due to minOrderSize)
// Instead, just mark TP2 as hit and activate trailing stop on full remaining position
if (percentToClose === 0) {
trade.tp2Hit = true
trade.trailingStopActive = true // Activate trailing stop immediately
console.log(`🏃 TP2-as-Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`)
console.log(`📊 No position closed at TP2 - full ${trade.currentSize.toFixed(2)} USD remains as runner`)
// Save state after TP2
await this.saveTradeState(trade)
return
}
// If percentToClose > 0, execute partial close
await this.executeExit(trade, percentToClose, 'TP2', currentPrice)
// If some position remains, mark TP2 as hit and activate trailing stop
@@ -787,9 +804,34 @@ export class PositionManager {
// If trailing stop is active, adjust SL dynamically
if (trade.trailingStopActive) {
// Calculate ATR-based trailing distance
let trailingDistancePercent: number
if (trade.atrAtEntry && trade.atrAtEntry > 0) {
// ATR-based: Use ATR% * multiplier
const atrPercent = (trade.atrAtEntry / currentPrice) * 100
const rawDistance = atrPercent * this.config.trailingStopAtrMultiplier
// Clamp between min and max
trailingDistancePercent = Math.max(
this.config.trailingStopMinPercent,
Math.min(this.config.trailingStopMaxPercent, rawDistance)
)
console.log(`📊 ATR-based trailing: ${trade.atrAtEntry.toFixed(4)} (${atrPercent.toFixed(2)}%) × ${this.config.trailingStopAtrMultiplier}x = ${trailingDistancePercent.toFixed(2)}%`)
} else {
// Fallback to configured legacy percent with min/max clamping
trailingDistancePercent = Math.max(
this.config.trailingStopMinPercent,
Math.min(this.config.trailingStopMaxPercent, this.config.trailingStopPercent)
)
console.log(`⚠️ No ATR data, using fallback: ${trailingDistancePercent.toFixed(2)}%`)
}
const trailingStopPrice = this.calculatePrice(
trade.peakPrice,
-this.config.trailingStopPercent, // Trail below peak
-trailingDistancePercent, // Trail below peak
trade.direction
)
@@ -802,7 +844,7 @@ export class PositionManager {
const oldSL = trade.stopLossPrice
trade.stopLossPrice = trailingStopPrice
console.log(`📈 Trailing SL updated: ${oldSL.toFixed(4)}${trailingStopPrice.toFixed(4)} (${this.config.trailingStopPercent}% below peak $${trade.peakPrice.toFixed(4)})`)
console.log(`📈 Trailing SL updated: ${oldSL.toFixed(4)}${trailingStopPrice.toFixed(4)} (${trailingDistancePercent.toFixed(2)}% below peak $${trade.peakPrice.toFixed(4)})`)
// Save state after trailing SL update (every 10 updates to avoid spam)
if (trade.priceCheckCount % 10 === 0) {