|
|
|
|
@@ -8,11 +8,12 @@
|
|
|
|
|
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 { 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')
|
|
|
|
|
@@ -35,8 +36,6 @@ export interface ExecuteTradeResponse {
|
|
|
|
|
direction?: 'long' | 'short'
|
|
|
|
|
entryPrice?: number
|
|
|
|
|
positionSize?: number
|
|
|
|
|
requestedPositionSize?: number
|
|
|
|
|
fillCoveragePercent?: number
|
|
|
|
|
leverage?: number
|
|
|
|
|
stopLoss?: number
|
|
|
|
|
takeProfit1?: number
|
|
|
|
|
@@ -88,34 +87,29 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
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 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
|
|
|
|
|
)
|
|
|
|
|
// Get symbol-specific position sizing
|
|
|
|
|
const { getPositionSizeForSymbol } = await import('@/config/trading')
|
|
|
|
|
const { size: positionSize, leverage, enabled } = getPositionSizeForSymbol(driftSymbol, config)
|
|
|
|
|
|
|
|
|
|
// Check if trading is enabled for this symbol
|
|
|
|
|
if (!enabled) {
|
|
|
|
|
@@ -134,9 +128,25 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
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)}`)
|
|
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
|
@@ -186,16 +196,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
|
|
|
|
|
// Update Position Manager tracking
|
|
|
|
|
const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1
|
|
|
|
|
const actualScaleNotional = scaleResult.actualSizeUSD ?? scaleSize
|
|
|
|
|
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + actualScaleNotional
|
|
|
|
|
const newTotalSize = sameDirectionPosition.currentSize + actualScaleNotional
|
|
|
|
|
|
|
|
|
|
if (scaleSize > 0) {
|
|
|
|
|
const coverage = (actualScaleNotional / scaleSize) * 100
|
|
|
|
|
if (coverage < 99.5) {
|
|
|
|
|
console.log(`⚠️ Scale fill coverage: ${coverage.toFixed(2)}% of requested $${scaleSize.toFixed(2)}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
@@ -285,20 +287,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate requested position size with leverage
|
|
|
|
|
const requestedPositionSizeUSD = positionSize * leverage
|
|
|
|
|
// 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(` Requested notional: $${requestedPositionSizeUSD}`)
|
|
|
|
|
console.log(` Total position: $${positionSizeUSD}`)
|
|
|
|
|
|
|
|
|
|
// Open position
|
|
|
|
|
const openResult = await openPosition({
|
|
|
|
|
symbol: driftSymbol,
|
|
|
|
|
direction: body.direction,
|
|
|
|
|
sizeUSD: requestedPositionSizeUSD,
|
|
|
|
|
sizeUSD: positionSizeUSD,
|
|
|
|
|
slippageTolerance: config.slippageTolerance,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@@ -316,7 +318,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
// CRITICAL: Check for phantom trade (position opened but size mismatch)
|
|
|
|
|
if (openResult.isPhantom) {
|
|
|
|
|
console.error(`🚨 PHANTOM TRADE DETECTED - Not adding to Position Manager`)
|
|
|
|
|
console.error(` Expected: $${requestedPositionSizeUSD.toFixed(2)}`)
|
|
|
|
|
console.error(` Expected: $${positionSizeUSD.toFixed(2)}`)
|
|
|
|
|
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
|
|
|
|
|
|
|
|
|
|
// Save phantom trade to database for analysis
|
|
|
|
|
@@ -336,7 +338,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
symbol: driftSymbol,
|
|
|
|
|
direction: body.direction,
|
|
|
|
|
entryPrice: openResult.fillPrice!,
|
|
|
|
|
positionSizeUSD: requestedPositionSizeUSD,
|
|
|
|
|
positionSizeUSD: positionSizeUSD,
|
|
|
|
|
leverage: config.leverage,
|
|
|
|
|
stopLossPrice: 0, // Not applicable for phantom
|
|
|
|
|
takeProfit1Price: 0,
|
|
|
|
|
@@ -356,7 +358,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
// Phantom-specific fields
|
|
|
|
|
status: 'phantom',
|
|
|
|
|
isPhantom: true,
|
|
|
|
|
expectedSizeUSD: requestedPositionSizeUSD,
|
|
|
|
|
expectedSizeUSD: positionSizeUSD,
|
|
|
|
|
actualSizeUSD: openResult.actualSizeUSD,
|
|
|
|
|
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
|
|
|
|
|
})
|
|
|
|
|
@@ -370,7 +372,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
{
|
|
|
|
|
success: false,
|
|
|
|
|
error: 'Phantom trade detected',
|
|
|
|
|
message: `Position opened but size mismatch detected. Expected $${requestedPositionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
|
|
|
|
|
message: `Position opened but size mismatch detected. Expected $${positionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
|
|
|
|
|
},
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
)
|
|
|
|
|
@@ -378,20 +380,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
@@ -425,15 +413,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
body.direction
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const dynamicTp2Percent = calculateDynamicTp2(
|
|
|
|
|
entryPrice,
|
|
|
|
|
body.atr || 0, // ATR from TradingView signal
|
|
|
|
|
config
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const tp2Price = calculatePrice(
|
|
|
|
|
entryPrice,
|
|
|
|
|
dynamicTp2Percent,
|
|
|
|
|
config.takeProfit2Percent,
|
|
|
|
|
body.direction
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@@ -441,7 +423,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
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)`)
|
|
|
|
|
console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`)
|
|
|
|
|
|
|
|
|
|
// Calculate emergency stop
|
|
|
|
|
const emergencyStopPrice = calculatePrice(
|
|
|
|
|
@@ -458,13 +440,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
direction: body.direction,
|
|
|
|
|
entryPrice,
|
|
|
|
|
entryTime: Date.now(),
|
|
|
|
|
positionSize: actualPositionSizeUSD,
|
|
|
|
|
positionSize: positionSizeUSD,
|
|
|
|
|
leverage: config.leverage,
|
|
|
|
|
stopLossPrice,
|
|
|
|
|
tp1Price,
|
|
|
|
|
tp2Price,
|
|
|
|
|
emergencyStopPrice,
|
|
|
|
|
currentSize: actualPositionSizeUSD,
|
|
|
|
|
currentSize: positionSizeUSD,
|
|
|
|
|
tp1Hit: false,
|
|
|
|
|
tp2Hit: false,
|
|
|
|
|
slMovedToBreakeven: false,
|
|
|
|
|
@@ -483,8 +465,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
originalAdx: body.adx, // Store for scaling validation
|
|
|
|
|
timesScaled: 0,
|
|
|
|
|
totalScaleAdded: 0,
|
|
|
|
|
atrAtEntry: body.atr,
|
|
|
|
|
runnerTrailingPercent: undefined,
|
|
|
|
|
priceCheckCount: 0,
|
|
|
|
|
lastPrice: entryPrice,
|
|
|
|
|
lastUpdateTime: Date.now(),
|
|
|
|
|
@@ -497,13 +477,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
try {
|
|
|
|
|
const exitRes = await placeExitOrders({
|
|
|
|
|
symbol: driftSymbol,
|
|
|
|
|
positionSizeUSD: actualPositionSizeUSD,
|
|
|
|
|
positionSizeUSD: positionSizeUSD,
|
|
|
|
|
entryPrice: entryPrice,
|
|
|
|
|
tp1Price,
|
|
|
|
|
tp2Price,
|
|
|
|
|
stopLossPrice,
|
|
|
|
|
tp1SizePercent: config.takeProfit1SizePercent ?? 50,
|
|
|
|
|
tp2SizePercent: config.takeProfit2SizePercent ?? 100, // Use ?? instead of || to allow 0
|
|
|
|
|
tp1SizePercent: config.takeProfit1SizePercent ?? 75,
|
|
|
|
|
tp2SizePercent: config.takeProfit2SizePercent ?? 0, // 0 = activate trailing stop, don't close
|
|
|
|
|
direction: body.direction,
|
|
|
|
|
// Dual stop parameters
|
|
|
|
|
useDualStops: config.useDualStops,
|
|
|
|
|
@@ -534,16 +514,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
symbol: driftSymbol,
|
|
|
|
|
direction: body.direction,
|
|
|
|
|
entryPrice: entryPrice,
|
|
|
|
|
positionSize: actualPositionSizeUSD,
|
|
|
|
|
requestedPositionSize: requestedPositionSizeUSD,
|
|
|
|
|
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
|
|
|
|
|
positionSize: positionSizeUSD,
|
|
|
|
|
leverage: config.leverage,
|
|
|
|
|
stopLoss: stopLossPrice,
|
|
|
|
|
takeProfit1: tp1Price,
|
|
|
|
|
takeProfit2: tp2Price,
|
|
|
|
|
stopLossPercent: config.stopLossPercent,
|
|
|
|
|
tp1Percent: config.takeProfit1Percent,
|
|
|
|
|
tp2Percent: dynamicTp2Percent,
|
|
|
|
|
tp2Percent: config.takeProfit2Percent,
|
|
|
|
|
entrySlippage: openResult.slippage,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
}
|
|
|
|
|
@@ -571,13 +549,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
symbol: driftSymbol,
|
|
|
|
|
direction: body.direction,
|
|
|
|
|
entryPrice,
|
|
|
|
|
positionSizeUSD: actualPositionSizeUSD,
|
|
|
|
|
positionSizeUSD: positionSizeUSD,
|
|
|
|
|
leverage: config.leverage,
|
|
|
|
|
stopLossPrice,
|
|
|
|
|
takeProfit1Price: tp1Price,
|
|
|
|
|
takeProfit2Price: tp2Price,
|
|
|
|
|
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
|
|
|
|
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
|
|
|
|
tp1SizePercent: config.takeProfit1SizePercent ?? 75,
|
|
|
|
|
tp2SizePercent: config.takeProfit2SizePercent ?? 0, // Use ?? to allow 0 for runner system
|
|
|
|
|
configSnapshot: config,
|
|
|
|
|
entryOrderTx: openResult.transactionSignature!,
|
|
|
|
|
tp1OrderTx: exitOrderSignatures[0],
|
|
|
|
|
@@ -596,8 +574,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|
|
|
|
volumeAtEntry: body.volumeRatio,
|
|
|
|
|
pricePositionAtEntry: body.pricePosition,
|
|
|
|
|
signalQualityScore: qualityResult.score,
|
|
|
|
|
expectedSizeUSD: requestedPositionSizeUSD,
|
|
|
|
|
actualSizeUSD: actualPositionSizeUSD,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
console.log(`💾 Trade saved with quality score: ${qualityResult.score}/100`)
|
|
|
|
|
|