feat: implement real-time price monitoring with automatic analysis triggering
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.
This commit is contained in:
330
components/RealTimePriceMonitor.tsx
Normal file
330
components/RealTimePriceMonitor.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user