Files
trading_bot_v3/components/TradingHistory.tsx
mindesbunister 19d4020622 feat: Implement real-time monitoring for Drift trading
 Features Added:
- Real-time event subscription using Drift SDK EventSubscriber
- Periodic fallback monitoring for position changes
- Interactive UI controls for starting/stopping monitoring
- Comprehensive data source status tracking
- Multi-source trade aggregation and deduplication

🔧 Backend Implementation:
- EventSubscriber integration with OrderActionRecord events
- Fallback to periodic monitoring (30s intervals) if events fail
- Real-time trade cache management (last 100 trades)
- Enhanced data availability status with monitoring state
- Improved trade history from 5+ different API sources

🎨 Frontend Enhancements:
- Live monitoring toggle button (🔴 Start Live / 🟢 Live)
- Real-time status panel showing active monitoring state
- Trade counter and last activity timestamps
- Clear cache functionality for real-time trades
- Enhanced status modal with monitoring details

🔗 API Endpoints:
- POST /api/drift/realtime-monitoring - Control monitoring
- GET /api/drift/realtime-monitoring - Check status
- GET /api/drift/data-status - Enhanced with monitoring state

🐳 Docker Integration:
- Updated container configuration for persistent monitoring
- Environment variable support for real-time features
- Database persistence for captured trades

💾 Database & Storage:
- Automatic storage of real-time detected trades
- Deduplication logic to prevent synthetic/duplicate trades
- Persistent cache across container restarts

🚀 Usage:
- Click 'Start Live' button in Trading History panel
- Monitor will attempt EventSubscriber, fallback to periodic checks
- All future trades automatically captured and stored
- Status panel shows monitoring state and trade statistics

This implements comprehensive real-time trading monitoring for Drift Protocol with robust fallback mechanisms and professional UI integration.
2025-07-13 13:29:10 +02:00

