Files
trading_bot_v3/components/TradingHistory.tsx
mindesbunister bf9022b699 Fix trading history: implement proper P&L tracking for closed positions
- Fixed trading history not showing closed positions with positive P&L
- Implemented multi-source trading history fetching (SDK, Data API, DLOB, local DB)
- Added proper P&L calculation using unrealized PnL from Drift positions
- Enhanced TradingHistory component with error handling and sync functionality
- Added manual sync button and better status messages
- Created /api/drift/sync-trades endpoint for manual trade synchronization
- Fixed database integration to properly store and retrieve trades with P&L
- Added comprehensive fallback mechanisms for data fetching
- Improved error messages and user feedback
- Added TRADING_HISTORY_IMPROVEMENTS.md documentation

This addresses the issue where recently closed positions with positive P&L
were not appearing in the trading history section.
2025-07-13 10:18:56 +02:00

257 lines
9.7 KiB
TypeScript
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, { useEffect, useState } from 'react'
interface Trade {
id: string
symbol: string
side: string
amount: number
price: number
status: string
executedAt: string
pnl?: number
}
export default function TradingHistory() {
const [trades, setTrades] = useState<Trade[]>([])
const [loading, setLoading] = useState(true)
const [isClient, setIsClient] = useState(false)
const [error, setError] = useState<string | null>(null)
const [message, setMessage] = useState<string>('')
const [syncing, setSyncing] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
const formatTime = (dateString: string) => {
if (!isClient) return '--:--:--'
return new Date(dateString).toLocaleTimeString()
}
const formatDate = (dateString: string) => {
if (!isClient) return '--/--/--'
return new Date(dateString).toLocaleDateString()
}
const handleSync = async () => {
try {
setSyncing(true)
const response = await fetch('/api/drift/sync-trades', {
method: 'POST'
})
if (response.ok) {
const data = await response.json()
setMessage(data.message || 'Sync completed')
// Refresh the trading history after sync
await fetchTrades()
} else {
setError('Failed to sync trades')
}
} catch (error) {
console.error('Sync error:', error)
setError('Error during sync')
} finally {
setSyncing(false)
}
}
const fetchTrades = async () => {
try {
setError(null)
setMessage('')
// Try Drift trading history first
const driftRes = await fetch('/api/drift/trading-history')
if (driftRes.ok) {
const data = await driftRes.json()
if (data.success && data.trades) {
setTrades(data.trades)
setMessage(data.message || `Found ${data.trades.length} trade(s)`)
} else {
setTrades([])
setMessage(data.message || 'No trades available')
if (data.error) {
setError(data.error)
}
}
} else {
// API failed - try fallback to local database
const res = await fetch('/api/trading-history')
if (res.ok) {
const data = await res.json()
setTrades(data || [])
setMessage(data?.length > 0 ? `Found ${data.length} local trade(s)` : 'No trading history available')
} else {
// Both APIs failed - show empty state
setTrades([])
setError('Failed to load trading history from both sources')
setMessage('Unable to fetch trading history. Please try again.')
}
}
} catch (error) {
console.error('Failed to fetch trades:', error)
setTrades([])
setError('Network error while fetching trades')
setMessage('Check your connection and try again.')
}
}
useEffect(() => {
async function loadTrades() {
setLoading(true)
await fetchTrades()
setLoading(false)
}
loadTrades()
}, [])
const getSideColor = (side: string) => {
return side.toLowerCase() === 'buy' ? 'text-green-400' : 'text-red-400'
}
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'filled': return 'text-green-400'
case 'pending': return 'text-yellow-400'
case 'cancelled': return 'text-red-400'
default: return 'text-gray-400'
}
}
const getPnLColor = (pnl?: number) => {
if (!pnl) return 'text-gray-400'
return pnl >= 0 ? 'text-green-400' : 'text-red-400'
}
return (
<div className="card card-gradient">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-white flex items-center">
<span className="w-8 h-8 bg-gradient-to-br from-purple-400 to-violet-600 rounded-lg flex items-center justify-center mr-3">
📊
</span>
Trading History
</h2>
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400">{message || `Latest ${trades.length} trades`}</span>
<button
onClick={handleSync}
disabled={syncing || loading}
className="text-xs text-blue-400 hover:text-blue-300 transition-colors disabled:text-gray-500"
title="Sync with Drift to check for new trades"
>
{syncing ? '🔄' : '🔄 Sync'}
</button>
<button
onClick={() => window.location.reload()}
className="text-xs text-gray-400 hover:text-gray-300 transition-colors"
title="Refresh page"
>
</button>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="spinner"></div>
<span className="ml-2 text-gray-400">Loading trades...</span>
</div>
) : error ? (
<div className="text-center py-8">
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-red-400 text-2xl"></span>
</div>
<p className="text-red-400 font-medium">Error Loading Trades</p>
<p className="text-gray-500 text-sm mt-2">{error}</p>
<p className="text-gray-500 text-sm">{message}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-red-500/20 text-red-400 rounded-lg hover:bg-red-500/30 transition-colors"
>
Retry
</button>
</div>
) : trades.length === 0 ? (
<div className="text-center py-8">
<div className="w-16 h-16 bg-gray-700/50 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-gray-400 text-2xl">📈</span>
</div>
<p className="text-gray-400 font-medium">No Trading History</p>
<p className="text-gray-500 text-sm mt-2">{message || 'Your completed trades will appear here'}</p>
<p className="text-gray-500 text-xs mt-2">
💡 If you recently closed positions with positive P&L, they may take a few minutes to appear
</p>
</div>
) : (
<div className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-700">
<th className="text-left py-3 px-4 text-gray-400 font-medium text-sm">Asset</th>
<th className="text-left py-3 px-4 text-gray-400 font-medium text-sm">Side</th>
<th className="text-right py-3 px-4 text-gray-400 font-medium text-sm">Amount</th>
<th className="text-right py-3 px-4 text-gray-400 font-medium text-sm">Price</th>
<th className="text-center py-3 px-4 text-gray-400 font-medium text-sm">Status</th>
<th className="text-right py-3 px-4 text-gray-400 font-medium text-sm">P&L</th>
<th className="text-right py-3 px-4 text-gray-400 font-medium text-sm">Time</th>
</tr>
</thead>
<tbody>
{trades.map((trade, index) => (
<tr key={trade.id} className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors">
<td className="py-4 px-4">
<div className="flex items-center">
<div className="w-8 h-8 bg-gradient-to-br from-orange-400 to-orange-600 rounded-full flex items-center justify-center mr-3">
<span className="text-white text-xs font-bold">
{trade.symbol.slice(0, 2)}
</span>
</div>
<span className="font-medium text-white">{trade.symbol}</span>
</div>
</td>
<td className="py-4 px-4">
<span className={`font-semibold ${getSideColor(trade.side)}`}>
{trade.side}
</span>
</td>
<td className="py-4 px-4 text-right font-mono text-gray-300">
{trade.amount}
</td>
<td className="py-4 px-4 text-right font-mono text-gray-300">
${trade.price.toLocaleString()}
</td>
<td className="py-4 px-4 text-center">
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
trade.status.toLowerCase() === 'filled' ? 'bg-green-100 text-green-800' :
trade.status.toLowerCase() === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{trade.status}
</span>
</td>
<td className="py-4 px-4 text-right">
<span className={`font-mono font-semibold ${getPnLColor(trade.pnl)}`}>
{trade.pnl ? `${trade.pnl >= 0 ? '+' : ''}$${trade.pnl.toFixed(2)}` : '--'}
</span>
</td>
<td className="py-4 px-4 text-right text-xs text-gray-400">
<div className="text-right">
<div>{formatTime(trade.executedAt)}</div>
<div className="text-gray-500">{formatDate(trade.executedAt)}</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)
}