/** * Test Trade API Endpoint * * Executes a test trade with current settings (no authentication required from settings page) * POST /api/trading/test */ import { NextRequest, NextResponse } from 'next/server' import { initializeDriftService } from '@/lib/drift/client' import { openPosition, placeExitOrders } from '@/lib/drift/orders' import { normalizeTradingViewSymbol, calculateDynamicTp2 } from '@/config/trading' import { getMergedConfig } from '@/config/trading' import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { createTrade } from '@/lib/database/trades' export interface TestTradeRequest { symbol?: string // Default: SOLUSDT direction?: 'long' | 'short' // Default: long } export interface TestTradeResponse { success: boolean positionId?: string symbol?: string direction?: 'long' | 'short' entryPrice?: number positionSize?: number requestedPositionSize?: number fillCoveragePercent?: number stopLoss?: number takeProfit1?: number takeProfit2?: number softStopPrice?: number hardStopPrice?: number useDualStops?: boolean timestamp?: string error?: string message?: string } export async function POST(request: NextRequest): Promise> { try { // Parse request body const body: TestTradeRequest = await request.json().catch(() => ({})) const symbol = body.symbol || 'SOLUSDT' const direction = body.direction || 'long' console.log('๐Ÿงช Test trade request:', { symbol, direction }) // Normalize symbol const driftSymbol = normalizeTradingViewSymbol(symbol) console.log(`๐Ÿ“Š Normalized symbol: ${symbol} โ†’ ${driftSymbol}`) // Get trading configuration const config = getMergedConfig() // Initialize Drift service to get account balance 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 } ) } // Get symbol-specific position sizing (with percentage support) const { getActualPositionSizeForSymbol } = await import('@/config/trading') const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol( driftSymbol, config, health.freeCollateral ) // 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}`) console.log(` Leverage: ${leverage}x`) console.log(` Using percentage: ${usePercentage}`) console.log(` Free collateral: $${health.freeCollateral.toFixed(2)}`) // Calculate position size with leverage const requestedPositionSizeUSD = positionSize * leverage console.log(`๐Ÿ’ฐ Opening ${direction} position:`) console.log(` Symbol: ${driftSymbol}`) console.log(` Base size: $${positionSize}`) console.log(` Leverage: ${leverage}x`) console.log(` Requested notional: $${requestedPositionSizeUSD}`) // Open position const openResult = await openPosition({ symbol: driftSymbol, direction: direction, sizeUSD: requestedPositionSizeUSD, 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 actualPositionSizeUSD = openResult.actualSizeUSD ?? requestedPositionSizeUSD const filledBaseSize = openResult.fillSize !== undefined ? Math.abs(openResult.fillSize) : (entryPrice > 0 ? actualPositionSizeUSD / entryPrice : 0) const fillCoverage = requestedPositionSizeUSD > 0 ? (actualPositionSizeUSD / requestedPositionSizeUSD) * 100 : 100 console.log('๐Ÿ“ Fill results:') console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${driftSymbol.split('-')[0]}`) console.log(` Filled notional: $${actualPositionSizeUSD.toFixed(2)}`) if (fillCoverage < 99.5) { console.log(` โš ๏ธ Partial fill: ${fillCoverage.toFixed(2)}% of requested size`) } const stopLossPrice = calculatePrice( entryPrice, config.stopLossPercent, direction ) // Calculate dual stop prices if enabled let softStopPrice: number | undefined let hardStopPrice: number | undefined if (config.useDualStops) { softStopPrice = calculatePrice( entryPrice, config.softStopPercent, direction ) hardStopPrice = calculatePrice( entryPrice, config.hardStopPercent, 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, direction ) // Use ATR-based dynamic TP2 with simulated ATR for testing const simulatedATR = entryPrice * 0.008 // Simulate 0.8% ATR for testing const dynamicTp2Percent = calculateDynamicTp2( entryPrice, simulatedATR, config ) const tp2Price = calculatePrice( entryPrice, dynamicTp2Percent, 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)} (${dynamicTp2Percent.toFixed(2)}% - ATR-based test)`) // Calculate emergency stop const emergencyStopPrice = calculatePrice( entryPrice, config.emergencyStopPercent, direction ) // Create active trade object const activeTrade: ActiveTrade = { id: `test-trade-${Date.now()}`, positionId: openResult.transactionSignature!, symbol: driftSymbol, direction: direction, entryPrice, entryTime: Date.now(), positionSize: actualPositionSizeUSD, leverage: leverage, stopLossPrice, tp1Price, tp2Price, emergencyStopPrice, currentSize: actualPositionSizeUSD, originalPositionSize: actualPositionSizeUSD, // Store original size for 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, priceCheckCount: 0, lastPrice: entryPrice, lastUpdateTime: Date.now(), } // Add to position manager for monitoring const positionManager = await getInitializedPositionManager() await positionManager.addTrade(activeTrade) console.log('โœ… Trade added to position manager for monitoring') // Create response object const response: TestTradeResponse = { success: true, positionId: openResult.transactionSignature, symbol: driftSymbol, direction: direction, entryPrice: entryPrice, positionSize: actualPositionSizeUSD, requestedPositionSize: requestedPositionSizeUSD, fillCoveragePercent: Number(fillCoverage.toFixed(2)), stopLoss: stopLossPrice, takeProfit1: tp1Price, takeProfit2: tp2Price, softStopPrice: softStopPrice, hardStopPrice: hardStopPrice, useDualStops: config.useDualStops, timestamp: new Date().toISOString(), } // Place on-chain TP/SL orders so they appear in Drift UI let exitOrderSignatures: string[] = [] try { const exitRes = await placeExitOrders({ symbol: driftSymbol, positionSizeUSD: actualPositionSizeUSD, entryPrice: entryPrice, tp1Price, tp2Price, stopLossPrice, tp1SizePercent: config.takeProfit1SizePercent ?? 50, tp2SizePercent: config.takeProfit2SizePercent ?? 0, // 0 = activate trailing stop for runner direction: 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 || [] } // 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) } // Save trade to database try { await createTrade({ positionId: openResult.transactionSignature!, symbol: driftSymbol, direction: direction, entryPrice, positionSizeUSD: actualPositionSizeUSD, leverage: leverage, stopLossPrice, takeProfit1Price: tp1Price, takeProfit2Price: tp2Price, tp1SizePercent: config.takeProfit1SizePercent ?? 50, tp2SizePercent: config.takeProfit2SizePercent ?? 0, // 0 = activate trailing stop for runner 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: 'test', timeframe: 'manual', expectedSizeUSD: requestedPositionSizeUSD, actualSizeUSD: actualPositionSizeUSD, }) 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('โœ… Test trade executed successfully!') return NextResponse.json(response) } catch (error) { console.error('โŒ Test 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) } }