Files
trading_bot_v4/docs/guides/ATR_SCALING_GUIDE.md
mindesbunister d3f385deac Add ATR-based position scaling guide
Comprehensive guide covering:
- How ATR is captured and stored (entry value frozen)
- Static ATR approach (Phases 1-3): Use entry ATR for entire trade
- Dynamic ATR approach (Phase 5+): Real-time updates via TradingView or bot calculation
- Use cases: Dynamic TP/SL, trailing stops, scaling in/out decisions
- Implementation path: Start simple with entry ATR, add real-time later if data supports
- Code examples for all approaches
- Troubleshooting common ATR issues
- Database schema considerations

Explains why waiting for data is critical before implementing advanced ATR features.
2025-10-31 13:34:18 +01:00

14 KiB
Raw Permalink Blame History

ATR-Based Position Scaling Guide

Overview

This guide explains how the trading bot uses Average True Range (ATR) for position management decisions, including scaling in/out of positions, dynamic stop losses, and take profit targets.


Current Architecture: Entry ATR Storage

How ATR is Captured

TradingView Signal → Contains ATR value (e.g., 2.15)
  ↓
Bot receives signal via n8n webhook
  ↓
Stores ATR in database: atrAtEntry = 2.15
  ↓
Position Manager uses stored ATR for entire trade lifecycle

Key Point: ATR value is "frozen" at entry time and stored in the Trade.atrAtEntry field.

Current Data Flow

// Entry signal from TradingView (via n8n)
{
  "symbol": "SOLUSDT",
  "direction": "long",
  "atr": 2.15,      // Sent once at entry
  "adx": 28,
  "rsi": 62,
  "volumeRatio": 1.3,
  "pricePosition": 45
}

// Stored in database
Trade {
  atrAtEntry: 2.15,
  entryPrice: 186.50,
  // ... other fields
}

// Used by Position Manager (every 2 seconds)
const atr = trade.atrAtEntry || 2.0  // Fallback if missing

Approach 1: Static ATR (Entry Value)

Status: Recommended for Phases 1-3 (Current Implementation)

How It Works

// In position-manager.ts monitoring loop
async checkTargets(trade: ActiveTrade, currentPrice: number) {
  // Use ATR from entry signal (static for entire trade)
  const atr = trade.atrAtEntry || 2.0
  
  // Calculate bands using ENTRY ATR
  const directionMultiplier = trade.direction === 'long' ? 1 : -1
  
  const band_05x = trade.entryPrice + (atr * 0.5 * directionMultiplier)
  const band_1x = trade.entryPrice + (atr * 1.0 * directionMultiplier)
  const band_15x = trade.entryPrice + (atr * 1.5 * directionMultiplier)
  const band_2x = trade.entryPrice + (atr * 2.0 * directionMultiplier)
  
  // Check current price against bands
  if (currentPrice >= band_1x && !trade.band1xCrossed) {
    console.log('🎯 Price crossed 1×ATR band')
    trade.band1xCrossed = true
    // Trigger actions (e.g., adjust trailing stop)
  }
}

Use Cases

1. Dynamic Take Profit Targets (Phase 2)

// Instead of fixed +1.5% and +3.0%
const tp1Price = trade.entryPrice + (atr * 1.5 * directionMultiplier)
const tp2Price = trade.entryPrice + (atr * 3.0 * directionMultiplier)

// If ATR = 2.0 and entry = $100:
// TP1 = $103 (3% move)
// TP2 = $106 (6% move)

// If ATR = 0.5 and entry = $100:
// TP1 = $100.75 (0.75% move)
// TP2 = $101.50 (1.5% move)

2. ATR-Based Trailing Stop (Phase 5)

// Instead of fixed 0.3% trailing stop
const trailingStopDistance = atr * 1.5  // Trail by 1.5×ATR

// Calculate trailing stop price
const trailingStopPrice = trade.direction === 'long'
  ? trade.peakPrice - trailingStopDistance
  : trade.peakPrice + trailingStopDistance

// If ATR = 2.0 (high volatility):
// Trailing stop = 3% below peak (gives room to breathe)

