diff --git a/app/api/automation/position-monitor/route.js b/app/api/automation/position-monitor/route.js index 99dd96f..34bba52 100644 --- a/app/api/automation/position-monitor/route.js +++ b/app/api/automation/position-monitor/route.js @@ -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({ diff --git a/app/api/drift/cleanup-orders/route.js b/app/api/drift/cleanup-orders/route.js new file mode 100644 index 0000000..6af57d7 --- /dev/null +++ b/app/api/drift/cleanup-orders/route.js @@ -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' + ] + }) +} diff --git a/drift-cleanup-daemon.js b/drift-cleanup-daemon.js new file mode 100644 index 0000000..1f4fe84 --- /dev/null +++ b/drift-cleanup-daemon.js @@ -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 } diff --git a/drift-cleanup-manager.js b/drift-cleanup-manager.js new file mode 100644 index 0000000..04579d1 --- /dev/null +++ b/drift-cleanup-manager.js @@ -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 } diff --git a/lib/drift-order-cleanup-service.js b/lib/drift-order-cleanup-service.js new file mode 100644 index 0000000..b91cc06 --- /dev/null +++ b/lib/drift-order-cleanup-service.js @@ -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 } +} diff --git a/lib/trading-system-integration.js b/lib/trading-system-integration.js new file mode 100644 index 0000000..37095a5 --- /dev/null +++ b/lib/trading-system-integration.js @@ -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 } diff --git a/test-drift-cleanup.js b/test-drift-cleanup.js new file mode 100644 index 0000000..2228b34 --- /dev/null +++ b/test-drift-cleanup.js @@ -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 } diff --git a/test-orphaned-cleanup-integration.js b/test-orphaned-cleanup-integration.js new file mode 100644 index 0000000..68d5316 --- /dev/null +++ b/test-orphaned-cleanup-integration.js @@ -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() +})()