Files
trading_bot_v3/components/TradeExecutionPanel.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

594 lines
21 KiB
JavaScript

'use client'
import React, { useState, useEffect } from 'react'
export default function TradeExecutionPanel({ analysis, symbol = 'SOL' }) {
const [tradeType, setTradeType] = useState('BUY')
const [amount, setAmount] = useState('')
const [customPrice, setCustomPrice] = useState('')
const [useRecommendedPrice, setUseRecommendedPrice] = useState(true)
const [isExecuting, setIsExecuting] = useState(false)
const [executionResult, setExecutionResult] = useState(null)
const [balance, setBalance] = useState(null)
// Trading mode and coin selection
const [tradingMode, setTradingMode] = useState('SPOT') // 'SPOT' or 'PERP'
const [fromCoin, setFromCoin] = useState('SOL')
const [toCoin, setToCoin] = useState('USDC')
const [perpCoin, setPerpCoin] = useState('SOL') // For perpetuals
// TP/SL functionality
const [enableStopLoss, setEnableStopLoss] = useState(false)
const [stopLoss, setStopLoss] = useState('')
const [enableTakeProfit, setEnableTakeProfit] = useState(false)
const [takeProfit, setTakeProfit] = useState('')
// Perp trading settings
const [leverage, setLeverage] = useState(1)
const [perpSize, setPerpSize] = useState('')
// Available coins for trading
const availableCoins = [
{ symbol: 'SOL', name: 'Solana', icon: '🟣' },
{ symbol: 'USDC', name: 'USD Coin', icon: '💵' },
{ symbol: 'USDT', name: 'Tether', icon: '💶' },
{ symbol: 'BTC', name: 'Bitcoin', icon: '₿' },
{ symbol: 'ETH', name: 'Ethereum', icon: '🔷' },
{ symbol: 'RAY', name: 'Raydium', icon: '⚡' },
{ symbol: 'ORCA', name: 'Orca', icon: '🐋' }
]
// Auto-fill TP/SL from AI analysis
useEffect(() => {
if (analysis) {
if (analysis.stopLoss?.price) {
setStopLoss(analysis.stopLoss.price.toString())
setEnableStopLoss(true)
}
if (analysis.takeProfits?.tp1?.price) {
setTakeProfit(analysis.takeProfits.tp1.price.toString())
setEnableTakeProfit(true)
}
}
}, [analysis])
// Get recommended price from analysis
const getRecommendedPrice = () => {
if (!analysis) return null
if (analysis.recommendation === 'BUY' && analysis.entry?.price) {
return analysis.entry.price
} else if (analysis.recommendation === 'SELL' && analysis.entry?.price) {
return analysis.entry.price
}
return null
}
const recommendedPrice = getRecommendedPrice()
// Fetch balance on component mount
useEffect(() => {
fetchBalance()
}, [])
const fetchBalance = async () => {
try {
const response = await fetch('/api/trading/balance')
const data = await response.json()
if (data.success) {
setBalance(data.balance)
}
} catch (error) {
console.error('Failed to fetch balance:', error)
}
}
const executeTrade = async () => {
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount')
return
}
setIsExecuting(true)
setExecutionResult(null)
try {
const tradePrice = useRecommendedPrice && recommendedPrice
? recommendedPrice
: customPrice ? parseFloat(customPrice) : undefined
// Prepare trade data based on trading mode
let tradeData = {
symbol: tradingMode === 'PERP' ? perpCoin : fromCoin,
side: tradeType,
amount: parseFloat(amount),
price: tradePrice,
orderType: tradePrice ? 'limit' : 'market',
useRealDEX: true, // Always use real DEX
tradingMode: tradingMode,
tradingPair: tradingMode === 'PERP' ? `${perpCoin}-PERP` : `${fromCoin}/${toCoin}`,
fromCoin: fromCoin,
toCoin: toCoin
}
// Add TP/SL if enabled
if (enableStopLoss && stopLoss) {
tradeData.stopLoss = parseFloat(stopLoss)
}
if (enableTakeProfit && takeProfit) {
tradeData.takeProfit = parseFloat(takeProfit)
}
// Add perpetuals specific data
if (tradingMode === 'PERP') {
tradeData.leverage = leverage
tradeData.perpSize = perpSize ? parseFloat(perpSize) : parseFloat(amount)
tradeData.perpCoin = perpCoin
}
// 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, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(tradeData)
})
const result = await response.json()
if (result.success) {
// 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 {
setExecutionResult({
success: false,
error: result.error,
message: result.message
})
}
} catch (error) {
setExecutionResult({
success: false,
error: 'Network error',
message: 'Failed to execute trade. Please try again.'
})
} finally {
setIsExecuting(false)
}
}
const getTradeButtonColor = () => {
if (tradeType === 'BUY') return 'bg-green-600 hover:bg-green-700'
return 'bg-red-600 hover:bg-red-700'
}
const getRecommendationColor = () => {
if (!analysis) return 'text-gray-400'
switch (analysis.recommendation) {
case 'BUY': return 'text-green-400'
case 'SELL': return 'text-red-400'
default: return 'text-yellow-400'
}
}
return (
<div className="card card-gradient p-6 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-white">Execute Trade</h2>
<div className="text-sm text-gray-400">
{symbol} Trading
</div>
</div>
{/* Balance Display */}
{balance && (
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-300 mb-2">Portfolio Balance</h3>
<div className="flex justify-between items-center">
<span className="text-lg font-bold text-white">
${balance.totalValue?.toFixed(2)}
</span>
<span className="text-sm text-gray-400">
Available: ${balance.availableBalance?.toFixed(2)}
</span>
</div>
</div>
)}
{/* AI Recommendation Display */}
{analysis && (
<div className="bg-gray-800 rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-300 mb-2">AI Recommendation</h3>
<div className="flex items-center justify-between mb-2">
<span className={`font-bold text-lg ${getRecommendationColor()}`}>
{analysis.recommendation}
</span>
<span className="text-sm text-gray-400">
{analysis.confidence}% confidence
</span>
</div>
{recommendedPrice && (
<div className="text-sm text-gray-300">
Entry: ${recommendedPrice.toFixed(4)}
{analysis.entry?.buffer && (
<span className="text-gray-400 ml-1">({analysis.entry.buffer})</span>
)}
</div>
)}
{analysis.stopLoss && (
<div className="text-sm text-gray-300">
Stop Loss: ${analysis.stopLoss.price.toFixed(4)}
</div>
)}
{analysis.takeProfits?.tp1 && (
<div className="text-sm text-gray-300">
TP1: ${analysis.takeProfits.tp1.price.toFixed(4)}
</div>
)}
<div className="text-xs text-gray-400 mt-2">
{analysis.reasoning}
</div>
</div>
)}
{/* Trading Mode Selection */}
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-300">Trading Mode</label>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setTradingMode('SPOT')}
className={`py-2 px-4 rounded-lg font-medium transition-colors text-sm ${
tradingMode === 'SPOT'
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
💱 Spot Trading
</button>
<button
onClick={() => setTradingMode('PERP')}
className={`py-2 px-4 rounded-lg font-medium transition-colors text-sm ${
tradingMode === 'PERP'
? 'bg-orange-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
⚡ Perpetuals
</button>
</div>
</div>
{/* Coin Selection for Spot Trading */}
{tradingMode === 'SPOT' && (
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-300">Swap Coins</label>
<div className="grid grid-cols-3 gap-2 items-center">
{/* From Coin */}
<div>
<label className="block text-xs text-gray-400 mb-1">From</label>
<select
value={fromCoin}
onChange={(e) => setFromCoin(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{availableCoins.map(coin => (
<option key={coin.symbol} value={coin.symbol}>
{coin.icon} {coin.symbol}
</option>
))}
</select>
</div>
{/* Swap Arrow */}
<div className="text-center">
<button
onClick={() => {
const temp = fromCoin
setFromCoin(toCoin)
setToCoin(temp)
}}
className="p-2 bg-gray-600 hover:bg-gray-500 rounded-lg transition-colors"
title="Swap coins"
>
🔄
</button>
</div>
{/* To Coin */}
<div>
<label className="block text-xs text-gray-400 mb-1">To</label>
<select
value={toCoin}
onChange={(e) => setToCoin(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{availableCoins.map(coin => (
<option key={coin.symbol} value={coin.symbol}>
{coin.icon} {coin.symbol}
</option>
))}
</select>
</div>
</div>
<div className="text-xs text-gray-400 text-center">
Swap {fromCoin} → {toCoin} via Jupiter DEX
</div>
</div>
)}
{/* Coin Selection and Leverage for Perpetuals */}
{tradingMode === 'PERP' && (
<div className="space-y-4">
{/* Perpetual Coin Selection */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Perpetual Asset</label>
<select
value={perpCoin}
onChange={(e) => setPerpCoin(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-orange-500"
>
{availableCoins.filter(coin => coin.symbol !== 'USDC' && coin.symbol !== 'USDT').map(coin => (
<option key={coin.symbol} value={coin.symbol}>
{coin.icon} {coin.symbol} Perpetual
</option>
))}
</select>
<div className="text-xs text-gray-400 mt-1">
Choose the asset you want to trade with leverage
</div>
</div>
{/* Leverage Selection */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Leverage</label>
<div className="grid grid-cols-4 gap-2">
{[1, 2, 5, 10].map(lev => (
<button
key={lev}
onClick={() => setLeverage(lev)}
className={`py-2 px-3 rounded-lg font-medium transition-colors text-sm ${
leverage === lev
? 'bg-orange-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{lev}x
</button>
))}
</div>
<div className="text-xs text-gray-400 mt-1">
⚠️ Higher leverage = Higher risk. Max 10x for safety.
</div>
</div>
</div>
)}
{/* Trade Type Selection */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setTradeType('BUY')}
className={`py-2 px-4 rounded-lg font-medium transition-colors ${
tradeType === 'BUY'
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tradingMode === 'PERP' ? 'LONG' : 'BUY'}
</button>
<button
onClick={() => setTradeType('SELL')}
className={`py-2 px-4 rounded-lg font-medium transition-colors ${
tradeType === 'SELL'
? 'bg-red-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tradingMode === 'PERP' ? 'SHORT' : 'SELL'}
</button>
</div>
{/* Amount Input */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
{tradingMode === 'PERP'
? `Position Size (${perpCoin})`
: `Amount (${fromCoin})`
}
</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
step="0.001"
min="0"
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="text-xs text-gray-400 mt-1">
{tradingMode === 'PERP'
? `Enter the ${perpCoin} position size to trade with ${leverage}x leverage`
: `Enter the amount of ${fromCoin} to swap for ${toCoin}`
}
</div>
</div>
{/* Price Selection */}
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-300">
Price Selection
</label>
{recommendedPrice && (
<label className="flex items-center space-x-3">
<input
type="radio"
name="priceType"
checked={useRecommendedPrice}
onChange={() => setUseRecommendedPrice(true)}
className="text-blue-500"
/>
<span className="text-gray-300">
Use AI Recommended Price: ${recommendedPrice.toFixed(4)}
</span>
</label>
)}
<label className="flex items-center space-x-3">
<input
type="radio"
name="priceType"
checked={!useRecommendedPrice}
onChange={() => setUseRecommendedPrice(false)}
className="text-blue-500"
/>
<span className="text-gray-300">Market Price / Custom</span>
</label>
{!useRecommendedPrice && (
<input
type="number"
value={customPrice}
onChange={(e) => setCustomPrice(e.target.value)}
placeholder="Enter price (leave empty for market)"
step="0.0001"
min="0"
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
)}
</div>
{/* Stop Loss & Take Profit */}
<div className="space-y-4 border border-gray-600 rounded-lg p-4">
<h3 className="text-sm font-bold text-white">Risk Management</h3>
{/* Stop Loss */}
<div className="space-y-2">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={enableStopLoss}
onChange={(e) => setEnableStopLoss(e.target.checked)}
className="rounded text-red-500"
/>
<span className="text-gray-300 font-medium">Stop Loss</span>
</label>
{enableStopLoss && (
<input
type="number"
value={stopLoss}
onChange={(e) => setStopLoss(e.target.value)}
placeholder="Stop loss price"
step="0.01"
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-red-500"
/>
)}
{enableStopLoss && analysis?.stopLoss && (
<div className="text-xs text-gray-400">
AI Suggested: ${analysis.stopLoss.price.toFixed(4)} - {analysis.stopLoss.rationale}
</div>
)}
</div>
{/* Take Profit */}
<div className="space-y-2">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={enableTakeProfit}
onChange={(e) => setEnableTakeProfit(e.target.checked)}
className="rounded text-green-500"
/>
<span className="text-gray-300 font-medium">Take Profit</span>
</label>
{enableTakeProfit && (
<input
type="number"
value={takeProfit}
onChange={(e) => setTakeProfit(e.target.value)}
placeholder="Take profit price"
step="0.01"
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500"
/>
)}
{enableTakeProfit && analysis?.takeProfits?.tp1 && (
<div className="text-xs text-gray-400">
AI Suggested: ${analysis.takeProfits.tp1.price.toFixed(4)} - {analysis.takeProfits.tp1.description}
</div>
)}
</div>
</div>
{/* Execute Button */}
<button
onClick={executeTrade}
disabled={isExecuting || !amount}
className={`w-full py-3 px-4 rounded-lg font-bold text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${getTradeButtonColor()}`}
>
{isExecuting ? 'Executing...' : `🚀 Execute ${tradeType} ${tradingMode === 'PERP' ? `${perpCoin} ${leverage}x` : `${fromCoin}→${toCoin}`}${(enableStopLoss || enableTakeProfit) ? ' + TP/SL' : ''}`}
</button>
{/* Execution Result */}
{executionResult && (
<div className={`p-4 rounded-lg ${
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 ? (
executionResult.type === 'limit_order' ? '📋 Limit Order Created' : ' Trade Executed'
) : ' Trade Failed'}
</div>
<div className="text-sm text-gray-300 mt-1">
{executionResult.message}
</div>
{executionResult.trade && (
<div className="text-xs text-gray-400 mt-2">
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>
)}
{/* Risk Warning */}
<div className="text-xs text-gray-500 border-t border-gray-700 pt-3">
⚠️ Trading involves significant risk of loss. Real trades executed via Jupiter DEX using your actual wallet funds.
</div>
</div>
)
}