// If ATR = 0.5 (low volatility):
// Trailing stop = 0.75% below peak (tighter protection)

3. Scaling In Decisions (Phase 6+)

// Scale in on healthy pullback (0.5×ATR from peak)
const scaleInTrigger = trade.direction === 'long'
  ? trade.peakPrice - (atr * 0.5)  // Long: pullback from high
  : trade.peakPrice + (atr * 0.5)  // Short: rally from low

// Conditions for scaling in
const qualityHigh = trade.signalQualityScore >= 80
const pullbackHealthy = trade.direction === 'long' 
  ? currentPrice >= scaleInTrigger && currentPrice < trade.peakPrice
  : currentPrice <= scaleInTrigger && currentPrice > trade.peakPrice
const notAlreadyScaled = !trade.hasScaledIn
const withinRiskLimits = trade.positionSize * 1.5 <= maxPositionSize

if (qualityHigh && pullbackHealthy && notAlreadyScaled && withinRiskLimits) {
  await scaleIntoPosition(trade, 0.5) // Add 50% more size
}

Advantages

  • Simple: No additional infrastructure needed
  • Consistent: Uses same volatility context as entry decision
  • No sync issues: No need to track TradingView state
  • Good for short-duration trades: Entry ATR valid for 30min-2 hour timeframes

Limitations

  • Stale data: ATR from entry may be outdated hours later
  • No adaptation: If volatility changes mid-trade, targets don't adjust
  • Example: Enter with ATR=2.0, but 3 hours later ATR drops to 0.8
    • Bot still uses 2.0 for calculations
    • May give too much room to runner (3% trailing stop instead of 1.2%)

Approach 2: Real-Time ATR Updates

Status: Future enhancement (Phase 5+)

Option A: TradingView Periodic Updates

Pine Script sends ATR updates while position is open:

//@version=5
strategy("ATR Monitor with Updates", overlay=true)

// Your entry logic
longSignal = yourLongCondition()
shortSignal = yourShortCondition()

if longSignal
    strategy.entry("Long", strategy.long)
if shortSignal
    strategy.entry("Short", strategy.short)

// Send ATR updates every candle close if position open
atr = ta.atr(14)
if strategy.position_size != 0 and barstate.isconfirmed
    message = "POSITION_UPDATE" +
              " | SYMBOL:" + syminfo.ticker +
              " | ATR:" + str.tostring(atr, "#.##") +
              " | PRICE:" + str.tostring(close, "#.####") +
              " | DIRECTION:" + (strategy.position_size > 0 ? "long" : "short")
    
    alert(message, alert.freq_once_per_bar_close)

Bot receives updates:

{
  "type": "POSITION_UPDATE",
  "symbol": "SOLUSDT",
  "atr": 2.35,  // Current ATR (updated)
  "price": 188.50,
  "direction": "long"
}

New API endpoint:

// app/api/trading/position-update/route.ts
export async function POST(request: NextRequest) {
  const body = await request.json()
  
  // Find active trade by symbol and direction
  const positionManager = await getInitializedPositionManager()
  const trade = positionManager.findTradeBySymbol(body.symbol, body.direction)
  
  if (trade) {
    // Update current ATR
    trade.currentATR = body.atr
    
    // Recalculate bands with fresh ATR
    const band_1x = trade.entryPrice + (body.atr * 1.0)
    const band_2x = trade.entryPrice + (body.atr * 2.0)
    
    // Update trailing stop distance dynamically
    trade.trailingStopDistance = body.atr * 1.5
    
    console.log(`📊 ATR updated: ${trade.atrAtEntry}${body.atr}`)
  }
  
  return NextResponse.json({ success: true })
}

Position Manager logic:

// In position-manager.ts
interface ActiveTrade {
  // ... existing fields
  atrAtEntry: number      // Original ATR from entry
  currentATR?: number     // Updated ATR (if receiving updates)
  trailingStopDistance?: number  // Dynamic trailing stop
}

async checkTargets(trade: ActiveTrade, currentPrice: number) {
  // Use current ATR if available, fallback to entry ATR
  const atr = trade.currentATR || trade.atrAtEntry || 2.0
  
  // Rest of logic uses current ATR
  const band_1x = trade.entryPrice + (atr * 1.0)
  // ...
}

