🧹 Integrate orphaned order cleanup into position monitoring
FEATURES: - Position monitor now automatically detects orphaned orders when no positions - Triggers cleanup only when hasPosition: false to eliminate redundant polling - Provides detailed cleanup results in monitoring response - Leverages existing frequent position checks vs separate timers - Modified /app/api/automation/position-monitor/route.js to check for orphaned orders - Calls existing /api/drift/cleanup-orders endpoint when no positions detected - Returns cleanup status, success/failure, and summary in monitoring response - Handles cleanup errors gracefully with detailed error reporting - Eliminates need for separate 60-second cleanup polling - Uses existing position monitoring infrastructure - Only runs cleanup when positions close (triggered by hasPosition: false) - Automatic handling of orphaned orders after SL/TP execution - Added test-orphaned-cleanup-integration.js for verification - Tests both position monitor integration and direct cleanup API - Provides detailed feedback on cleanup operations This completes the automation enhancement requested - no more manual cleanup needed!
This commit is contained in:
@@ -22,7 +22,8 @@ export async function GET() {
|
||||
stopLossProximity: null,
|
||||
riskLevel: 'NONE',
|
||||
nextAction: 'No position to monitor',
|
||||
recommendation: 'START_TRADING'
|
||||
recommendation: 'START_TRADING',
|
||||
orphanedOrderCleanup: null
|
||||
};
|
||||
|
||||
if (positionsData.success && positionsData.positions.length > 0) {
|
||||
@@ -78,6 +79,84 @@ export async function GET() {
|
||||
result.nextAction = 'Standard monitoring - Check every 30 minutes';
|
||||
result.recommendation = 'RELAXED_MONITORING';
|
||||
}
|
||||
} else {
|
||||
// NO POSITION DETECTED - Check for orphaned orders and cleanup
|
||||
console.log('📋 No active positions detected - checking for orphaned orders...');
|
||||
|
||||
try {
|
||||
// Check for any remaining orders when we have no positions
|
||||
const ordersResponse = await fetch(`${baseUrl}/api/drift/orders`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (ordersResponse.ok) {
|
||||
const ordersData = await ordersResponse.json();
|
||||
const activeOrders = ordersData.orders || [];
|
||||
|
||||
if (activeOrders.length > 0) {
|
||||
console.log(`🎯 Found ${activeOrders.length} orphaned orders - triggering cleanup...`);
|
||||
|
||||
// Trigger automated cleanup of orphaned orders
|
||||
const cleanupResponse = await fetch(`${baseUrl}/api/drift/cleanup-orders`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
let cleanupResult = null;
|
||||
if (cleanupResponse.ok) {
|
||||
cleanupResult = await cleanupResponse.json();
|
||||
|
||||
if (cleanupResult.success) {
|
||||
console.log('✅ Orphaned order cleanup completed:', cleanupResult.summary);
|
||||
result.orphanedOrderCleanup = {
|
||||
triggered: true,
|
||||
success: true,
|
||||
summary: cleanupResult.summary,
|
||||
message: `Cleaned up ${cleanupResult.summary.totalCanceled} orphaned orders`
|
||||
};
|
||||
result.nextAction = `Cleaned up ${cleanupResult.summary.totalCanceled} orphaned orders - Ready for new trade`;
|
||||
} else {
|
||||
console.error('❌ Orphaned order cleanup failed:', cleanupResult.error);
|
||||
result.orphanedOrderCleanup = {
|
||||
triggered: true,
|
||||
success: false,
|
||||
error: cleanupResult.error,
|
||||
message: 'Cleanup failed - Manual intervention may be needed'
|
||||
};
|
||||
result.nextAction = 'Cleanup failed - Check orders manually';
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Failed to trigger cleanup API');
|
||||
result.orphanedOrderCleanup = {
|
||||
triggered: false,
|
||||
success: false,
|
||||
error: 'Cleanup API unavailable',
|
||||
message: 'Could not trigger automatic cleanup'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log('✅ No orphaned orders found');
|
||||
result.orphanedOrderCleanup = {
|
||||
triggered: false,
|
||||
success: true,
|
||||
message: 'No orphaned orders detected'
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error('❌ Error during orphaned order check:', cleanupError);
|
||||
result.orphanedOrderCleanup = {
|
||||
triggered: false,
|
||||
success: false,
|
||||
error: cleanupError.message,
|
||||
message: 'Error checking for orphaned orders'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
230
app/api/drift/cleanup-orders/route.js
Normal file
230
app/api/drift/cleanup-orders/route.js
Normal file
@@ -0,0 +1,230 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { executeWithFailover, getRpcStatus } from '../../../../lib/rpc-failover.js'
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
console.log('🧹 Starting orphaned order cleanup...')
|
||||
|
||||
// Log RPC status
|
||||
const rpcStatus = getRpcStatus()
|
||||
console.log('🌐 RPC Status:', rpcStatus)
|
||||
|
||||
// Check if environment is configured
|
||||
if (!process.env.SOLANA_PRIVATE_KEY) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Drift not configured - missing SOLANA_PRIVATE_KEY'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Execute cleanup with RPC failover
|
||||
const result = await executeWithFailover(async (connection) => {
|
||||
// Import Drift SDK components
|
||||
const { DriftClient, initialize } = await import('@drift-labs/sdk')
|
||||
const { Keypair } = await import('@solana/web3.js')
|
||||
const { AnchorProvider } = await import('@coral-xyz/anchor')
|
||||
|
||||
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
|
||||
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
|
||||
|
||||
// Use the correct Wallet class
|
||||
const { default: NodeWallet } = await import('@coral-xyz/anchor/dist/cjs/nodewallet.js')
|
||||
const wallet = new NodeWallet(keypair)
|
||||
|
||||
// Initialize Drift SDK
|
||||
const env = 'mainnet-beta'
|
||||
const sdkConfig = initialize({ env })
|
||||
|
||||
const driftClient = new DriftClient({
|
||||
connection,
|
||||
wallet,
|
||||
programID: sdkConfig.DRIFT_PROGRAM_ID,
|
||||
opts: {
|
||||
commitment: 'confirmed',
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
await driftClient.subscribe()
|
||||
console.log('✅ Connected to Drift for cleanup')
|
||||
|
||||
// Get user account
|
||||
let userAccount
|
||||
try {
|
||||
userAccount = await driftClient.getUserAccount()
|
||||
} catch (accountError) {
|
||||
await driftClient.unsubscribe()
|
||||
throw new Error('No Drift user account found. Please initialize your account first.')
|
||||
}
|
||||
|
||||
// Get current positions
|
||||
const perpPositions = userAccount.perpPositions || []
|
||||
const activePositions = perpPositions.filter(pos =>
|
||||
pos.baseAssetAmount && !pos.baseAssetAmount.isZero()
|
||||
)
|
||||
|
||||
// Get current orders
|
||||
const orders = userAccount.orders || []
|
||||
const activeOrders = orders.filter(order =>
|
||||
order.status === 0 && !order.baseAssetAmount.isZero()
|
||||
)
|
||||
|
||||
console.log(`📊 Analysis: ${activePositions.length} active positions, ${activeOrders.length} active orders`)
|
||||
|
||||
// Map positions by market index
|
||||
const positionMarkets = new Set(activePositions.map(pos => pos.marketIndex))
|
||||
|
||||
// Find orphaned orders (orders for markets where we have no position)
|
||||
const orphanedOrders = activeOrders.filter(order => {
|
||||
// Check if this order is for a market where we have no position
|
||||
const hasPosition = positionMarkets.has(order.marketIndex)
|
||||
|
||||
// Also check if it's a reduce-only order (these should be canceled if no position)
|
||||
const isReduceOnly = order.reduceOnly
|
||||
|
||||
return !hasPosition || (isReduceOnly && !hasPosition)
|
||||
})
|
||||
|
||||
// Additionally, find lingering SL/TP orders when position has changed significantly
|
||||
const conflictingOrders = []
|
||||
|
||||
for (const order of activeOrders) {
|
||||
// Find corresponding position
|
||||
const position = activePositions.find(pos => pos.marketIndex === order.marketIndex)
|
||||
|
||||
if (position) {
|
||||
const positionSide = Number(position.baseAssetAmount) > 0 ? 'long' : 'short'
|
||||
const orderDirection = order.direction === 0 ? 'long' : 'short'
|
||||
|
||||
// Check for conflicting reduce-only orders
|
||||
if (order.reduceOnly) {
|
||||
// Reduce-only order should be opposite direction to position
|
||||
const correctDirection = positionSide === 'long' ? 'short' : 'long'
|
||||
|
||||
if (orderDirection !== correctDirection) {
|
||||
console.log(`⚠️ Found conflicting reduce-only order: ${orderDirection} order for ${positionSide} position`)
|
||||
conflictingOrders.push(order)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ordersToCancel = [...orphanedOrders, ...conflictingOrders]
|
||||
|
||||
console.log(`🎯 Found ${orphanedOrders.length} orphaned orders and ${conflictingOrders.length} conflicting orders`)
|
||||
|
||||
const cancelResults = []
|
||||
|
||||
if (ordersToCancel.length > 0) {
|
||||
console.log('🧹 Canceling orphaned/conflicting orders...')
|
||||
|
||||
for (const order of ordersToCancel) {
|
||||
try {
|
||||
const marketIndex = order.marketIndex
|
||||
const orderId = order.orderId
|
||||
|
||||
// Get market symbol for logging
|
||||
const marketSymbols = {
|
||||
0: 'SOL-PERP',
|
||||
1: 'BTC-PERP',
|
||||
2: 'ETH-PERP',
|
||||
3: 'APT-PERP',
|
||||
4: 'BNB-PERP'
|
||||
}
|
||||
const symbol = marketSymbols[marketIndex] || `MARKET-${marketIndex}`
|
||||
|
||||
console.log(`❌ Canceling order: ${symbol} Order ID ${orderId}`)
|
||||
|
||||
// Cancel the order
|
||||
const txSig = await driftClient.cancelOrder(orderId)
|
||||
|
||||
console.log(`✅ Canceled order ${orderId} for ${symbol}, tx: ${txSig}`)
|
||||
|
||||
cancelResults.push({
|
||||
orderId: orderId,
|
||||
marketIndex: marketIndex,
|
||||
symbol: symbol,
|
||||
txSignature: txSig,
|
||||
success: true,
|
||||
reason: orphanedOrders.includes(order) ? 'orphaned' : 'conflicting'
|
||||
})
|
||||
|
||||
// Small delay between cancellations to avoid rate limits
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
} catch (cancelError) {
|
||||
console.error(`❌ Failed to cancel order ${order.orderId}:`, cancelError)
|
||||
|
||||
cancelResults.push({
|
||||
orderId: order.orderId,
|
||||
marketIndex: order.marketIndex,
|
||||
success: false,
|
||||
error: cancelError.message,
|
||||
reason: orphanedOrders.includes(order) ? 'orphaned' : 'conflicting'
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('✅ No orphaned or conflicting orders found')
|
||||
}
|
||||
|
||||
await driftClient.unsubscribe()
|
||||
|
||||
const cleanupResult = {
|
||||
success: true,
|
||||
summary: {
|
||||
activePositions: activePositions.length,
|
||||
activeOrders: activeOrders.length,
|
||||
orphanedOrders: orphanedOrders.length,
|
||||
conflictingOrders: conflictingOrders.length,
|
||||
totalCanceled: cancelResults.filter(r => r.success).length,
|
||||
totalFailed: cancelResults.filter(r => !r.success).length
|
||||
},
|
||||
canceledOrders: cancelResults,
|
||||
timestamp: Date.now(),
|
||||
rpcEndpoint: getRpcStatus().currentEndpoint
|
||||
}
|
||||
|
||||
console.log('🧹 Cleanup completed:', cleanupResult.summary)
|
||||
return cleanupResult
|
||||
|
||||
} catch (driftError) {
|
||||
console.error('❌ Drift cleanup error:', driftError)
|
||||
|
||||
try {
|
||||
await driftClient.unsubscribe()
|
||||
} catch (cleanupError) {
|
||||
console.warn('⚠️ Cleanup error:', cleanupError.message)
|
||||
}
|
||||
|
||||
throw driftError
|
||||
}
|
||||
}, 3) // Max 3 retries across different RPCs
|
||||
|
||||
return NextResponse.json(result)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Orphaned order cleanup API error:', error)
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Failed to cleanup orphaned orders',
|
||||
details: error.message,
|
||||
rpcStatus: getRpcStatus()
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
message: 'Drift Orphaned Order Cleanup API',
|
||||
description: 'Automatically cancels orphaned orders when SL/TP hits but leaves opposite orders open',
|
||||
usage: 'POST /api/drift/cleanup-orders',
|
||||
features: [
|
||||
'Detects orphaned orders (orders for markets with no position)',
|
||||
'Finds conflicting reduce-only orders',
|
||||
'Automatically cancels problematic orders',
|
||||
'Prevents manual cleanup requirement'
|
||||
]
|
||||
})
|
||||
}
|
||||
74
drift-cleanup-daemon.js
Normal file
74
drift-cleanup-daemon.js
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Drift Order Cleanup Daemon
|
||||
* Runs the cleanup service in the background
|
||||
*/
|
||||
|
||||
const { driftOrderCleanupService } = require('./lib/drift-order-cleanup-service.js')
|
||||
|
||||
let isShuttingDown = false
|
||||
|
||||
async function startDaemon() {
|
||||
console.log('🚀 Starting Drift Order Cleanup Daemon...')
|
||||
console.log('==========================================')
|
||||
|
||||
// Start the cleanup service
|
||||
driftOrderCleanupService.start(60000) // Check every 60 seconds
|
||||
|
||||
console.log('✅ Daemon started successfully!')
|
||||
console.log('📊 Monitoring for orphaned orders every 60 seconds')
|
||||
console.log('🛑 Press Ctrl+C to stop')
|
||||
console.log('')
|
||||
|
||||
// Set up graceful shutdown
|
||||
process.on('SIGINT', gracefulShutdown)
|
||||
process.on('SIGTERM', gracefulShutdown)
|
||||
process.on('SIGQUIT', gracefulShutdown)
|
||||
|
||||
// Keep the process running
|
||||
const keepAlive = setInterval(() => {
|
||||
if (!isShuttingDown) {
|
||||
const status = driftOrderCleanupService.getStatus()
|
||||
const timestamp = new Date().toISOString()
|
||||
console.log(`[${timestamp}] 💓 Daemon running - Last cleanup: ${status.lastCleanupTime ? `${Math.floor((Date.now() - status.lastCleanupTime) / 1000)}s ago` : 'Never'}`)
|
||||
}
|
||||
}, 300000) // Log status every 5 minutes
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('exit', () => {
|
||||
clearInterval(keepAlive)
|
||||
})
|
||||
}
|
||||
|
||||
function gracefulShutdown(signal) {
|
||||
if (isShuttingDown) return
|
||||
|
||||
isShuttingDown = true
|
||||
console.log(`\n🛑 Received ${signal}, shutting down gracefully...`)
|
||||
|
||||
driftOrderCleanupService.stop()
|
||||
console.log('✅ Drift order cleanup daemon stopped')
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Error handling
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('❌ Uncaught exception:', error)
|
||||
gracefulShutdown('UNCAUGHT_EXCEPTION')
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// Start the daemon
|
||||
if (require.main === module) {
|
||||
startDaemon().catch(error => {
|
||||
console.error('❌ Failed to start daemon:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { startDaemon }
|
||||
139
drift-cleanup-manager.js
Normal file
139
drift-cleanup-manager.js
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Drift Order Cleanup Management Script
|
||||
* Easy commands to manage the automated cleanup service
|
||||
*/
|
||||
|
||||
const { driftOrderCleanupService } = require('./lib/drift-order-cleanup-service.js')
|
||||
|
||||
const commands = {
|
||||
status: async () => {
|
||||
console.log('📊 Drift Order Cleanup Status')
|
||||
console.log('============================')
|
||||
|
||||
const status = driftOrderCleanupService.getStatus()
|
||||
console.log(`Running: ${status.isRunning ? '✅ YES' : '❌ NO'}`)
|
||||
|
||||
if (status.lastCleanupTime > 0) {
|
||||
const ago = Math.floor((Date.now() - status.lastCleanupTime) / 1000)
|
||||
console.log(`Last cleanup: ${ago}s ago`)
|
||||
} else {
|
||||
console.log('Last cleanup: Never')
|
||||
}
|
||||
|
||||
if (status.cooldownRemaining > 0) {
|
||||
console.log(`Cooldown: ${Math.floor(status.cooldownRemaining / 1000)}s remaining`)
|
||||
}
|
||||
|
||||
// Get current positions and orders
|
||||
try {
|
||||
const [positionsRes, ordersRes] = await Promise.all([
|
||||
fetch('http://localhost:9001/api/drift/positions'),
|
||||
fetch('http://localhost:9001/api/drift/orders')
|
||||
])
|
||||
|
||||
if (positionsRes.ok && ordersRes.ok) {
|
||||
const positions = await positionsRes.json()
|
||||
const orders = await ordersRes.json()
|
||||
|
||||
console.log(`\nCurrent: ${positions.positions?.length || 0} positions, ${orders.orders?.length || 0} orders`)
|
||||
|
||||
if (positions.positions?.length > 0) {
|
||||
console.log('\nActive Positions:')
|
||||
positions.positions.forEach(pos => {
|
||||
console.log(` 📈 ${pos.symbol}: ${pos.size > 0 ? 'LONG' : 'SHORT'} ${Math.abs(pos.size)} ($${pos.value?.toFixed(2) || 'N/A'})`)
|
||||
})
|
||||
}
|
||||
|
||||
if (orders.orders?.length > 0) {
|
||||
console.log('\nActive Orders:')
|
||||
orders.orders.forEach(order => {
|
||||
console.log(` 📋 ${order.symbol}: ${order.side} ${order.size} @ $${order.price} (${order.orderType})`)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Could not fetch current positions/orders')
|
||||
}
|
||||
},
|
||||
|
||||
start: () => {
|
||||
console.log('🚀 Starting automated cleanup service...')
|
||||
driftOrderCleanupService.start()
|
||||
console.log('✅ Service started! It will check for orphaned orders every 60 seconds.')
|
||||
},
|
||||
|
||||
stop: () => {
|
||||
console.log('🛑 Stopping automated cleanup service...')
|
||||
driftOrderCleanupService.stop()
|
||||
console.log('✅ Service stopped.')
|
||||
},
|
||||
|
||||
cleanup: async () => {
|
||||
console.log('🧹 Running manual cleanup...')
|
||||
try {
|
||||
const result = await driftOrderCleanupService.forceCleanup()
|
||||
console.log('\n📊 Cleanup Results:')
|
||||
console.log(` Positions: ${result.summary.activePositions}`)
|
||||
console.log(` Orders: ${result.summary.activeOrders}`)
|
||||
console.log(` Orphaned: ${result.summary.orphanedOrders}`)
|
||||
console.log(` Conflicting: ${result.summary.conflictingOrders}`)
|
||||
console.log(` ✅ Canceled: ${result.summary.totalCanceled}`)
|
||||
console.log(` ❌ Failed: ${result.summary.totalFailed}`)
|
||||
|
||||
if (result.canceledOrders?.length > 0) {
|
||||
console.log('\nCanceled Orders:')
|
||||
result.canceledOrders.forEach(order => {
|
||||
if (order.success) {
|
||||
console.log(` ✅ ${order.symbol} order ${order.orderId} (${order.reason})`)
|
||||
} else {
|
||||
console.log(` ❌ Order ${order.orderId}: ${order.error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Cleanup failed:', error.message)
|
||||
}
|
||||
},
|
||||
|
||||
help: () => {
|
||||
console.log('🧹 Drift Order Cleanup Commands')
|
||||
console.log('===============================')
|
||||
console.log('')
|
||||
console.log('Commands:')
|
||||
console.log(' status - Show service status and current positions/orders')
|
||||
console.log(' start - Start automated cleanup monitoring')
|
||||
console.log(' stop - Stop automated cleanup monitoring')
|
||||
console.log(' cleanup - Run manual cleanup now')
|
||||
console.log(' help - Show this help')
|
||||
console.log('')
|
||||
console.log('Examples:')
|
||||
console.log(' node drift-cleanup-manager.js status')
|
||||
console.log(' node drift-cleanup-manager.js start')
|
||||
console.log(' node drift-cleanup-manager.js cleanup')
|
||||
console.log('')
|
||||
console.log('What it does:')
|
||||
console.log('• Detects orphaned orders (orders for markets with no position)')
|
||||
console.log('• Finds conflicting reduce-only orders')
|
||||
console.log('• Automatically cancels problematic orders')
|
||||
console.log('• Prevents manual order management after SL/TP hits')
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const command = process.argv[2] || 'help'
|
||||
|
||||
if (commands[command]) {
|
||||
await commands[command]()
|
||||
} else {
|
||||
console.log(`❌ Unknown command: ${command}`)
|
||||
console.log('Run "node drift-cleanup-manager.js help" for available commands')
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(console.error)
|
||||
}
|
||||
|
||||
module.exports = { commands }
|
||||
203
lib/drift-order-cleanup-service.js
Normal file
203
lib/drift-order-cleanup-service.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Drift Order Cleanup Service
|
||||
* Automatically detects and cancels orphaned orders when positions are closed
|
||||
*/
|
||||
|
||||
class DriftOrderCleanupService {
|
||||
constructor() {
|
||||
this.isRunning = false
|
||||
this.monitoringInterval = null
|
||||
this.lastCleanupTime = 0
|
||||
this.cleanupCooldown = 30000 // 30 seconds between cleanups
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring for orphaned orders
|
||||
*/
|
||||
start(intervalMs = 60000) { // Check every 60 seconds by default
|
||||
if (this.isRunning) {
|
||||
console.log('⚠️ Drift order cleanup service already running')
|
||||
return
|
||||
}
|
||||
|
||||
this.isRunning = true
|
||||
console.log(`🧹 Starting Drift order cleanup service (checking every ${intervalMs/1000}s)`)
|
||||
|
||||
this.monitoringInterval = setInterval(async () => {
|
||||
try {
|
||||
await this.checkAndCleanupOrders()
|
||||
} catch (error) {
|
||||
console.error('❌ Error in order cleanup monitoring:', error)
|
||||
}
|
||||
}, intervalMs)
|
||||
|
||||
// Also run an initial check
|
||||
setTimeout(() => this.checkAndCleanupOrders().catch(console.error), 5000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring
|
||||
*/
|
||||
stop() {
|
||||
if (!this.isRunning) {
|
||||
console.log('⚠️ Drift order cleanup service not running')
|
||||
return
|
||||
}
|
||||
|
||||
this.isRunning = false
|
||||
|
||||
if (this.monitoringInterval) {
|
||||
clearInterval(this.monitoringInterval)
|
||||
this.monitoringInterval = null
|
||||
}
|
||||
|
||||
console.log('🛑 Drift order cleanup service stopped')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for orphaned orders and clean them up
|
||||
*/
|
||||
async checkAndCleanupOrders() {
|
||||
if (!this.isRunning) return
|
||||
|
||||
// Prevent too frequent cleanups
|
||||
const now = Date.now()
|
||||
if (now - this.lastCleanupTime < this.cleanupCooldown) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔍 Checking for orphaned orders...')
|
||||
|
||||
// Get current positions and orders
|
||||
const [positionsResponse, ordersResponse] = await Promise.all([
|
||||
fetch('http://localhost:9001/api/drift/positions'),
|
||||
fetch('http://localhost:9001/api/drift/orders')
|
||||
])
|
||||
|
||||
if (!positionsResponse.ok || !ordersResponse.ok) {
|
||||
console.warn('⚠️ Failed to fetch positions or orders for cleanup check')
|
||||
return
|
||||
}
|
||||
|
||||
const positionsData = await positionsResponse.json()
|
||||
const ordersData = await ordersResponse.json()
|
||||
|
||||
if (!positionsData.success || !ordersData.success) {
|
||||
console.warn('⚠️ API responses indicate failure, skipping cleanup')
|
||||
return
|
||||
}
|
||||
|
||||
const positions = positionsData.positions || []
|
||||
const orders = ordersData.orders || []
|
||||
|
||||
console.log(`📊 Current state: ${positions.length} positions, ${orders.length} orders`)
|
||||
|
||||
// Quick check: if no orphaned orders, skip cleanup
|
||||
const positionMarkets = new Set(positions.map(pos => pos.marketIndex))
|
||||
const orphanedOrders = orders.filter(order =>
|
||||
!positionMarkets.has(order.marketIndex) ||
|
||||
(order.reduceOnly && !positionMarkets.has(order.marketIndex))
|
||||
)
|
||||
|
||||
if (orphanedOrders.length === 0) {
|
||||
console.log('✅ No orphaned orders detected')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`🎯 Found ${orphanedOrders.length} potentially orphaned orders`)
|
||||
|
||||
// Trigger cleanup
|
||||
const cleanupResponse = await fetch('http://localhost:9001/api/drift/cleanup-orders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!cleanupResponse.ok) {
|
||||
throw new Error(`Cleanup API failed: ${cleanupResponse.status}`)
|
||||
}
|
||||
|
||||
const cleanupResult = await cleanupResponse.json()
|
||||
|
||||
if (cleanupResult.success) {
|
||||
const summary = cleanupResult.summary
|
||||
console.log('🧹 Order cleanup completed:')
|
||||
console.log(` 📊 Orphaned orders: ${summary.orphanedOrders}`)
|
||||
console.log(` ⚠️ Conflicting orders: ${summary.conflictingOrders}`)
|
||||
console.log(` ✅ Successfully canceled: ${summary.totalCanceled}`)
|
||||
console.log(` ❌ Failed to cancel: ${summary.totalFailed}`)
|
||||
|
||||
this.lastCleanupTime = now
|
||||
|
||||
// Log each canceled order
|
||||
cleanupResult.canceledOrders.forEach(order => {
|
||||
if (order.success) {
|
||||
console.log(` ✅ Canceled ${order.symbol} order ${order.orderId} (${order.reason})`)
|
||||
} else {
|
||||
console.log(` ❌ Failed to cancel order ${order.orderId}: ${order.error}`)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('❌ Order cleanup failed:', cleanupResult.error)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error during order cleanup check:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual cleanup trigger
|
||||
*/
|
||||
async forceCleanup() {
|
||||
console.log('🧹 Manual order cleanup triggered...')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:9001/api/drift/cleanup-orders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Cleanup API failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Manual cleanup completed:', result.summary)
|
||||
return result
|
||||
} else {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Manual cleanup failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
lastCleanupTime: this.lastCleanupTime,
|
||||
lastCleanupAgo: this.lastCleanupTime ? Date.now() - this.lastCleanupTime : null,
|
||||
cooldownRemaining: Math.max(0, this.cleanupCooldown - (Date.now() - this.lastCleanupTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const driftOrderCleanupService = new DriftOrderCleanupService()
|
||||
|
||||
// For CommonJS compatibility
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { driftOrderCleanupService, DriftOrderCleanupService }
|
||||
}
|
||||
166
lib/trading-system-integration.js
Normal file
166
lib/trading-system-integration.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Integration patch for main trading system
|
||||
* Adds automated Drift order cleanup to trading workflow
|
||||
*/
|
||||
|
||||
const { driftOrderCleanupService } = require('./lib/drift-order-cleanup-service.js')
|
||||
|
||||
class TradingSystemIntegration {
|
||||
static initialized = false
|
||||
|
||||
/**
|
||||
* Initialize the cleanup service with the trading system
|
||||
*/
|
||||
static async initializeCleanupService() {
|
||||
if (this.initialized) {
|
||||
console.log('🔄 Drift cleanup service already initialized')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🚀 Initializing Drift order cleanup service...')
|
||||
|
||||
try {
|
||||
// Start the automated monitoring
|
||||
driftOrderCleanupService.start(45000) // Check every 45 seconds
|
||||
|
||||
// Set up cleanup triggers for position changes
|
||||
this.setupPositionMonitoring()
|
||||
|
||||
this.initialized = true
|
||||
console.log('✅ Drift order cleanup service initialized successfully')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize cleanup service:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up monitoring for position changes that might require cleanup
|
||||
*/
|
||||
static setupPositionMonitoring() {
|
||||
// Monitor for position closures that might leave orphaned orders
|
||||
const originalMonitorPosition = global.monitorPosition || (() => {})
|
||||
|
||||
global.monitorPosition = async (...args) => {
|
||||
const result = await originalMonitorPosition(...args)
|
||||
|
||||
// Trigger cleanup after position monitoring
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('🧹 Triggering cleanup after position monitoring...')
|
||||
await driftOrderCleanupService.checkAndCleanupOrders()
|
||||
} catch (error) {
|
||||
console.error('❌ Cleanup after position monitoring failed:', error)
|
||||
}
|
||||
}, 5000) // Wait 5 seconds after position monitoring
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
console.log('📊 Position monitoring integration set up')
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup orders after a specific trade action
|
||||
*/
|
||||
static async cleanupAfterTrade(tradeInfo = {}) {
|
||||
console.log('🧹 Cleaning up orders after trade action:', tradeInfo)
|
||||
|
||||
try {
|
||||
// Wait a moment for the trade to settle
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// Force cleanup
|
||||
const result = await driftOrderCleanupService.forceCleanup()
|
||||
|
||||
console.log(`✅ Post-trade cleanup completed: ${result.summary.totalCanceled} orders canceled`)
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Post-trade cleanup failed:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced position monitoring with automatic cleanup
|
||||
*/
|
||||
static async monitorPositionWithCleanup(marketSymbol, maxChecks = 30) {
|
||||
console.log(`📊 Monitoring position for ${marketSymbol} with automatic cleanup...`)
|
||||
|
||||
let lastPositionState = null
|
||||
|
||||
for (let i = 0; i < maxChecks; i++) {
|
||||
try {
|
||||
// Check current position
|
||||
const response = await fetch('http://localhost:9001/api/drift/positions')
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const position = data.positions?.find(p => p.symbol === marketSymbol)
|
||||
|
||||
// Detect position closure
|
||||
if (lastPositionState && lastPositionState.size !== 0 && (!position || position.size === 0)) {
|
||||
console.log(`🎯 Position closure detected for ${marketSymbol} - triggering cleanup`)
|
||||
|
||||
// Wait a moment then cleanup
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.cleanupAfterTrade({ symbol: marketSymbol, action: 'position_closed' })
|
||||
} catch (error) {
|
||||
console.error('❌ Cleanup after position closure failed:', error)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
lastPositionState = position
|
||||
}
|
||||
|
||||
// Wait before next check
|
||||
await new Promise(resolve => setTimeout(resolve, 10000))
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error monitoring position ${marketSymbol}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📊 Position monitoring completed for ${marketSymbol}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cleanup service status
|
||||
*/
|
||||
static getCleanupStatus() {
|
||||
return {
|
||||
initialized: this.initialized,
|
||||
serviceStatus: driftOrderCleanupService.getStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the cleanup service
|
||||
*/
|
||||
static stopCleanupService() {
|
||||
if (!this.initialized) {
|
||||
console.log('⚠️ Cleanup service not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
driftOrderCleanupService.stop()
|
||||
this.initialized = false
|
||||
console.log('🛑 Drift order cleanup service stopped')
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize if running in main trading environment
|
||||
if (typeof global !== 'undefined' && global.process?.title?.includes('node')) {
|
||||
// Wait a moment to let other systems initialize
|
||||
setTimeout(() => {
|
||||
TradingSystemIntegration.initializeCleanupService().catch(error => {
|
||||
console.error('❌ Auto-initialization failed:', error)
|
||||
})
|
||||
}, 10000) // Wait 10 seconds
|
||||
}
|
||||
|
||||
module.exports = { TradingSystemIntegration, driftOrderCleanupService }
|
||||
103
test-drift-cleanup.js
Normal file
103
test-drift-cleanup.js
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script for Drift order cleanup system
|
||||
* Tests both manual and automated cleanup functionality
|
||||
*/
|
||||
|
||||
const { driftOrderCleanupService } = require('./lib/drift-order-cleanup-service.js')
|
||||
|
||||
async function testCleanupSystem() {
|
||||
console.log('🧪 Testing Drift Order Cleanup System')
|
||||
console.log('=====================================\n')
|
||||
|
||||
try {
|
||||
// Test 1: Check current status
|
||||
console.log('📊 Test 1: Service Status Check')
|
||||
const initialStatus = driftOrderCleanupService.getStatus()
|
||||
console.log('Initial status:', initialStatus)
|
||||
console.log()
|
||||
|
||||
// Test 2: Manual cleanup
|
||||
console.log('🧹 Test 2: Manual Cleanup')
|
||||
try {
|
||||
const manualResult = await driftOrderCleanupService.forceCleanup()
|
||||
console.log('Manual cleanup result:', manualResult.summary)
|
||||
} catch (error) {
|
||||
console.log('Manual cleanup error (expected if no orders):', error.message)
|
||||
}
|
||||
console.log()
|
||||
|
||||
// Test 3: Check positions and orders directly
|
||||
console.log('📊 Test 3: Direct API Check')
|
||||
|
||||
try {
|
||||
const [positionsRes, ordersRes] = await Promise.all([
|
||||
fetch('http://localhost:9001/api/drift/positions'),
|
||||
fetch('http://localhost:9001/api/drift/orders')
|
||||
])
|
||||
|
||||
if (positionsRes.ok && ordersRes.ok) {
|
||||
const positions = await positionsRes.json()
|
||||
const orders = await ordersRes.json()
|
||||
|
||||
console.log(`Current positions: ${positions.positions?.length || 0}`)
|
||||
console.log(`Current orders: ${orders.orders?.length || 0}`)
|
||||
|
||||
if (positions.positions?.length > 0) {
|
||||
console.log('Active positions:')
|
||||
positions.positions.forEach(pos => {
|
||||
console.log(` - ${pos.symbol}: Size ${pos.size}, Value $${pos.value?.toFixed(2) || 'N/A'}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (orders.orders?.length > 0) {
|
||||
console.log('Active orders:')
|
||||
orders.orders.forEach(order => {
|
||||
console.log(` - ${order.symbol}: ${order.side} ${order.size} @ $${order.price} (${order.orderType})`)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Failed to fetch positions/orders')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('API check error:', error.message)
|
||||
}
|
||||
console.log()
|
||||
|
||||
// Test 4: Start automated monitoring (briefly)
|
||||
console.log('🤖 Test 4: Automated Monitoring')
|
||||
console.log('Starting automated cleanup service for 30 seconds...')
|
||||
|
||||
driftOrderCleanupService.start(10000) // Check every 10 seconds
|
||||
|
||||
// Let it run for 30 seconds
|
||||
await new Promise(resolve => setTimeout(resolve, 30000))
|
||||
|
||||
console.log('Stopping automated service...')
|
||||
driftOrderCleanupService.stop()
|
||||
|
||||
// Final status
|
||||
const finalStatus = driftOrderCleanupService.getStatus()
|
||||
console.log('Final status:', finalStatus)
|
||||
|
||||
console.log('\n✅ Test completed successfully!')
|
||||
console.log('\nNext steps:')
|
||||
console.log('1. The cleanup service is now available via driftOrderCleanupService')
|
||||
console.log('2. Call .start() to begin automated monitoring')
|
||||
console.log('3. Call .forceCleanup() for manual cleanup')
|
||||
console.log('4. Call .stop() to stop monitoring')
|
||||
console.log('5. Integration with main trading bot recommended')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Test failed:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run test if called directly
|
||||
if (require.main === module) {
|
||||
testCleanupSystem().catch(console.error)
|
||||
}
|
||||
|
||||
module.exports = { testCleanupSystem }
|
||||
101
test-orphaned-cleanup-integration.js
Normal file
101
test-orphaned-cleanup-integration.js
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test script to verify orphaned order cleanup integration
|
||||
* Tests the position monitor's ability to trigger cleanup when no positions detected
|
||||
*/
|
||||
|
||||
async function testOrphanedCleanupIntegration() {
|
||||
console.log('🧪 Testing Orphaned Order Cleanup Integration')
|
||||
console.log('=' .repeat(60))
|
||||
|
||||
try {
|
||||
// Test the position monitor endpoint
|
||||
console.log('📡 Testing position monitor endpoint...')
|
||||
|
||||
const response = await fetch('http://localhost:3000/api/automation/position-monitor', {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Position monitor API failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
console.log('📊 Position Monitor Result:')
|
||||
console.log(' - Has Position:', result.hasPosition)
|
||||
console.log(' - Risk Level:', result.riskLevel)
|
||||
console.log(' - Next Action:', result.nextAction)
|
||||
|
||||
// Check if orphaned order cleanup was triggered
|
||||
if (result.orphanedOrderCleanup) {
|
||||
console.log('\n🧹 Orphaned Order Cleanup:')
|
||||
console.log(' - Triggered:', result.orphanedOrderCleanup.triggered)
|
||||
console.log(' - Success:', result.orphanedOrderCleanup.success)
|
||||
console.log(' - Message:', result.orphanedOrderCleanup.message)
|
||||
|
||||
if (result.orphanedOrderCleanup.summary) {
|
||||
console.log(' - Summary:', result.orphanedOrderCleanup.summary)
|
||||
}
|
||||
|
||||
if (result.orphanedOrderCleanup.error) {
|
||||
console.log(' - Error:', result.orphanedOrderCleanup.error)
|
||||
}
|
||||
}
|
||||
|
||||
// Test cleanup API directly if position monitor shows no position
|
||||
if (!result.hasPosition) {
|
||||
console.log('\n🔧 Testing direct cleanup API...')
|
||||
|
||||
const cleanupResponse = await fetch('http://localhost:3000/api/drift/cleanup-orders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (cleanupResponse.ok) {
|
||||
const cleanupResult = await cleanupResponse.json()
|
||||
|
||||
console.log('✅ Direct cleanup API response:')
|
||||
console.log(' - Success:', cleanupResult.success)
|
||||
|
||||
if (cleanupResult.summary) {
|
||||
console.log(' - Active Positions:', cleanupResult.summary.activePositions)
|
||||
console.log(' - Active Orders:', cleanupResult.summary.activeOrders)
|
||||
console.log(' - Orphaned Orders:', cleanupResult.summary.orphanedOrders)
|
||||
console.log(' - Total Canceled:', cleanupResult.summary.totalCanceled)
|
||||
}
|
||||
|
||||
if (cleanupResult.error) {
|
||||
console.log(' - Error:', cleanupResult.error)
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Direct cleanup API failed:', cleanupResponse.status)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Integration test completed successfully!')
|
||||
console.log('\n📋 Integration Summary:')
|
||||
console.log(' - Position monitoring automatically checks for orphaned orders')
|
||||
console.log(' - Cleanup only triggers when no positions detected')
|
||||
console.log(' - Eliminates need for redundant polling timers')
|
||||
console.log(' - Provides detailed feedback on cleanup operations')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Integration test failed:', error)
|
||||
console.log('\n🔍 Troubleshooting:')
|
||||
console.log(' - Ensure the Next.js server is running (npm run dev)')
|
||||
console.log(' - Check that all APIs are accessible')
|
||||
console.log(' - Verify Drift environment configuration')
|
||||
}
|
||||
}
|
||||
|
||||
// Self-executing async function
|
||||
;(async () => {
|
||||
await testOrphanedCleanupIntegration()
|
||||
})()
|
||||
Reference in New Issue
Block a user