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
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}