From 465f6bdb82534997c2e34945d8495dd5657c7556 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Wed, 31 Dec 2025 11:45:21 +0100 Subject: [PATCH] critical: Bug #90 - Use placeAndTakePerpOrder for immediate MARKET order fills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE: placePerpOrder() only places orders on Drift order book, doesn't fill. SOLUTION: placeAndTakePerpOrder() places AND matches against makers atomically. Real Incident (Dec 31, 2025): - Dec 30 18:17: SHORT opened at $124.36 - Dec 31 00:30: LONG signal received - should flip position - Transaction confirmed but Solscan showed 'Place' not 'Fill' - Position remained open, eventually hit SL twice - Total loss: ~$40.21 Files changed: - lib/drift/orders.ts (line 662): placePerpOrder → placeAndTakePerpOrder - docs/COMMON_PITFALLS.md: Added Bug #90 documentation Deployment: Dec 31, 2025 11:38 CET (container trading-bot-v4) --- docs/COMMON_PITFALLS.md | 109 +++++++++++++++++++++++++++++++++++++++- lib/drift/orders.ts | 24 ++++++--- 2 files changed, 126 insertions(+), 7 deletions(-) diff --git a/docs/COMMON_PITFALLS.md b/docs/COMMON_PITFALLS.md index bf95698..79f5cb1 100644 --- a/docs/COMMON_PITFALLS.md +++ b/docs/COMMON_PITFALLS.md @@ -99,6 +99,7 @@ This document is the **comprehensive reference** for all documented pitfalls, bu | 71 | 🔴 CRITICAL | Revenge System | Dec 3, 2025 | Revenge system missing external closure integration | | 72 | 🔴 CRITICAL | Telegram | Dec 4, 2025 | Telegram webhook conflicts with polling bot | | 89 | 🔴 CRITICAL | Drift Protocol | Dec 16, 2025 | Drift fractional position remnants after SL execution | +| 90 | 🔴 CRITICAL | Drift Protocol | Dec 31, 2025 | placePerpOrder only places orders, doesn't fill - use placeAndTakePerpOrder | --- @@ -1642,6 +1643,112 @@ if (fractionalPositions.length > 0) { --- +### Pitfall #90: placePerpOrder Only Places Order, Doesn't Fill - Use placeAndTakePerpOrder (CRITICAL - FIXED Dec 31, 2025) + +**Severity:** 🔴 CRITICAL +**Category:** Drift Protocol / SDK +**Date:** Dec 31, 2025 +**Financial Impact:** ~$40+ loss from failed position flip + +**Symptom:** +- Position flip signal received (LONG to close SHORT and open LONG) +- Close transaction confirms on-chain +- BUT position remains open on Drift +- n8n shows "Flip failed - position did not close" +- Solscan shows transaction as "Place long X SOL perp order at price 0" (NOT a fill) + +**Root Cause:** +The bot was using `driftClient.placePerpOrder()` which only **places** an order on the Drift order book. For MARKET orders to execute immediately, you must use `driftClient.placeAndTakePerpOrder()` which places the order AND matches it against makers atomically. + +**Code Evidence (before fix):** +```typescript +// lib/drift/orders.ts line 662 (BROKEN) +const txSig = await retryWithBackoff(async () => { + return await driftClient.placePerpOrder(orderParams) // ❌ Only places order! +}, 3, 8000) +``` + +**Real Incident (Dec 31, 2025):** +- Dec 30 18:17 CET: SHORT opened at $124.36 via TradingView signal +- Dec 31 00:30 CET: LONG signal received - should flip position +- Close transaction: 5E713pD6... confirmed on-chain +- Solscan shows: "Place long 24.72 SOL perp order at price 0" (order placed, NOT filled) +- Position remained open: 24.72 SOL SHORT still on Drift +- n8n logs: "Flip failed - position did not close" +- Dec 31 05:01 CET: SHORT hit SOFT_SL at $125.30 (P&L: -$23.29) +- Dec 31 05:01 CET: Autosync detected orphan, created new trade record +- Dec 31 10:35 CET: Autosync position hit SL at $125.99 (P&L: -$16.92) +- **Total loss from bug: ~$40.21** + +**THE FIX (Dec 31, 2025):** +```typescript +// lib/drift/orders.ts line 662 (FIXED) +// CRITICAL FIX (Dec 31, 2025 - Bug #90): Use placeAndTakePerpOrder instead of placePerpOrder +// ROOT CAUSE: placePerpOrder only PLACES an order on the Drift order book. +// For MARKET orders, the order sits there waiting for a taker, which may never come. +// SOLUTION: placeAndTakePerpOrder places the order AND fills it against makers atomically. +// This ensures MARKET orders execute immediately instead of just being placed. +console.log('🚀 Placing market close order with IMMEDIATE FILL (placeAndTakePerpOrder)...') +const txSig = await retryWithBackoff(async () => { + return await driftClient.placeAndTakePerpOrder(orderParams) // ✅ Places AND fills! +}, 3, 8000) +``` + +**Drift SDK Method Comparison:** +| Method | Behavior | Use Case | +|--------|----------|----------| +| `placePerpOrder()` | Places order on order book only | LIMIT orders waiting for fill | +| `placeAndTakePerpOrder()` | Places AND matches against makers | MARKET orders needing immediate fill | + +**Key Evidence from Solscan:** +- Transaction: 5E713pD6N4c4BbvfY1vL77g17pDPQZqhTFvWXF7YnELN... +- Action: "Place long 24.72 SOL perp order at price 0" +- Result: Order placed on book but NOT filled +- Position: Remained open (24.72 SOL SHORT) + +**Why This Is Critical:** +- **Position flips fail silently** - transaction confirms but position doesn't close +- **Risk accumulates** - trader thinks position flipped, but exposure continues +- **Opposite signal never opens** - LONG can't open because old SHORT still exists +- **Cascade of losses** - unprotected position eventually hits SL with larger loss + +**Prevention Rules:** +1. ALWAYS use `placeAndTakePerpOrder()` for MARKET orders that need immediate fill +2. ALWAYS verify position size on Drift after close transactions +3. CHECK Solscan for "Place" vs "Fill" actions in transaction details +4. NEVER trust transaction confirmation alone - verify position state + +**Red Flags Indicating This Bug:** +- Transaction confirms but position still shows on Drift +- Solscan shows "Place" instead of "Fill" action +- n8n logs "Flip failed - position did not close" +- `needsVerification: true` returned from close operation +- Position continues moving after supposed close + +**Verification After Fix:** +```bash +# Check container is running with fix +docker inspect trading-bot-v4 --format='{{.State.StartedAt}}' +# Should be after Dec 31, 2025 10:38 UTC + +# Test next close operation +docker logs -f trading-bot-v4 | grep "placeAndTakePerpOrder\|IMMEDIATE FILL" +``` + +**Files Changed:** +- lib/drift/orders.ts (line 662): `placePerpOrder` → `placeAndTakePerpOrder` +- Added console.log for visibility: "🚀 Placing market close order with IMMEDIATE FILL..." + +**Git Commit:** [PENDING] "critical: Bug #90 - Use placeAndTakePerpOrder for immediate MARKET order fills" + +**Deployment:** Dec 31, 2025 11:38 CET (container trading-bot-v4) + +**Status:** ✅ FIXED AND DEPLOYED + +**Lesson Learned:** The Drift SDK has TWO methods for placing perp orders. `placePerpOrder()` only places orders on the book (suitable for LIMIT orders). `placeAndTakePerpOrder()` places AND fills immediately (required for MARKET orders). Always use the latter for market orders to ensure immediate execution. Transaction confirmation does NOT mean the order was filled - verify position state after close operations. + +--- + ## Appendix: Pattern Recognition ### Common Root Causes @@ -1672,5 +1779,5 @@ if (fractionalPositions.length > 0) { --- -**Last Updated:** December 4, 2025 +**Last Updated:** December 31, 2025 **Maintainer:** AI Agent team following "NOTHING gets lost" principle diff --git a/lib/drift/orders.ts b/lib/drift/orders.ts index 476de4d..0f6ed2a 100644 --- a/lib/drift/orders.ts +++ b/lib/drift/orders.ts @@ -644,7 +644,7 @@ export async function closePosition( } } - // Prepare close order (opposite direction) - use simple structure like v3 + // Prepare close order (opposite direction) const orderParams = { orderType: OrderType.MARKET, marketIndex: marketConfig.driftMarketIndex, @@ -655,14 +655,26 @@ export async function closePosition( reduceOnly: true, // Important: only close existing position } - // Place market close order using simple placePerpOrder (like v3) - // CRITICAL: Wrap in retry logic for rate limit protection - logger.log('🚀 Placing REAL market close order with retry protection...') + // CRITICAL FIX (Dec 31, 2025 - Bug #89): Use placeAndTakePerpOrder instead of placePerpOrder + // + // ROOT CAUSE: placePerpOrder only PLACES an order on the order book. + // For MARKET orders, the transaction confirms when the order is placed, NOT when it's filled. + // This caused the flip operation to fail - close tx confirmed but position never closed. + // + // SOLUTION: placeAndTakePerpOrder places the order AND fills it against makers atomically. + // This guarantees the position is closed when the transaction confirms. + // + // Evidence: Solscan showed tx 5E713pD6... as "Place long 24.72 SOL perp order at price 0" + // - The order was placed on the book but never filled + // - Position remained open despite "confirmed" transaction + // + // See: https://github.com/drift-labs/drift-vaults/blob/main/tests/testHelpers.ts#L1542 + console.log('🚀 Placing market close order with IMMEDIATE FILL (placeAndTakePerpOrder)...') const txSig = await retryWithBackoff(async () => { - return await driftClient.placePerpOrder(orderParams) + return await driftClient.placeAndTakePerpOrder(orderParams) }, 3, 8000) // 8s base delay, 3 max retries - logger.log(`✅ Close order placed! Transaction: ${txSig}`) + console.log(`✅ Close order executed! Transaction: ${txSig}`) // CRITICAL: Confirm transaction on-chain to prevent phantom closes // BUT: Use timeout to prevent API hangs during network congestion