Files
trading_bot_v4/app/api/trading/execute/route.ts
mindesbunister d4aeeb4f99 fix: add MAE/MFE fields to all ActiveTrade initializations
Updated execute, test, and test-db endpoints to include:
- maxFavorableExcursion: 0
- maxAdverseExcursion: 0
- maxFavorablePrice: entryPrice
- maxAdversePrice: entryPrice

Required for TypeScript compilation after adding MAE/MFE tracking
2025-11-02 23:04:02 +01:00

392 lines
12 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 } 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
// Context metrics from TradingView
atr?: number
adx?: number
rsi?: number
volumeRatio?: number
pricePosition?: number
qualityScore?: number // Calculated by check-risk endpoint
}
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 (0-100)
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}`)
// 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,
maxFavorableExcursion: 0,
maxAdverseExcursion: 0,
maxFavorablePrice: entryPrice,
maxAdversePrice: 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,
entryPrice: entryPrice,
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)
}
// 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(),
qualityScore: body.qualityScore, // Include quality score in response
}
// 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,
// Context metrics from TradingView
atrAtEntry: body.atr,
adxAtEntry: body.adx,
rsiAtEntry: body.rsi,
volumeAtEntry: body.volumeRatio,
pricePositionAtEntry: body.pricePosition,
signalQualityScore: body.qualityScore,
})
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
}
// NOW add to position manager for monitoring (after database save)
await positionManager.addTrade(activeTrade)
console.log('✅ Trade added to position manager for monitoring')
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)
}
}