- Fixed cleanup-orders logic to NEVER cancel reduce-only orders (SL/TP) - Updated position monitor to be more conservative with cleanup - This was causing SL/TP orders to be canceled after position creation - Positions were left unprotected due to aggressive cleanup logic
236 lines
9.2 KiB
JavaScript
236 lines
9.2 KiB
JavaScript
import { NextResponse } from 'next/server';
|
|
|
|
export async function GET() {
|
|
try {
|
|
// Get current positions with real-time data
|
|
const baseUrl = process.env.INTERNAL_API_URL || 'http://localhost:3000';
|
|
const positionsResponse = await fetch(`${baseUrl}/api/drift/positions`, {
|
|
cache: 'no-store', // Force fresh data
|
|
headers: {
|
|
'Cache-Control': 'no-cache'
|
|
}
|
|
});
|
|
const positionsData = await positionsResponse.json();
|
|
|
|
// Use real-time price from Drift positions data
|
|
let currentPrice = 185.0; // Fallback price
|
|
|
|
const result = {
|
|
timestamp: new Date().toISOString(),
|
|
hasPosition: false,
|
|
position: null,
|
|
stopLossProximity: null,
|
|
riskLevel: 'NONE',
|
|
nextAction: 'No position to monitor',
|
|
recommendation: 'MONITOR_ONLY', // Don't auto-trigger restarts
|
|
orphanedOrderCleanup: null
|
|
};
|
|
|
|
if (positionsData.success && positionsData.positions.length > 0) {
|
|
const position = positionsData.positions[0];
|
|
|
|
// Use real-time mark price from Drift
|
|
currentPrice = position.markPrice || position.entryPrice || currentPrice;
|
|
|
|
result.hasPosition = true;
|
|
result.position = {
|
|
symbol: position.symbol,
|
|
side: position.side,
|
|
size: position.size,
|
|
entryPrice: position.entryPrice,
|
|
currentPrice: currentPrice,
|
|
unrealizedPnl: position.unrealizedPnl,
|
|
notionalValue: position.notionalValue
|
|
};
|
|
|
|
// Calculate stop loss proximity (mock - you'd need actual SL from order data)
|
|
let stopLossPrice;
|
|
if (position.side === 'long') {
|
|
stopLossPrice = position.entryPrice * 0.95; // 5% below entry
|
|
} else {
|
|
stopLossPrice = position.entryPrice * 1.05; // 5% above entry
|
|
}
|
|
|
|
const distanceToSL = Math.abs(currentPrice - stopLossPrice) / currentPrice;
|
|
const proximityPercent = distanceToSL * 100;
|
|
|
|
result.stopLossProximity = {
|
|
stopLossPrice: stopLossPrice,
|
|
currentPrice: currentPrice,
|
|
distancePercent: proximityPercent.toFixed(2),
|
|
isNear: proximityPercent < 2.0 // Within 2% = NEAR
|
|
};
|
|
|
|
// Risk assessment
|
|
if (proximityPercent < 1.0) {
|
|
result.riskLevel = 'CRITICAL';
|
|
result.nextAction = 'IMMEDIATE ANALYSIS REQUIRED - Price very close to SL';
|
|
result.recommendation = 'EMERGENCY_ANALYSIS';
|
|
} else if (proximityPercent < 2.0) {
|
|
result.riskLevel = 'HIGH';
|
|
result.nextAction = 'Enhanced monitoring - Analyze within 5 minutes';
|
|
result.recommendation = 'URGENT_MONITORING';
|
|
} else if (proximityPercent < 5.0) {
|
|
result.riskLevel = 'MEDIUM';
|
|
result.nextAction = 'Regular monitoring - Check every 10 minutes';
|
|
result.recommendation = 'NORMAL_MONITORING';
|
|
} else {
|
|
result.riskLevel = 'LOW';
|
|
result.nextAction = 'Standard monitoring - Check every 30 minutes';
|
|
result.recommendation = 'RELAXED_MONITORING';
|
|
}
|
|
} else {
|
|
// NO POSITION DETECTED - Check for orphaned orders and cleanup
|
|
|
|
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('📋 No active positions detected - checking for truly orphaned orders...');
|
|
|
|
// Filter for truly orphaned orders (ONLY non-reduce-only orders without positions)
|
|
// 🛡️ CRITICAL: NEVER clean up reduce-only orders as these are SL/TP protecting positions
|
|
const trulyOrphanedOrders = activeOrders.filter(order => {
|
|
// Only consider non-reduce-only orders for cleanup
|
|
// Reduce-only orders (SL/TP) should NEVER be automatically canceled
|
|
return !order.reduceOnly
|
|
});
|
|
|
|
if (trulyOrphanedOrders.length > 0) {
|
|
console.log(`🎯 Found ${trulyOrphanedOrders.length} truly orphaned orders (non-reduce-only) - triggering cleanup...`);
|
|
|
|
// Trigger automated cleanup of truly orphaned orders only
|
|
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} truly 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 {
|
|
// All orders are reduce-only (likely SL/TP) - do not clean up
|
|
console.log('✅ All remaining orders are reduce-only (likely SL/TP) - skipping cleanup to preserve risk management');
|
|
result.orphanedOrderCleanup = {
|
|
triggered: false,
|
|
success: true,
|
|
message: 'All orders are reduce-only (SL/TP) - preserved for risk management'
|
|
};
|
|
}
|
|
} else {
|
|
// Only log occasionally when no orders found (not every check)
|
|
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'
|
|
};
|
|
}
|
|
}
|
|
|
|
// Auto-restart logic disabled to prevent interference with manual trading
|
|
/*
|
|
if (!result.hasPosition && result.recommendation === 'START_TRADING') {
|
|
try {
|
|
console.log('🚀 AUTO-RESTART: No position detected with START_TRADING recommendation - checking if automation should restart');
|
|
|
|
// Check if automation is currently stopped
|
|
const statusResponse = await fetch(`${baseUrl}/api/automation/status`);
|
|
if (statusResponse.ok) {
|
|
const statusData = await statusResponse.json();
|
|
|
|
if (!statusData.isRunning) {
|
|
console.log('🔄 Automation is stopped - triggering auto-restart for new cycle');
|
|
result.autoRestart = {
|
|
triggered: true,
|
|
reason: 'No position + START_TRADING recommendation',
|
|
message: 'System ready for new trading cycle'
|
|
};
|
|
|
|
// Note: We don't automatically start here to avoid conflicts
|
|
// The UI should detect this and offer restart option
|
|
} else {
|
|
console.log('✅ Automation already running - no restart needed');
|
|
result.autoRestart = {
|
|
triggered: false,
|
|
reason: 'Automation already active',
|
|
message: 'System monitoring active'
|
|
};
|
|
}
|
|
}
|
|
} catch (restartError) {
|
|
console.warn('⚠️ Could not check automation status for auto-restart:', restartError.message);
|
|
result.autoRestart = {
|
|
triggered: false,
|
|
error: restartError.message,
|
|
message: 'Could not check restart requirements'
|
|
};
|
|
}
|
|
}
|
|
*/
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
monitor: result
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Position monitor error:', error);
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Failed to get position monitoring data',
|
|
message: error.message
|
|
}, { status: 500 });
|
|
}
|
|
}
|