/** * 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 { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager' 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 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 } ) } // 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 ) 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, slMovedToBreakeven: false, slMovedToProfit: false, realizedPnL: 0, unrealizedPnL: 0, peakPnL: 0, priceCheckCount: 0, lastPrice: entryPrice, lastUpdateTime: Date.now(), } // Add to position manager for monitoring const positionManager = getPositionManager() 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, stopLoss: stopLossPrice, takeProfit1: tp1Price, takeProfit2: tp2Price, stopLossPercent: config.stopLossPercent, tp1Percent: config.takeProfit1Percent, tp2Percent: config.takeProfit2Percent, entrySlippage: openResult.slippage, timestamp: new Date().toISOString(), } // Place on-chain TP/SL orders so they appear in Drift UI (reduce-only LIMIT orders) try { const exitRes = await placeExitOrders({ symbol: driftSymbol, positionSizeUSD: positionSizeUSD, tp1Price, tp2Price, stopLossPrice, tp1SizePercent: config.takeProfit1SizePercent || 50, tp2SizePercent: config.takeProfit2SizePercent || 100, direction: body.direction, }) if (!exitRes.success) { console.error('❌ Failed to place on-chain exit orders:', exitRes.error) } else { console.log('📨 Exit orders placed on-chain:', exitRes.signatures) } // Attach signatures to response when available if (exitRes.signatures && exitRes.signatures.length > 0) { ;(response as any).exitOrderSignatures = exitRes.signatures } } catch (err) { console.error('❌ Unexpected error placing exit orders:', err) } // TODO: Save trade to database (add Prisma integration later) 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) } }