fix: implement trade button integration with AI analysis
- Add trade button next to each analysis result - Fix TradeModal to properly receive and display analysis data - Update TypeScript interfaces to match actual data structure - Pre-fill Entry Price, Stop Loss, and Take Profit values from AI analysis - Fix duplicate variable declarations causing build errors - Remove TradeExecutionPanel from analysis page (reverted to original design) Trade button now opens modal with correct pre-filled values Analysis data properly passed between components Build errors resolved
This commit is contained in:
@@ -1,28 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState } from 'react'
|
|
||||||
import AIAnalysisPanel from '../../components/AIAnalysisPanel.tsx'
|
import AIAnalysisPanel from '../../components/AIAnalysisPanel'
|
||||||
|
|
||||||
export default function AnalysisPage() {
|
export default function AnalysisPage() {
|
||||||
const [analysisResult, setAnalysisResult] = useState(null)
|
|
||||||
const [currentSymbol, setCurrentSymbol] = useState('SOL')
|
|
||||||
|
|
||||||
const handleAnalysisComplete = (analysis, symbol) => {
|
|
||||||
setAnalysisResult(analysis)
|
|
||||||
setCurrentSymbol(symbol || 'SOL')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="text-center mb-8">
|
||||||
<div>
|
<h1 className="text-3xl font-bold text-white mb-4">
|
||||||
<h1 className="text-3xl font-bold text-white">AI Analysis</h1>
|
🤖 AI-Powered Market Analysis
|
||||||
<p className="text-gray-400 mt-2">Get comprehensive market insights powered by AI analysis</p>
|
</h1>
|
||||||
</div>
|
<p className="text-gray-400 max-w-2xl mx-auto">
|
||||||
|
Get professional trading insights with multi-timeframe analysis, precise entry/exit levels,
|
||||||
|
and institutional-quality recommendations powered by OpenAI.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-6xl mx-auto">
|
<AIAnalysisPanel />
|
||||||
<AIAnalysisPanel onAnalysisComplete={handleAnalysisComplete} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
770
app/chart-trading-demo/page.tsx
Normal file
770
app/chart-trading-demo/page.tsx
Normal file
@@ -0,0 +1,770 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
|
import SimpleChart from '../../components/SimpleChart'
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
id: string
|
||||||
|
symbol: string
|
||||||
|
side: 'BUY' | 'SELL'
|
||||||
|
amount: number
|
||||||
|
entryPrice: number
|
||||||
|
stopLoss: number
|
||||||
|
takeProfit: number
|
||||||
|
currentPrice: number
|
||||||
|
unrealizedPnl: number
|
||||||
|
leverage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChartTradingDemo() {
|
||||||
|
const [selectedSymbol, setSelectedSymbol] = useState('SOL')
|
||||||
|
const [leverage, setLeverage] = useState(1)
|
||||||
|
const [payingAmount, setPayingAmount] = useState('')
|
||||||
|
const [payingToken, setPayingToken] = useState('USDC')
|
||||||
|
const [receivingAmount, setReceivingAmount] = useState('')
|
||||||
|
const [stopLoss, setStopLoss] = useState('')
|
||||||
|
const [takeProfit, setTakeProfit] = useState('')
|
||||||
|
const [isSymbolDropdownOpen, setIsSymbolDropdownOpen] = useState(false)
|
||||||
|
const [isPayingTokenDropdownOpen, setIsPayingTokenDropdownOpen] = useState(false)
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const payingTokenDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [positions, setPositions] = useState<Position[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [tradeStatus, setTradeStatus] = useState<string>('')
|
||||||
|
const [liveBalances, setLiveBalances] = useState<{[key: string]: number}>({})
|
||||||
|
const [marketPrices, setMarketPrices] = useState<{[key: string]: {price: number, change: number}}>({});
|
||||||
|
|
||||||
|
const symbols = [
|
||||||
|
{ symbol: 'SOL', name: 'Solana', price: marketPrices.SOL?.price || 166.21, change: marketPrices.SOL?.change || 2.45, icon: '🟣' },
|
||||||
|
{ symbol: 'BTC', name: 'Bitcoin', price: marketPrices.BTC?.price || 42150.00, change: marketPrices.BTC?.change || -1.23, icon: '🟠' },
|
||||||
|
{ symbol: 'ETH', name: 'Ethereum', price: marketPrices.ETH?.price || 2580.50, change: marketPrices.ETH?.change || 3.12, icon: '⬜' },
|
||||||
|
{ symbol: 'BONK', name: 'Bonk', price: marketPrices.BONK?.price || 0.00003456, change: marketPrices.BONK?.change || 15.67, icon: '🐕' },
|
||||||
|
{ symbol: 'JUP', name: 'Jupiter', price: marketPrices.JUP?.price || 0.8234, change: marketPrices.JUP?.change || 5.43, icon: '🪐' },
|
||||||
|
{ symbol: 'WIF', name: 'dogwifhat', price: marketPrices.WIF?.price || 3.42, change: marketPrices.WIF?.change || -8.21, icon: '🧢' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const payingTokens = [
|
||||||
|
{ symbol: 'USDC', name: 'USD Coin', balance: liveBalances.USDC || 0, icon: '💵' },
|
||||||
|
{ symbol: 'USDT', name: 'Tether', balance: liveBalances.USDT || 0, icon: '💰' },
|
||||||
|
{ symbol: 'SOL', name: 'Solana', balance: liveBalances.SOL || 0, icon: '🟣' },
|
||||||
|
{ symbol: 'BTC', name: 'Bitcoin', balance: liveBalances.BTC || 0, icon: '🟠' },
|
||||||
|
{ symbol: 'ETH', name: 'Ethereum', balance: liveBalances.ETH || 0, icon: '⬜' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsSymbolDropdownOpen(false)
|
||||||
|
}
|
||||||
|
if (payingTokenDropdownRef.current && !payingTokenDropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsPayingTokenDropdownOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load real wallet balances and positions
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRealData = async () => {
|
||||||
|
try {
|
||||||
|
// Fetch real wallet balances
|
||||||
|
const balanceResponse = await fetch('/api/wallet/balance')
|
||||||
|
if (balanceResponse.ok) {
|
||||||
|
const balanceData = await balanceResponse.json()
|
||||||
|
if (balanceData.success && balanceData.balance?.positions) {
|
||||||
|
// Convert positions array to balances object
|
||||||
|
const balances: {[key: string]: number} = {}
|
||||||
|
balanceData.balance.positions.forEach((position: any) => {
|
||||||
|
balances[position.symbol] = position.amount
|
||||||
|
})
|
||||||
|
setLiveBalances(balances)
|
||||||
|
console.log('✅ Loaded wallet balances:', balances)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch real positions
|
||||||
|
const positionsResponse = await fetch('/api/trading/positions')
|
||||||
|
if (positionsResponse.ok) {
|
||||||
|
const positionsData = await positionsResponse.json()
|
||||||
|
if (positionsData.success) {
|
||||||
|
setPositions(positionsData.positions || [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch market prices
|
||||||
|
const pricesResponse = await fetch('/api/prices')
|
||||||
|
if (pricesResponse.ok) {
|
||||||
|
const pricesData = await pricesResponse.json()
|
||||||
|
if (pricesData.success) {
|
||||||
|
setMarketPrices(pricesData.prices || {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading real data:', error)
|
||||||
|
setTradeStatus('Error loading wallet data')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRealData()
|
||||||
|
|
||||||
|
// Refresh data every 30 seconds
|
||||||
|
const interval = setInterval(loadRealData, 30000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getCurrentSymbolData = () => {
|
||||||
|
return symbols.find(s => s.symbol === selectedSymbol) || symbols[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentPayingTokenData = () => {
|
||||||
|
const tokenData = payingTokens.find(t => t.symbol === payingToken) || payingTokens[0]
|
||||||
|
console.log(`Getting token data for ${payingToken}:`, tokenData, 'Live balances:', liveBalances)
|
||||||
|
return tokenData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate receiving amount based on paying amount
|
||||||
|
useEffect(() => {
|
||||||
|
if (payingAmount && payingToken === 'USDC') {
|
||||||
|
const symbolData = getCurrentSymbolData()
|
||||||
|
const symbolPrice = symbolData.price
|
||||||
|
const receiving = parseFloat(payingAmount) / symbolPrice
|
||||||
|
setReceivingAmount(receiving.toFixed(6))
|
||||||
|
} else if (payingAmount && payingToken === selectedSymbol) {
|
||||||
|
setReceivingAmount(payingAmount)
|
||||||
|
} else {
|
||||||
|
setReceivingAmount('')
|
||||||
|
}
|
||||||
|
}, [payingAmount, payingToken, selectedSymbol])
|
||||||
|
|
||||||
|
const handleTrade = async (side: 'BUY' | 'SELL') => {
|
||||||
|
const amount = parseFloat(payingAmount)
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
alert('Please enter a valid amount')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setTradeStatus(`Executing ${side} order...`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute real trade based on leverage
|
||||||
|
const tradeEndpoint = leverage > 1 ? '/api/trading/execute-perp' : '/api/trading/execute-dex'
|
||||||
|
|
||||||
|
const tradePayload = {
|
||||||
|
symbol: selectedSymbol,
|
||||||
|
side,
|
||||||
|
amount,
|
||||||
|
stopLoss: stopLoss ? parseFloat(stopLoss) : undefined,
|
||||||
|
takeProfit: takeProfit ? parseFloat(takeProfit) : undefined,
|
||||||
|
leverage: leverage > 1 ? leverage : undefined,
|
||||||
|
fromCoin: payingToken,
|
||||||
|
toCoin: selectedSymbol,
|
||||||
|
useRealDEX: true
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Executing trade:', tradePayload)
|
||||||
|
|
||||||
|
const response = await fetch(tradeEndpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(tradePayload)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setTradeStatus(`${side} order executed successfully!`)
|
||||||
|
|
||||||
|
// Refresh positions and balances
|
||||||
|
const positionsResponse = await fetch('/api/trading/positions')
|
||||||
|
if (positionsResponse.ok) {
|
||||||
|
const positionsData = await positionsResponse.json()
|
||||||
|
if (positionsData.success) {
|
||||||
|
setPositions(positionsData.positions || [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const balanceResponse = await fetch('/api/wallet/balance')
|
||||||
|
if (balanceResponse.ok) {
|
||||||
|
const balanceData = await balanceResponse.json()
|
||||||
|
if (balanceData.success && balanceData.balance?.positions) {
|
||||||
|
// Convert positions array to balances object
|
||||||
|
const balances: {[key: string]: number} = {}
|
||||||
|
balanceData.balance.positions.forEach((position: any) => {
|
||||||
|
balances[position.symbol] = position.amount
|
||||||
|
})
|
||||||
|
setLiveBalances(balances)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
setPayingAmount('')
|
||||||
|
setReceivingAmount('')
|
||||||
|
setStopLoss('')
|
||||||
|
setTakeProfit('')
|
||||||
|
|
||||||
|
alert(`✅ ${side} order executed successfully!\nTransaction: ${result.signature || 'Completed'}`)
|
||||||
|
} else {
|
||||||
|
setTradeStatus(`Trade failed: ${result.error || 'Unknown error'}`)
|
||||||
|
alert(`❌ Trade failed: ${result.message || result.error || 'Unknown error'}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Trade execution error:', error)
|
||||||
|
setTradeStatus('Trade execution failed')
|
||||||
|
alert('❌ Trade execution failed. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
setTimeout(() => setTradeStatus(''), 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClosePosition = async (positionId: string) => {
|
||||||
|
const position = positions.find(pos => pos.id === positionId)
|
||||||
|
if (!position) return
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to close ${position.symbol} ${position.side} position?`)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setTradeStatus('Closing position...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/trading/positions/${positionId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setTradeStatus('Position closed successfully!')
|
||||||
|
|
||||||
|
// Refresh positions and balances
|
||||||
|
const positionsResponse = await fetch('/api/trading/positions')
|
||||||
|
if (positionsResponse.ok) {
|
||||||
|
const positionsData = await positionsResponse.json()
|
||||||
|
if (positionsData.success) {
|
||||||
|
setPositions(positionsData.positions || [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const balanceResponse = await fetch('/api/wallet/balance')
|
||||||
|
if (balanceResponse.ok) {
|
||||||
|
const balanceData = await balanceResponse.json()
|
||||||
|
if (balanceData.success && balanceData.balance?.positions) {
|
||||||
|
// Convert positions array to balances object
|
||||||
|
const balances: {[key: string]: number} = {}
|
||||||
|
balanceData.balance.positions.forEach((position: any) => {
|
||||||
|
balances[position.symbol] = position.amount
|
||||||
|
})
|
||||||
|
setLiveBalances(balances)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`✅ Position closed successfully!\nP&L: ${result.realizedPnl ? (result.realizedPnl >= 0 ? '+' : '') + '$' + result.realizedPnl.toFixed(2) : 'Calculated at settlement'}`)
|
||||||
|
} else {
|
||||||
|
setTradeStatus(`Failed to close position: ${result.error}`)
|
||||||
|
alert(`❌ Failed to close position: ${result.message || result.error}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Position close error:', error)
|
||||||
|
setTradeStatus('Failed to close position')
|
||||||
|
alert('❌ Failed to close position. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
setTimeout(() => setTradeStatus(''), 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style jsx>{`
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eab308;
|
||||||
|
border: 2px solid #1f2937;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 0 1px #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eab308;
|
||||||
|
border: 2px solid #1f2937;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 0 1px #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-track {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-track {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<div className="h-screen bg-gray-900 flex flex-col">
|
||||||
|
{/* Top Bar */}
|
||||||
|
<div className="bg-gray-800 border-b border-gray-700 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-6">
|
||||||
|
<h1 className="text-xl font-bold text-white">Live Trading Terminal</h1>
|
||||||
|
|
||||||
|
{/* Symbol Selector Dropdown */}
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSymbolDropdownOpen(!isSymbolDropdownOpen)}
|
||||||
|
className="flex items-center space-x-3 bg-gray-700 hover:bg-gray-600 rounded-lg px-4 py-2 text-white transition-all border border-gray-600"
|
||||||
|
>
|
||||||
|
<span className="text-lg">{getCurrentSymbolData().icon}</span>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-medium">{selectedSymbol}/USDC</div>
|
||||||
|
<div className="text-xs text-gray-400">{getCurrentSymbolData().name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-mono">${getCurrentSymbolData().price.toLocaleString()}</div>
|
||||||
|
<div className={`text-xs ${getCurrentSymbolData().change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{getCurrentSymbolData().change >= 0 ? '+' : ''}{getCurrentSymbolData().change}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<svg className={`w-4 h-4 transition-transform ${isSymbolDropdownOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown Menu */}
|
||||||
|
{isSymbolDropdownOpen && (
|
||||||
|
<div className="absolute top-full left-0 mt-2 w-80 bg-gray-800 border border-gray-600 rounded-lg shadow-xl z-50 max-h-80 overflow-y-auto">
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="text-xs text-gray-400 uppercase tracking-wide px-3 py-2 border-b border-gray-700">
|
||||||
|
Select Trading Pair
|
||||||
|
</div>
|
||||||
|
{symbols.map(({ symbol, name, price, change, icon }) => (
|
||||||
|
<button
|
||||||
|
key={symbol}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSymbol(symbol)
|
||||||
|
setIsSymbolDropdownOpen(false)
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center space-x-3 px-3 py-3 text-left rounded-lg transition-all ${
|
||||||
|
selectedSymbol === symbol
|
||||||
|
? 'bg-blue-600/20 border border-blue-500/30'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-lg">{icon}</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-white">{symbol}/USDC</div>
|
||||||
|
<div className="text-xs text-gray-400">{name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm font-mono text-white">
|
||||||
|
${price < 1 ? price.toFixed(8) : price.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className={`text-xs ${change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{change >= 0 ? '+' : ''}{change}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedSymbol === symbol && (
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Market Status */}
|
||||||
|
<div className="flex items-center space-x-4 text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full animate-pulse ${isLoading ? 'bg-yellow-400' : 'bg-green-400'}`}></div>
|
||||||
|
<span className="text-gray-300">{isLoading ? 'Processing...' : 'Live Trading'}</span>
|
||||||
|
</div>
|
||||||
|
{tradeStatus && (
|
||||||
|
<div className="text-blue-400 text-xs">
|
||||||
|
{tradeStatus}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-gray-400">
|
||||||
|
{new Date().toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Trading Interface */}
|
||||||
|
<div className="flex-1 flex">
|
||||||
|
{/* Chart Area (70% width) */}
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<SimpleChart symbol={selectedSymbol} positions={positions} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trading Panel (30% width) */}
|
||||||
|
<div className="w-96 border-l border-gray-700 p-4 space-y-4">
|
||||||
|
<div className="bg-gray-800 rounded-lg p-4">
|
||||||
|
<h3 className="text-white text-lg font-semibold mb-4">{selectedSymbol}/USDC</h3>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">
|
||||||
|
${getCurrentSymbolData().price.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className={`text-sm ${getCurrentSymbolData().change >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{getCurrentSymbolData().change >= 0 ? '+' : ''}{getCurrentSymbolData().change}%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Selector */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<label className="block text-gray-400 text-sm">Leverage</label>
|
||||||
|
<div className="text-sm text-yellow-400 font-medium">
|
||||||
|
{leverage}x
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Slider */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={leverage}
|
||||||
|
onChange={(e) => setLeverage(parseInt(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, #eab308 0%, #eab308 ${((leverage - 1) / 99) * 100}%, #374151 ${((leverage - 1) / 99) * 100}%, #374151 100%)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Leverage Marks */}
|
||||||
|
<div className="flex justify-between mt-2 px-1">
|
||||||
|
{[1, 2, 5, 10, 20, 50, 100].map((mark) => (
|
||||||
|
<button
|
||||||
|
key={mark}
|
||||||
|
onClick={() => setLeverage(mark)}
|
||||||
|
className={`text-xs transition-all ${
|
||||||
|
leverage === mark
|
||||||
|
? 'text-yellow-400 font-medium'
|
||||||
|
: 'text-gray-500 hover:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{mark}x
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Leverage Warning */}
|
||||||
|
{leverage > 10 && (
|
||||||
|
<div className="mt-2 flex items-center space-x-2 text-xs text-orange-400">
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>High leverage increases liquidation risk</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Trade Buttons */}
|
||||||
|
<div className="mt-6 space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleTrade('BUY')}
|
||||||
|
disabled={!payingAmount || isLoading}
|
||||||
|
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white py-3 px-4 rounded-lg font-medium transition-all"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Processing...' : `Long/Buy ${leverage > 1 ? `(${leverage}x)` : ''}`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleTrade('SELL')}
|
||||||
|
disabled={!payingAmount || isLoading}
|
||||||
|
className="w-full bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white py-3 px-4 rounded-lg font-medium transition-all"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Processing...' : `Short/Sell ${leverage > 1 ? `(${leverage}x)` : ''}`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trade Form */}
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
{/* You're Paying Section */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">You're paying</label>
|
||||||
|
<div className="bg-gray-700 rounded-lg p-3 border border-gray-600">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={payingAmount}
|
||||||
|
onChange={(e) => setPayingAmount(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
className="bg-transparent text-white text-lg font-medium focus:outline-none flex-1 mr-3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Paying Token Selector */}
|
||||||
|
<div className="relative" ref={payingTokenDropdownRef}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsPayingTokenDropdownOpen(!isPayingTokenDropdownOpen)}
|
||||||
|
className="flex items-center space-x-2 bg-gray-600 hover:bg-gray-500 rounded-lg px-3 py-2 transition-all"
|
||||||
|
>
|
||||||
|
<span className="text-sm">{getCurrentPayingTokenData().icon}</span>
|
||||||
|
<span className="text-white font-medium">{payingToken}</span>
|
||||||
|
<svg className={`w-4 h-4 text-gray-300 transition-transform ${isPayingTokenDropdownOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Paying Token Dropdown */}
|
||||||
|
{isPayingTokenDropdownOpen && (
|
||||||
|
<div className="absolute top-full right-0 mt-2 w-64 bg-gray-800 border border-gray-600 rounded-lg shadow-xl z-50">
|
||||||
|
<div className="p-2">
|
||||||
|
{payingTokens.map(({ symbol, name, balance, icon }) => (
|
||||||
|
<button
|
||||||
|
key={symbol}
|
||||||
|
onClick={() => {
|
||||||
|
setPayingToken(symbol)
|
||||||
|
setIsPayingTokenDropdownOpen(false)
|
||||||
|
}} className={`w-full flex items-center justify-between px-3 py-2 text-left rounded-lg transition-all ${
|
||||||
|
payingToken === symbol
|
||||||
|
? 'bg-blue-600/20 border border-blue-500/30'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm">{icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">{symbol}</div>
|
||||||
|
<div className="text-xs text-gray-400">{name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{balance.toFixed(balance < 1 ? 6 : 2)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Balance and MAX button */}
|
||||||
|
<div className="flex items-center justify-between mt-2 text-xs text-gray-400">
|
||||||
|
<span>Balance: {getCurrentPayingTokenData().balance.toFixed(getCurrentPayingTokenData().balance < 1 ? 6 : 2)} {payingToken}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setPayingAmount(getCurrentPayingTokenData().balance.toString())}
|
||||||
|
disabled={getCurrentPayingTokenData().balance === 0}
|
||||||
|
className="text-blue-400 hover:text-blue-300 disabled:text-gray-500 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{getCurrentPayingTokenData().balance === 0 && (
|
||||||
|
<div className="text-xs text-red-400 mt-1">
|
||||||
|
⚠️ No {payingToken} balance available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{payingAmount && parseFloat(payingAmount) > getCurrentPayingTokenData().balance && (
|
||||||
|
<div className="text-xs text-red-400 mt-1">
|
||||||
|
⚠️ Insufficient balance ({getCurrentPayingTokenData().balance.toFixed(getCurrentPayingTokenData().balance < 1 ? 6 : 2)} {payingToken} available)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{leverage > 1 && payingAmount && (
|
||||||
|
<div className="text-xs text-yellow-400 mt-1">
|
||||||
|
Position value: ${(parseFloat(payingAmount) * leverage).toLocaleString()} ({leverage}x leverage)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* You're Receiving Section */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">You're receiving</label>
|
||||||
|
<div className="bg-gray-700 rounded-lg p-3 border border-gray-600">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-lg font-medium text-white">
|
||||||
|
{receivingAmount || '0.00'}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 bg-gray-600 rounded-lg px-3 py-2">
|
||||||
|
<span className="text-sm">{getCurrentSymbolData().icon}</span>
|
||||||
|
<span className="text-white font-medium">{selectedSymbol}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-2">
|
||||||
|
Rate: 1 {payingToken} = {payingToken === 'USDC' ? (1 / getCurrentSymbolData().price).toFixed(8) : '1.00'} {selectedSymbol}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">Stop Loss</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={stopLoss}
|
||||||
|
onChange={(e) => setStopLoss(e.target.value)}
|
||||||
|
placeholder="Optional"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
className="w-full bg-gray-700 text-white rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-400 text-sm mb-2">Take Profit</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={takeProfit}
|
||||||
|
onChange={(e) => setTakeProfit(e.target.value)}
|
||||||
|
placeholder="Optional"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
className="w-full bg-gray-700 text-white rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Risk Information */}
|
||||||
|
{payingAmount && leverage > 1 && (
|
||||||
|
<div className="bg-yellow-900/20 border border-yellow-600/20 rounded-lg p-3">
|
||||||
|
<div className="text-yellow-400 text-xs font-medium mb-1">⚠️ Leveraged Position Risk</div>
|
||||||
|
<div className="text-xs text-gray-300">
|
||||||
|
Position value: ${(parseFloat(payingAmount) * leverage).toLocaleString()} ({leverage}x leverage)
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-300">
|
||||||
|
Liquidation risk: High with {leverage}x leverage
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Panel - Positions */}
|
||||||
|
<div className="border-t border-gray-700 bg-gray-800">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex space-x-6">
|
||||||
|
<button className="text-white font-medium border-b-2 border-blue-500 pb-2">
|
||||||
|
Positions ({positions.length})
|
||||||
|
</button>
|
||||||
|
<button className="text-gray-400 hover:text-white pb-2">
|
||||||
|
Orders (0)
|
||||||
|
</button>
|
||||||
|
<button className="text-gray-400 hover:text-white pb-2">
|
||||||
|
History
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{positions.length > 0 && (
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Total P&L: <span className={`${
|
||||||
|
positions.reduce((sum, pos) => sum + pos.unrealizedPnl, 0) >= 0
|
||||||
|
? 'text-green-400' : 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{positions.reduce((sum, pos) => sum + pos.unrealizedPnl, 0) >= 0 ? '+' : ''}
|
||||||
|
${positions.reduce((sum, pos) => sum + pos.unrealizedPnl, 0).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Positions Table */}
|
||||||
|
<div className="max-h-48 overflow-y-auto">
|
||||||
|
{isLoading && positions.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-400 mr-2"></div>
|
||||||
|
Loading positions...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && positions.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
No open positions
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{positions.map((position: Position) => (
|
||||||
|
<div
|
||||||
|
key={position.id}
|
||||||
|
className="bg-gray-900 rounded-lg p-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${
|
||||||
|
position.side === 'BUY' ? 'bg-green-400' : 'bg-red-400'
|
||||||
|
}`}></div>
|
||||||
|
<div>
|
||||||
|
<div className="text-white font-medium">
|
||||||
|
{position.symbol} • {position.side}
|
||||||
|
{position.leverage > 1 && (
|
||||||
|
<span className="ml-2 text-yellow-400 text-sm">
|
||||||
|
{position.leverage}x
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
Size: {position.amount} • Entry: ${position.entryPrice?.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-red-400">
|
||||||
|
SL: ${position.stopLoss.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-green-400">
|
||||||
|
TP: ${position.takeProfit.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-white font-medium">
|
||||||
|
${(position.amount * position.currentPrice).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div className={`text-sm ${
|
||||||
|
position.unrealizedPnl >= 0 ? 'text-green-400' : 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{position.unrealizedPnl >= 0 ? '+' : ''}${(position.unrealizedPnl || 0).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{position.leverage > 1 && (
|
||||||
|
<div className="text-xs text-yellow-400">
|
||||||
|
{position.leverage}x Leveraged
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleClosePosition(position.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed transition-all"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Processing...' : 'Close'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,10 +7,13 @@ interface TradeModalProps {
|
|||||||
tradeData: {
|
tradeData: {
|
||||||
entry: string
|
entry: string
|
||||||
tp: string
|
tp: string
|
||||||
|
tp2?: string
|
||||||
sl: string
|
sl: string
|
||||||
risk: string
|
symbol?: string
|
||||||
reward: string
|
timeframe?: string
|
||||||
action: 'BUY' | 'SELL'
|
risk?: string
|
||||||
|
reward?: string
|
||||||
|
action?: 'BUY' | 'SELL'
|
||||||
} | null
|
} | null
|
||||||
onExecute: (data: any) => void
|
onExecute: (data: any) => void
|
||||||
}
|
}
|
||||||
@@ -55,6 +58,21 @@ export default function TradeModal({ isOpen, onClose, tradeData, onExecute }: Tr
|
|||||||
}
|
}
|
||||||
}, [isOpen])
|
}, [isOpen])
|
||||||
|
|
||||||
|
// Update form data when tradeData changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (tradeData) {
|
||||||
|
console.log('🔄 TradeModal updating form with new tradeData:', tradeData)
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
entry: tradeData.entry || '',
|
||||||
|
tp1: tradeData.tp || '',
|
||||||
|
tp2: tradeData.tp2 || '',
|
||||||
|
sl: tradeData.sl || '',
|
||||||
|
tradingCoin: tradeData.symbol ? tradeData.symbol.replace('USD', '') : 'SOL'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}, [tradeData])
|
||||||
|
|
||||||
const fetchWalletBalance = async () => {
|
const fetchWalletBalance = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/wallet/balance')
|
const response = await fetch('/api/wallet/balance')
|
||||||
|
|||||||
Reference in New Issue
Block a user