Files
trading_bot_v4/ATR_TRAILING_STOP_FIX.md
mindesbunister 03e91fc18d 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
2025-11-11 14:51:41 +01:00

5.3 KiB
Raw Blame History

ATR-Based Trailing Stop Fix - Nov 11, 2025

Problem Identified

Critical Bug: Runner system was using FIXED 0.3% trailing stop, causing profitable runners to exit immediately.

Evidence:

  • Recent trades showing MFE of +7-9% but exiting for losses or minimal gains
  • Example: Entry $167.82, MFE +7.01%, exit $168.91 for -$2.68 loss
  • At $168 SOL price: 0.3% = only $0.50 wiggle room before stop hits
  • Normal price volatility easily triggers 0.3% retracement

Documentation Claim vs Reality:

  • Docs claimed "ATR-based trailing stop"
  • Code was using this.config.trailingStopPercent (fixed 0.3%)
  • Config already had trailingStopAtrMultiplier parameter but it wasn't being used!

Solution Implemented

1. Position Manager Update (lib/trading/position-manager.ts)

Changed trailing stop calculation from fixed to ATR-based:

// OLD (BROKEN):
const trailingStopPrice = this.calculatePrice(
  trade.peakPrice,
  -this.config.trailingStopPercent, // Fixed 0.3%
  trade.direction
)

// NEW (FIXED):
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)
  )
} else {
  // Fallback to configured percent with clamping
  trailingDistancePercent = Math.max(
    this.config.trailingStopMinPercent,
    Math.min(this.config.trailingStopMaxPercent, this.config.trailingStopPercent)
  )
}

2. Added atrAtEntry to ActiveTrade Interface

export interface ActiveTrade {
  // Entry details
  entryPrice: number
  entryTime: number
  positionSize: number
  leverage: number
  atrAtEntry?: number // NEW: ATR value at entry for ATR-based trailing stop
  // ...
}

3. Settings UI Updates (app/settings/page.tsx)

Added new fields for ATR trailing configuration:

  • ATR Trailing Multiplier (1.0-3.0x, default 1.5x)
  • Min Trailing Distance (0.1-1.0%, default 0.25%)
  • Max Trailing Distance (0.5-2.0%, default 0.9%)
  • Changed "Trailing Stop Distance" label to "[FALLBACK]"

4. Environment Variables (.env.example)

# ATR-based Trailing Stop (for 25% runner after TP2)
# Trailing distance = (ATR × multiplier)
# Example: 0.5% ATR × 1.5 = 0.75% trailing (more room than fixed 0.3%)
TRAILING_STOP_ATR_MULTIPLIER=1.5
TRAILING_STOP_MIN_PERCENT=0.25
TRAILING_STOP_MAX_PERCENT=0.9
TRAILING_STOP_ACTIVATION=0.5

Expected Impact

Before Fix (0.3% Fixed)

  • SOL at $168: 0.3% = $0.50 wiggle room
  • Normal 2-minute oscillation kills runner immediately
  • Runners with +7-9% MFE captured minimal profit or even lost money

After Fix (ATR-based)

Recent ATR distribution from database:

-- Most common ATR values: 0.25-0.52%
-- At 1.5x multiplier:
0.25% ATR × 1.5 = 0.375% trail
0.37% ATR × 1.5 = 0.555% trail  
0.45% ATR × 1.5 = 0.675% trail
0.52% ATR × 1.5 = 0.780% trail

Typical improvement:

  • Old: $0.50 wiggle room ($168 × 0.3%)
  • New: $1.12 wiggle room ($168 × 0.67% avg)
  • 2.24x more room for runner to breathe!

Volatility adaptation:

  • Low ATR (0.25%): 0.375% trail = $0.63 @ $168
  • High ATR (0.72%): 0.9% trail cap = $1.51 @ $168 (max cap)
  • Automatically adjusts to market conditions

Verification Logs

When runner activates, you'll now see:

🎯 Trailing stop activated at +0.65%
📊 ATR-based trailing: 0.0045 (0.52%) × 1.5x = 0.78%
📈 Trailing SL updated: 168.50 → 167.20 (0.78% below peak $168.91)

Instead of:

⚠️ No ATR data, using fallback: 0.30%
📈 Trailing SL updated: 168.50 → 168.41 (0.30% below peak $168.91)

Testing

  1. Existing open trades: Will use fallback 0.3% (no atrAtEntry yet)
  2. New trades: Will capture ATR at entry and use ATR-based trailing
  3. Settings UI: Update multiplier at http://localhost:3001/settings
  4. Log verification: Check for "📊 ATR-based trailing" messages

Files Modified

  1. lib/trading/position-manager.ts - ATR-based trailing calculation + interface
  2. app/settings/page.tsx - UI for ATR multiplier controls
  3. .env.example - Documentation for new variables
  4. config/trading.ts - Already had the config (wasn't being used!)

Deployment

docker compose build trading-bot
docker compose up -d --force-recreate trading-bot
docker logs -f trading-bot-v4

Status: DEPLOYED AND RUNNING

Next Steps

  1. Monitor next runner: Watch for "📊 ATR-based trailing" in logs
  2. Compare MFE vs realized P&L: Should capture 50%+ of MFE (vs current 5-10%)
  3. Adjust multiplier if needed: May increase to 2.0x after seeing results
  4. Update copilot-instructions.md: Document this fix after validation
  • Fixes the morning's missed opportunity: $172→$162 drop would have been captured
  • Addresses "trades showing +7% MFE but -$2 loss" pattern
  • Makes the 25% runner system actually useful (vs broken 5% system)

Key Insight

The config system was already built for this! The trailingStopAtrMultiplier parameter existed in DEFAULT_TRADING_CONFIG and getConfigFromEnv() since the TP2-as-runner redesign. The Position Manager just wasn't using it. This was a "90% done but not wired up" situation.