- Automatic fallback after 2 consecutive rate limits - Primary: Alchemy (300M CU/month, stable for normal ops) - Fallback: Helius (10 req/sec, backup for startup bursts) - Reduced startup validation: 6h window, 5 trades (was 24h, 20 trades) - Multi-position safety check (prevents order cancellation conflicts) - Rate limit-aware retry logic with exponential backoff Implementation: - lib/drift/client.ts: Added fallbackConnection, switchToFallbackRpc() - .env: SOLANA_FALLBACK_RPC_URL configuration - lib/startup/init-position-manager.ts: Reduced validation scope - lib/trading/position-manager.ts: Multi-position order protection Tested: System switched to fallback on startup, Position Manager active Result: 1 active trade being monitored after automatic RPC switch
152 lines
5.4 KiB
TypeScript
152 lines
5.4 KiB
TypeScript
/**
|
|
* Position Manager Startup Initialization
|
|
*
|
|
* Ensures Position Manager starts monitoring on bot startup
|
|
* This prevents orphaned trades when the bot restarts
|
|
*/
|
|
|
|
import { getInitializedPositionManager } from '../trading/position-manager'
|
|
import { initializeDriftService } from '../drift/client'
|
|
import { getPrismaClient } from '../database/trades'
|
|
import { getMarketConfig } from '../../config/trading'
|
|
|
|
let initStarted = false
|
|
|
|
export async function initializePositionManagerOnStartup() {
|
|
if (initStarted) {
|
|
return
|
|
}
|
|
|
|
initStarted = true
|
|
|
|
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()
|
|
|
|
console.log(`✅ Position Manager ready - ${status.activeTradesCount} active trades`)
|
|
|
|
if (status.activeTradesCount > 0) {
|
|
console.log(`📊 Monitoring: ${status.symbols.join(', ')}`)
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Failed to initialize Position Manager on startup:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate that open trades in database match actual Drift positions
|
|
*
|
|
* CRITICAL FIX (Nov 14, 2025):
|
|
* - Also checks trades marked as "closed" in DB that might still be open on Drift
|
|
* - Happens when close transaction fails but bot marks it as closed anyway
|
|
* - Restores Position Manager tracking for these orphaned positions
|
|
*/
|
|
async function validateOpenTrades() {
|
|
try {
|
|
const prisma = getPrismaClient()
|
|
|
|
// Get both truly open trades AND recently "closed" trades (last 24h)
|
|
// Recently closed trades might still be open if close transaction failed
|
|
// TEMPORARILY REDUCED: Check only last 5 closed trades to avoid rate limiting on startup
|
|
const [openTrades, recentlyClosedTrades] = await Promise.all([
|
|
prisma.trade.findMany({
|
|
where: { status: 'open' },
|
|
orderBy: { entryTime: 'asc' }
|
|
}),
|
|
prisma.trade.findMany({
|
|
where: {
|
|
exitReason: { not: null },
|
|
exitTime: { gte: new Date(Date.now() - 6 * 60 * 60 * 1000) } // Last 6 hours (reduced from 24h)
|
|
},
|
|
orderBy: { exitTime: 'desc' },
|
|
take: 5 // Reduced from 20 to avoid rate limiting
|
|
})
|
|
])
|
|
|
|
const allTradesToCheck = [...openTrades, ...recentlyClosedTrades]
|
|
|
|
if (allTradesToCheck.length === 0) {
|
|
console.log('✅ No open trades to validate')
|
|
return
|
|
}
|
|
|
|
console.log(`🔍 Validating ${openTrades.length} open + ${recentlyClosedTrades.length} recently closed trades against Drift...`)
|
|
|
|
const driftService = await initializeDriftService()
|
|
const driftPositions = await driftService.getAllPositions() // Get all positions once
|
|
|
|
for (const trade of allTradesToCheck) {
|
|
try {
|
|
const marketConfig = getMarketConfig(trade.symbol)
|
|
|
|
// Find matching Drift position by symbol
|
|
const position = driftPositions.find(p => p.symbol === trade.symbol)
|
|
|
|
if (!position || position.size === 0) {
|
|
// No position on Drift
|
|
if (trade.status === 'open') {
|
|
console.log(`⚠️ PHANTOM TRADE: ${trade.symbol} marked open in DB but not found on Drift`)
|
|
console.log(` 🗑️ Auto-closing 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,
|
|
}
|
|
})
|
|
}
|
|
// If already closed in DB and not on Drift, that's correct - skip
|
|
continue
|
|
}
|
|
|
|
// Position EXISTS on Drift
|
|
const driftDirection = position.side.toLowerCase() as 'long' | 'short'
|
|
|
|
if (driftDirection !== trade.direction) {
|
|
console.log(`⚠️ DIRECTION MISMATCH: ${trade.symbol} DB=${trade.direction} Drift=${driftDirection}`)
|
|
continue
|
|
}
|
|
|
|
// CRITICAL: If DB says closed but Drift shows open, restore tracking!
|
|
if (trade.exitReason !== null) {
|
|
console.log(`🔴 CRITICAL: ${trade.symbol} marked as CLOSED in DB but still OPEN on Drift!`)
|
|
console.log(` DB exit: ${trade.exitReason} at ${trade.exitTime?.toISOString()}`)
|
|
console.log(` Drift: ${position.size} ${trade.symbol} ${driftDirection} @ $${position.entryPrice.toFixed(2)}`)
|
|
console.log(` 🔄 Reopening trade in DB to restore Position Manager tracking...`)
|
|
|
|
await prisma.trade.update({
|
|
where: { id: trade.id },
|
|
data: {
|
|
status: 'open',
|
|
exitReason: null,
|
|
exitTime: null,
|
|
exitPrice: null,
|
|
// Keep original realizedPnL from partial closes if any
|
|
}
|
|
})
|
|
|
|
console.log(` ✅ Trade restored - Position Manager will now monitor it`)
|
|
} else {
|
|
console.log(`✅ ${trade.symbol} ${trade.direction}: Position verified on Drift`)
|
|
}
|
|
|
|
} catch (posError) {
|
|
console.error(`❌ Error validating trade ${trade.symbol}:`, posError)
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error in validateOpenTrades:', error)
|
|
}
|
|
}
|