- Real-time positions tracking with live P&L updates - PositionsPanel component with auto-refresh every 10s - Position creation on trade execution (DEX, Perp, Standard) - One-click position closing functionality - Stop Loss and Take Profit display with monitoring - /api/trading/positions API for CRUD operations - Real-time price updates via CoinGecko integration - Automatic position creation on successful trades - In-memory positions storage with P&L calculations - Enhanced trading page layout with positions panel - Entry price, current price, and unrealized P&L - Percentage-based P&L calculations - Portfolio summary with total value and total P&L - Transaction ID tracking for audit trail - Support for leverage positions and TP/SL orders Confirmed Working: - Position created: SOL/USDC BUY 0.02 @ 68.10 - Real-time P&L: -/bin/bash.0052 (-0.15%) - TP/SL monitoring: SL 60, TP 80 - Transaction: 5qYx7nmpgE3fHEZpjJCMtJNb1jSQVGfKhKNzJNgJ5VGV4xG2cSSpr1wtfPfbmx8zSjwHnzSgZiWsMnAWmCFQ2RVx - Clear positions display on trading page - Real-time updates without manual refresh - Intuitive close buttons for quick position management - Separate wallet holdings vs active trading positions - Professional trading interface with P&L visualization
210 lines
7.3 KiB
JavaScript
210 lines
7.3 KiB
JavaScript
'use client'
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
export default function PositionsPanel() {
|
|
const [positions, setPositions] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [totalPnl, setTotalPnl] = useState(0)
|
|
const [totalValue, setTotalValue] = useState(0)
|
|
|
|
useEffect(() => {
|
|
fetchPositions()
|
|
// Refresh positions every 10 seconds
|
|
const interval = setInterval(fetchPositions, 10000)
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
const fetchPositions = async () => {
|
|
try {
|
|
const response = await fetch('/api/trading/positions')
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
setPositions(data.positions || [])
|
|
setTotalPnl(data.totalPnl || 0)
|
|
setTotalValue(data.totalValue || 0)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch positions:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const closePosition = async (positionId, currentPrice) => {
|
|
try {
|
|
const response = await fetch('/api/trading/positions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'close',
|
|
positionId: positionId,
|
|
exitPrice: currentPrice
|
|
})
|
|
})
|
|
|
|
if (response.ok) {
|
|
fetchPositions() // Refresh positions
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to close position:', error)
|
|
}
|
|
}
|
|
|
|
const getPnlColor = (pnl) => {
|
|
if (pnl > 0) return 'text-green-400'
|
|
if (pnl < 0) return 'text-red-400'
|
|
return 'text-gray-400'
|
|
}
|
|
|
|
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">Open Positions</h2>
|
|
<div className="text-gray-400">Loading positions...</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">Open Positions</h2>
|
|
<button
|
|
onClick={fetchPositions}
|
|
className="text-blue-400 hover:text-blue-300 text-sm"
|
|
>
|
|
🔄 Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{/* Portfolio Summary */}
|
|
{positions.length > 0 && (
|
|
<div className="bg-gray-800 rounded-lg p-4 mb-4">
|
|
<div className="grid grid-cols-3 gap-4 text-center">
|
|
<div>
|
|
<div className="text-xs text-gray-400">Total Positions</div>
|
|
<div className="text-lg font-bold text-white">{positions.length}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-gray-400">Total Value</div>
|
|
<div className="text-lg font-bold text-white">{formatCurrency(totalValue)}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-xs text-gray-400">Unrealized P&L</div>
|
|
<div className={`text-lg font-bold ${getPnlColor(totalPnl)}`}>
|
|
{formatCurrency(totalPnl)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{positions.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<div className="text-gray-400 mb-2">📊 No open positions</div>
|
|
<div className="text-sm text-gray-500">Execute a trade to see positions here</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{positions.map((position) => (
|
|
<div
|
|
key={position.id}
|
|
className="bg-gray-800 rounded-lg p-4 border 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">
|
|
{position.symbol}
|
|
</span>
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
|
position.side === 'BUY'
|
|
? 'bg-green-600 text-white'
|
|
: 'bg-red-600 text-white'
|
|
}`}>
|
|
{position.side}
|
|
</span>
|
|
{position.leverage > 1 && (
|
|
<span className="px-2 py-1 rounded text-xs bg-orange-600 text-white">
|
|
{position.leverage}x
|
|
</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => closePosition(position.id, position.currentPrice)}
|
|
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white text-xs rounded transition-colors"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
|
<div>
|
|
<div className="text-gray-400">Size</div>
|
|
<div className="text-white font-medium">{position.amount}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400">Entry Price</div>
|
|
<div className="text-white font-medium">{formatCurrency(position.entryPrice)}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400">Current Price</div>
|
|
<div className="text-white font-medium">
|
|
{position.currentPrice ? formatCurrency(position.currentPrice) : 'Loading...'}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-gray-400">P&L</div>
|
|
<div className={`font-medium ${getPnlColor(position.unrealizedPnl)}`}>
|
|
{position.unrealizedPnl ? formatCurrency(position.unrealizedPnl) : '$0.00'}
|
|
{position.pnlPercentage && (
|
|
<span className="text-xs ml-1">
|
|
({position.pnlPercentage > 0 ? '+' : ''}{position.pnlPercentage.toFixed(2)}%)
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stop Loss / Take Profit */}
|
|
{(position.stopLoss || position.takeProfit) && (
|
|
<div className="mt-3 pt-3 border-t border-gray-600">
|
|
<div className="flex space-x-4 text-xs">
|
|
{position.stopLoss && (
|
|
<div className="text-red-400">
|
|
🛑 SL: {formatCurrency(position.stopLoss)}
|
|
</div>
|
|
)}
|
|
{position.takeProfit && (
|
|
<div className="text-green-400">
|
|
🎯 TP: {formatCurrency(position.takeProfit)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Position Info */}
|
|
<div className="mt-2 text-xs text-gray-500">
|
|
Opened: {new Date(position.timestamp).toLocaleString()}
|
|
{position.txId && (
|
|
<span className="ml-2">• TX: {position.txId.substring(0, 8)}...</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|