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.
This commit is contained in:
502
docs/guides/ATR_SCALING_GUIDE.md
Normal file
502
docs/guides/ATR_SCALING_GUIDE.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user