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

503 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```typescript
// 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
```typescript
// 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)**
```typescript
// 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)**
```typescript
// 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+)**
```typescript
// 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:**
```pine
//@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:**
```json
{
"type": "POSITION_UPDATE",
"symbol": "SOLUSDT",
"atr": 2.35, // Current ATR (updated)
"price": 188.50,
"direction": "long"
}
```
**New API endpoint:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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
---
## Recommended Implementation Path
### 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:**
```typescript
// 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:**
```sql
-- 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:**
```typescript
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:**
```pine
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:**
```typescript
// 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
```prisma
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)
```prisma
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!
---
## Related Documentation
- `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