Files
trading_bot_v4/app/api/trading/execute/route.ts
mindesbunister 25776413d0 feat: Add signalSource field to identify manual vs TradingView trades
- Set signalSource='manual' for Telegram trades, 'tradingview' for TradingView
- Updated analytics queries to exclude manual trades from indicator analysis
- getTradingStats() filters manual trades (TradingView performance only)
- Version comparison endpoint filters manual trades
- Created comprehensive filtering guide: docs/MANUAL_TRADE_FILTERING.md
- Ensures clean data for indicator optimization without contamination
2025-11-14 22:55:14 +01:00

747 lines
28 KiB
TypeScript

/**
* 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 } 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'
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
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
error?: string
message?: string
}
export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTradeResponse>> {
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`)
}
// Get trading configuration
const config = getMergedConfig()
// 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)}`)
// Get symbol-specific position sizing (supports percentage-based sizing)
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.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('❌ 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)})`)
// 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)
}
}
// Small delay to ensure position is fully closed on-chain
await new Promise(resolve => setTimeout(resolve, 2000))
}
// 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,
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!
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: leverage, // Use actual symbol-specific 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,
// 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)
let qualityResult
try {
// Calculate quality score if metrics available
console.log('🔍 DEBUG: Calling scoreSignalQuality()...')
qualityResult = await scoreSignalQuality({
atr: body.atr || 0,
adx: body.adx || 0,
rsi: body.rsi || 0,
volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0,
direction: body.direction,
symbol: driftSymbol,
currentPrice: openResult.fillPrice,
timeframe: body.timeframe,
})
console.log('🔍 DEBUG: scoreSignalQuality() completed, score:', 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(', ')}`)
} 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')
// 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(),
}
// 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)
}
}