470 lines
18 KiB
TypeScript
Raw Permalink 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.
This file contains Unicode characters that might be confused with other characters. 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
}
interface DataSource {
name: string
available: boolean
description: string
}
interface DataStatus {
status: string
sources: DataSource[]
recommendations: string[]
}
interface RealtimeStatus {
isActive: boolean
tradesCount: number
lastTradeTime?: string
}
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)
const [dataStatus, setDataStatus] = useState<DataStatus | null>(null)
const [showStatus, setShowStatus] = useState(false)
const [realtimeStatus, setRealtimeStatus] = useState<RealtimeStatus | null>(null)
const [realtimeLoading, setRealtimeLoading] = 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.')
}
}
const fetchDataStatus = async () => {
try {
const response = await fetch('/api/drift/data-status')
if (response.ok) {
const status = await response.json()
setDataStatus(status)
}
} catch (error) {
console.error('Failed to fetch data status:', error)
}
}
const fetchRealtimeStatus = async () => {
try {
const response = await fetch('/api/drift/realtime-monitoring')
if (response.ok) {
const data = await response.json()
setRealtimeStatus(data.status)
}
} catch (error) {
console.error('Failed to fetch realtime status:', error)
}
}
const handleRealtimeToggle = async () => {
try {
setRealtimeLoading(true)
const action = realtimeStatus?.isActive ? 'stop' : 'start'
const response = await fetch('/api/drift/realtime-monitoring', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action })
})
if (response.ok) {
const data = await response.json()
setRealtimeStatus(data.status)
setMessage(data.message)
// If we started monitoring, refresh trades after a moment
if (action === 'start') {
setTimeout(async () => {
await fetchTrades()
}, 2000)
}
} else {
const errorData = await response.json()
setError(errorData.error || 'Failed to toggle real-time monitoring')
}
} catch (error) {
console.error('Real-time toggle error:', error)
setError('Error toggling real-time monitoring')
} finally {
setRealtimeLoading(false)
}
}
const handleClearRealtimeCache = async () => {
try {
const response = await fetch('/api/drift/realtime-monitoring', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action: 'clear' })
})
if (response.ok) {
const data = await response.json()
setRealtimeStatus(data.status)
setMessage(data.message)
await fetchTrades() // Refresh to show cleared state
}
} catch (error) {
console.error('Clear cache error:', error)
setError('Error clearing real-time cache')
}
}
useEffect(() => {
async function loadTrades() {
setLoading(true)
await fetchTrades()
await fetchDataStatus()
await fetchRealtimeStatus()
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={() => setShowStatus(!showStatus)}
className="text-xs text-gray-400 hover:text-gray-300 transition-colors"
title="Show data source status"
>
Status
</button>
<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={handleRealtimeToggle}
disabled={realtimeLoading || loading}
className={`text-xs transition-colors disabled:text-gray-500 ${
realtimeStatus?.isActive
? 'text-green-400 hover:text-green-300'
: 'text-yellow-400 hover:text-yellow-300'
}`}
title={realtimeStatus?.isActive ? 'Stop real-time monitoring' : 'Start real-time monitoring'}
>
{realtimeLoading ? '⏳' : realtimeStatus?.isActive ? '🟢 Live' : '🔴 Start Live'}
</button>
{realtimeStatus?.isActive && realtimeStatus.tradesCount > 0 && (
<button
onClick={handleClearRealtimeCache}
className="text-xs text-orange-400 hover:text-orange-300 transition-colors"
title="Clear real-time trades cache"
>
🗑 Clear
</button>
)}
<button
onClick={() => window.location.reload()}
className="text-xs text-gray-400 hover:text-gray-300 transition-colors"
title="Refresh page"
>
</button>
</div>
</div>
{showStatus && dataStatus && (
<div className="mb-6 p-4 bg-gray-800/50 rounded-lg border border-gray-700">
<h3 className="text-sm font-semibold text-white mb-3">Data Source Status: {dataStatus.status}</h3>
{/* Real-time Monitoring Status */}
{realtimeStatus && (
<div className="mb-4 p-3 bg-gray-700/30 rounded border-l-4 border-blue-500">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-white">Real-time Monitoring</h4>
<span className={`text-xs px-2 py-1 rounded ${
realtimeStatus.isActive ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'
}`}>
{realtimeStatus.isActive ? '🟢 ACTIVE' : '🔴 INACTIVE'}
</span>
</div>
<div className="grid grid-cols-2 gap-4 text-xs">
<div>
<span className="text-gray-400">Tracked Trades:</span>
<span className="ml-2 text-white">{realtimeStatus.tradesCount}</span>
</div>
<div>
<span className="text-gray-400">Last Activity:</span>
<span className="ml-2 text-white">
{realtimeStatus.lastTradeTime
? formatTime(realtimeStatus.lastTradeTime)
: 'None'
}
</span>
</div>
</div>
{realtimeStatus.isActive && (
<p className="text-xs text-blue-400 mt-2">
📡 Monitoring for new trades automatically
</p>
)}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
{dataStatus.sources.map((source, index) => (
<div key={index} className="flex items-center justify-between p-2 bg-gray-700/50 rounded">
<div>
<span className="text-sm font-medium text-white">{source.name}</span>
<p className="text-xs text-gray-400">{source.description}</p>
</div>
<span className={`text-xs px-2 py-1 rounded ${
source.available ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
{source.available ? 'Available' : 'Limited'}
</span>
</div>
))}
</div>
<div className="border-t border-gray-600 pt-3">
<h4 className="text-sm font-medium text-white mb-2">Important Notes:</h4>
<ul className="space-y-1">
{dataStatus.recommendations.map((rec, index) => (
<li key={index} className="text-xs text-gray-400 flex items-start">
<span className="mr-2"></span>
<span>{rec}</span>
</li>
))}
</ul>
</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 Available</p>
<p className="text-gray-500 text-sm mt-2">{message || 'Your completed trades will appear here'}</p>
<div className="mt-4 space-y-2 text-xs text-gray-500">
<p>💡 <strong>Why no history?</strong></p>
<ul className="ml-4 space-y-1">
<li> Complete historical trading data is not publicly accessible via Drift APIs</li>
<li> Only current positions and recent fills are available</li>
<li> Historical S3 data stopped updating in January 2025</li>
</ul>
<p className="mt-3">📋 <strong>For full trade history:</strong></p>
<ul className="ml-4 space-y-1">
<li> Visit the official <a href="https://app.drift.trade" target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:text-blue-300">Drift app</a></li>
<li> Enable real-time monitoring for future trades</li>
<li> Click "Status" above for detailed data source information</li>
</ul>
</div>
</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>
)
}