🧹 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'
|
||||
]
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user