# Runner System Fix - TP2 Not Closing Position - November 10, 2025 ## Problem You were **100% correct**! The runner system was NOT working. After TP1 closed 75% of the position, when TP2 price level was hit, the **on-chain TP1 order** (incorrectly placed at TP2 price) executed and closed the entire remaining 25%, instead of activating the trailing stop runner. **Evidence from Drift:** - Entry: $167.78 - TP1 hit: 1.45 SOL closed at $168.431 (0.39% - correct 75% close) - **TP2 hit: 1.45 SOL closed at $168.431** ← This should NOT have happened! - Final close: 0.02 SOL remaining closed at $169.105 ## Root Cause In `handlePostTp1Adjustments()` (line 1019 of position-manager.ts), after TP1 hit, the code was: ```typescript await this.refreshExitOrders(trade, { stopLossPrice: newStopLossPrice, tp1Price: trade.tp2Price, // ← BUG: Placing new TP1 order at TP2 price! tp1SizePercent: 100, // ← This closes 100% remaining tp2Price: trade.tp2Price, tp2SizePercent: 0, // ← This is correct (0% close for runner) context, }) ``` **What happened:** 1. Trade opens → TP1 + TP2 + SL orders placed on-chain 2. TP1 hits → 75% closed ✓ 3. Bot cancels all orders and places NEW orders with `tp1Price: trade.tp2Price` 4. This creates a **TP1 LIMIT order at the TP2 price level** 5. When price hits TP2, the TP1 order executes → closes full remaining 25% 6. Runner never activates ❌ ## The Fix ### 1. Position Manager (`lib/trading/position-manager.ts`) After TP1 hits, check if `tp2SizePercent` is 0 (runner system): ```typescript // CRITICAL FIX: For runner system (tp2SizePercent=0), don't place any TP orders // The remaining 25% should only have stop loss and be managed by software trailing stop const shouldPlaceTpOrders = this.config.takeProfit2SizePercent > 0 if (shouldPlaceTpOrders) { // Traditional system: place TP2 order for remaining position await this.refreshExitOrders(trade, { stopLossPrice: newStopLossPrice, tp1Price: trade.tp2Price, tp1SizePercent: 100, tp2Price: trade.tp2Price, tp2SizePercent: 0, context, }) } else { // Runner system: Only place stop loss, no TP orders // The 25% runner will be managed by software trailing stop console.log(`🏃 Runner system active - placing ONLY stop loss at breakeven, no TP orders`) await this.refreshExitOrders(trade, { stopLossPrice: newStopLossPrice, tp1Price: 0, // No TP1 order tp1SizePercent: 0, tp2Price: 0, // No TP2 order tp2SizePercent: 0, context, }) } ``` ### 2. Drift Orders (`lib/drift/orders.ts`) Skip placing TP orders when price is 0: ```typescript // Place TP1 LIMIT reduce-only (skip if tp1Price is 0 - runner system) if (tp1USD > 0 && options.tp1Price > 0) { // ... place order } // Place TP2 LIMIT reduce-only (skip if tp2Price is 0 - runner system) if (tp2USD > 0 && options.tp2Price > 0) { // ... place order } ``` ## How It Works Now **Configuration** (`TAKE_PROFIT_2_SIZE_PERCENT=0`): - TP1: 75% close at +0.4% - TP2: 0% close (just trigger point for trailing stop) - Runner: 25% with ATR-based trailing stop **Execution Flow (TP2-as-Runner):** 1. **Entry** → Place on-chain orders: - TP1 LIMIT: 75% at +0.4% - TP2 LIMIT: 0% (skipped because `tp2SizePercent=0`) - SL: 100% at -1.5% 2. **TP1 Hits** → Software detects 75% closure: - Cancel all existing orders - Check `config.takeProfit2SizePercent === 0` - **For runner system:** Place ONLY stop loss at breakeven (no TP orders!) - Remaining 25% now has SL at breakeven, no TP targets 3. **TP2 Price Level Reached** → Software monitoring: - Detects price ≥ TP2 trigger - Marks `trade.tp2Hit = true` - Sets `trade.peakPrice = currentPrice` - Calculates `trade.runnerTrailingPercent` (ATR-based, ~0.5-1.5%) - **NO position close** - just activates trailing logic - Logs: `🎊 TP2 HIT: SOL at 0.70% - Activating 25% runner!` 4. **Runner Phase** → Trailing stop: - Every 2 seconds: Update `peakPrice` if new high (long) / low (short) - Calculate trailing SL: `peakPrice - (peakPrice × runnerTrailingPercent)` - If price drops below trailing SL → close remaining 25% - Logs: `🏃 Runner activated on full remaining position: 25.0% | trailing buffer 0.873%` ## Why This Matters **Old System (with bug):** - 75% at TP1 (+0.4%) = small profit - 25% closed at TP2 (+0.7%) = fixed small profit - **Total: +0.475% average** (~$10 on $210 position) **New System (runner working):** - 75% at TP1 (+0.4%) = $6.30 - 25% runner trails extended moves (can hit +2%, +5%, +10%!) - **Potential: +0.4% base + runner bonus** ($6 + $2-10+ on lucky trades) **Example:** If price runs from $167 → $170 (+1.8%): - TP1: 75% at +0.4% = $6.30 - Runner: 25% at +1.8% = **$9.45** (vs $3.68 if closed at TP2) - **Total: $15.75** vs old system's $10.50 ## Verification Next trade will show logs like: ``` 🎉 TP1 HIT: SOL-PERP at 0.42% 🔒 (software TP1 execution) SL moved to +0.0% ... remaining): $168.00 🏃 Runner system active - placing ONLY stop loss at breakeven, no TP orders 🗑️ (software TP1 execution) Cancelling existing exit orders before refresh... ✅ (software TP1 execution) Cancelled 3 old orders 🛡️ (software TP1 execution) Placing refreshed exit orders: size=$525.00 SL=$168.00 TP=$0.00 ✅ (software TP1 execution) Exit orders refreshed on-chain [Later when TP2 price hit] 🎊 TP2 HIT: SOL-PERP at 0.72% - Activating 25% runner! 🏃 Runner activated on full remaining position: 25.0% | trailing buffer 0.873% ``` ## Deployment ✅ **Code Fixed**: Position Manager + Drift Orders ✅ **Docker Rebuilt**: Image sha256:f42ddaa98dfb... ✅ **Bot Restarted**: trading-bot-v4 running with runner system active ✅ **Ready for Testing**: Next trade will use proper runner logic ## Files Changed - `lib/trading/position-manager.ts` (handlePostTp1Adjustments - added runner system check) - `lib/drift/orders.ts` (placeExitOrders - skip TP orders when price is 0) - `docs/history/RUNNER_SYSTEM_FIX_20251110.md` (this file) ## Next Trade Expectations Watch for these in logs: 1. TP1 hits → "Runner system active - placing ONLY stop loss" 2. On-chain orders refresh shows `TP=$0.00` (no TP order) 3. When price hits TP2 level → "TP2 HIT - Activating 25% runner!" 4. NO position close at TP2, only trailing stop activation 5. Runner trails price until stop hit or manual close **You were absolutely right** - the system was placing a TP order that shouldn't exist. Now fixed! 🏃‍♂️