Advantages

  • Always current: Uses latest volatility data
  • Adapts to market: If volatility spikes, targets widen automatically
  • More accurate: Trailing stops adjust to current conditions

Limitations

  • Complex: Requires new endpoint and TradingView webhook setup
  • Webhook spam: Sends updates every 5-15 minutes (candle close frequency)
  • Sync issues: TradingView doesn't know if bot actually has position open
    • If bot closes position, TradingView keeps sending updates
    • Need to handle "position not found" gracefully
  • API rate limits: More webhook calls to your server

Implementation Checklist

  • Create /api/trading/position-update endpoint
  • Add currentATR field to ActiveTrade interface
  • Update Position Manager to use currentATR || atrAtEntry
  • Modify Pine Script to send periodic ATR updates
  • Add n8n workflow node to parse ATR updates
  • Test with position open/close sync
  • Add database field to track ATR history: atrUpdates: Json[]

Option B: Bot Calculates ATR Itself

Bot fetches historical candles and calculates ATR:

// lib/indicators/atr.ts
export function calculateATR(candles: OHLC[], period: number = 14): number {
  if (candles.length < period) {
    throw new Error(`Need at least ${period} candles for ATR`)
  }
  
  const trueRanges: number[] = []
  
  for (let i = 1; i < candles.length; i++) {
    const high = candles[i].high
    const low = candles[i].low
    const prevClose = candles[i - 1].close
    
    const tr = Math.max(
      high - low,                    // Current high-low
      Math.abs(high - prevClose),    // Current high - previous close
      Math.abs(low - prevClose)      // Current low - previous close
    )
    
    trueRanges.push(tr)
  }
  
  // Simple Moving Average of True Range
  const atr = trueRanges.slice(-period).reduce((a, b) => a + b, 0) / period
  
  return atr
}

// In position-manager.ts
async updateATR(trade: ActiveTrade) {
  // Fetch last 14 candles from price feed
  const candles = await fetchRecentCandles(trade.symbol, 14, '5m')
  const currentATR = calculateATR(candles)
  
  trade.currentATR = currentATR
  
  console.log(`📊 Calculated ATR: ${currentATR.toFixed(2)}`)
}

Advantages

  • Autonomous: No TradingView dependency
  • Always fresh: Calculate on-demand
  • No webhooks: No additional API calls to your server

Limitations

  • Complex: Need to implement ATR calculation in TypeScript
  • Data source: Need reliable OHLC candle data
    • Pyth Network: Primarily provides spot prices, may not have full OHLC
    • Drift SDK: May have orderbook data but not historical candles
    • Alternative: Fetch from Binance/CoinGecko API
  • API calls: Need to fetch candles every update cycle (rate limits)
  • Performance: Additional latency for fetching + calculating

Phase 1-3: Use Entry ATR (Current)

What to do:

  • Store atrAtEntry from TradingView signals (already implemented)
  • Use static ATR for all calculations during trade
  • Validate that scaling strategies work with entry ATR

Configuration:

// config/trading.ts
export interface TradingConfig {
  // ... existing config
  useATRTargets: boolean           // Enable ATR-based TP1/TP2
  atrMultiplierTP1: number         // 1.5×ATR for TP1
  atrMultiplierTP2: number         // 3.0×ATR for TP2
  atrMultiplierTrailing: number    // 1.5×ATR for trailing stop
  atrFallback: number              // Default ATR if missing (2.0)
}

Phase 4: Add ATR Normalization

Analyze collected data:

-- What's typical ATR range for SOL-PERP?
SELECT 
  ROUND(MIN("atrAtEntry")::numeric, 2) as min_atr,
  ROUND(AVG("atrAtEntry")::numeric, 2) as avg_atr,
  ROUND(MAX("atrAtEntry")::numeric, 2) as max_atr,
  ROUND(STDDEV("atrAtEntry")::numeric, 2) as stddev_atr
FROM "Trade"
WHERE "atrAtEntry" IS NOT NULL AND "atrAtEntry" > 0;

