Files
trading_bot_v4/app/api/trading/execute/route.ts
mindesbunister d602744938 critical: Fix 1-minute signal price logging - query Drift oracle directly
Problem:
- 1-minute signals logged wrong prices (4-55 vs actual 41-144 SOL)
- Pyth price cache empty for 1-minute signals
- Fallback to body.signalPrice (from n8n) contained pricePosition value
- signalPrice === pricePosition in every request (39.29 = 39.29)

Failed Attempts (5):
1. Added @ close to TradingView indicator (commit 212a36f)
2. Added signalPrice regex to n8n (commit 99a5223)
3. Fixed regex to avoid POS collision (commit ff402ed)
4. Added DEBUG logging to n8n (no output visible)
5. FINAL FIX: Query Drift oracle directly

Solution:
- Bypass Pyth cache and n8n signalPrice entirely
- Query Drift Protocol oracle directly for real-time price
- Lines 145-149 in execute/route.ts:
  * OLD: latestPrice?.price || body.signalPrice || 0
  * NEW: await driftService.getOraclePrice(marketConfig.driftMarketIndex)
- Drift oracle is source of truth (on-chain), no caching issues
- Added getMarketConfig import to resolve marketConfig

Impact:
- Logs will show actual SOL price ($141.XX) instead of indicator values ($24-55)
- Accurate price logging critical for REAL MONEY system P&L tracking
- BlockedSignal database records will have correct signalPrice values

Files Changed:
- app/api/trading/execute/route.ts (lines 11, 145-149)
  * Added getMarketConfig to imports
  * Replaced Pyth cache fallback with direct Drift oracle query
2025-11-27 12:48:37 +01:00

1026 lines
40 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, getMinQualityScoreForDirection, getMarketConfig } 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'
import { getSmartEntryTimer } from '@/lib/trading/smart-entry-timer'
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<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`)
}
// 📊 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 timeframe = body.timeframe || '5'
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: timeframe,
direction: body.direction,
symbol: driftSymbol,
currentPrice: body.signalPrice || 0,
skipFrequencyCheck: timeframe !== '5', // Skip overtrading/flip-flop for data collection
})
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 (1min, 15min, 1H, 4H, Daily) for analysis
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
// CRITICAL FIX: Query Drift oracle directly instead of Pyth cache (cache might be stale for 1min signals)
const driftService = await initializeDriftService()
const marketConfig = getMarketConfig(driftSymbol)
const currentPrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
// 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}`)
// 🎯 SMART ENTRY TIMING - Check if we should wait for better entry (Phase 2 - Nov 27, 2025)
const smartEntryTimer = getSmartEntryTimer()
if (smartEntryTimer.isEnabled() && body.signalPrice) {
console.log(`🎯 Smart Entry: Evaluating entry timing...`)
// Get current price to check if already at favorable level
const priceMonitor = getPythPriceMonitor()
const latestPrice = priceMonitor.getCachedPrice(driftSymbol)
const currentPrice = latestPrice?.price || body.signalPrice
const priceChange = ((currentPrice - body.signalPrice) / body.signalPrice) * 100
const isPullbackDirection = body.direction === 'long' ? priceChange < 0 : priceChange > 0
const pullbackMagnitude = Math.abs(priceChange)
const pullbackMin = parseFloat(process.env.SMART_ENTRY_PULLBACK_MIN || '0.15')
const pullbackMax = parseFloat(process.env.SMART_ENTRY_PULLBACK_MAX || '0.50')
console.log(` Signal Price: $${body.signalPrice.toFixed(2)}`)
console.log(` Current Price: $${currentPrice.toFixed(2)} (${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%)`)
if (isPullbackDirection && pullbackMagnitude >= pullbackMin && pullbackMagnitude <= pullbackMax) {
// Already at favorable entry - execute immediately!
console.log(`✅ Smart Entry: Already at favorable level (${pullbackMagnitude.toFixed(2)}% pullback)`)
console.log(` Executing immediately - no need to wait`)
} else if (!isPullbackDirection || pullbackMagnitude < pullbackMin) {
// Not favorable yet - queue for smart entry
console.log(`⏳ Smart Entry: Queuing signal for optimal entry timing`)
console.log(` Waiting for ${body.direction === 'long' ? 'dip' : 'bounce'} of ${pullbackMin}-${pullbackMax}%`)
// Queue the signal with full context
const queuedSignal = smartEntryTimer.queueSignal({
symbol: driftSymbol,
direction: body.direction,
signalPrice: body.signalPrice,
atr: body.atr,
adx: body.adx,
rsi: body.rsi,
volumeRatio: body.volumeRatio,
pricePosition: body.pricePosition,
indicatorVersion: body.indicatorVersion,
qualityScore: qualityResult.score,
})
// Return success immediately (n8n workflow continues)
return NextResponse.json({
success: true,
message: 'Signal queued for smart entry timing',
smartEntry: {
enabled: true,
queuedAt: new Date().toISOString(),
signalId: queuedSignal.id,
targetPullback: `${pullbackMin}-${pullbackMax}%`,
maxWait: `${parseInt(process.env.SMART_ENTRY_MAX_WAIT_MS || '120000') / 1000}s`,
currentPullback: `${priceChange.toFixed(2)}%`,
},
positionId: `queued-${queuedSignal.id}`,
symbol: driftSymbol,
direction: body.direction,
qualityScore: qualityResult.score,
}, { status: 200 })
} else if (pullbackMagnitude > pullbackMax) {
// Pullback too large - might be reversal, execute with caution
console.log(`⚠️ Smart Entry: Pullback too large (${pullbackMagnitude.toFixed(2)}% > ${pullbackMax}%)`)
console.log(` Possible reversal - executing at current price with caution`)
}
}
// 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))
}