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 |
|
||||
| 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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user