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.
This commit is contained in:
mindesbunister
2025-07-13 13:29:10 +02:00
parent bf9022b699
commit 19d4020622
6 changed files with 1321 additions and 105 deletions

View File

@@ -67,4 +67,4 @@ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Start the app
CMD ["npm", "run", "dev"]
CMD ["npm", "run", "dev:docker"]

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from 'next/server'
import { driftTradingService } from '../../../../lib/drift-trading'
export async function GET(request: NextRequest) {
try {
const status = await driftTradingService.getDataAvailabilityStatus()
return NextResponse.json(status)
} catch (error) {
console.error('Error getting data status:', error)
// Return fallback status
return NextResponse.json({
status: 'Error Checking Status',
sources: [
{
name: 'System Check',
available: false,
description: 'Unable to check data source availability'
}
],
recommendations: [
'Try refreshing the page',
'Check your internet connection',
'Contact support if the issue persists'
]
})
}
}

View File

@@ -0,0 +1,90 @@
import { NextRequest, NextResponse } from 'next/server'
import { driftTradingService } from '../../../../lib/drift-trading'
export async function POST(request: NextRequest) {
try {
const { action } = await request.json()
if (action === 'start') {
console.log('🚀 Starting real-time monitoring...')
const result = await driftTradingService.startRealtimeMonitoring()
if (result.success) {
return NextResponse.json({
success: true,
message: 'Real-time monitoring started successfully',
status: driftTradingService.getRealtimeMonitoringStatus()
})
} else {
return NextResponse.json({
success: false,
error: result.error,
message: 'Failed to start real-time monitoring'
}, { status: 500 })
}
} else if (action === 'stop') {
console.log('🛑 Stopping real-time monitoring...')
await driftTradingService.stopRealtimeMonitoring()
return NextResponse.json({
success: true,
message: 'Real-time monitoring stopped',
status: driftTradingService.getRealtimeMonitoringStatus()
})
} else if (action === 'status') {
const status = driftTradingService.getRealtimeMonitoringStatus()
return NextResponse.json({
success: true,
status,
message: status.isActive ? 'Real-time monitoring is active' : 'Real-time monitoring is not active'
})
} else if (action === 'clear') {
driftTradingService.clearRealtimeTradesCache()
return NextResponse.json({
success: true,
message: 'Real-time trades cache cleared',
status: driftTradingService.getRealtimeMonitoringStatus()
})
} else {
return NextResponse.json({
success: false,
error: 'Invalid action. Use: start, stop, status, or clear'
}, { status: 400 })
}
} catch (error: any) {
console.error('❌ Error in realtime monitoring endpoint:', error)
return NextResponse.json({
success: false,
error: error.message,
message: 'Internal server error'
}, { status: 500 })
}
}
export async function GET(request: NextRequest) {
try {
const status = driftTradingService.getRealtimeMonitoringStatus()
return NextResponse.json({
success: true,
status,
message: status.isActive ? 'Real-time monitoring is active' : 'Real-time monitoring is not active'
})
} catch (error: any) {
console.error('❌ Error getting monitoring status:', error)
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 })
}
}

View File

@@ -12,6 +12,24 @@ interface Trade {
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)
@@ -19,6 +37,10 @@ export default function TradingHistory() {
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)
@@ -98,10 +120,94 @@ export default function TradingHistory() {
}
}
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()
@@ -136,6 +242,13 @@ export default function TradingHistory() {
</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}
@@ -144,6 +257,27 @@ export default function TradingHistory() {
>
{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"
@@ -154,6 +288,74 @@ export default function TradingHistory() {
</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>
@@ -179,11 +381,22 @@ export default function TradingHistory() {
<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-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>
<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 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">

View File

@@ -15,21 +15,28 @@ services:
# Playwright/TradingView automation settings
- CHROMIUM_PATH=/usr/bin/chromium
- DISABLE_CHROME_SANDBOX=true
- DISPLAY=:99
- DISPLAY=${DISPLAY:-:0}
# CAPTCHA handling
- ALLOW_MANUAL_CAPTCHA=true
# Database configuration
- DATABASE_URL=file:./prisma/dev.db
# Load environment variables from .env file
env_file:
- .env
# Default port mapping
ports:
- "3000:3000"
# Base volumes
volumes:
- ./screenshots:/app/screenshots
- ./videos:/app/videos
- ./.tradingview-session:/app/.tradingview-session
- ./prisma:/app/prisma
# X11 forwarding for GUI display (when ALLOW_MANUAL_CAPTCHA=true)
- /tmp/.X11-unix:/tmp/.X11-unix:rw
# X11 and display configuration for manual CAPTCHA solving
network_mode: host
privileged: true
# Health check
healthcheck:

File diff suppressed because it is too large Load Diff