-- Result example:
-- min_atr: 0.5, avg_atr: 2.0, max_atr: 3.5, stddev: 0.8

Implement normalization:

function normalizeATR(atr: number, baseline: number = 2.0): number {
  // Returns factor relative to baseline
  return atr / baseline
}

// Usage
const atrFactor = normalizeATR(trade.atrAtEntry, 2.0)
const tp1Price = trade.entryPrice + (baseline_tp1_percent * atrFactor)

// If ATR = 3.0 (high volatility):
// atrFactor = 1.5, TP1 = entry + (1.5% × 1.5) = entry + 2.25%

// If ATR = 1.0 (low volatility):
// atrFactor = 0.5, TP1 = entry + (1.5% × 0.5) = entry + 0.75%

Phase 5+: Consider Real-Time Updates

Decision gate:

  • Do trades last > 2 hours frequently?
  • Does ATR change significantly during typical trade duration?
  • Would dynamic updates improve performance measurably?

If YES to all three:

  • Implement Option A (TradingView updates) OR Option B (Bot calculates)
  • A/B test: 20 trades with real-time ATR vs 20 with entry ATR
  • Compare: Win rate, avg P&L, trailing stop effectiveness

If NO:

  • Stay with entry ATR (simpler, good enough)

Troubleshooting

Problem: ATR values are 0 or missing

Cause: TradingView not sending ATR or n8n not extracting it

Solution:

  1. Check TradingView alert message: Should include ATR:{{plot_0}} or similar
  2. Check n8n "Parse Signal Enhanced" node: Should extract atr field
  3. Verify webhook payload in n8n execution log
  4. Ensure Pine Script has atr = ta.atr(14) and plots it

Problem: ATR seems too high/low

Cause: Using wrong timeframe or different calculation method

TradingView ATR calculation:

atr = ta.atr(14)  // 14-period ATR

Bot should use same period (14) if calculating itself.

Typical ATR ranges for SOL-PERP:

  • 5-minute chart: 0.3 - 1.5 (low to high volatility)
  • 15-minute chart: 0.8 - 3.0 (low to high volatility)
  • Daily chart: 3.0 - 10.0 (low to high volatility)

Problem: Trailing stop too tight/loose with ATR

Cause: Wrong ATR multiplier

Solution:

// Test different multipliers
const trailingStopDistance = atr * 1.5  // Start here
// Too tight? Increase to 2.0
// Too loose? Decrease to 1.0

// Log and analyze
console.log(`ATR: ${atr}, Trailing: ${trailingStopDistance} (${(trailingStopDistance/trade.entryPrice*100).toFixed(2)}%)`)

Database Schema

Current Fields

model Trade {
  // ... other fields
  atrAtEntry        Float?  // ATR% when trade opened
  adxAtEntry        Float?  // ADX trend strength
  rsiAtEntry        Float?  // RSI momentum
  volumeAtEntry     Float?  // Volume relative to MA
  pricePositionAtEntry Float? // Price position in range
}

Future Enhancement (Real-Time Updates)

model Trade {
  // ... existing fields
  atrHistory        Json?   // Array of ATR updates: [{time, atr, price}]
}

// Example atrHistory value:
// [
//   {"time": "2025-10-31T10:00:00Z", "atr": 2.15, "price": 186.50},
//   {"time": "2025-10-31T10:15:00Z", "atr": 2.28, "price": 188.20},
//   {"time": "2025-10-31T10:30:00Z", "atr": 2.45, "price": 189.10}
// ]

Key Takeaways

  1. Entry ATR is sufficient for Phases 1-3 - Don't overcomplicate early
  2. Real-time ATR updates are optional - Only add if data proves benefit
  3. Test with data - Run analysis queries to validate ATR effectiveness
  4. Start simple, optimize later - Use entry ATR → Analyze results → Then enhance

Most important: Let the system collect data first. Implement ATR-based logic AFTER you have 20-50 trades with real ATR values to validate the approach!


  • POSITION_SCALING_ROADMAP.md - 6-phase optimization plan
  • .github/copilot-instructions.md - Architecture overview
  • docs/guides/TESTING.md - How to test ATR-based features
  • config/trading.ts - ATR configuration options