critical: Bug #90 - Use placeAndTakePerpOrder for immediate MARKET order fills
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)
This commit is contained in:
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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
|
## Appendix: Pattern Recognition
|
||||||
|
|
||||||
### Common Root Causes
|
### 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
|
**Maintainer:** AI Agent team following "NOTHING gets lost" principle
|
||||||
|
|||||||
@@ -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 = {
|
const orderParams = {
|
||||||
orderType: OrderType.MARKET,
|
orderType: OrderType.MARKET,
|
||||||
marketIndex: marketConfig.driftMarketIndex,
|
marketIndex: marketConfig.driftMarketIndex,
|
||||||
@@ -655,14 +655,26 @@ export async function closePosition(
|
|||||||
reduceOnly: true, // Important: only close existing position
|
reduceOnly: true, // Important: only close existing position
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place market close order using simple placePerpOrder (like v3)
|
// CRITICAL FIX (Dec 31, 2025 - Bug #89): Use placeAndTakePerpOrder instead of placePerpOrder
|
||||||
// CRITICAL: Wrap in retry logic for rate limit protection
|
//
|
||||||
logger.log('🚀 Placing REAL market close order with retry protection...')
|
// 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 () => {
|
const txSig = await retryWithBackoff(async () => {
|
||||||
return await driftClient.placePerpOrder(orderParams)
|
return await driftClient.placeAndTakePerpOrder(orderParams)
|
||||||
}, 3, 8000) // 8s base delay, 3 max retries
|
}, 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
|
// CRITICAL: Confirm transaction on-chain to prevent phantom closes
|
||||||
// BUT: Use timeout to prevent API hangs during network congestion
|
// BUT: Use timeout to prevent API hangs during network congestion
|
||||||
|
|||||||
Reference in New Issue
Block a user