Files
trading_bot_v3/components/PendingOrdersPanel.js
mindesbunister b0b63d5db0 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
2025-07-14 17:19:58 +02:00

247 lines
8.5 KiB
JavaScript

'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>
)
}