New Features: - Real-time price monitoring service with 5-minute update cycles - Automatic analysis triggering when prices approach TP/SL levels (15%/25% thresholds) - Comprehensive price monitoring UI component with live updates - Integration with automation service for smart analysis scheduling - PnL tracking and position status monitoring - EventEmitter-based real-time updates - CoinGecko API integration with rate limiting - TP/SL approach detection with configurable thresholds - Alert system for critical price movements - Database integration for trade tracking - Price monitor startup/shutdown with automation lifecycle - Event listeners for TP_APPROACH, SL_APPROACH, CRITICAL alerts - Automatic screenshot capture and AI analysis on price triggers - Enhanced progress tracking for price-based analysis - Intelligent analysis context with price movement data - RealTimePriceMonitor component with live price display - Trade monitoring cards with P&L and distance to TP/SL - Active alerts panel with price threshold notifications - Monitoring service controls (start/stop/force update) - Integration with automation page for comprehensive oversight - GET: Retrieve monitoring data, alerts, and current prices - POST: Control monitoring service and force price updates - Real-time data formatting and status management - Comprehensive price monitor integration tests - Basic functionality validation scripts - API endpoint testing capabilities This implements the user's request for real-time price monitoring with automatic analysis triggering when prices approach critical levels, providing enhanced oversight of active trading positions.
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
"use client"
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
interface TradeMonitoring {
|
|
tradeId: string
|
|
symbol: string
|
|
side: 'BUY' | 'SELL'
|
|
entryPrice: number
|
|
stopLoss?: number
|
|
takeProfit?: number
|
|
currentPrice?: number
|
|
currentPnL?: number
|
|
pnlPercentage?: number
|
|
distanceToTP?: number
|
|
distanceToSL?: number
|
|
status: 'ACTIVE' | 'APPROACHING_TP' | 'APPROACHING_SL' | 'CRITICAL'
|
|
}
|
|
|
|
interface PriceAlert {
|
|
id: string
|
|
symbol: string
|
|
tradeId: string
|
|
alertType: 'TP_APPROACH' | 'SL_APPROACH' | 'BREAKOUT' | 'REVERSAL'
|
|
currentPrice: number
|
|
targetPrice: number
|
|
threshold: number
|
|
triggered: boolean
|
|
createdAt: string
|
|
}
|
|
|
|
export default function RealTimePriceMonitor() {
|
|
const [monitoringData, setMonitoringData] = useState<TradeMonitoring[]>([])
|
|
const [alerts, setAlerts] = useState<PriceAlert[]>([])
|
|
const [prices, setPrices] = useState<Record<string, number>>({})
|
|
const [lastUpdated, setLastUpdated] = useState<string>('')
|
|
const [monitoringActive, setMonitoringActive] = useState(false)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
fetchMonitoringData()
|
|
|
|
// Update every 30 seconds for UI refresh
|
|
const interval = setInterval(fetchMonitoringData, 30000)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
const fetchMonitoringData = async () => {
|
|
try {
|
|
const response = await fetch('/api/price-monitor')
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
setMonitoringData(data.data.trades || [])
|
|
setAlerts(data.data.alerts || [])
|
|
setPrices(data.data.prices || {})
|
|
setLastUpdated(data.data.lastUpdated || '')
|
|
setMonitoringActive(data.data.monitoringActive || false)
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching monitoring data:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const startMonitoring = async () => {
|
|
try {
|
|
const response = await fetch('/api/price-monitor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'start_monitoring' })
|
|
})
|
|
|
|
if (response.ok) {
|
|
setMonitoringActive(true)
|
|
await fetchMonitoringData()
|
|
}
|
|
} catch (error) {
|
|
console.error('Error starting monitoring:', error)
|
|
}
|
|
}
|
|
|
|
const stopMonitoring = async () => {
|
|
try {
|
|
const response = await fetch('/api/price-monitor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'stop_monitoring' })
|
|
})
|
|
|
|
if (response.ok) {
|
|
setMonitoringActive(false)
|
|
}
|
|
} catch (error) {
|
|
console.error('Error stopping monitoring:', error)
|
|
}
|
|
}
|
|
|
|
const forceUpdatePrice = async (symbol: string) => {
|
|
try {
|
|
const response = await fetch('/api/price-monitor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'force_update', symbol })
|
|
})
|
|
|
|
if (response.ok) {
|
|
await fetchMonitoringData()
|
|
}
|
|
} catch (error) {
|
|
console.error('Error force updating price:', error)
|
|
}
|
|
}
|
|
|
|
const getStatusColor = (status: TradeMonitoring['status']) => {
|
|
switch (status) {
|
|
case 'CRITICAL': return 'text-red-400 bg-red-900/20 border-red-600'
|
|
case 'APPROACHING_TP': return 'text-green-400 bg-green-900/20 border-green-600'
|
|
case 'APPROACHING_SL': return 'text-yellow-400 bg-yellow-900/20 border-yellow-600'
|
|
default: return 'text-blue-400 bg-blue-900/20 border-blue-600'
|
|
}
|
|
}
|
|
|
|
const getPnLColor = (pnl?: number) => {
|
|
if (!pnl) return 'text-gray-400'
|
|
return pnl >= 0 ? 'text-green-400' : 'text-red-400'
|
|
}
|
|
|
|
const formatCurrency = (amount?: number) => {
|
|
if (amount === undefined || amount === null) return 'N/A'
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 4
|
|
}).format(amount)
|
|
}
|
|
|
|
const formatPercentage = (pct?: number) => {
|
|
if (pct === undefined || pct === null) return 'N/A'
|
|
return `${pct >= 0 ? '+' : ''}${pct.toFixed(2)}%`
|
|
}
|
|
|
|
const formatDistance = (distance?: number) => {
|
|
if (distance === undefined || distance === null) return 'N/A'
|
|
return `${(distance * 100).toFixed(1)}%`
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="card card-gradient p-6">
|
|
<div className="animate-pulse">
|
|
<div className="h-6 bg-gray-700 rounded mb-4"></div>
|
|
<div className="space-y-3">
|
|
<div className="h-4 bg-gray-700 rounded"></div>
|
|
<div className="h-4 bg-gray-700 rounded"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header Controls */}
|
|
<div className="card card-gradient p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-white">Real-Time Price Monitor</h2>
|
|
<p className="text-gray-400 text-sm">
|
|
Tracks active trades and triggers analysis when approaching TP/SL
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
<div className={`px-3 py-1 rounded-full text-xs font-medium ${
|
|
monitoringActive ? 'bg-green-600 text-white' : 'bg-gray-600 text-gray-300'
|
|
}`}>
|
|
{monitoringActive ? '🟢 Active' : '🔴 Stopped'}
|
|
</div>
|
|
{monitoringActive ? (
|
|
<button
|
|
onClick={stopMonitoring}
|
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium"
|
|
>
|
|
Stop Monitor
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={startMonitoring}
|
|
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium"
|
|
>
|
|
Start Monitor
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{lastUpdated && (
|
|
<div className="text-xs text-gray-400">
|
|
Last updated: {new Date(lastUpdated).toLocaleString()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Active Alerts */}
|
|
{alerts.length > 0 && (
|
|
<div className="card card-gradient p-6">
|
|
<h3 className="text-lg font-bold text-white mb-4">🚨 Active Alerts</h3>
|
|
<div className="space-y-3">
|
|
{alerts.map((alert) => (
|
|
<div key={alert.id} className="p-4 bg-red-900/20 border border-red-600 rounded-lg">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<span className="font-semibold text-red-400">{alert.symbol}</span>
|
|
<span className="ml-2 text-sm text-gray-300">
|
|
{alert.alertType.replace('_', ' ')}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm text-gray-300">
|
|
Current: {formatCurrency(alert.currentPrice)} → Target: {formatCurrency(alert.targetPrice)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Current Prices */}
|
|
<div className="card card-gradient p-6">
|
|
<h3 className="text-lg font-bold text-white mb-4">💰 Current Prices</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{Object.entries(prices).map(([symbol, price]) => (
|
|
<div key={symbol} className="p-3 bg-gray-800 rounded-lg border border-gray-700">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-gray-300 text-sm">{symbol}</span>
|
|
<button
|
|
onClick={() => forceUpdatePrice(symbol)}
|
|
className="text-xs text-blue-400 hover:text-blue-300"
|
|
>
|
|
🔄
|
|
</button>
|
|
</div>
|
|
<div className="text-white font-bold text-lg">
|
|
{formatCurrency(price)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active Trades Monitoring */}
|
|
<div className="card card-gradient p-6">
|
|
<h3 className="text-lg font-bold text-white mb-4">📊 Active Trades Monitor</h3>
|
|
|
|
{monitoringData.length === 0 ? (
|
|
<p className="text-gray-400">No active trades to monitor</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{monitoringData.map((trade) => (
|
|
<div key={trade.tradeId} className="p-4 bg-gray-800 rounded-lg border border-gray-700">
|
|
{/* Trade Header */}
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center space-x-3">
|
|
<span className={`font-semibold px-2 py-1 rounded text-xs ${
|
|
trade.side === 'BUY' ? 'bg-green-600 text-white' : 'bg-red-600 text-white'
|
|
}`}>
|
|
{trade.side}
|
|
</span>
|
|
<span className="text-white font-bold">{trade.symbol}</span>
|
|
<span className={`px-2 py-1 rounded text-xs border ${getStatusColor(trade.status)}`}>
|
|
{trade.status.replace('_', ' ')}
|
|
</span>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-white font-semibold">
|
|
Current: {formatCurrency(trade.currentPrice)}
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
Entry: {formatCurrency(trade.entryPrice)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trade Details Grid */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<div className="text-gray-400">P&L</div>
|
|
<div className={`font-semibold ${getPnLColor(trade.currentPnL)}`}>
|
|
{formatCurrency(trade.currentPnL)}
|
|
</div>
|
|
<div className={`text-xs ${getPnLColor(trade.currentPnL)}`}>
|
|
{formatPercentage(trade.pnlPercentage)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-gray-400">Take Profit</div>
|
|
<div className="text-white">{formatCurrency(trade.takeProfit)}</div>
|
|
<div className="text-xs text-green-400">
|
|
{trade.distanceToTP ? `${formatDistance(trade.distanceToTP)} away` : 'N/A'}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-gray-400">Stop Loss</div>
|
|
<div className="text-white">{formatCurrency(trade.stopLoss)}</div>
|
|
<div className="text-xs text-red-400">
|
|
{trade.distanceToSL ? `${formatDistance(trade.distanceToSL)} away` : 'N/A'}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-gray-400">Status</div>
|
|
<div className="text-white font-medium">
|
|
{trade.status === 'CRITICAL' ? '🚨 Critical' :
|
|
trade.status === 'APPROACHING_TP' ? '🎯 Near TP' :
|
|
trade.status === 'APPROACHING_SL' ? '⚠️ Near SL' : '✅ Normal'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|