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:
mindesbunister
2025-12-31 11:45:21 +01:00
parent d0323fddd7
commit 465f6bdb82
2 changed files with 126 additions and 7 deletions

View File

@@ -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

View File

@@ -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