From 673a49302af015b9d4a2de24e48bc58355a23591 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Sun, 16 Nov 2025 03:00:22 +0100 Subject: [PATCH] critical: Fix breakeven SL using wrong entry price after TP1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CRITICAL BUG: Drift SDK's position.entryPrice RECALCULATES after partial closes - After TP1, Drift returns COST BASIS of remaining position, NOT original entry - Example: SHORT @ 38.52 → TP1 @ 70% → Drift shows entry 40.01 (runner's basis) - Result: Breakeven SL set .50 ABOVE actual entry = guaranteed loss if triggered Fix: - Always use database trade.entryPrice for breakeven calculations - Drift's position.entryPrice = current state (runner cost basis) - Database entryPrice = original entry (authoritative for breakeven) - Added logging to show both values for verification Impact: - Every TP1 → breakeven transition was using WRONG price - Locking in losses instead of true breakeven protection - Financial loss bug affecting every trade with TP1 Files: - lib/trading/position-manager.ts: Line 513 - use trade.entryPrice not position.entryPrice - .github/copilot-instructions.md: Added Common Pitfall #43, deprecated old #44 Incident: Nov 16, 02:47 CET - SHORT entry 38.52, breakeven SL set at 40.01 Position closed by ghost detection before SL could trigger (lucky) --- .env | 2 +- .github/copilot-instructions.md | 73 ++++++++++++++++++--------------- lib/trading/position-manager.ts | 11 ++--- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/.env b/.env index e890ea3..edec1ab 100644 --- a/.env +++ b/.env @@ -376,7 +376,7 @@ TRAILING_STOP_ACTIVATION=0.4 MIN_QUALITY_SCORE=60 SOLANA_ENABLED=true SOLANA_POSITION_SIZE=100 -SOLANA_LEVERAGE=15 +SOLANA_LEVERAGE=10 SOLANA_USE_PERCENTAGE_SIZE=true ETHEREUM_ENABLED=false ETHEREUM_POSITION_SIZE=50 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0d47d97..bd6d1df 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1833,7 +1833,39 @@ trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK - **Git commit:** bdf1be1 "fix: Add DNS retry logic to Telegram bot" - **Lesson:** Python urllib3 has same transient DNS issues as Node.js - apply same retry pattern -43. **Drift account leverage must be set in UI, not via API (CRITICAL - Nov 16, 2025):** +43. **Drift SDK position.entryPrice RECALCULATES after partial closes (CRITICAL - FINANCIAL LOSS BUG - Fixed Nov 16, 2025):** + - **Symptom:** Breakeven SL set $1.50+ ABOVE actual entry price, guaranteeing loss if triggered + - **Root Cause:** Drift SDK's `position.entryPrice` returns COST BASIS of remaining position after TP1, NOT original entry + - **Real incident (Nov 16, 02:47 CET):** + * SHORT opened at $138.52 entry + * TP1 hit, 70% closed at profit + * System queried Drift for "actual entry": returned $140.01 (runner's cost basis) + * Breakeven SL set at $140.01 (instead of $138.52) + * Result: "Breakeven" SL $1.50 ABOVE entry = guaranteed $2.52 loss if hit + * Position closed by ghost detection before SL could trigger (lucky) + - **Why Drift recalculates:** + * After partial close, remaining position has different realized P&L + * SDK calculates: `position.entryPrice = quoteAssetAmount / baseAssetAmount` + * This gives AVERAGE price of remaining position, not ORIGINAL entry + * For runners after TP1, this is ALWAYS wrong for breakeven calculation + - **Impact:** Every TP1 → breakeven SL transition uses wrong price, locks in losses instead of breakeven + - **Fix:** Always use database `trade.entryPrice` for breakeven SL (line 513 in position-manager.ts) + ```typescript + // BEFORE (BROKEN): + const actualEntryPrice = position.entryPrice || trade.entryPrice + trade.stopLossPrice = actualEntryPrice + + // AFTER (FIXED): + const breakevenPrice = trade.entryPrice // Use ORIGINAL entry from database + console.log(`📊 Breakeven SL: Using original entry price $${breakevenPrice.toFixed(4)} (Drift shows $${position.entryPrice.toFixed(4)} for remaining position)`) + trade.stopLossPrice = breakevenPrice + ``` + - **Common Pitfall #44 context:** Original fix (528a0f4) tried to use Drift's entry for "accuracy" but introduced this bug + - **Lesson:** Drift SDK data is authoritative for CURRENT state, but database is authoritative for ORIGINAL entry + - **Verification:** After TP1, logs now show: "Using original entry price $138.52 (Drift shows $140.01 for remaining position)" + - **Git commit:** [pending] "critical: Use database entry price for breakeven SL, not Drift's recalculated value" + +44. **Drift account leverage must be set in UI, not via API (CRITICAL - Nov 16, 2025):** - **Symptom:** InsufficientCollateral errors when opening positions despite bot configured for 15x leverage - **Root Cause:** Drift Protocol account leverage is an on-chain account setting, cannot be changed via SDK/API - **Error message:** `AnchorError occurred. Error Code: InsufficientCollateral. Error Number: 6003. Error Message: Insufficient collateral.` @@ -1874,39 +1906,14 @@ trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK - **Drift documentation:** Account leverage must be set in UI, is persistent on-chain setting - **Lesson:** On-chain account settings cannot be changed via API - always verify account state matches bot assumptions before production trading -44. **Breakeven SL using database entry price instead of Drift's actual entry (Fixed Nov 16, 2025):** - - **Symptom:** After TP1 hits, SL moved to "breakeven" but at wrong price - SHORT with $139.07 entry gets SL at $139.18 ($0.11 loss if hit) - - **Root Cause:** Position Manager used `trade.entryPrice` from database, which can differ from Drift's actual fill price by $0.10-0.15 - - **Price discrepancy sources:** - * Database stores initial expected price - * Drift fills at market price (slippage) - * Orphaned position restorations use stale database price (see Common Pitfall #33) - * Oracle price delays during execution - - **Real incident (Nov 16, 01:37 CET):** - * User screenshot: Entry $139.07, SL $139.18, Current $138.58 - * Database: entryPrice=$139.18291 - * Drift UI: Entry Price=$139.07 - * Discrepancy: $0.11 (would lose money if SL hit on "breakeven") - - **Impact:** Breakeven SL not truly breakeven - gives back profit or locks in unnecessary loss - - **Fix:** Query Drift SDK for actual entry price when setting breakeven SL - ```typescript - // In lib/trading/position-manager.ts (line ~511): - // BEFORE: - trade.stopLossPrice = trade.entryPrice // Uses database value - - // AFTER: - const actualEntryPrice = position.entryPrice || trade.entryPrice - console.log(`📊 Breakeven calculation: DB entry=$${trade.entryPrice.toFixed(4)}, Drift entry=$${actualEntryPrice.toFixed(4)}`) - trade.stopLossPrice = actualEntryPrice // Uses Drift's on-chain calculated price - ``` - - **Drift SDK accuracy:** `position.entryPrice` calculated from on-chain data (quoteAssetAmount / baseAssetAmount) = authoritative - - **Fallback:** If position.entryPrice unavailable, falls back to database entry (rare edge case) - - **Logging:** Now shows both DB and Drift entry prices for verification - - **Related issues:** Common Pitfall #33 (orphaned position restoration with wrong entry) - - **Git commit:** 528a0f4 "fix: Use Drift's actual entry price for breakeven SL" - - **Lesson:** For critical price calculations (breakeven, TP, SL), always prefer on-chain data over cached database values +45. **DEPRECATED - See Common Pitfall #43 for the actual bug (Nov 16, 2025):** + - **Original diagnosis was WRONG:** Thought database entry was stale, so used Drift's position.entryPrice + - **Reality:** Drift's position.entryPrice RECALCULATES after partial closes (cost basis of runner, not original entry) + - **Real fix:** Always use DATABASE entry price for breakeven - it's authoritative for original entry + - **This "fix" (commit 528a0f4) INTRODUCED the critical bug in Common Pitfall #43** + - **See Common Pitfall #43 for full details of the financial loss bug this caused** -45. **100% position sizing causes InsufficientCollateral (Fixed Nov 16, 2025):** +46. **100% position sizing causes InsufficientCollateral (Fixed Nov 16, 2025):** - **Symptom:** Bot configured for 100% position size gets InsufficientCollateral errors, but Drift UI can open same size position - **Root Cause:** Drift's margin calculation includes fees, slippage buffers, and rounding - exact 100% leaves no room - **Error details:** diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 23d5036..8b920fe 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -508,13 +508,14 @@ export class PositionManager { trade.tp1Hit = true trade.currentSize = positionSizeUSD - // CRITICAL: Query Drift for ACTUAL entry price (more accurate than database) - // The position.entryPrice from Drift SDK is calculated from on-chain data - const actualEntryPrice = position.entryPrice || trade.entryPrice - console.log(`📊 Breakeven calculation: DB entry=$${trade.entryPrice.toFixed(4)}, Drift entry=$${actualEntryPrice.toFixed(4)}`) + // CRITICAL: Use DATABASE entry price for breakeven (Drift recalculates after partial closes) + // Drift's position.entryPrice is the COST BASIS of remaining position, NOT original entry + // For a true breakeven SL, we need the ORIGINAL entry price from when position opened + const breakevenPrice = trade.entryPrice + console.log(`📊 Breakeven SL: Using original entry price $${breakevenPrice.toFixed(4)} (Drift shows $${position.entryPrice.toFixed(4)} for remaining position)`) // Move SL to TRUE breakeven after TP1 - trade.stopLossPrice = actualEntryPrice + trade.stopLossPrice = breakevenPrice trade.slMovedToBreakeven = true console.log(`🛡️ Stop loss moved to breakeven: $${trade.stopLossPrice.toFixed(4)}`)