Merge development branch: improved UI, coin icons, and comprehensive documentation
- Add proper CoinGecko coin icons for BTC, ETH, SOL - Clean up homepage layout and remove clutter - Add comprehensive .github/copilot-instructions.md with full architecture documentation - Include timeframe fixes, trading integration improvements, and Docker optimizations - Maintain all trading functionality and AI analysis features
This commit is contained in:
@@ -1,19 +1,70 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { enhancedScreenshotService } from '../../../lib/enhanced-screenshot-simple'
|
||||
import { enhancedScreenshotService } from '../../../lib/enhanced-screenshot'
|
||||
import { aiAnalysisService } from '../../../lib/ai-analysis'
|
||||
import { progressTracker } from '../../../lib/progress-tracker'
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { symbol, layouts, timeframes, selectedLayouts, analyze = true } = body
|
||||
const { symbol, layouts, timeframe, timeframes, selectedLayouts, analyze = true } = body
|
||||
|
||||
console.log('📊 Enhanced screenshot request:', { symbol, layouts, timeframes, selectedLayouts })
|
||||
console.log('📊 Enhanced screenshot request:', { symbol, layouts, timeframe, timeframes, selectedLayouts })
|
||||
|
||||
// Generate unique session ID for progress tracking
|
||||
const sessionId = `analysis_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
console.log('🔍 Created session ID:', sessionId)
|
||||
|
||||
// Create progress tracking session with initial steps
|
||||
const initialSteps = [
|
||||
{
|
||||
id: 'init',
|
||||
title: 'Initializing Analysis',
|
||||
description: 'Starting AI-powered trading analysis...',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
title: 'TradingView Authentication',
|
||||
description: 'Logging into TradingView accounts',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'navigation',
|
||||
title: 'Chart Navigation',
|
||||
description: 'Navigating to chart layouts',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'loading',
|
||||
title: 'Chart Data Loading',
|
||||
description: 'Waiting for chart data and indicators',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'capture',
|
||||
title: 'Screenshot Capture',
|
||||
description: 'Capturing high-quality screenshots',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'analysis',
|
||||
title: 'AI Analysis',
|
||||
description: 'Analyzing screenshots with AI',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
|
||||
// Create the progress session
|
||||
console.log('🔍 Creating progress session with steps:', initialSteps.length)
|
||||
progressTracker.createSession(sessionId, initialSteps)
|
||||
console.log('🔍 Progress session created successfully')
|
||||
|
||||
// Prepare configuration for screenshot service
|
||||
const config = {
|
||||
symbol: symbol || 'BTCUSD',
|
||||
timeframe: timeframes?.[0] || '60', // Use first timeframe for now
|
||||
timeframe: timeframe || timeframes?.[0] || '60', // Use single timeframe, fallback to first of array, then default
|
||||
layouts: layouts || selectedLayouts || ['ai'],
|
||||
sessionId, // Pass session ID for progress tracking
|
||||
credentials: {
|
||||
email: process.env.TRADINGVIEW_EMAIL,
|
||||
password: process.env.TRADINGVIEW_PASSWORD
|
||||
@@ -22,35 +73,32 @@ export async function POST(request) {
|
||||
|
||||
console.log('🔧 Using config:', config)
|
||||
|
||||
// Capture screenshots using the working service
|
||||
const screenshots = await enhancedScreenshotService.captureWithLogin(config)
|
||||
console.log('📸 Screenshots captured:', screenshots)
|
||||
|
||||
let screenshots = []
|
||||
let analysis = null
|
||||
|
||||
// Perform AI analysis if requested and screenshots were captured
|
||||
if (analyze && screenshots.length > 0) {
|
||||
// Perform AI analysis if requested
|
||||
if (analyze) {
|
||||
try {
|
||||
console.log('🤖 Starting AI analysis...')
|
||||
|
||||
// Extract just the filenames from full paths
|
||||
const filenames = screenshots.map(path => path.split('/').pop())
|
||||
|
||||
if (filenames.length === 1) {
|
||||
analysis = await aiAnalysisService.analyzeScreenshot(filenames[0])
|
||||
} else {
|
||||
analysis = await aiAnalysisService.analyzeMultipleScreenshots(filenames)
|
||||
}
|
||||
|
||||
console.log('✅ AI analysis completed')
|
||||
console.log('🤖 Starting automated capture and analysis...')
|
||||
const result = await aiAnalysisService.captureAndAnalyzeWithConfig(config, sessionId)
|
||||
screenshots = result.screenshots
|
||||
analysis = result.analysis
|
||||
console.log('✅ Automated capture and analysis completed')
|
||||
} catch (analysisError) {
|
||||
console.error('❌ AI analysis failed:', analysisError)
|
||||
// Continue without analysis rather than failing the whole request
|
||||
console.error('❌ Automated capture and analysis failed:', analysisError)
|
||||
// Fall back to screenshot only
|
||||
screenshots = await enhancedScreenshotService.captureWithLogin(config, sessionId)
|
||||
}
|
||||
} else {
|
||||
// Capture screenshots only
|
||||
screenshots = await enhancedScreenshotService.captureWithLogin(config, sessionId)
|
||||
}
|
||||
|
||||
console.log('📸 Final screenshots:', screenshots)
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
sessionId, // Return session ID for progress tracking
|
||||
timestamp: Date.now(),
|
||||
symbol: config.symbol,
|
||||
layouts: config.layouts,
|
||||
|
||||
58
app/api/progress/[sessionId]/stream/route.ts
Normal file
58
app/api/progress/[sessionId]/stream/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { progressTracker } from '../../../../../lib/progress-tracker'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
const { sessionId } = await params
|
||||
|
||||
// Create a readable stream for Server-Sent Events
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Send initial progress if session exists
|
||||
const initialProgress = progressTracker.getProgress(sessionId)
|
||||
if (initialProgress) {
|
||||
const data = `data: ${JSON.stringify(initialProgress)}\n\n`
|
||||
controller.enqueue(encoder.encode(data))
|
||||
}
|
||||
|
||||
// Listen for progress updates
|
||||
const progressHandler = (progress: any) => {
|
||||
const data = `data: ${JSON.stringify(progress)}\n\n`
|
||||
controller.enqueue(encoder.encode(data))
|
||||
}
|
||||
|
||||
// Listen for completion
|
||||
const completeHandler = () => {
|
||||
const data = `data: ${JSON.stringify({ type: 'complete' })}\n\n`
|
||||
controller.enqueue(encoder.encode(data))
|
||||
controller.close()
|
||||
}
|
||||
|
||||
// Subscribe to events
|
||||
progressTracker.on(`progress:${sessionId}`, progressHandler)
|
||||
progressTracker.on(`progress:${sessionId}:complete`, completeHandler)
|
||||
|
||||
// Cleanup on stream close
|
||||
request.signal.addEventListener('abort', () => {
|
||||
progressTracker.off(`progress:${sessionId}`, progressHandler)
|
||||
progressTracker.off(`progress:${sessionId}:complete`, completeHandler)
|
||||
controller.close()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
'Access-Control-Allow-Headers': 'Cache-Control'
|
||||
}
|
||||
})
|
||||
}
|
||||
52
app/api/progress/route.ts
Normal file
52
app/api/progress/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { progressTracker } from '../../../lib/progress-tracker'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const sessionId = searchParams.get('sessionId')
|
||||
|
||||
if (!sessionId) {
|
||||
return new Response('Session ID required', { status: 400 })
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Send initial progress if session exists
|
||||
const currentProgress = progressTracker.getProgress(sessionId)
|
||||
if (currentProgress) {
|
||||
const data = `data: ${JSON.stringify(currentProgress)}\n\n`
|
||||
controller.enqueue(new TextEncoder().encode(data))
|
||||
}
|
||||
|
||||
// Listen for progress updates
|
||||
const progressHandler = (progress: any) => {
|
||||
const data = `data: ${JSON.stringify(progress)}\n\n`
|
||||
controller.enqueue(new TextEncoder().encode(data))
|
||||
}
|
||||
|
||||
const completeHandler = () => {
|
||||
const data = `data: ${JSON.stringify({ type: 'complete' })}\n\n`
|
||||
controller.enqueue(new TextEncoder().encode(data))
|
||||
controller.close()
|
||||
}
|
||||
|
||||
progressTracker.on(`progress:${sessionId}`, progressHandler)
|
||||
progressTracker.on(`progress:${sessionId}:complete`, completeHandler)
|
||||
|
||||
// Cleanup on close
|
||||
return () => {
|
||||
progressTracker.off(`progress:${sessionId}`, progressHandler)
|
||||
progressTracker.off(`progress:${sessionId}:complete`, completeHandler)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Cache-Control'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export async function POST(request) {
|
||||
symbol,
|
||||
side,
|
||||
amount,
|
||||
amountUSD,
|
||||
stopLoss,
|
||||
takeProfit,
|
||||
useRealDEX = false,
|
||||
@@ -17,10 +18,16 @@ export async function POST(request) {
|
||||
toCoin
|
||||
} = body
|
||||
|
||||
// For Docker environment, use internal port 3000. For dev, use the host header
|
||||
const host = request.headers.get('host') || 'localhost:3000'
|
||||
const isDocker = process.env.DOCKER_ENV === 'true'
|
||||
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
|
||||
|
||||
console.log('🔄 Execute DEX trade request:', {
|
||||
symbol,
|
||||
side,
|
||||
amount,
|
||||
amountUSD,
|
||||
stopLoss,
|
||||
takeProfit,
|
||||
useRealDEX,
|
||||
@@ -64,13 +71,14 @@ export async function POST(request) {
|
||||
console.log('🔍 Validating wallet balance before DEX trade...')
|
||||
|
||||
try {
|
||||
const validationResponse = await fetch('http://localhost:3000/api/trading/validate', {
|
||||
const validationResponse = await fetch(`${baseUrl}/api/trading/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
symbol,
|
||||
side,
|
||||
amount,
|
||||
amountUSD,
|
||||
tradingMode: 'SPOT',
|
||||
fromCoin,
|
||||
toCoin
|
||||
@@ -194,7 +202,7 @@ export async function POST(request) {
|
||||
// Add trade to history with clear spot swap indication
|
||||
try {
|
||||
// Use localhost for internal container communication
|
||||
await fetch('http://localhost:3000/api/trading/history', {
|
||||
await fetch(`${baseUrl}/api/trading/history`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
311
app/api/trading/execute-drift/route.js
Normal file
311
app/api/trading/execute-drift/route.js
Normal file
@@ -0,0 +1,311 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
symbol,
|
||||
side,
|
||||
amount,
|
||||
leverage = 1,
|
||||
stopLoss,
|
||||
takeProfit,
|
||||
useRealDEX = false
|
||||
} = body
|
||||
|
||||
console.log('🔥 Drift Perpetuals trade request:', {
|
||||
symbol,
|
||||
side,
|
||||
amount,
|
||||
leverage,
|
||||
stopLoss,
|
||||
takeProfit,
|
||||
useRealDEX
|
||||
})
|
||||
|
||||
// Validate inputs
|
||||
if (!symbol || !side || !amount) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Missing required fields: symbol, side, amount'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['BUY', 'SELL', 'LONG', 'SHORT'].includes(side.toUpperCase())) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid side. Must be LONG/SHORT or BUY/SELL'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Amount must be greater than 0'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (leverage < 1 || leverage > 10) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Leverage must be between 1x and 10x'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!useRealDEX) {
|
||||
// Simulation mode
|
||||
console.log('🎮 Executing SIMULATED Drift perpetual trade')
|
||||
|
||||
const currentPrice = symbol === 'SOL' ? 166.75 : symbol === 'BTC' ? 121819 : 3041.66
|
||||
const leveragedAmount = amount * leverage
|
||||
const entryFee = leveragedAmount * 0.001 // 0.1% opening fee
|
||||
const liquidationPrice = side.toUpperCase().includes('LONG') || side.toUpperCase() === 'BUY'
|
||||
? currentPrice * (1 - 0.9 / leverage) // Approximate liquidation price
|
||||
: currentPrice * (1 + 0.9 / leverage)
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
trade: {
|
||||
txId: `drift_sim_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`,
|
||||
orderId: `drift_order_${Date.now()}`,
|
||||
symbol: symbol.toUpperCase(),
|
||||
side: side.toUpperCase(),
|
||||
positionSize: amount,
|
||||
leverage: leverage,
|
||||
leveragedAmount: leveragedAmount,
|
||||
entryPrice: currentPrice,
|
||||
liquidationPrice: liquidationPrice,
|
||||
entryFee: entryFee,
|
||||
timestamp: Date.now(),
|
||||
status: 'OPEN',
|
||||
platform: 'Drift Protocol (Simulation)',
|
||||
stopLoss: stopLoss,
|
||||
takeProfit: takeProfit,
|
||||
monitoring: !!(stopLoss || takeProfit),
|
||||
pnl: 0
|
||||
},
|
||||
message: `${side.toUpperCase()} perpetual position opened: $${amount} at ${leverage}x leverage - SIMULATED`
|
||||
})
|
||||
}
|
||||
|
||||
// Real Drift trading implementation
|
||||
console.log('💰 Executing REAL Drift perpetual trade')
|
||||
|
||||
// Import Drift SDK components
|
||||
const { DriftClient, initialize, MarketType, PositionDirection, OrderType } = await import('@drift-labs/sdk')
|
||||
const { Connection, Keypair } = await import('@solana/web3.js')
|
||||
const { Wallet } = await import('@coral-xyz/anchor')
|
||||
|
||||
// Initialize connection and wallet
|
||||
const connection = new Connection(
|
||||
process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
|
||||
'confirmed'
|
||||
)
|
||||
|
||||
if (!process.env.SOLANA_PRIVATE_KEY) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Drift trading not configured - missing SOLANA_PRIVATE_KEY'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
|
||||
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
|
||||
const wallet = new Wallet(keypair)
|
||||
|
||||
console.log('🚀 Initializing Drift client...')
|
||||
|
||||
// Initialize Drift SDK
|
||||
const env = 'mainnet-beta'
|
||||
const sdkConfig = initialize({ env })
|
||||
|
||||
const driftClient = new DriftClient({
|
||||
connection,
|
||||
wallet,
|
||||
programID: sdkConfig.DRIFT_PROGRAM_ID,
|
||||
opts: {
|
||||
commitment: 'confirmed',
|
||||
},
|
||||
})
|
||||
|
||||
await driftClient.subscribe()
|
||||
|
||||
try {
|
||||
// Get market index for the symbol
|
||||
const marketIndex = symbol === 'SOL' ? 0 : symbol === 'BTC' ? 1 : 0 // SOL-PERP is typically index 0
|
||||
|
||||
// Determine position direction
|
||||
const direction = side.toUpperCase().includes('LONG') || side.toUpperCase() === 'BUY'
|
||||
? PositionDirection.LONG
|
||||
: PositionDirection.SHORT
|
||||
|
||||
// Calculate position size in base asset units
|
||||
const currentPrice = 166.75 // Get from oracle in production
|
||||
const baseAssetAmount = (amount * leverage) / currentPrice * 1e9 // Convert to lamports for SOL
|
||||
|
||||
console.log('📊 Trade parameters:', {
|
||||
marketIndex,
|
||||
direction: direction === PositionDirection.LONG ? 'LONG' : 'SHORT',
|
||||
baseAssetAmount: baseAssetAmount.toString(),
|
||||
leverage
|
||||
})
|
||||
|
||||
// Place market order
|
||||
const orderParams = {
|
||||
orderType: OrderType.MARKET,
|
||||
marketType: MarketType.PERP,
|
||||
direction,
|
||||
baseAssetAmount: Math.floor(baseAssetAmount),
|
||||
marketIndex,
|
||||
}
|
||||
|
||||
console.log('🎯 Placing Drift market order...')
|
||||
const txSig = await driftClient.placeOrder(orderParams)
|
||||
|
||||
console.log('✅ Drift order placed:', txSig)
|
||||
|
||||
// Set up stop loss and take profit if specified
|
||||
let stopLossOrderId = null
|
||||
let takeProfitOrderId = null
|
||||
|
||||
if (stopLoss) {
|
||||
try {
|
||||
const stopLossParams = {
|
||||
orderType: OrderType.LIMIT,
|
||||
marketType: MarketType.PERP,
|
||||
direction: direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG,
|
||||
baseAssetAmount: Math.floor(baseAssetAmount),
|
||||
price: stopLoss * 1e6, // Price in 6 decimal format
|
||||
marketIndex,
|
||||
triggerPrice: stopLoss * 1e6,
|
||||
triggerCondition: direction === PositionDirection.LONG ? 'below' : 'above',
|
||||
}
|
||||
|
||||
const slTxSig = await driftClient.placeOrder(stopLossParams)
|
||||
stopLossOrderId = slTxSig
|
||||
console.log('🛑 Stop loss order placed:', slTxSig)
|
||||
} catch (slError) {
|
||||
console.warn('⚠️ Stop loss order failed:', slError.message)
|
||||
}
|
||||
}
|
||||
|
||||
if (takeProfit) {
|
||||
try {
|
||||
const takeProfitParams = {
|
||||
orderType: OrderType.LIMIT,
|
||||
marketType: MarketType.PERP,
|
||||
direction: direction === PositionDirection.LONG ? PositionDirection.SHORT : PositionDirection.LONG,
|
||||
baseAssetAmount: Math.floor(baseAssetAmount),
|
||||
price: takeProfit * 1e6, // Price in 6 decimal format
|
||||
marketIndex,
|
||||
triggerPrice: takeProfit * 1e6,
|
||||
triggerCondition: direction === PositionDirection.LONG ? 'above' : 'below',
|
||||
}
|
||||
|
||||
const tpTxSig = await driftClient.placeOrder(takeProfitParams)
|
||||
takeProfitOrderId = tpTxSig
|
||||
console.log('🎯 Take profit order placed:', tpTxSig)
|
||||
} catch (tpError) {
|
||||
console.warn('⚠️ Take profit order failed:', tpError.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate liquidation price
|
||||
const liquidationPrice = direction === PositionDirection.LONG
|
||||
? currentPrice * (1 - 0.9 / leverage)
|
||||
: currentPrice * (1 + 0.9 / leverage)
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
trade: {
|
||||
txId: txSig,
|
||||
orderId: `drift_${Date.now()}`,
|
||||
symbol: symbol.toUpperCase(),
|
||||
side: direction === PositionDirection.LONG ? 'LONG' : 'SHORT',
|
||||
positionSize: amount,
|
||||
leverage: leverage,
|
||||
leveragedAmount: amount * leverage,
|
||||
entryPrice: currentPrice,
|
||||
liquidationPrice: liquidationPrice,
|
||||
entryFee: (amount * leverage) * 0.001,
|
||||
timestamp: Date.now(),
|
||||
status: 'PENDING',
|
||||
platform: 'Drift Protocol',
|
||||
dex: 'DRIFT_REAL',
|
||||
stopLoss: stopLoss,
|
||||
takeProfit: takeProfit,
|
||||
stopLossOrderId: stopLossOrderId,
|
||||
takeProfitOrderId: takeProfitOrderId,
|
||||
monitoring: !!(stopLoss || takeProfit),
|
||||
pnl: 0
|
||||
},
|
||||
message: `${direction === PositionDirection.LONG ? 'LONG' : 'SHORT'} perpetual position opened: $${amount} at ${leverage}x leverage`,
|
||||
warnings: [
|
||||
`⚠️ Liquidation risk at $${liquidationPrice.toFixed(4)}`,
|
||||
'📊 Position requires active monitoring',
|
||||
'💰 Real funds at risk'
|
||||
]
|
||||
}
|
||||
|
||||
return NextResponse.json(result)
|
||||
|
||||
} finally {
|
||||
// Clean up
|
||||
try {
|
||||
await driftClient.unsubscribe()
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Cleanup warning:', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Drift perpetual trade execution error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
message: `Failed to execute Drift perpetual trade: ${error.message}`,
|
||||
details: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
message: 'Drift Protocol Perpetuals Trading API',
|
||||
endpoints: {
|
||||
'POST /api/trading/execute-drift': 'Execute real perpetual trades via Drift Protocol',
|
||||
},
|
||||
status: 'Active',
|
||||
features: [
|
||||
'Real leveraged perpetual trading (1x-10x)',
|
||||
'Long/Short positions with liquidation risk',
|
||||
'Stop Loss & Take Profit orders',
|
||||
'Real-time position tracking',
|
||||
'Automatic margin management'
|
||||
],
|
||||
requirements: [
|
||||
'SOLANA_PRIVATE_KEY environment variable',
|
||||
'Sufficient USDC collateral in Drift account',
|
||||
'Active Drift user account'
|
||||
],
|
||||
note: 'This API executes real trades with real money and liquidation risk. Use with caution.'
|
||||
})
|
||||
}
|
||||
230
app/api/trading/execute-leverage/route.js
Normal file
230
app/api/trading/execute-leverage/route.js
Normal file
@@ -0,0 +1,230 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { jupiterDEXService } from '@/lib/jupiter-dex-service'
|
||||
import { jupiterTriggerService } from '@/lib/jupiter-trigger-service'
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
symbol,
|
||||
side,
|
||||
amount,
|
||||
leverage = 1,
|
||||
stopLoss,
|
||||
takeProfit,
|
||||
useRealDEX = false
|
||||
} = body
|
||||
|
||||
console.log('🚀 Jupiter Leveraged Spot Trade request:', {
|
||||
symbol,
|
||||
side,
|
||||
amount,
|
||||
leverage,
|
||||
stopLoss,
|
||||
takeProfit,
|
||||
useRealDEX
|
||||
})
|
||||
|
||||
// Validate inputs
|
||||
if (!symbol || !side || !amount) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Missing required fields: symbol, side, amount'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!['BUY', 'SELL'].includes(side.toUpperCase())) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid side. Must be BUY or SELL'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Amount must be greater than 0'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (leverage < 1 || leverage > 10) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Leverage must be between 1x and 10x'
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!useRealDEX) {
|
||||
// Simulation mode
|
||||
console.log('🎮 Executing SIMULATED leveraged spot trade')
|
||||
|
||||
const currentPrice = symbol === 'SOL' ? 166.75 : symbol === 'BTC' ? 121819 : 3041.66
|
||||
const leveragedAmount = amount * leverage
|
||||
const estimatedTokens = side === 'BUY' ? leveragedAmount / currentPrice : leveragedAmount
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
trade: {
|
||||
txId: `jupiter_leverage_sim_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`,
|
||||
symbol: symbol.toUpperCase(),
|
||||
side: side.toUpperCase(),
|
||||
amount: leveragedAmount,
|
||||
leverage: leverage,
|
||||
originalAmount: amount,
|
||||
estimatedTokens: estimatedTokens,
|
||||
entryPrice: currentPrice,
|
||||
timestamp: Date.now(),
|
||||
status: 'FILLED',
|
||||
platform: 'Jupiter DEX (Leveraged Spot)',
|
||||
stopLoss: stopLoss,
|
||||
takeProfit: takeProfit,
|
||||
triggerOrders: stopLoss || takeProfit ? 'PENDING' : 'NONE'
|
||||
},
|
||||
message: `${side.toUpperCase()} ${leveragedAmount} USD worth of ${symbol} (${leverage}x leveraged spot trade) - SIMULATED`
|
||||
})
|
||||
}
|
||||
|
||||
// Real trading with Jupiter DEX + Trigger Orders
|
||||
console.log('💰 Executing REAL leveraged spot trade via Jupiter DEX + Trigger Orders')
|
||||
|
||||
// Step 1: Execute the main trade with leveraged amount
|
||||
const leveragedAmount = amount * leverage
|
||||
const tradingPair = symbol === 'SOL' ? (side === 'BUY' ? 'USDC/SOL' : 'SOL/USDC') : 'SOL/USDC'
|
||||
|
||||
const tradeResult = await jupiterDEXService.executeTrade({
|
||||
symbol,
|
||||
side,
|
||||
amount: leveragedAmount,
|
||||
tradingPair,
|
||||
quickSwap: false
|
||||
})
|
||||
|
||||
if (!tradeResult.success) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `Main trade failed: ${tradeResult.error}`
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
console.log('✅ Main leveraged trade executed:', tradeResult.txId)
|
||||
|
||||
// Step 2: Calculate position size for trigger orders
|
||||
const currentPrice = symbol === 'SOL' ? 166.75 : 3041.66 // Get from price API in production
|
||||
const tokenAmount = side === 'BUY'
|
||||
? leveragedAmount / currentPrice // If buying SOL, calculate SOL amount
|
||||
: leveragedAmount // If selling, amount is already in the token
|
||||
|
||||
// Step 3: Create trigger orders for stop loss and take profit
|
||||
let triggerResults = null
|
||||
if (stopLoss || takeProfit) {
|
||||
console.log('📋 Creating trigger orders for TP/SL...')
|
||||
|
||||
triggerResults = await jupiterTriggerService.createTradingOrders({
|
||||
tokenSymbol: symbol,
|
||||
amount: tokenAmount,
|
||||
stopLoss: stopLoss,
|
||||
takeProfit: takeProfit,
|
||||
slippageBps: 50, // 0.5% slippage for trigger orders
|
||||
expiredAt: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30 days expiry
|
||||
})
|
||||
|
||||
if (triggerResults.success) {
|
||||
console.log('✅ Trigger orders created:', {
|
||||
stopLoss: triggerResults.stopLossOrder,
|
||||
takeProfit: triggerResults.takeProfitOrder
|
||||
})
|
||||
} else {
|
||||
console.warn('⚠️ Trigger orders failed:', triggerResults.error)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Return comprehensive result
|
||||
const result = {
|
||||
success: true,
|
||||
trade: {
|
||||
txId: tradeResult.txId,
|
||||
orderId: tradeResult.orderId,
|
||||
symbol: symbol.toUpperCase(),
|
||||
side: side.toUpperCase(),
|
||||
amount: leveragedAmount,
|
||||
leverage: leverage,
|
||||
originalAmount: amount,
|
||||
tokenAmount: tokenAmount,
|
||||
entryPrice: currentPrice,
|
||||
timestamp: Date.now(),
|
||||
status: 'FILLED',
|
||||
platform: 'Jupiter DEX (Leveraged Spot)',
|
||||
dex: 'JUPITER_DEX_REAL',
|
||||
stopLoss: stopLoss,
|
||||
takeProfit: takeProfit
|
||||
},
|
||||
triggerOrders: triggerResults ? {
|
||||
stopLossOrderId: triggerResults.stopLossOrder,
|
||||
takeProfitOrderId: triggerResults.takeProfitOrder,
|
||||
status: triggerResults.success ? 'CREATED' : 'FAILED',
|
||||
error: triggerResults.error
|
||||
} : null,
|
||||
message: `${side.toUpperCase()} $${leveragedAmount} worth of ${symbol} executed successfully`,
|
||||
explanation: [
|
||||
`🔥 Leveraged Spot Trade: Used ${leverage}x leverage to trade $${leveragedAmount} instead of $${amount}`,
|
||||
`💰 Main Trade: ${side === 'BUY' ? 'Bought' : 'Sold'} ~${tokenAmount.toFixed(6)} ${symbol} via Jupiter DEX`,
|
||||
stopLoss ? `🛑 Stop Loss: Trigger order created at $${stopLoss}` : null,
|
||||
takeProfit ? `🎯 Take Profit: Trigger order created at $${takeProfit}` : null,
|
||||
`📈 This gives you ${leverage}x exposure to ${symbol} price movements using spot trading`
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
return NextResponse.json(result)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Leveraged spot trade execution error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to execute leveraged spot trade. Please try again.'
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
message: 'Jupiter Leveraged Spot Trading API',
|
||||
description: 'Leveraged trading using Jupiter DEX spot swaps + Trigger Orders for TP/SL',
|
||||
endpoints: {
|
||||
'POST /api/trading/execute-leverage': 'Execute leveraged spot trades with trigger orders',
|
||||
},
|
||||
features: [
|
||||
'Leveraged spot trading (1x-10x)',
|
||||
'Direct wallet trading (no deposits needed)',
|
||||
'Jupiter Trigger Orders for Stop Loss & Take Profit',
|
||||
'Real-time execution via Jupiter DEX',
|
||||
'Automatic position monitoring'
|
||||
],
|
||||
advantages: [
|
||||
'✅ No fund deposits required (unlike Drift)',
|
||||
'✅ Real leverage effect through increased position size',
|
||||
'✅ Professional stop loss & take profit via Jupiter Triggers',
|
||||
'✅ Best execution through Jupiter routing',
|
||||
'✅ Low fees (0.03% for stables, 0.1% others)'
|
||||
],
|
||||
note: 'Uses Jupiter DEX for main trades and Jupiter Trigger API for stop loss/take profit orders.'
|
||||
})
|
||||
}
|
||||
@@ -106,6 +106,11 @@ export async function POST(request) {
|
||||
const body = await request.json()
|
||||
const { action, orderId, ...orderData } = body
|
||||
|
||||
// For Docker environment, use internal port 3000. For dev, use the host header
|
||||
const host = request.headers.get('host') || 'localhost:3000'
|
||||
const isDocker = process.env.DOCKER_ENV === 'true'
|
||||
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
|
||||
|
||||
if (action === 'add') {
|
||||
// Load existing orders
|
||||
const pendingOrders = loadPendingOrders()
|
||||
@@ -187,7 +192,7 @@ export async function POST(request) {
|
||||
|
||||
try {
|
||||
// Execute the trade by calling the trading API
|
||||
const tradeResponse = await fetch('http://localhost:3000/api/trading', {
|
||||
const tradeResponse = await fetch(`${baseUrl}/api/trading`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -3,14 +3,19 @@ import { NextResponse } from 'next/server'
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { symbol, side, amount, price, tradingMode = 'SPOT', fromCoin, toCoin } = body
|
||||
const { symbol, side, amount, amountUSD, price, tradingMode = 'SPOT', fromCoin, toCoin } = body
|
||||
|
||||
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol}`)
|
||||
console.log(`🔍 Validating trade: ${side} ${amount} ${symbol} (USD: ${amountUSD})`)
|
||||
|
||||
// For Docker environment, use internal port 3000. For dev, use the host header
|
||||
const host = request.headers.get('host') || 'localhost:3000'
|
||||
const isDocker = process.env.DOCKER_ENV === 'true'
|
||||
const baseUrl = isDocker ? 'http://localhost:3000' : `http://${host}`
|
||||
|
||||
// Fetch real wallet balance from the wallet API
|
||||
let walletBalance
|
||||
try {
|
||||
const walletResponse = await fetch('http://localhost:3000/api/wallet/balance')
|
||||
const walletResponse = await fetch(`${baseUrl}/api/wallet/balance`)
|
||||
const walletData = await walletResponse.json()
|
||||
|
||||
if (walletData.success && walletData.wallet) {
|
||||
@@ -42,15 +47,16 @@ export async function POST(request) {
|
||||
|
||||
if (tradingMode === 'SPOT') {
|
||||
if (side.toUpperCase() === 'BUY') {
|
||||
// For BUY orders, need USDC or USD equivalent
|
||||
const tradePrice = price || 166.5 // Use provided price or current SOL price
|
||||
requiredBalance = amount * tradePrice
|
||||
// For BUY orders, use the USD amount directly (not amount * price)
|
||||
requiredBalance = amountUSD || (amount * (price || 166.5))
|
||||
requiredCurrency = 'USD'
|
||||
availableBalance = walletBalance.usdValue
|
||||
|
||||
console.log(`💰 BUY validation: Need $${requiredBalance} USD, Have $${availableBalance}`)
|
||||
} else {
|
||||
// For SELL orders, need the actual token
|
||||
// For SELL orders, need the actual token amount
|
||||
requiredBalance = amount
|
||||
requiredCurrency = fromCoin || symbol
|
||||
requiredCurrency = fromCoin || symbol.replace('USD', '')
|
||||
|
||||
// Find the token balance
|
||||
const tokenPosition = walletBalance.positions.find(pos =>
|
||||
@@ -59,14 +65,16 @@ export async function POST(request) {
|
||||
)
|
||||
|
||||
availableBalance = tokenPosition ? tokenPosition.amount : walletBalance.solBalance
|
||||
console.log(`💰 SELL validation: Need ${requiredBalance} ${requiredCurrency}, Have ${availableBalance}`)
|
||||
}
|
||||
} else if (tradingMode === 'PERP') {
|
||||
// For perpetuals, only need margin
|
||||
const leverage = 10 // Default leverage
|
||||
const tradePrice = price || 166.5
|
||||
requiredBalance = (amount * tradePrice) / leverage
|
||||
requiredBalance = (amountUSD || (amount * (price || 166.5))) / leverage
|
||||
requiredCurrency = 'USD'
|
||||
availableBalance = walletBalance.usdValue
|
||||
|
||||
console.log(`💰 PERP validation: Need $${requiredBalance} USD margin, Have $${availableBalance}`)
|
||||
}
|
||||
|
||||
console.log(`💰 Balance check: Need ${requiredBalance} ${requiredCurrency}, Have ${availableBalance}`)
|
||||
|
||||
Reference in New Issue
Block a user