diff --git a/docs/guides/ATR_SCALING_GUIDE.md b/docs/guides/ATR_SCALING_GUIDE.md new file mode 100644 index 0000000..1ff25f8 --- /dev/null +++ b/docs/guides/ATR_SCALING_GUIDE.md @@ -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