/** * Execute Trade API Endpoint * * Called by n8n workflow when TradingView signal is received * POST /api/trading/execute */ import { NextRequest, NextResponse } from 'next/server' import { initializeDriftService } from '@/lib/drift/client' import { openPosition, placeExitOrders } from '@/lib/drift/orders' import { normalizeTradingViewSymbol } from '@/config/trading' import { getMergedConfig } from '@/config/trading' import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { createTrade } from '@/lib/database/trades' export interface ExecuteTradeRequest { symbol: string // TradingView symbol (e.g., 'SOLUSDT') direction: 'long' | 'short' timeframe: string // e.g., '5' signalStrength?: 'strong' | 'moderate' | 'weak' signalPrice?: number } export interface ExecuteTradeResponse { success: boolean positionId?: string symbol?: string direction?: 'long' | 'short' entryPrice?: number positionSize?: number leverage?: number stopLoss?: number takeProfit1?: number takeProfit2?: number stopLossPercent?: number tp1Percent?: number tp2Percent?: number entrySlippage?: number timestamp?: string error?: string message?: string } export async function POST(request: NextRequest): Promise> { try { // Verify authorization const authHeader = request.headers.get('authorization') const expectedAuth = `Bearer ${process.env.API_SECRET_KEY}` if (!authHeader || authHeader !== expectedAuth) { return NextResponse.json( { success: false, error: 'Unauthorized', message: 'Invalid API key', }, { status: 401 } ) } // Parse request body const body: ExecuteTradeRequest = await request.json() console.log('🎯 Trade execution request received:', body) // Validate required fields if (!body.symbol || !body.direction) { return NextResponse.json( { success: false, error: 'Missing required fields', message: 'symbol and direction are required', }, { status: 400 } ) } // Normalize symbol const driftSymbol = normalizeTradingViewSymbol(body.symbol) console.log(`📊 Normalized symbol: ${body.symbol} → ${driftSymbol}`) // Get trading configuration const config = getMergedConfig() // Initialize Drift service if not already initialized const driftService = await initializeDriftService() // Check account health before trading const health = await driftService.getAccountHealth() console.log('💊 Account health:', health) if (health.freeCollateral <= 0) { return NextResponse.json( { success: false, error: 'Insufficient collateral', message: `Free collateral: $${health.freeCollateral.toFixed(2)}`, }, { status: 400 } ) } // AUTO-FLIP: Check for existing opposite direction position const positionManager = await getInitializedPositionManager() const existingTrades = Array.from(positionManager.getActiveTrades().values()) const oppositePosition = existingTrades.find( trade => trade.symbol === driftSymbol && trade.direction !== body.direction ) if (oppositePosition) { console.log(`🔄 Signal flip detected! Closing ${oppositePosition.direction} to open ${body.direction}`) // Close opposite position const { closePosition } = await import('@/lib/drift/orders') const closeResult = await closePosition({ symbol: driftSymbol, percentToClose: 100, slippageTolerance: config.slippageTolerance, }) if (!closeResult.success) { console.error('❌ Failed to close opposite position:', closeResult.error) // Continue anyway - we'll try to open the new position } else { console.log(`✅ Closed ${oppositePosition.direction} position at $${closeResult.closePrice?.toFixed(4)} (P&L: $${closeResult.realizedPnL?.toFixed(2)})`) // Position Manager will handle cleanup (including order cancellation) // The executeExit method already removes the trade and updates database } // Small delay to ensure position is fully closed await new Promise(resolve => setTimeout(resolve, 1000)) } // Calculate position size with leverage const positionSizeUSD = config.positionSize * config.leverage console.log(`💰 Opening ${body.direction} position:`) console.log(` Symbol: ${driftSymbol}`) console.log(` Base size: $${config.positionSize}`) console.log(` Leverage: ${config.leverage}x`) console.log(` Total position: $${positionSizeUSD}`) // Open position const openResult = await openPosition({ symbol: driftSymbol, direction: body.direction, sizeUSD: positionSizeUSD, slippageTolerance: config.slippageTolerance, }) if (!openResult.success) { return NextResponse.json( { success: false, error: 'Position open failed', message: openResult.error, }, { status: 500 } ) } // Calculate stop loss and take profit prices const entryPrice = openResult.fillPrice! const stopLossPrice = calculatePrice( entryPrice, config.stopLossPercent, body.direction ) // Calculate dual stop prices if enabled let softStopPrice: number | undefined let hardStopPrice: number | undefined if (config.useDualStops) { softStopPrice = calculatePrice( entryPrice, config.softStopPercent, body.direction ) hardStopPrice = calculatePrice( entryPrice, config.hardStopPercent, body.direction ) console.log('🛡️🛡️ Dual stop system enabled:') console.log(` Soft stop: $${softStopPrice.toFixed(4)} (${config.softStopPercent}%)`) console.log(` Hard stop: $${hardStopPrice.toFixed(4)} (${config.hardStopPercent}%)`) } const tp1Price = calculatePrice( entryPrice, config.takeProfit1Percent, body.direction ) const tp2Price = calculatePrice( entryPrice, config.takeProfit2Percent, body.direction ) console.log('📊 Trade targets:') console.log(` Entry: $${entryPrice.toFixed(4)}`) console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`) console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`) console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`) // Calculate emergency stop const emergencyStopPrice = calculatePrice( entryPrice, config.emergencyStopPercent, body.direction ) // Create active trade object const activeTrade: ActiveTrade = { id: `trade-${Date.now()}`, positionId: openResult.transactionSignature!, symbol: driftSymbol, direction: body.direction, entryPrice, entryTime: Date.now(), positionSize: positionSizeUSD, leverage: config.leverage, stopLossPrice, tp1Price, tp2Price, emergencyStopPrice, currentSize: positionSizeUSD, tp1Hit: false, tp2Hit: false, slMovedToBreakeven: false, slMovedToProfit: false, trailingStopActive: false, realizedPnL: 0, unrealizedPnL: 0, peakPnL: 0, peakPrice: entryPrice, priceCheckCount: 0, lastPrice: entryPrice, lastUpdateTime: Date.now(), } // CRITICAL FIX: Place on-chain TP/SL orders BEFORE adding to Position Manager // This prevents race condition where Position Manager detects "external closure" // while orders are still being placed, leaving orphaned stop loss orders let exitOrderSignatures: string[] = [] try { const exitRes = await placeExitOrders({ symbol: driftSymbol, positionSizeUSD: positionSizeUSD, tp1Price, tp2Price, stopLossPrice, tp1SizePercent: config.takeProfit1SizePercent || 50, tp2SizePercent: config.takeProfit2SizePercent || 100, direction: body.direction, // Dual stop parameters useDualStops: config.useDualStops, softStopPrice: softStopPrice, softStopBuffer: config.softStopBuffer, hardStopPrice: hardStopPrice, }) if (!exitRes.success) { console.error('❌ Failed to place on-chain exit orders:', exitRes.error) } else { console.log('📨 Exit orders placed on-chain:', exitRes.signatures) exitOrderSignatures = exitRes.signatures || [] } } catch (err) { console.error('❌ Unexpected error placing exit orders:', err) } // Add to position manager for monitoring AFTER orders are placed await positionManager.addTrade(activeTrade) console.log('✅ Trade added to position manager for monitoring') // Create response object const response: ExecuteTradeResponse = { success: true, positionId: openResult.transactionSignature, symbol: driftSymbol, direction: body.direction, entryPrice: entryPrice, positionSize: positionSizeUSD, leverage: config.leverage, stopLoss: stopLossPrice, takeProfit1: tp1Price, takeProfit2: tp2Price, stopLossPercent: config.stopLossPercent, tp1Percent: config.takeProfit1Percent, tp2Percent: config.takeProfit2Percent, entrySlippage: openResult.slippage, timestamp: new Date().toISOString(), } // Attach exit order signatures to response if (exitOrderSignatures.length > 0) { (response as any).exitOrderSignatures = exitOrderSignatures } // Save trade to database try { await createTrade({ positionId: openResult.transactionSignature!, symbol: driftSymbol, direction: body.direction, entryPrice, positionSizeUSD: positionSizeUSD, leverage: config.leverage, stopLossPrice, takeProfit1Price: tp1Price, takeProfit2Price: tp2Price, tp1SizePercent: config.takeProfit1SizePercent || 50, tp2SizePercent: config.takeProfit2SizePercent || 100, configSnapshot: config, entryOrderTx: openResult.transactionSignature!, tp1OrderTx: exitOrderSignatures[0], tp2OrderTx: exitOrderSignatures[1], slOrderTx: config.useDualStops ? undefined : exitOrderSignatures[2], softStopOrderTx: config.useDualStops ? exitOrderSignatures[2] : undefined, hardStopOrderTx: config.useDualStops ? exitOrderSignatures[3] : undefined, softStopPrice, hardStopPrice, signalStrength: body.signalStrength, timeframe: body.timeframe, }) console.log('💾 Trade saved to database') } catch (dbError) { console.error('❌ Failed to save trade to database:', dbError) // Don't fail the trade if database save fails } console.log('✅ Trade executed successfully!') return NextResponse.json(response) } catch (error) { console.error('❌ Trade execution error:', error) return NextResponse.json( { success: false, error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error', }, { status: 500 } ) } } /** * Helper function to calculate price based on percentage */ function calculatePrice( entryPrice: number, percent: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return entryPrice * (1 + percent / 100) } else { return entryPrice * (1 - percent / 100) } }