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:
mindesbunister
2025-10-31 13:34:18 +01:00
parent 27c6a06d31
commit d3f385deac

View 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