fix: Add phantom trade detection and prevention safeguards

**Root Causes:**
1. Auto-flip logic could create phantom trades if close failed
2. Position size mismatches (0.01 SOL vs 11.92 SOL expected) not caught
3. Multiple trades for same symbol+direction in database

**Preventive Measures:**

1. **Startup Validation (lib/startup/init-position-manager.ts)**
   - Validates all open trades against Drift positions on startup
   - Auto-closes phantom trades with <50% expected size
   - Logs size mismatches for manual review
   - Prevents Position Manager from tracking ghost positions

2. **Duplicate Position Prevention (app/api/trading/execute/route.ts)**
   - Blocks opening same-direction position on same symbol
   - Returns 400 error if duplicate detected
   - Only allows auto-flip (opposite direction close + open)

3. **Runtime Phantom Detection (lib/trading/position-manager.ts)**
   - Checks position size every 2s monitoring cycle
   - Auto-closes if size ratio <50% (extreme mismatch)
   - Logs as 'manual' exit with AUTO_CLEANUP tx
   - Removes from monitoring immediately

4. **Quality Score Fix (app/api/trading/check-risk/route.ts)**
   - Hardcoded minScore=60 (removed non-existent config reference)

**Prevention Summary:**
-  Startup validation catches historical phantoms
-  Duplicate check prevents new phantoms
-  Runtime detection catches size mismatches <30s after they occur
-  All three layers work together for defense-in-depth

Issue: User had LONG (phantom) + SHORT (undersized 0.01 SOL vs 11.92 expected)
Fix: Both detected and closed, bot now clean with 0 active trades
This commit is contained in:
mindesbunister
2025-11-03 13:53:12 +01:00
parent 1313031acd
commit 6b1d32a72d
5 changed files with 139 additions and 3 deletions

View File

@@ -6,6 +6,9 @@
*/
import { getInitializedPositionManager } from '../trading/position-manager'
import { initializeDriftService } from '../drift/client'
import { getPrismaClient } from '../database/trades'
import { getMarketConfig } from '../../config/trading'
let initStarted = false
@@ -19,6 +22,9 @@ export async function initializePositionManagerOnStartup() {
console.log('🚀 Initializing Position Manager on startup...')
try {
// Validate open trades against Drift positions BEFORE starting Position Manager
await validateOpenTrades()
const manager = await getInitializedPositionManager()
const status = manager.getStatus()
@@ -31,3 +37,82 @@ export async function initializePositionManagerOnStartup() {
console.error('❌ Failed to initialize Position Manager on startup:', error)
}
}
/**
* Validate that open trades in database match actual Drift positions
* Closes phantom trades that don't exist on-chain
*/
async function validateOpenTrades() {
try {
const prisma = getPrismaClient()
const openTrades = await prisma.trade.findMany({
where: { status: 'open' },
orderBy: { entryTime: 'asc' }
})
if (openTrades.length === 0) {
console.log('✅ No open trades to validate')
return
}
console.log(`🔍 Validating ${openTrades.length} open trade(s) against Drift positions...`)
const driftService = await initializeDriftService()
for (const trade of openTrades) {
try {
const marketConfig = getMarketConfig(trade.symbol)
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
// Calculate expected position size in base assets
const expectedSizeBase = trade.positionSizeUSD / trade.entryPrice
const actualSizeBase = position?.size || 0
// Check if position exists and size matches (with 50% tolerance for partial fills)
const sizeDiff = Math.abs(expectedSizeBase - actualSizeBase)
const sizeRatio = actualSizeBase / expectedSizeBase
if (!position || position.side === 'none' || sizeRatio < 0.5) {
console.log(`⚠️ PHANTOM TRADE DETECTED:`)
console.log(` Trade ID: ${trade.id.substring(0, 20)}...`)
console.log(` Symbol: ${trade.symbol} ${trade.direction}`)
console.log(` Expected size: ${expectedSizeBase.toFixed(4)}`)
console.log(` Actual size: ${actualSizeBase.toFixed(4)}`)
console.log(` Entry: $${trade.entryPrice} at ${trade.entryTime.toISOString()}`)
console.log(` 🗑️ Auto-closing phantom trade...`)
// Close phantom trade
await prisma.trade.update({
where: { id: trade.id },
data: {
status: 'closed',
exitTime: new Date(),
exitReason: 'PHANTOM_TRADE_CLEANUP',
exitPrice: trade.entryPrice,
realizedPnL: 0,
realizedPnLPercent: 0,
}
})
console.log(` ✅ Phantom trade closed`)
} else if (sizeDiff > expectedSizeBase * 0.1) {
console.log(`⚠️ SIZE MISMATCH (${(sizeRatio * 100).toFixed(1)}% of expected):`)
console.log(` Trade ID: ${trade.id.substring(0, 20)}...`)
console.log(` Symbol: ${trade.symbol} ${trade.direction}`)
console.log(` Expected: ${expectedSizeBase.toFixed(4)}, Actual: ${actualSizeBase.toFixed(4)}`)
console.log(` Will monitor with adjusted size`)
} else {
console.log(`${trade.symbol} ${trade.direction}: Size OK (${actualSizeBase.toFixed(4)})`)
}
} catch (posError) {
console.error(`❌ Error validating trade ${trade.symbol}:`, posError)
// Don't auto-close on error - might be temporary
}
}
} catch (error) {
console.error('❌ Error in validateOpenTrades:', error)
// Don't throw - allow Position Manager to start anyway
}
}