Fix critical balance validation and add comprehensive trading features
- Fixed CoinGecko API rate limiting with fallback SOL price (68.11) - Corrected internal API calls to use proper Docker container ports - Fixed balance validation to prevent trades exceeding wallet funds - Blocked 0.5 SOL trades with only 0.073 SOL available (~2.24) - Added persistent storage for positions, trades, and pending orders - Implemented limit order system with auto-fill monitoring - Created pending orders panel and management API - Added trades history tracking and display panel - Enhanced position tracking with P&L calculations - Added wallet balance validation API endpoint - Positions stored in data/positions.json - Trade history stored in data/trades.json - Pending orders with auto-fill logic - Real-time balance validation before trades - All trades now validate against actual wallet balance - Insufficient balance trades are properly blocked - Added comprehensive error handling and logging - Fixed Docker networking for internal API calls - SPOT and leveraged trading modes - Limit orders with price monitoring - Stop loss and take profit support - DEX integration with Jupiter - Real-time position updates and P&L tracking Tested and verified all balance validation works correctly
This commit is contained in:
246
components/PendingOrdersPanel.js
Normal file
246
components/PendingOrdersPanel.js
Normal file
@@ -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 (
|
||||
<div className="card card-gradient p-6">
|
||||
<h2 className="text-xl font-bold text-white mb-4">Pending Orders</h2>
|
||||
<div className="text-gray-400">Loading orders...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-gradient p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-white">Pending Orders</h2>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-sm text-gray-400">
|
||||
{pendingOrders.length} order{pendingOrders.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<button
|
||||
onClick={fetchPendingOrders}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pendingOrders.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-gray-400 mb-2">📋 No pending orders</div>
|
||||
<div className="text-sm text-gray-500">Limit orders will appear here when created</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{pendingOrders.map((order) => {
|
||||
const status = getOrderStatus(order)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={order.id}
|
||||
className={`bg-gray-800 rounded-lg p-4 border ${
|
||||
status.urgent ? 'border-green-500 animate-pulse' : 'border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-lg font-bold text-white">
|
||||
{order.symbol}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
order.side === 'BUY'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-red-600 text-white'
|
||||
}`}>
|
||||
{order.side} LIMIT
|
||||
</span>
|
||||
{order.tradingMode === 'PERP' && (
|
||||
<span className="px-2 py-1 rounded text-xs bg-purple-600 text-white">
|
||||
PERP
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{status.urgent && (
|
||||
<span className="text-green-400 text-xs font-medium animate-pulse">
|
||||
🎯 READY
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => cancelOrder(order.id)}
|
||||
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white text-xs rounded transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm mb-3">
|
||||
<div>
|
||||
<div className="text-gray-400">Amount</div>
|
||||
<div className="text-white font-medium">{order.amount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400">Limit Price</div>
|
||||
<div className="text-white font-medium">{formatCurrency(order.limitPrice)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400">Current Price</div>
|
||||
<div className="text-white font-medium">
|
||||
{order.currentPrice ? formatCurrency(order.currentPrice) : 'Loading...'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400">Status</div>
|
||||
<div className={`font-medium ${status.color}`}>
|
||||
{status.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stop Loss / Take Profit */}
|
||||
{(order.stopLoss || order.takeProfit) && (
|
||||
<div className="pt-3 border-t border-gray-600">
|
||||
<div className="flex space-x-4 text-xs">
|
||||
{order.stopLoss && (
|
||||
<div className="text-red-400">
|
||||
🛑 SL: {formatCurrency(order.stopLoss)}
|
||||
</div>
|
||||
)}
|
||||
{order.takeProfit && (
|
||||
<div className="text-green-400">
|
||||
🎯 TP: {formatCurrency(order.takeProfit)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Order Info */}
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Created: {new Date(order.timestamp).toLocaleString()}
|
||||
{order.expiresAt && (
|
||||
<span className="ml-2">• Expires: {new Date(order.expiresAt).toLocaleString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
}`}>
|
||||
<div className={`font-bold ${executionResult.success ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{executionResult.success ? '✅ Trade Executed' : '❌ Trade Failed'}
|
||||
{executionResult.success ? (
|
||||
executionResult.type === 'limit_order' ? '📋 Limit Order Created' : '✅ Trade Executed'
|
||||
) : '❌ Trade Failed'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-300 mt-1">
|
||||
{executionResult.message}
|
||||
@@ -557,6 +572,15 @@ export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) {
|
||||
TX ID: {executionResult.trade.txId}
|
||||
</div>
|
||||
)}
|
||||
{executionResult.order && (
|
||||
<div className="text-xs text-gray-400 mt-2">
|
||||
Order ID: {executionResult.order.id}
|
||||
<br />
|
||||
Limit Price: ${executionResult.order.limitPrice}
|
||||
<br />
|
||||
Status: {executionResult.order.status}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
175
components/TradesHistoryPanel.js
Normal file
175
components/TradesHistoryPanel.js
Normal file
@@ -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 (
|
||||
<div className="card card-gradient p-6">
|
||||
<h2 className="text-xl font-bold text-white mb-4">Recent Trades</h2>
|
||||
<div className="text-gray-400">Loading trades...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-gradient p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-white">Recent Trades</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={fetchTrades}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm"
|
||||
>
|
||||
🔄 Refresh
|
||||
</button>
|
||||
{trades.length > 0 && (
|
||||
<button
|
||||
onClick={clearHistory}
|
||||
className="text-red-400 hover:text-red-300 text-sm"
|
||||
>
|
||||
🗑️ Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{trades.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-gray-400 mb-2">📈 No trades yet</div>
|
||||
<div className="text-sm text-gray-500">Execute a trade to see history here</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{trades.map((trade) => (
|
||||
<div
|
||||
key={trade.id}
|
||||
className="bg-gray-800 rounded-lg p-4 border border-gray-600"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-white font-medium">{trade.symbol}</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
trade.side === 'BUY'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-red-600 text-white'
|
||||
}`}>
|
||||
{trade.side}
|
||||
</span>
|
||||
<span className="px-2 py-1 rounded text-xs bg-blue-600 text-white">
|
||||
{trade.dex}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{formatTime(trade.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400">Amount</div>
|
||||
<div className="text-white font-medium">{trade.amount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400">Price</div>
|
||||
<div className="text-white font-medium">{formatCurrency(trade.price)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400">Status</div>
|
||||
<div className="text-green-400 font-medium">{trade.status}</div>
|
||||
</div>
|
||||
{trade.pnl !== null && trade.pnl !== undefined && (
|
||||
<div>
|
||||
<div className="text-gray-400">P&L</div>
|
||||
<div className={`font-medium ${getPnlColor(trade.pnl)}`}>
|
||||
{trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Additional Info */}
|
||||
<div className="mt-3 flex justify-between items-center text-xs">
|
||||
<div className="text-gray-500">
|
||||
TX: {trade.txId?.substring(0, 12)}...
|
||||
{trade.notes && (
|
||||
<span className="ml-2 text-yellow-400">• {trade.notes}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
Fee: ${trade.fee?.toFixed(4) || '0.0000'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user