docs: Add Common Pitfall #47 - position close verification gap
This commit is contained in:
64
.github/copilot-instructions.md
vendored
64
.github/copilot-instructions.md
vendored
@@ -1949,6 +1949,70 @@ trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK
|
|||||||
- **Git commit:** 7129cbf "fix: Add 99% safety buffer for 100% position sizing"
|
- **Git commit:** 7129cbf "fix: Add 99% safety buffer for 100% position sizing"
|
||||||
- **Lesson:** When integrating with DEX protocols, never use 100% of resources - always leave safety margin for protocol-level calculations
|
- **Lesson:** When integrating with DEX protocols, never use 100% of resources - always leave safety margin for protocol-level calculations
|
||||||
|
|
||||||
|
47. **Position close verification gap - 6 hours unmonitored (CRITICAL - Fixed Nov 16, 2025):**
|
||||||
|
- **Symptom:** Close transaction confirmed on-chain, database marked "SL closed", but position stayed open on Drift for 6+ hours unmonitored
|
||||||
|
- **Root Cause:** Transaction confirmation ≠ Drift internal state updated immediately (5-10 second propagation delay)
|
||||||
|
- **Real incident (Nov 16, 02:51 CET):**
|
||||||
|
* Trailing stop triggered at 02:51:57
|
||||||
|
* Close transaction confirmed on-chain ✅
|
||||||
|
* Position Manager immediately queried Drift → still showed open (stale state)
|
||||||
|
* Ghost detection eventually marked it "closed" in database
|
||||||
|
* But position actually stayed open on Drift until 08:51 restart
|
||||||
|
* **6 hours unprotected** - no monitoring, no TP/SL backup, only orphaned on-chain orders
|
||||||
|
- **Why dangerous:**
|
||||||
|
* Database said "closed" so container restarts wouldn't restore monitoring
|
||||||
|
* Position exposed to unlimited risk if price moved against
|
||||||
|
* Only saved by luck (container restart at 08:51 detected orphaned position)
|
||||||
|
* Startup validator caught mismatch: "CRITICAL: marked as CLOSED in DB but still OPEN on Drift"
|
||||||
|
- **Impact:** Every trailing stop or SL exit vulnerable to this race condition
|
||||||
|
- **Fix (2-layer verification):**
|
||||||
|
```typescript
|
||||||
|
// In lib/drift/orders.ts closePosition() (line ~634):
|
||||||
|
if (params.percentToClose === 100) {
|
||||||
|
console.log('🗑️ Position fully closed, cancelling remaining orders...')
|
||||||
|
await cancelAllOrders(params.symbol)
|
||||||
|
|
||||||
|
// CRITICAL: Verify position actually closed on Drift
|
||||||
|
// Transaction confirmed ≠ Drift state updated immediately
|
||||||
|
console.log('⏳ Waiting 5s for Drift state to propagate...')
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||||
|
|
||||||
|
const verifyPosition = await driftService.getPosition(marketConfig.driftMarketIndex)
|
||||||
|
if (verifyPosition && Math.abs(verifyPosition.size) >= 0.01) {
|
||||||
|
console.error(`🔴 CRITICAL: Close confirmed BUT position still exists!`)
|
||||||
|
console.error(` Transaction: ${txSig}, Drift size: ${verifyPosition.size}`)
|
||||||
|
// Return success but flag that monitoring should continue
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
transactionSignature: txSig,
|
||||||
|
closePrice: oraclePrice,
|
||||||
|
closedSize: sizeToClose,
|
||||||
|
realizedPnL,
|
||||||
|
needsVerification: true, // Flag for Position Manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✅ Position verified closed on Drift')
|
||||||
|
}
|
||||||
|
|
||||||
|
// In lib/trading/position-manager.ts executeExit() (line ~1206):
|
||||||
|
if ((result as any).needsVerification) {
|
||||||
|
console.log(`⚠️ Close confirmed but position still exists on Drift`)
|
||||||
|
console.log(` Keeping ${trade.symbol} in monitoring until Drift confirms closure`)
|
||||||
|
console.log(` Ghost detection will handle final cleanup once Drift updates`)
|
||||||
|
// Keep monitoring - don't mark closed yet
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Behavior now:**
|
||||||
|
* Close transaction confirmed → wait 5 seconds
|
||||||
|
* Query Drift to verify position actually gone
|
||||||
|
* If still exists: Keep monitoring, log critical error, wait for ghost detection
|
||||||
|
* If verified closed: Proceed with database update and cleanup
|
||||||
|
* Ghost detection becomes safety net, not primary close mechanism
|
||||||
|
- **Prevents:** Premature database "closed" marking while position still open on Drift
|
||||||
|
- **Git commit:** c607a66 "critical: Fix position close verification to prevent ghost positions"
|
||||||
|
- **Lesson:** In DEX trading, always verify state changes actually propagated before updating local state
|
||||||
|
|
||||||
## File Conventions
|
## File Conventions
|
||||||
|
|
||||||
- **API routes:** `app/api/[feature]/[action]/route.ts` (Next.js 15 App Router)
|
- **API routes:** `app/api/[feature]/[action]/route.ts` (Next.js 15 App Router)
|
||||||
|
|||||||
Reference in New Issue
Block a user