diff --git a/app/api/trading/execute-dex/route.js b/app/api/trading/execute-dex/route.js index 4123b91..d1bc998 100644 --- a/app/api/trading/execute-dex/route.js +++ b/app/api/trading/execute-dex/route.js @@ -11,7 +11,10 @@ export async function POST(request) { takeProfit, useRealDEX = false, tradingPair, - quickSwap = false + quickSwap = false, + closePosition = false, + fromCoin, + toCoin } = body console.log('🔄 Execute DEX trade request:', { @@ -56,6 +59,47 @@ export async function POST(request) { ) } + // Validate balance before proceeding (skip for position closing) + if (!closePosition) { + console.log('🔍 Validating wallet balance before DEX trade...') + + try { + const validationResponse = await fetch('http://localhost:3000/api/trading/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + symbol, + side, + amount, + tradingMode: 'SPOT', + fromCoin, + toCoin + }) + }) + + const validationResult = await validationResponse.json() + + if (!validationResult.success) { + console.log('❌ DEX balance validation failed:', validationResult.message) + return NextResponse.json({ + success: false, + error: validationResult.error, + message: validationResult.message, + validation: validationResult + }, { status: validationResponse.status }) + } + + console.log('✅ DEX balance validation passed') + } catch (validationError) { + console.error('❌ DEX balance validation error:', validationError) + return NextResponse.json({ + success: false, + error: 'VALIDATION_FAILED', + message: 'Could not validate wallet balance for DEX trade. Please try again.' + }, { status: 500 }) + } + } + // For now, simulate the trade until Jupiter integration is fully tested if (!useRealDEX) { console.log('🎮 Executing SIMULATED trade (real DEX integration available)') @@ -147,29 +191,54 @@ export async function POST(request) { message: `${side.toUpperCase()} order executed on Jupiter DEX${stopLoss || takeProfit ? ' with TP/SL monitoring' : ''}` } - // Create position for successful trade + // Add trade to history try { - const positionResponse = await fetch('http://localhost:3000/api/trading/positions', { + await fetch('http://localhost:3000/api/trading/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add', - symbol: tradingPair || `${symbol}/USDC`, + symbol: tradingPair || `${fromCoin || symbol}/${toCoin || 'USDC'}`, side: side.toUpperCase(), amount: amount, - entryPrice: 168.1, // You'll need to get this from the actual trade execution - stopLoss: stopLoss, - takeProfit: takeProfit, + price: 168.1, // Get from actual execution + type: 'market', + status: 'executed', txId: tradeResult.txId, - leverage: 1 + dex: 'JUPITER', + notes: closePosition ? 'Position closing trade' : null }) }) - - if (positionResponse.ok) { - console.log('✅ Position created for DEX trade') - } + console.log('✅ Trade added to history') } catch (error) { - console.error('❌ Failed to create position:', error) + console.error('❌ Failed to add trade to history:', error) + } + + // Create position only if not closing an existing position + if (!closePosition) { + try { + const positionResponse = await fetch('http://localhost:3000/api/trading/positions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'add', + symbol: tradingPair || `${fromCoin || symbol}/${toCoin || 'USDC'}`, + side: side.toUpperCase(), + amount: amount, + entryPrice: 168.1, // Get from actual execution + stopLoss: stopLoss, + takeProfit: takeProfit, + txId: tradeResult.txId, + leverage: 1 + }) + }) + + if (positionResponse.ok) { + console.log('✅ Position created for DEX trade') + } + } catch (error) { + console.error('❌ Failed to create position:', error) + } } return NextResponse.json(tradeResponse) diff --git a/app/api/trading/history/route.js b/app/api/trading/history/route.js new file mode 100644 index 0000000..12b1ac7 --- /dev/null +++ b/app/api/trading/history/route.js @@ -0,0 +1,123 @@ +import { NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +// Persistent storage for trades using JSON file +const TRADES_FILE = path.join(process.cwd(), 'data', 'trades.json') + +// Ensure data directory exists +const dataDir = path.join(process.cwd(), 'data') +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }) +} + +// Helper functions for persistent storage +function loadTrades() { + try { + if (fs.existsSync(TRADES_FILE)) { + const data = fs.readFileSync(TRADES_FILE, 'utf8') + return JSON.parse(data) + } + } catch (error) { + console.error('Error loading trades:', error) + } + return [] +} + +function saveTrades(trades) { + try { + fs.writeFileSync(TRADES_FILE, JSON.stringify(trades, null, 2)) + } catch (error) { + console.error('Error saving trades:', error) + } +} + +export async function GET() { + try { + // Load trades from persistent storage + const tradesHistory = loadTrades() + + // Sort trades by timestamp (newest first) + const sortedTrades = tradesHistory.sort((a, b) => b.timestamp - a.timestamp) + + return NextResponse.json({ + success: true, + trades: sortedTrades, + totalTrades: sortedTrades.length + }) + + } catch (error) { + console.error('Error fetching trades history:', error) + return NextResponse.json({ + success: false, + error: 'Failed to fetch trades history', + trades: [] + }, { status: 500 }) + } +} + +export async function POST(request) { + try { + const body = await request.json() + const { action, ...tradeData } = body + + if (action === 'add') { + // Load existing trades + const tradesHistory = loadTrades() + + // Add new trade to history + const newTrade = { + id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, + symbol: tradeData.symbol, + side: tradeData.side, + amount: parseFloat(tradeData.amount), + price: parseFloat(tradeData.price), + type: tradeData.type || 'market', + status: tradeData.status || 'executed', + timestamp: Date.now(), + txId: tradeData.txId || null, + fee: tradeData.fee || 0, + pnl: tradeData.pnl || null, // For closing trades + dex: tradeData.dex || 'JUPITER', + notes: tradeData.notes || null + } + + tradesHistory.push(newTrade) + + // Keep only last 100 trades to prevent memory issues + if (tradesHistory.length > 100) { + tradesHistory.splice(0, tradesHistory.length - 100) + } + + // Save to persistent storage + saveTrades(tradesHistory) + + return NextResponse.json({ + success: true, + trade: newTrade, + message: `Trade added to history: ${newTrade.side} ${newTrade.amount} ${newTrade.symbol}` + }) + + } else if (action === 'clear') { + // Clear trade history + saveTrades([]) + + return NextResponse.json({ + success: true, + message: 'Trade history cleared' + }) + } + + return NextResponse.json({ + success: false, + error: 'Invalid action' + }, { status: 400 }) + + } catch (error) { + console.error('Error managing trades history:', error) + return NextResponse.json({ + success: false, + error: 'Failed to manage trades history' + }, { status: 500 }) + } +} diff --git a/app/api/trading/orders/route.js b/app/api/trading/orders/route.js new file mode 100644 index 0000000..9510e41 --- /dev/null +++ b/app/api/trading/orders/route.js @@ -0,0 +1,255 @@ +import { NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' + +// Persistent storage for pending orders using JSON file +const PENDING_ORDERS_FILE = path.join(process.cwd(), 'data', 'pending-orders.json') + +// Ensure data directory exists +const dataDir = path.join(process.cwd(), 'data') +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }) +} + +// Helper functions for persistent storage +function loadPendingOrders() { + try { + if (fs.existsSync(PENDING_ORDERS_FILE)) { + const data = fs.readFileSync(PENDING_ORDERS_FILE, 'utf8') + return JSON.parse(data) + } + } catch (error) { + console.error('Error loading pending orders:', error) + } + return [] +} + +function savePendingOrders(orders) { + try { + fs.writeFileSync(PENDING_ORDERS_FILE, JSON.stringify(orders, null, 2)) + } catch (error) { + console.error('Error saving pending orders:', error) + } +} + +// Helper function to map symbols to CoinGecko IDs +function getCoinGeckoId(symbol) { + const mapping = { + 'SOL': 'solana', + 'SOLUSD': 'solana', + 'BTC': 'bitcoin', + 'ETH': 'ethereum', + 'USDC': 'usd-coin', + 'USDT': 'tether', + 'RAY': 'raydium', + 'ORCA': 'orca' + } + return mapping[symbol.replace('USD', '')] || 'solana' +} + +export async function GET() { + try { + // Load pending orders from persistent storage + const pendingOrders = loadPendingOrders() + + // Check current prices and update order status + const updatedOrders = await Promise.all( + pendingOrders.filter(order => order.status === 'PENDING').map(async (order) => { + try { + // Get current price from CoinGecko + const priceResponse = await fetch( + `https://api.coingecko.com/api/v3/simple/price?ids=${getCoinGeckoId(order.symbol)}&vs_currencies=usd` + ) + const priceData = await priceResponse.json() + const currentPrice = priceData[getCoinGeckoId(order.symbol)]?.usd + + if (currentPrice) { + order.currentPrice = currentPrice + + // Check if limit order should be filled + const shouldFill = ( + (order.side === 'BUY' && currentPrice <= order.limitPrice) || + (order.side === 'SELL' && currentPrice >= order.limitPrice) + ) + + if (shouldFill) { + console.log(`🎯 Limit order ready to fill: ${order.side} ${order.amount} ${order.symbol} at $${order.limitPrice}`) + } + } + + return order + } catch (error) { + console.error(`Error updating order ${order.id}:`, error) + return order + } + }) + ) + + return NextResponse.json({ + success: true, + orders: updatedOrders, + totalOrders: updatedOrders.length + }) + + } catch (error) { + console.error('Error fetching pending orders:', error) + return NextResponse.json({ + success: false, + error: 'Failed to fetch pending orders', + orders: [] + }, { status: 500 }) + } +} + +export async function POST(request) { + try { + const body = await request.json() + const { action, orderId, ...orderData } = body + + if (action === 'add') { + // Load existing orders + const pendingOrders = loadPendingOrders() + + // Add new pending order + const newOrder = { + id: `order_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, + symbol: orderData.symbol, + side: orderData.side, + amount: parseFloat(orderData.amount), + limitPrice: parseFloat(orderData.limitPrice), + orderType: 'LIMIT', + status: 'PENDING', + timestamp: Date.now(), + stopLoss: orderData.stopLoss ? parseFloat(orderData.stopLoss) : null, + takeProfit: orderData.takeProfit ? parseFloat(orderData.takeProfit) : null, + tradingMode: orderData.tradingMode || 'SPOT', + fromCoin: orderData.fromCoin, + toCoin: orderData.toCoin, + expiresAt: orderData.expiresAt || null + } + + pendingOrders.push(newOrder) + savePendingOrders(pendingOrders) + + console.log(`📋 Limit order created: ${newOrder.side} ${newOrder.amount} ${newOrder.symbol} at $${newOrder.limitPrice}`) + + return NextResponse.json({ + success: true, + order: newOrder, + message: `Limit order created: ${newOrder.side} ${newOrder.amount} ${newOrder.symbol} at $${newOrder.limitPrice}` + }) + + } else if (action === 'cancel') { + // Load existing orders + const pendingOrders = loadPendingOrders() + + // Find and cancel order + const orderIndex = pendingOrders.findIndex(order => order.id === orderId) + + if (orderIndex === -1) { + return NextResponse.json({ + success: false, + error: 'Order not found' + }, { status: 404 }) + } + + const order = pendingOrders[orderIndex] + order.status = 'CANCELLED' + order.cancelledAt = Date.now() + + // Remove from pending orders or mark as cancelled + pendingOrders.splice(orderIndex, 1) + savePendingOrders(pendingOrders) + + console.log(`❌ Limit order cancelled: ${order.id}`) + + return NextResponse.json({ + success: true, + order: order, + message: `Order cancelled: ${order.side} ${order.amount} ${order.symbol}` + }) + + } else if (action === 'fill') { + // Fill a limit order (called when price target is reached) + const pendingOrders = loadPendingOrders() + + const orderIndex = pendingOrders.findIndex(order => order.id === orderId) + + if (orderIndex === -1) { + return NextResponse.json({ + success: false, + error: 'Order not found' + }, { status: 404 }) + } + + const order = pendingOrders[orderIndex] + const fillPrice = orderData.fillPrice || order.limitPrice + + try { + // Execute the trade by calling the trading API + const tradeResponse = await fetch('http://localhost:3000/api/trading', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + symbol: order.symbol, + side: order.side.toLowerCase(), + amount: order.amount, + type: 'limit', + price: fillPrice, + stopLoss: order.stopLoss, + takeProfit: order.takeProfit, + tradingMode: order.tradingMode, + fromCoin: order.fromCoin, + toCoin: order.toCoin, + limitOrderId: order.id + }) + }) + + if (tradeResponse.ok) { + const tradeData = await tradeResponse.json() + + // Update order status + order.status = 'FILLED' + order.filledAt = Date.now() + order.fillPrice = fillPrice + order.tradeId = tradeData.trade?.id + + // Remove from pending orders + pendingOrders.splice(orderIndex, 1) + savePendingOrders(pendingOrders) + + console.log(`✅ Limit order filled: ${order.side} ${order.amount} ${order.symbol} at $${fillPrice}`) + + return NextResponse.json({ + success: true, + order: order, + trade: tradeData.trade, + message: `Order filled: ${order.side} ${order.amount} ${order.symbol} at $${fillPrice}` + }) + } else { + throw new Error('Failed to execute trade for filled order') + } + + } catch (error) { + console.error('Error filling limit order:', error) + return NextResponse.json({ + success: false, + error: 'Failed to fill order', + message: error.message + }, { status: 500 }) + } + } + + return NextResponse.json({ + success: false, + error: 'Invalid action' + }, { status: 400 }) + + } catch (error) { + console.error('Error managing pending order:', error) + return NextResponse.json({ + success: false, + error: 'Failed to manage pending order' + }, { status: 500 }) + } +} diff --git a/app/api/trading/positions/route.js b/app/api/trading/positions/route.js index 77bc678..45e2774 100644 --- a/app/api/trading/positions/route.js +++ b/app/api/trading/positions/route.js @@ -1,10 +1,63 @@ import { NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' -// In-memory positions storage (in production, this would be a database) -let activePositions = [] +// Persistent storage for positions using JSON file +const POSITIONS_FILE = path.join(process.cwd(), 'data', 'positions.json') +const TRADES_FILE = path.join(process.cwd(), 'data', 'trades.json') + +// Ensure data directory exists +const dataDir = path.join(process.cwd(), 'data') +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }) +} + +// Helper functions for persistent storage +function loadPositions() { + try { + if (fs.existsSync(POSITIONS_FILE)) { + const data = fs.readFileSync(POSITIONS_FILE, 'utf8') + return JSON.parse(data) + } + } catch (error) { + console.error('Error loading positions:', error) + } + return [] +} + +function savePositions(positions) { + try { + fs.writeFileSync(POSITIONS_FILE, JSON.stringify(positions, null, 2)) + } catch (error) { + console.error('Error saving positions:', error) + } +} + +function loadTrades() { + try { + if (fs.existsSync(TRADES_FILE)) { + const data = fs.readFileSync(TRADES_FILE, 'utf8') + return JSON.parse(data) + } + } catch (error) { + console.error('Error loading trades:', error) + } + return [] +} + +function saveTrades(trades) { + try { + fs.writeFileSync(TRADES_FILE, JSON.stringify(trades, null, 2)) + } catch (error) { + console.error('Error saving trades:', error) + } +} export async function GET() { try { + // Load positions from persistent storage + const activePositions = loadPositions() + // Calculate current P&L for each position using real-time prices const updatedPositions = await Promise.all( activePositions.map(async (position) => { @@ -63,6 +116,9 @@ export async function POST(request) { const { action, positionId, ...positionData } = body if (action === 'add') { + // Load existing positions + const activePositions = loadPositions() + // Add new position const newPosition = { id: `pos_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, @@ -79,6 +135,9 @@ export async function POST(request) { } activePositions.push(newPosition) + savePositions(activePositions) + + console.log(`✅ Position created: ${newPosition.side} ${newPosition.amount} ${newPosition.symbol}`) return NextResponse.json({ success: true, @@ -87,7 +146,10 @@ export async function POST(request) { }) } else if (action === 'close') { - // Close position + // Load existing positions + const activePositions = loadPositions() + + // Close position by executing reverse trade const positionIndex = activePositions.findIndex(pos => pos.id === positionId) if (positionIndex === -1) { @@ -97,21 +159,93 @@ export async function POST(request) { }, { status: 404 }) } - const closedPosition = activePositions[positionIndex] - closedPosition.status = 'CLOSED' - closedPosition.closedAt = Date.now() - closedPosition.exitPrice = positionData.exitPrice + const position = activePositions[positionIndex] + const exitPrice = positionData.exitPrice || position.currentPrice || position.entryPrice - // Remove from active positions - activePositions.splice(positionIndex, 1) + // Calculate P&L + const priceDiff = exitPrice - position.entryPrice + const realizedPnl = position.side === 'BUY' + ? priceDiff * position.amount + : -priceDiff * position.amount - return NextResponse.json({ - success: true, - position: closedPosition, - message: `Position closed: ${closedPosition.symbol}` - }) + console.log(`🔄 Closing position ${positionId}: ${position.side} ${position.amount} ${position.symbol}`) + console.log(`💰 Entry: $${position.entryPrice}, Exit: $${exitPrice}, P&L: $${realizedPnl.toFixed(4)}`) + + try { + // Add closing trade to history + const existingTrades = loadTrades() + const closingTrade = { + id: `trade_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, + symbol: position.symbol, + side: position.side === 'BUY' ? 'SELL' : 'BUY', // Reverse side for closing + amount: position.amount, + price: exitPrice, + type: 'market', + status: 'executed', + timestamp: Date.now(), + txId: `close_${position.id}`, + fee: 0, + pnl: realizedPnl, + dex: 'SIMULATION', + notes: `Closed position ${position.id} with P&L $${realizedPnl.toFixed(4)}` + } + + existingTrades.push(closingTrade) + + // Keep only last 100 trades + if (existingTrades.length > 100) { + existingTrades.splice(0, existingTrades.length - 100) + } + + saveTrades(existingTrades) + console.log('✅ Closing trade added to history') + + console.log(`💰 Position closed with P&L: $${realizedPnl.toFixed(4)}`) + + // Update position status and remove from active + position.status = 'CLOSED' + position.closedAt = Date.now() + position.exitPrice = exitPrice + position.realizedPnl = realizedPnl + position.closeTxId = closingTrade.txId + + // Remove from active positions + activePositions.splice(positionIndex, 1) + savePositions(activePositions) + + return NextResponse.json({ + success: true, + position: position, + closingTrade: closingTrade, + realizedPnl: realizedPnl, + message: `Position closed: ${position.symbol} with P&L ${realizedPnl >= 0 ? '+' : ''}$${realizedPnl.toFixed(4)}` + }) + + } catch (error) { + console.error('Error closing position:', error) + + // Fallback: just remove position + position.status = 'CLOSED' + position.closedAt = Date.now() + position.exitPrice = exitPrice + position.realizedPnl = realizedPnl + + activePositions.splice(positionIndex, 1) + savePositions(activePositions) + + return NextResponse.json({ + success: true, + position: position, + realizedPnl: realizedPnl, + message: `Position closed (fallback): ${position.symbol}`, + warning: 'Failed to add to trade history' + }) + } } else if (action === 'update') { + // Load existing positions + const activePositions = loadPositions() + // Update position (for SL/TP changes) const positionIndex = activePositions.findIndex(pos => pos.id === positionId) @@ -126,6 +260,8 @@ export async function POST(request) { if (positionData.stopLoss !== undefined) position.stopLoss = positionData.stopLoss if (positionData.takeProfit !== undefined) position.takeProfit = positionData.takeProfit + savePositions(activePositions) + return NextResponse.json({ success: true, position, diff --git a/app/api/trading/route.ts b/app/api/trading/route.ts index 1458195..a0ebfc9 100644 --- a/app/api/trading/route.ts +++ b/app/api/trading/route.ts @@ -3,7 +3,19 @@ import { NextResponse } from 'next/server' export async function POST(request: Request) { try { const body = await request.json() - const { symbol, side, amount, type = 'market' } = body + const { + symbol, + side, + amount, + type = 'market', + price, + stopLoss, + takeProfit, + tradingMode = 'SPOT', + fromCoin, + toCoin, + limitOrderId + } = body // Validate input if (!symbol || !side || !amount) { @@ -12,20 +24,109 @@ export async function POST(request: Request) { }, { status: 400 }) } - // Mock trading execution + // Validate balance before proceeding (skip for limit order fills) + if (!limitOrderId) { + console.log('🔍 Validating wallet balance before trade execution...') + + try { + const validationResponse = await fetch('http://localhost:3000/api/trading/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + symbol, + side, + amount, + price, + tradingMode, + fromCoin, + toCoin + }) + }) + + const validationResult = await validationResponse.json() + + if (!validationResult.success) { + console.log('❌ Balance validation failed:', validationResult.message) + return NextResponse.json({ + success: false, + error: validationResult.error, + message: validationResult.message, + validation: validationResult + }, { status: validationResponse.status }) + } + + console.log('✅ Balance validation passed') + } catch (validationError) { + console.error('❌ Balance validation error:', validationError) + return NextResponse.json({ + success: false, + error: 'VALIDATION_FAILED', + message: 'Could not validate wallet balance. Please try again.' + }, { status: 500 }) + } + } + + // Handle limit orders + if (type === 'limit' && price && !limitOrderId) { + console.log(`📋 Creating limit order: ${side.toUpperCase()} ${amount} ${symbol} at $${price}`) + + // Create pending order instead of executing immediately + try { + const orderResponse = await fetch('http://localhost:3000/api/trading/orders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'add', + symbol: symbol, + side: side.toUpperCase(), + amount: amount, + limitPrice: price, + stopLoss: stopLoss, + takeProfit: takeProfit, + tradingMode: tradingMode, + fromCoin: fromCoin, + toCoin: toCoin + }) + }) + + if (orderResponse.ok) { + const orderData = await orderResponse.json() + return NextResponse.json({ + success: true, + order: orderData.order, + type: 'limit_order_created', + message: `Limit order created: ${side.toUpperCase()} ${amount} ${symbol} at $${price}` + }) + } else { + throw new Error('Failed to create limit order') + } + } catch (error) { + console.error('Failed to create limit order:', error) + return NextResponse.json({ + error: 'Failed to create limit order', + message: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }) + } + } + + // Get current market price for market orders or limit order fills + const currentPrice = type === 'market' ? (side === 'buy' ? 168.11 : 168.09) : price + + // Mock trading execution (market order or limit order fill) const mockTrade = { - id: `trade_${Date.now()}`, + id: limitOrderId ? `fill_${limitOrderId}` : `trade_${Date.now()}`, symbol, side, // 'buy' or 'sell' amount: parseFloat(amount), type, - price: side === 'buy' ? 144.11 : 144.09, // Mock prices + price: currentPrice, status: 'executed', timestamp: new Date().toISOString(), - fee: parseFloat(amount) * 0.001 // 0.1% fee + fee: parseFloat(amount) * 0.001, // 0.1% fee + limitOrderId: limitOrderId || null } - console.log('Simulated trade executed:', mockTrade) + console.log('Trade executed:', mockTrade) // Automatically create position for this trade try { @@ -34,11 +135,14 @@ export async function POST(request: Request) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add', - symbol: mockTrade.symbol, + symbol: fromCoin && toCoin ? `${fromCoin}/${toCoin}` : mockTrade.symbol, side: mockTrade.side.toUpperCase(), amount: mockTrade.amount, entryPrice: mockTrade.price, - txId: mockTrade.id + stopLoss: stopLoss, + takeProfit: takeProfit, + txId: mockTrade.id, + leverage: tradingMode === 'PERP' ? 10 : 1 }) }) diff --git a/app/api/trading/validate/route.js b/app/api/trading/validate/route.js new file mode 100644 index 0000000..124554e --- /dev/null +++ b/app/api/trading/validate/route.js @@ -0,0 +1,142 @@ +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 + + console.log(`🔍 Validating trade: ${side} ${amount} ${symbol}`) + + // For now, use hardcoded wallet balance values for validation + // In production, this would fetch from the actual wallet API + const mockWalletBalance = { + solBalance: 0.0728, // Current actual balance + usdValue: 12.12, // Current USD value + positions: [ + { symbol: 'SOL', amount: 0.0728, price: 166.5 } + ] + } + + // Determine required balance for the trade + let requiredBalance = 0 + let requiredCurrency = '' + let availableBalance = 0 + + 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 + requiredCurrency = 'USD' + availableBalance = mockWalletBalance.usdValue + } else { + // For SELL orders, need the actual token + requiredBalance = amount + requiredCurrency = fromCoin || symbol + + // Find the token balance + const tokenPosition = mockWalletBalance.positions.find(pos => + pos.symbol === requiredCurrency || + pos.symbol === symbol + ) + + availableBalance = tokenPosition ? tokenPosition.amount : 0 + } + } else if (tradingMode === 'PERP') { + // For perpetuals, only need margin + const leverage = 10 // Default leverage + const tradePrice = price || 166.5 + requiredBalance = (amount * tradePrice) / leverage + requiredCurrency = 'USD' + availableBalance = mockWalletBalance.usdValue + } + + console.log(`💰 Balance check: Need ${requiredBalance} ${requiredCurrency}, Have ${availableBalance}`) + + // Validate sufficient balance + if (requiredBalance > availableBalance) { + const shortfall = requiredBalance - availableBalance + + return NextResponse.json({ + success: false, + error: 'INSUFFICIENT_BALANCE', + message: `Insufficient balance. Need ${requiredBalance.toFixed(6)} ${requiredCurrency}, have ${availableBalance.toFixed(6)}. Shortfall: ${shortfall.toFixed(6)}`, + required: requiredBalance, + available: availableBalance, + shortfall: shortfall, + currency: requiredCurrency + }, { status: 400 }) + } + + // Validate minimum trade size + const minTradeUsd = 1.0 // Minimum $1 trade + const tradeValueUsd = side.toUpperCase() === 'BUY' + ? requiredBalance + : amount * (price || 166.5) + + if (tradeValueUsd < minTradeUsd) { + return NextResponse.json({ + success: false, + error: 'TRADE_TOO_SMALL', + message: `Trade value too small. Minimum trade: $${minTradeUsd}, your trade: $${tradeValueUsd.toFixed(2)}`, + minTradeUsd: minTradeUsd, + tradeValueUsd: tradeValueUsd + }, { status: 400 }) + } + + // Validate maximum trade size (safety check) + const maxTradePercent = 0.95 // Max 95% of balance per trade + const maxAllowedTrade = availableBalance * maxTradePercent + + if (requiredBalance > maxAllowedTrade) { + return NextResponse.json({ + success: false, + error: 'TRADE_TOO_LARGE', + message: `Trade too large. Maximum allowed: ${maxAllowedTrade.toFixed(6)} ${requiredCurrency} (95% of balance)`, + maxAllowed: maxAllowedTrade, + requested: requiredBalance, + currency: requiredCurrency + }, { status: 400 }) + } + + // If we get here, the trade is valid + return NextResponse.json({ + success: true, + validation: { + requiredBalance: requiredBalance, + availableBalance: availableBalance, + currency: requiredCurrency, + tradeValueUsd: tradeValueUsd, + valid: true + }, + message: `Trade validation passed: ${side} ${amount} ${symbol}` + }) + + } catch (error) { + console.error('❌ Balance validation error:', error) + return NextResponse.json({ + success: false, + error: 'VALIDATION_ERROR', + message: 'Failed to validate trade balance: ' + error.message + }, { status: 500 }) + } +} + +export async function GET() { + return NextResponse.json({ + message: 'Trade Balance Validation API', + description: 'Validates if wallet has sufficient balance for proposed trades', + endpoints: { + 'POST /api/trading/validate': 'Validate trade against wallet balance' + }, + parameters: { + symbol: 'Trading symbol (SOL, BTC, etc.)', + side: 'BUY or SELL', + amount: 'Trade amount', + price: 'Trade price (optional, uses current market price)', + tradingMode: 'SPOT or PERP', + fromCoin: 'Source currency', + toCoin: 'Target currency' + } + }) +} diff --git a/app/api/wallet/balance/route.js b/app/api/wallet/balance/route.js index 868cef8..d2c4f4b 100644 --- a/app/api/wallet/balance/route.js +++ b/app/api/wallet/balance/route.js @@ -25,19 +25,31 @@ export async function GET() { const balance = await connection.getBalance(keypair.publicKey) const solBalance = balance / 1000000000 // Convert lamports to SOL - // Get current SOL price - const priceResponse = await fetch( - 'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true' - ) + // Get current SOL price with fallback + let solPrice = 168.11 // Fallback price from our current market data + let change24h = 0 - if (!priceResponse.ok) { - throw new Error('Failed to fetch SOL price') + try { + const priceResponse = await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true' + ) + + if (priceResponse.ok) { + const priceData = await priceResponse.json() + if (priceData.solana?.usd) { + solPrice = priceData.solana.usd + change24h = priceData.solana.usd_24h_change || 0 + console.log(`💰 Using live SOL price: $${solPrice}`) + } else { + console.log(`⚠️ Using fallback SOL price: $${solPrice} (CoinGecko data invalid)`) + } + } else { + console.log(`⚠️ Using fallback SOL price: $${solPrice} (CoinGecko rate limited)`) + } + } catch (priceError) { + console.log(`⚠️ Using fallback SOL price: $${solPrice} (CoinGecko error: ${priceError.message})`) } - const priceData = await priceResponse.json() - const solPrice = priceData.solana?.usd || 0 - const change24h = priceData.solana?.usd_24h_change || 0 - const usdValue = solBalance * solPrice console.log(`💎 Real wallet: ${solBalance.toFixed(4)} SOL ($${usdValue.toFixed(2)})`) @@ -46,15 +58,17 @@ export async function GET() { success: true, balance: { totalValue: usdValue, - availableBalance: usdValue, // All SOL is available for trading - positions: [{ - symbol: 'SOL', - price: solPrice, - change24h: change24h, - volume24h: 0, // Not applicable for wallet balance - amount: solBalance, - usdValue: usdValue - }] + availableBalance: usdValue, + positions: [ + { + symbol: 'SOL', + price: solPrice, + change24h: change24h, + volume24h: 0, + amount: solBalance, + usdValue: usdValue + } + ] }, wallet: { publicKey: keypair.publicKey.toString(), diff --git a/app/trading/page.js b/app/trading/page.js index 922d008..d0f9155 100644 --- a/app/trading/page.js +++ b/app/trading/page.js @@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react' import TradeExecutionPanel from '../../components/TradeExecutionPanel.js' import PositionsPanel from '../../components/PositionsPanel.js' +import PendingOrdersPanel from '../../components/PendingOrdersPanel.js' +import TradesHistoryPanel from '../../components/TradesHistoryPanel.js' export default function TradingPage() { const [selectedSymbol, setSelectedSymbol] = useState('SOL') @@ -85,6 +87,9 @@ export default function TradingPage() { {/* Open Positions */} + {/* Pending Orders */} + + {/* Portfolio Overview */}

Wallet Overview

@@ -129,6 +134,9 @@ export default function TradingPage() {
)} + + {/* Recent Trades */} + diff --git a/components/PendingOrdersPanel.js b/components/PendingOrdersPanel.js new file mode 100644 index 0000000..55ac821 --- /dev/null +++ b/components/PendingOrdersPanel.js @@ -0,0 +1,246 @@ +'use client' +import React, { useState, useEffect } from 'react' + +export default function PendingOrdersPanel() { + const [pendingOrders, setPendingOrders] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchPendingOrders() + // Refresh orders every 5 seconds to check for fills + const interval = setInterval(fetchPendingOrders, 5000) + return () => clearInterval(interval) + }, []) + + const fetchPendingOrders = async () => { + try { + const response = await fetch('/api/trading/orders') + const data = await response.json() + + if (data.success) { + setPendingOrders(data.orders || []) + + // Check if any orders are ready to fill + const ordersToFill = data.orders.filter(order => { + if (order.status !== 'PENDING' || !order.currentPrice) return false + + return ( + (order.side === 'BUY' && order.currentPrice <= order.limitPrice) || + (order.side === 'SELL' && order.currentPrice >= order.limitPrice) + ) + }) + + // Auto-fill orders that have reached their target price + for (const order of ordersToFill) { + try { + await fetch('/api/trading/orders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'fill', + orderId: order.id, + fillPrice: order.currentPrice + }) + }) + console.log(`🎯 Auto-filled limit order: ${order.id}`) + } catch (error) { + console.error('Failed to auto-fill order:', error) + } + } + } + } catch (error) { + console.error('Failed to fetch pending orders:', error) + } finally { + setLoading(false) + } + } + + const cancelOrder = async (orderId) => { + try { + const response = await fetch('/api/trading/orders', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'cancel', + orderId: orderId + }) + }) + + if (response.ok) { + fetchPendingOrders() // Refresh orders + } + } catch (error) { + console.error('Failed to cancel order:', error) + } + } + + const getOrderStatus = (order) => { + if (!order.currentPrice) return 'Monitoring...' + + const distance = Math.abs(order.currentPrice - order.limitPrice) + const percentageAway = (distance / order.limitPrice) * 100 + + if (order.side === 'BUY') { + if (order.currentPrice <= order.limitPrice) { + return { text: 'Ready to Fill!', color: 'text-green-400', urgent: true } + } else { + return { + text: `$${(order.currentPrice - order.limitPrice).toFixed(4)} above target`, + color: 'text-yellow-400', + urgent: false + } + } + } else { + if (order.currentPrice >= order.limitPrice) { + return { text: 'Ready to Fill!', color: 'text-green-400', urgent: true } + } else { + return { + text: `$${(order.limitPrice - order.currentPrice).toFixed(4)} below target`, + color: 'text-yellow-400', + urgent: false + } + } + } + } + + const formatCurrency = (amount) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 4 + }).format(amount) + } + + if (loading) { + return ( +
+

Pending Orders

+
Loading orders...
+
+ ) + } + + return ( +
+
+

Pending Orders

+
+ + {pendingOrders.length} order{pendingOrders.length !== 1 ? 's' : ''} + + +
+
+ + {pendingOrders.length === 0 ? ( +
+
📋 No pending orders
+
Limit orders will appear here when created
+
+ ) : ( +
+ {pendingOrders.map((order) => { + const status = getOrderStatus(order) + + return ( +
+
+
+ + {order.symbol} + + + {order.side} LIMIT + + {order.tradingMode === 'PERP' && ( + + PERP + + )} +
+
+ {status.urgent && ( + + 🎯 READY + + )} + +
+
+ +
+
+
Amount
+
{order.amount}
+
+
+
Limit Price
+
{formatCurrency(order.limitPrice)}
+
+
+
Current Price
+
+ {order.currentPrice ? formatCurrency(order.currentPrice) : 'Loading...'} +
+
+
+
Status
+
+ {status.text} +
+
+
+ + {/* Stop Loss / Take Profit */} + {(order.stopLoss || order.takeProfit) && ( +
+
+ {order.stopLoss && ( +
+ 🛑 SL: {formatCurrency(order.stopLoss)} +
+ )} + {order.takeProfit && ( +
+ 🎯 TP: {formatCurrency(order.takeProfit)} +
+ )} +
+
+ )} + + {/* Order Info */} +
+ Created: {new Date(order.timestamp).toLocaleString()} + {order.expiresAt && ( + • Expires: {new Date(order.expiresAt).toLocaleString()} + )} +
+
+ ) + })} +
+ )} +
+ ) +} diff --git a/components/TradeExecutionPanel.js b/components/TradeExecutionPanel.js index 0978f40..b835215 100644 --- a/components/TradeExecutionPanel.js +++ b/components/TradeExecutionPanel.js @@ -127,11 +127,14 @@ export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) { tradeData.perpCoin = perpCoin } - // Determine API endpoint based on trading mode + // Determine API endpoint based on trading mode and order type let apiEndpoint = '/api/trading/execute-dex' if (tradingMode === 'PERP') { apiEndpoint = '/api/trading/execute-perp' + } else if (tradePrice) { + // Limit orders go through the main trading API + apiEndpoint = '/api/trading' } const response = await fetch(apiEndpoint, { @@ -145,11 +148,21 @@ export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) { const result = await response.json() if (result.success) { - setExecutionResult({ - success: true, - trade: result.trade, - message: result.message - }) + // Check if this was a limit order creation + if (result.type === 'limit_order_created') { + setExecutionResult({ + success: true, + order: result.order, + type: 'limit_order', + message: result.message + }) + } else { + setExecutionResult({ + success: true, + trade: result.trade, + message: result.message + }) + } // Refresh balance after successful trade await fetchBalance() } else { @@ -547,7 +560,9 @@ export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) { executionResult.success ? 'bg-green-900 border border-green-600' : 'bg-red-900 border border-red-600' }`}>
- {executionResult.success ? '✅ Trade Executed' : '❌ Trade Failed'} + {executionResult.success ? ( + executionResult.type === 'limit_order' ? '📋 Limit Order Created' : '✅ Trade Executed' + ) : '❌ Trade Failed'}
{executionResult.message} @@ -557,6 +572,15 @@ export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) { TX ID: {executionResult.trade.txId}
)} + {executionResult.order && ( +
+ Order ID: {executionResult.order.id} +
+ Limit Price: ${executionResult.order.limitPrice} +
+ Status: {executionResult.order.status} +
+ )} )} diff --git a/components/TradesHistoryPanel.js b/components/TradesHistoryPanel.js new file mode 100644 index 0000000..2701531 --- /dev/null +++ b/components/TradesHistoryPanel.js @@ -0,0 +1,175 @@ +'use client' +import React, { useState, useEffect } from 'react' + +export default function TradesHistoryPanel() { + const [trades, setTrades] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchTrades() + // Refresh trades every 15 seconds + const interval = setInterval(fetchTrades, 15000) + return () => clearInterval(interval) + }, []) + + const fetchTrades = async () => { + try { + const response = await fetch('/api/trading/history') + const data = await response.json() + + if (data.success) { + setTrades(data.trades || []) + } + } catch (error) { + console.error('Failed to fetch trades:', error) + } finally { + setLoading(false) + } + } + + const clearHistory = async () => { + try { + const response = await fetch('/api/trading/history', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'clear' }) + }) + + if (response.ok) { + setTrades([]) + } + } catch (error) { + console.error('Failed to clear history:', error) + } + } + + const getSideColor = (side) => { + return side === 'BUY' ? 'text-green-400' : 'text-red-400' + } + + const getPnlColor = (pnl) => { + if (pnl === null || pnl === 0) return 'text-gray-400' + return pnl > 0 ? 'text-green-400' : 'text-red-400' + } + + const formatCurrency = (amount) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 4 + }).format(amount) + } + + const formatTime = (timestamp) => { + return new Date(timestamp).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + } + + if (loading) { + return ( +
+

Recent Trades

+
Loading trades...
+
+ ) + } + + return ( +
+
+

Recent Trades

+
+ + {trades.length > 0 && ( + + )} +
+
+ + {trades.length === 0 ? ( +
+
📈 No trades yet
+
Execute a trade to see history here
+
+ ) : ( +
+ {trades.map((trade) => ( +
+
+
+ {trade.symbol} + + {trade.side} + + + {trade.dex} + +
+
+ {formatTime(trade.timestamp)} +
+
+ +
+
+
Amount
+
{trade.amount}
+
+
+
Price
+
{formatCurrency(trade.price)}
+
+
+
Status
+
{trade.status}
+
+ {trade.pnl !== null && trade.pnl !== undefined && ( +
+
P&L
+
+ {trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)} +
+
+ )} +
+ + {/* Additional Info */} +
+
+ TX: {trade.txId?.substring(0, 12)}... + {trade.notes && ( + • {trade.notes} + )} +
+
+ Fee: ${trade.fee?.toFixed(4) || '0.0000'} +
+
+
+ ))} +
+ )} +
+ ) +} diff --git a/data/positions.json b/data/positions.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/positions.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/data/trades.json b/data/trades.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/trades.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/lib/wallet-balance-tracker.js b/lib/wallet-balance-tracker.js new file mode 100644 index 0000000..86252fc --- /dev/null +++ b/lib/wallet-balance-tracker.js @@ -0,0 +1,86 @@ +import fs from 'fs' +import path from 'path' + +// Persistent storage for wallet balance changes +const WALLET_TRANSACTIONS_FILE = path.join(process.cwd(), 'data', 'wallet-transactions.json') + +// Ensure data directory exists +const dataDir = path.join(process.cwd(), 'data') +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }) +} + +// Helper functions for wallet transaction tracking +function loadWalletTransactions() { + try { + if (fs.existsSync(WALLET_TRANSACTIONS_FILE)) { + const data = fs.readFileSync(WALLET_TRANSACTIONS_FILE, 'utf8') + return JSON.parse(data) + } + } catch (error) { + console.error('Error loading wallet transactions:', error) + } + return [] +} + +function saveWalletTransactions(transactions) { + try { + fs.writeFileSync(WALLET_TRANSACTIONS_FILE, JSON.stringify(transactions, null, 2)) + } catch (error) { + console.error('Error saving wallet transactions:', error) + } +} + +export function addWalletTransaction(transaction) { + const transactions = loadWalletTransactions() + + const newTransaction = { + id: `wtx_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`, + type: transaction.type, // 'trade_open', 'trade_close', 'swap', etc. + symbol: transaction.symbol, + amount: transaction.amount, // Positive = received, Negative = sent + usdValue: transaction.usdValue, + timestamp: Date.now(), + txId: transaction.txId, + positionId: transaction.positionId || null, + notes: transaction.notes || null + } + + transactions.push(newTransaction) + + // Keep only last 500 transactions + if (transactions.length > 500) { + transactions.splice(0, transactions.length - 500) + } + + saveWalletTransactions(transactions) + + console.log('💼 Wallet transaction recorded:', newTransaction) + return newTransaction +} + +export function getWalletTransactions() { + return loadWalletTransactions() +} + +export function getWalletBalanceAdjustment() { + const transactions = loadWalletTransactions() + + // Calculate total adjustments by symbol + const adjustments = {} + + transactions.forEach(tx => { + if (!adjustments[tx.symbol]) { + adjustments[tx.symbol] = { amount: 0, usdValue: 0 } + } + adjustments[tx.symbol].amount += tx.amount + adjustments[tx.symbol].usdValue += tx.usdValue + }) + + return adjustments +} + +export function clearWalletTransactions() { + saveWalletTransactions([]) + console.log('💼 Wallet transactions cleared') +}