/** * 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, closePosition } from '@/lib/drift/orders' import { normalizeTradingViewSymbol, getMinQualityScoreForDirection } from '@/config/trading' import { getMergedConfig } from '@/config/trading' import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { createTrade, updateTradeExit } from '@/lib/database/trades' import { scoreSignalQuality } from '@/lib/trading/signal-quality' import { getMarketDataCache } from '@/lib/trading/market-data-cache' import { getPythPriceMonitor } from '@/lib/pyth/price-monitor' import { logCriticalError, logTradeExecution } from '@/lib/utils/persistent-logger' export interface ExecuteTradeRequest { symbol: string // TradingView symbol (e.g., 'SOLUSDT') direction: 'long' | 'short' timeframe: string // e.g., '5' signalStrength?: 'strong' | 'moderate' | 'weak' signalPrice?: number // Context metrics from TradingView atr?: number adx?: number rsi?: number volumeRatio?: number pricePosition?: number maGap?: number // V9: MA gap convergence metric indicatorVersion?: string // Pine Script version (v5, v6, etc.) } 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 qualityScore?: number // Signal quality score for Telegram notification (Nov 24, 2025) 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}`) // 🆕 Cache incoming market data from TradingView signals if (body.atr && body.adx && body.rsi) { const marketCache = getMarketDataCache() marketCache.set(driftSymbol, { symbol: driftSymbol, atr: body.atr, adx: body.adx, rsi: body.rsi, volumeRatio: body.volumeRatio || 1.0, pricePosition: body.pricePosition || 50, currentPrice: body.signalPrice || 0, timestamp: Date.now(), timeframe: body.timeframe || '5' }) console.log(`📊 Market data auto-cached for ${driftSymbol} from trade signal`) } // 📊 CALCULATE QUALITY SCORE BEFORE TIMEFRAME CHECK (Nov 26, 2025) // CRITICAL: Score ALL signals (5min + data collection) for proper multi-timeframe analysis // This enables quality-filtered win rate comparison across timeframes const qualityResult = await scoreSignalQuality({ atr: body.atr || 0, adx: body.adx || 0, rsi: body.rsi || 0, volumeRatio: body.volumeRatio || 0, pricePosition: body.pricePosition || 0, maGap: body.maGap, // V9: MA gap convergence scoring timeframe: body.timeframe || '5', direction: body.direction, symbol: driftSymbol, currentPrice: body.signalPrice || 0, }) console.log(`📊 Signal quality: ${qualityResult.score} (${qualityResult.score >= 90 ? 'PASS' : 'BLOCKED'})`) if (qualityResult.reasons?.length > 0) { console.log(` Reasons: ${qualityResult.reasons.join(', ')}`) } // Get min quality threshold for this direction const config = getMergedConfig() const minQualityScore = getMinQualityScoreForDirection(body.direction, config) // 🔬 MULTI-TIMEFRAME DATA COLLECTION // Only execute trades from 5min timeframe OR manual Telegram trades // Save other timeframes (15min, 1H, 4H, Daily) for analysis const timeframe = body.timeframe || '5' if (timeframe !== '5' && timeframe !== 'manual') { console.log(`📊 DATA COLLECTION: ${timeframe}min signal from ${driftSymbol}, saving for analysis (not executing)`) // Get current price for entry tracking const priceMonitor = getPythPriceMonitor() const latestPrice = priceMonitor.getCachedPrice(driftSymbol) const currentPrice = latestPrice?.price || body.signalPrice || 0 // Save to BlockedSignal for cross-timeframe analysis const { createBlockedSignal } = await import('@/lib/database/trades') try { await createBlockedSignal({ symbol: driftSymbol, direction: body.direction, blockReason: 'DATA_COLLECTION_ONLY', blockDetails: `Multi-timeframe data collection: ${timeframe}min signals saved but not executed (only 5min executes). Quality score: ${qualityResult.score} (threshold: ${minQualityScore})`, atr: body.atr, adx: body.adx, rsi: body.rsi, volumeRatio: body.volumeRatio, pricePosition: body.pricePosition, timeframe: timeframe, signalPrice: currentPrice, signalQualityScore: qualityResult.score, // CRITICAL: Real quality score for analysis signalQualityVersion: 'v9', // Current indicator version minScoreRequired: minQualityScore, scoreBreakdown: { reasons: qualityResult.reasons }, }) console.log(`✅ ${timeframe}min signal saved at $${currentPrice.toFixed(2)} for future analysis (quality: ${qualityResult.score}, threshold: ${minQualityScore})`) } catch (dbError) { console.error(`❌ Failed to save ${timeframe}min signal:`, dbError) } return NextResponse.json({ success: false, error: 'Data collection only', message: `Signal from ${timeframe}min timeframe saved for analysis. Only 5min signals are executed. Check BlockedSignal table for data.`, dataCollection: { timeframe: timeframe, symbol: driftSymbol, direction: body.direction, qualityScore: qualityResult.score, threshold: minQualityScore, saved: true, } }, { status: 200 }) // 200 not 400 - this is expected behavior } console.log(`✅ 5min signal confirmed - proceeding with trade execution`) // Initialize Drift service and check account health before sizing const driftService = await initializeDriftService() const health = await driftService.getAccountHealth() console.log(`🩺 Account health: Free collateral $${health.freeCollateral.toFixed(2)}`) // Quality score already calculated above (before timeframe check) // Now use it for adaptive leverage and position sizing console.log(`📊 Signal quality score: ${qualityResult.score} (using for adaptive leverage)`) // Get symbol-specific position sizing with quality score for adaptive leverage // ENHANCED Nov 25, 2025: Pass direction for SHORT-specific leverage tiers const { getActualPositionSizeForSymbol } = await import('@/config/trading') const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol( driftSymbol, config, health.freeCollateral, qualityResult.score, // Pass quality score for adaptive leverage body.direction // Pass direction for SHORT-specific tiers (Q90+=15x, Q80-89=10x) ) // Check if trading is enabled for this symbol if (!enabled) { console.log(`⛔ Trading disabled for ${driftSymbol}`) return NextResponse.json( { success: false, error: 'Symbol trading disabled', message: `Trading is currently disabled for ${driftSymbol}. Enable it in settings.`, }, { status: 400 } ) } console.log(`📐 Symbol-specific sizing for ${driftSymbol}:`) console.log(` Enabled: ${enabled}`) console.log(` Position size: $${positionSize.toFixed(2)} (${usePercentage ? 'percentage' : 'fixed'})`) console.log(` Leverage: ${leverage}x`) 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 ) // Check for same direction position (scaling vs duplicate) const sameDirectionPosition = existingTrades.find( trade => trade.symbol === driftSymbol && trade.direction === body.direction ) if (sameDirectionPosition) { // Position scaling enabled - scale into existing position if (config.enablePositionScaling) { console.log(`📈 POSITION SCALING: Adding to existing ${body.direction} position on ${driftSymbol}`) // Calculate scale size const scaleSize = (positionSize * leverage) * (config.scaleSizePercent / 100) console.log(`💰 Scaling position:`) console.log(` Original size: $${sameDirectionPosition.positionSize}`) console.log(` Scale size: $${scaleSize} (${config.scaleSizePercent}% of original)`) console.log(` Leverage: ${leverage}x`) // Open additional position const scaleResult = await openPosition({ symbol: driftSymbol, direction: body.direction, sizeUSD: scaleSize, slippageTolerance: config.slippageTolerance, }) if (!scaleResult.success) { console.error('❌ Failed to scale position:', scaleResult.error) return NextResponse.json( { success: false, error: 'Position scaling failed', message: scaleResult.error, }, { status: 500 } ) } console.log(`✅ Scaled into position at $${scaleResult.fillPrice?.toFixed(4)}`) // Update Position Manager tracking const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1 const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + scaleSize const newTotalSize = sameDirectionPosition.currentSize + (scaleResult.fillSize || 0) // Update the trade tracking (simplified - just update the active trade object) sameDirectionPosition.timesScaled = timesScaled sameDirectionPosition.totalScaleAdded = totalScaleAdded sameDirectionPosition.currentSize = newTotalSize console.log(`📊 Position scaled: ${timesScaled}x total, $${totalScaleAdded.toFixed(2)} added`) return NextResponse.json({ success: true, action: 'scaled', positionId: sameDirectionPosition.positionId, symbol: driftSymbol, direction: body.direction, scalePrice: scaleResult.fillPrice, scaleSize: scaleSize, totalSize: newTotalSize, timesScaled: timesScaled, timestamp: new Date().toISOString(), }) } // Scaling disabled - block duplicate console.log(`⛔ DUPLICATE POSITION BLOCKED: Already have ${body.direction} position on ${driftSymbol}`) return NextResponse.json( { success: false, error: 'Duplicate position detected', message: `Already have an active ${body.direction} position on ${driftSymbol}. Enable position scaling in settings to add to this position.`, }, { status: 400 } ) } if (oppositePosition) { console.log(`🔄 Signal flip detected! Closing ${oppositePosition.direction} to open ${body.direction}`) // CRITICAL: Remove from Position Manager FIRST to prevent race condition // where Position Manager detects "external closure" while we're deliberately closing it console.log(`🗑️ Removing ${oppositePosition.direction} position from Position Manager before flip...`) await positionManager.removeTrade(oppositePosition.id) console.log(`✅ Removed from Position Manager`) // Close opposite position on Drift const { closePosition } = await import('@/lib/drift/orders') const closeResult = await closePosition({ symbol: driftSymbol, percentToClose: 100, slippageTolerance: config.slippageTolerance, }) if (!closeResult.success) { console.error('❌ CRITICAL: Failed to close opposite position:', closeResult.error) console.error(' Cannot open new position while opposite direction exists!') return NextResponse.json( { success: false, error: 'Flip failed - could not close opposite position', message: `Failed to close ${oppositePosition.direction} position: ${closeResult.error}. Not opening new ${body.direction} position to avoid hedge.`, }, { status: 500 } ) } console.log(`✅ Closed ${oppositePosition.direction} position at $${closeResult.closePrice?.toFixed(4)} (P&L: $${closeResult.realizedPnL?.toFixed(2)})`) // CRITICAL: Check if position actually closed on Drift (not just transaction confirmed) // The needsVerification flag means transaction confirmed but position still exists if (closeResult.needsVerification) { console.log(`⚠️ Close tx confirmed but position still on Drift - waiting for propagation...`) // Wait up to 15 seconds for Drift to update let waitTime = 0 const maxWait = 15000 const checkInterval = 2000 while (waitTime < maxWait) { await new Promise(resolve => setTimeout(resolve, checkInterval)) waitTime += checkInterval const position = await driftService.getPosition((await import('@/config/trading')).getMarketConfig(driftSymbol).driftMarketIndex) if (!position || Math.abs(position.size) < 0.01) { console.log(`✅ Position confirmed closed on Drift after ${waitTime/1000}s`) break } console.log(`⏳ Still waiting for Drift closure (${waitTime/1000}s elapsed)...`) } if (waitTime >= maxWait) { console.error(`❌ CRITICAL: Position still on Drift after ${maxWait/1000}s!`) return NextResponse.json( { success: false, error: 'Flip failed - position did not close', message: `Close transaction confirmed but position still exists on Drift after ${maxWait/1000}s. Not opening new position to avoid hedge.`, }, { status: 500 } ) } } // Save the closure to database try { const holdTimeSeconds = Math.floor((Date.now() - oppositePosition.entryTime) / 1000) const priceProfitPercent = oppositePosition.direction === 'long' ? ((closeResult.closePrice! - oppositePosition.entryPrice) / oppositePosition.entryPrice) * 100 : ((oppositePosition.entryPrice - closeResult.closePrice!) / oppositePosition.entryPrice) * 100 const realizedPnL = closeResult.realizedPnL ?? (oppositePosition.currentSize * priceProfitPercent) / 100 await updateTradeExit({ positionId: oppositePosition.positionId, exitPrice: closeResult.closePrice!, exitReason: 'manual', // Manually closed for flip realizedPnL: realizedPnL, exitOrderTx: closeResult.transactionSignature || 'FLIP_CLOSE', holdTimeSeconds, maxDrawdown: Math.abs(Math.min(0, oppositePosition.maxAdverseExcursion)), maxGain: Math.max(0, oppositePosition.maxFavorableExcursion), maxFavorableExcursion: oppositePosition.maxFavorableExcursion, maxAdverseExcursion: oppositePosition.maxAdverseExcursion, maxFavorablePrice: oppositePosition.maxFavorablePrice, maxAdversePrice: oppositePosition.maxAdversePrice, }) console.log(`💾 Saved opposite position closure to database`) } catch (dbError) { console.error('❌ Failed to save opposite position closure:', dbError) } console.log(`✅ Flip sequence complete - ready to open ${body.direction} position`) } // Calculate position size with leverage const positionSizeUSD = positionSize * leverage console.log(`💰 Opening ${body.direction} position:`) console.log(` Symbol: ${driftSymbol}`) console.log(` Base size: $${positionSize}`) console.log(` Leverage: ${leverage}x`) console.log(` Total position: $${positionSizeUSD}`) // Helper function for rate limit spacing const rpcDelay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) // Open position const openResult = await openPosition({ symbol: driftSymbol, direction: body.direction, sizeUSD: positionSizeUSD, slippageTolerance: config.slippageTolerance, }) // Wait 2 seconds before placing exit orders to space out RPC calls await rpcDelay(2000) if (!openResult.success) { return NextResponse.json( { success: false, error: 'Position open failed', message: openResult.error, }, { status: 500 } ) } // CRITICAL: Check for phantom trade (position opened but size mismatch) if (openResult.isPhantom) { console.error(`🚨 PHANTOM TRADE DETECTED - Auto-closing for safety`) console.error(` Expected: $${positionSizeUSD.toFixed(2)}`) console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`) // IMMEDIATELY close the phantom position (safety first) let closeResult let closedAtPrice = openResult.fillPrice! let closePnL = 0 try { console.log(`⚠️ Closing phantom position immediately for safety...`) // Wait 2 seconds to space out RPC calls await rpcDelay(2000) closeResult = await closePosition({ symbol: driftSymbol, percentToClose: 100, // Close 100% of whatever size exists slippageTolerance: config.slippageTolerance, }) if (closeResult.success) { closedAtPrice = closeResult.closePrice || openResult.fillPrice! // Calculate P&L (usually small loss/gain) const priceChange = body.direction === 'long' ? ((closedAtPrice - openResult.fillPrice!) / openResult.fillPrice!) : ((openResult.fillPrice! - closedAtPrice) / openResult.fillPrice!) closePnL = (openResult.actualSizeUSD || 0) * priceChange console.log(`✅ Phantom position closed at $${closedAtPrice.toFixed(2)}`) console.log(`💰 Phantom P&L: $${closePnL.toFixed(2)}`) } else { console.error(`❌ Failed to close phantom position: ${closeResult.error}`) } } catch (closeError) { console.error(`❌ Error closing phantom position:`, closeError) } // Save phantom trade to database for analysis let phantomTradeId: string | undefined try { const qualityResult = await scoreSignalQuality({ atr: body.atr || 0, adx: body.adx || 0, rsi: body.rsi || 0, volumeRatio: body.volumeRatio || 0, pricePosition: body.pricePosition || 0, maGap: body.maGap, // V9: MA gap convergence scoring direction: body.direction, symbol: driftSymbol, currentPrice: openResult.fillPrice, timeframe: body.timeframe, }) // Create trade record (without exit info initially) const trade = await createTrade({ positionId: openResult.transactionSignature!, symbol: driftSymbol, direction: body.direction, entryPrice: openResult.fillPrice!, positionSizeUSD: openResult.actualSizeUSD || positionSizeUSD, leverage: leverage, stopLossPrice: 0, takeProfit1Price: 0, takeProfit2Price: 0, tp1SizePercent: 0, tp2SizePercent: 0, configSnapshot: config, entryOrderTx: openResult.transactionSignature!, signalStrength: body.signalStrength, timeframe: body.timeframe, atrAtEntry: body.atr, adxAtEntry: body.adx, rsiAtEntry: body.rsi, volumeAtEntry: body.volumeRatio, pricePositionAtEntry: body.pricePosition, signalQualityScore: qualityResult.score, indicatorVersion: body.indicatorVersion || 'v5', status: 'phantom', isPhantom: true, expectedSizeUSD: positionSizeUSD, actualSizeUSD: openResult.actualSizeUSD, phantomReason: 'ORACLE_PRICE_MISMATCH', }) phantomTradeId = trade.id console.log(`💾 Phantom trade saved to database for analysis`) // If close succeeded, update with exit info if (closeResult?.success) { await updateTradeExit({ positionId: openResult.transactionSignature!, exitPrice: closedAtPrice, exitReason: 'manual', // Phantom auto-close (manual category) realizedPnL: closePnL, exitOrderTx: closeResult.transactionSignature || 'PHANTOM_CLOSE', holdTimeSeconds: 0, // Phantom trades close immediately maxDrawdown: Math.abs(Math.min(0, closePnL)), maxGain: Math.max(0, closePnL), maxFavorableExcursion: Math.max(0, closePnL), maxAdverseExcursion: Math.min(0, closePnL), }) console.log(`💾 Phantom exit info updated in database`) } } catch (dbError) { console.error('❌ Failed to save phantom trade:', dbError) } // Prepare notification message for n8n to send via Telegram const phantomNotification = `⚠️ PHANTOM TRADE AUTO-CLOSED\n\n` + `Symbol: ${driftSymbol}\n` + `Direction: ${body.direction.toUpperCase()}\n` + `Expected Size: $${positionSizeUSD.toFixed(2)}\n` + `Actual Size: $${(openResult.actualSizeUSD || 0).toFixed(2)} (${((openResult.actualSizeUSD || 0) / positionSizeUSD * 100).toFixed(1)}%)\n\n` + `Entry: $${openResult.fillPrice!.toFixed(2)}\n` + `Exit: $${closedAtPrice.toFixed(2)}\n` + `P&L: $${closePnL.toFixed(2)}\n\n` + `Reason: Size mismatch detected - likely oracle price issue or exchange rejection\n` + `Action: Position auto-closed for safety (unmonitored positions = risk)\n\n` + `TX: ${openResult.transactionSignature?.slice(0, 20)}...` console.log(`📱 Phantom notification prepared:`, phantomNotification) // Return HTTP 200 with warning (not 500) so n8n workflow continues to notification return NextResponse.json( { success: true, // Changed from false - position was handled safely warning: 'Phantom trade detected and auto-closed', isPhantom: true, message: phantomNotification, // Full notification message for n8n phantomDetails: { expectedSize: positionSizeUSD, actualSize: openResult.actualSizeUSD, sizeRatio: (openResult.actualSizeUSD || 0) / positionSizeUSD, autoClosed: closeResult?.success || false, pnl: closePnL, entryTx: openResult.transactionSignature, exitTx: closeResult?.transactionSignature, } }, { status: 200 } // Changed from 500 - allows n8n to continue ) } // Calculate stop loss and take profit prices const entryPrice = openResult.fillPrice! // ATR-based TP/SL calculation (PRIMARY SYSTEM - Nov 17, 2025) let tp1Percent = config.takeProfit1Percent // Fallback let tp2Percent = config.takeProfit2Percent // Fallback let slPercent = config.stopLossPercent // Fallback if (config.useAtrBasedTargets && body.atr && body.atr > 0) { // Calculate dynamic percentages from ATR tp1Percent = calculatePercentFromAtr( body.atr, entryPrice, config.atrMultiplierTp1, config.minTp1Percent, config.maxTp1Percent ) tp2Percent = calculatePercentFromAtr( body.atr, entryPrice, config.atrMultiplierTp2, config.minTp2Percent, config.maxTp2Percent ) slPercent = -Math.abs(calculatePercentFromAtr( body.atr, entryPrice, config.atrMultiplierSl, config.minSlPercent, config.maxSlPercent )) console.log(`📊 ATR-based targets (ATR: ${body.atr.toFixed(4)} = ${((body.atr/entryPrice)*100).toFixed(2)}%):`) console.log(` TP1: ${config.atrMultiplierTp1}x ATR = ${tp1Percent.toFixed(2)}%`) console.log(` TP2: ${config.atrMultiplierTp2}x ATR = ${tp2Percent.toFixed(2)}%`) console.log(` SL: ${config.atrMultiplierSl}x ATR = ${slPercent.toFixed(2)}%`) } else { console.log(`⚠️ Using fixed percentage targets (ATR not available or disabled)`) } const stopLossPrice = calculatePrice( entryPrice, slPercent, 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, tp1Percent, body.direction ) const tp2Price = calculatePrice( entryPrice, tp2Percent, body.direction ) console.log('📊 Trade targets:') console.log(` Entry: $${entryPrice.toFixed(4)}`) console.log(` SL: $${stopLossPrice.toFixed(4)} (${slPercent.toFixed(2)}%)`) console.log(` TP1: $${tp1Price.toFixed(4)} (${tp1Percent.toFixed(2)}%)`) console.log(` TP2: $${tp2Price.toFixed(4)} (${tp2Percent.toFixed(2)}%)`) // 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: leverage, // Use actual symbol-specific leverage stopLossPrice, tp1Price, tp2Price, emergencyStopPrice, currentSize: positionSizeUSD, originalPositionSize: positionSizeUSD, // Store original size for accurate P&L takeProfitPrice1: tp1Price, takeProfitPrice2: tp2Price, tp1Hit: false, tp2Hit: false, slMovedToBreakeven: false, slMovedToProfit: false, trailingStopActive: false, realizedPnL: 0, unrealizedPnL: 0, peakPnL: 0, peakPrice: entryPrice, // MAE/MFE tracking maxFavorableExcursion: 0, maxAdverseExcursion: 0, maxFavorablePrice: entryPrice, maxAdversePrice: entryPrice, // Position scaling tracking originalAdx: body.adx, // Store for scaling validation timesScaled: 0, totalScaleAdded: 0, 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 { console.log('🔍 DEBUG: About to call placeExitOrders()...') console.log('🔍 DEBUG: Parameters:', { symbol: driftSymbol, positionSizeUSD, entryPrice, tp1Price, tp2Price, stopLossPrice, direction: body.direction }) const exitRes = await placeExitOrders({ symbol: driftSymbol, positionSizeUSD: positionSizeUSD, entryPrice: entryPrice, tp1Price, tp2Price, stopLossPrice, tp1SizePercent: config.takeProfit1SizePercent ?? 75, tp2SizePercent: config.takeProfit2SizePercent ?? 0, // 0 = activate trailing stop, don't close direction: body.direction, // Dual stop parameters useDualStops: config.useDualStops, softStopPrice: softStopPrice, softStopBuffer: config.softStopBuffer, hardStopPrice: hardStopPrice, }) console.log('🔍 DEBUG: placeExitOrders() returned:', exitRes.success ? 'SUCCESS' : 'FAILED') 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) } console.log('🔍 DEBUG: Exit orders section complete, about to calculate quality score...') // Save trade to database FIRST (CRITICAL: Must succeed before Position Manager) try { // Quality score already calculated earlier for adaptive leverage console.log('🔍 DEBUG: Using quality score from earlier calculation:', qualityResult.score) console.log('🔍 DEBUG: About to call createTrade()...') await createTrade({ positionId: openResult.transactionSignature!, symbol: driftSymbol, direction: body.direction, entryPrice, positionSizeUSD: positionSizeUSD, leverage: leverage, // Use actual symbol-specific leverage, not global config stopLossPrice, takeProfit1Price: tp1Price, takeProfit2Price: tp2Price, tp1SizePercent: config.takeProfit1SizePercent ?? 75, tp2SizePercent: config.takeProfit2SizePercent ?? 0, // Use ?? to allow 0 for runner system 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, signalSource: body.timeframe === 'manual' ? 'manual' : 'tradingview', // Identify manual Telegram trades signalStrength: body.signalStrength, timeframe: body.timeframe, // Context metrics from TradingView atrAtEntry: body.atr, adxAtEntry: body.adx, rsiAtEntry: body.rsi, volumeAtEntry: body.volumeRatio, pricePositionAtEntry: body.pricePosition, signalQualityScore: qualityResult.score, indicatorVersion: body.indicatorVersion || 'v5', // Default to v5 for backward compatibility }) console.log('🔍 DEBUG: createTrade() completed successfully') console.log(`💾 Trade saved with quality score: ${qualityResult.score}/100`) console.log(`📊 Quality reasons: ${qualityResult.reasons.join(', ')}`) // Log successful trade execution to persistent file logTradeExecution(true, { symbol: driftSymbol, direction: body.direction, entryPrice, positionSize: positionSizeUSD, transactionSignature: openResult.transactionSignature }) } catch (dbError) { console.error('❌ CRITICAL: Failed to save trade to database:', dbError) console.error(' Position is OPEN on Drift but NOT tracked!') console.error(' Manual intervention required - close position immediately') // Log to persistent file (survives container restarts) logCriticalError('Database save failed during trade execution', { symbol: driftSymbol, direction: body.direction, entryPrice, positionSize: positionSizeUSD, transactionSignature: openResult.transactionSignature, error: dbError instanceof Error ? dbError.message : String(dbError), stack: dbError instanceof Error ? dbError.stack : undefined }) logTradeExecution(false, { symbol: driftSymbol, direction: body.direction, entryPrice, positionSize: positionSizeUSD, transactionSignature: openResult.transactionSignature, error: dbError instanceof Error ? dbError.message : 'Database save failed' }) // CRITICAL: If database save fails, we MUST NOT add to Position Manager // Return error to user so they know to close manually return NextResponse.json( { success: false, error: 'Database save failed - position unprotected', message: `Position opened on Drift but database save failed. CLOSE POSITION MANUALLY IMMEDIATELY. Transaction: ${openResult.transactionSignature}`, }, { status: 500 } ) } // Add to position manager for monitoring ONLY AFTER database save succeeds 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: leverage, // Use actual symbol-specific leverage, not global config stopLoss: stopLossPrice, takeProfit1: tp1Price, takeProfit2: tp2Price, stopLossPercent: config.stopLossPercent, tp1Percent: config.takeProfit1Percent, tp2Percent: config.takeProfit2Percent, entrySlippage: openResult.slippage, timestamp: new Date().toISOString(), qualityScore: qualityResult.score, // Add quality score for Telegram notification (Nov 24, 2025) } // Attach exit order signatures to response if (exitOrderSignatures.length > 0) { (response as any).exitOrderSignatures = exitOrderSignatures } 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) } } /** * Calculate TP/SL from ATR with safety bounds (NEW - Nov 17, 2025) * Returns percentage to use with calculatePrice() */ function calculatePercentFromAtr( atrValue: number, entryPrice: number, atrMultiplier: number, minPercent: number, maxPercent: number ): number { // Convert ATR to percentage of entry price const atrPercent = (atrValue / entryPrice) * 100 // Apply multiplier const targetPercent = atrPercent * atrMultiplier // Clamp between min/max bounds return Math.max(minPercent, Math.min(maxPercent, targetPercent)) }