Implement API health check badge in NavBar, enhance PlatformLineChart with metric filtering, and refactor PlatformSummary for improved layout and styling. Update dashboard to prioritize Platform Summary tab.
This commit is contained in:
@@ -9,6 +9,12 @@ import Logo from '../../../assets/img/logo.png'
|
||||
import {Loader} from '../../atoms'
|
||||
import useCookie from '../../../hooks/useCookie'
|
||||
|
||||
interface HealthCheckResponse {
|
||||
status: string
|
||||
totalDuration?: string
|
||||
entries?: Record<string, any>
|
||||
}
|
||||
|
||||
const navigation = [
|
||||
{ href: '/bots', name: 'Strategies' },
|
||||
{ href: '/backtest', name: 'Backtests' },
|
||||
@@ -18,13 +24,83 @@ const navigation = [
|
||||
{ href: '/admin', name: 'Admin' },
|
||||
]
|
||||
|
||||
// API Health Check Badge Component
|
||||
const ApiHealthBadge = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
|
||||
const { data: healthData, isLoading, error } = useQuery<HealthCheckResponse>({
|
||||
queryKey: ['apiHealth', apiUrl],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/health`)
|
||||
if (!response.ok) throw new Error('Health check failed')
|
||||
const data = await response.json()
|
||||
// Handle .NET health check response format
|
||||
// The status might be in the root or we need to check entries
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Health check error:', error)
|
||||
return { status: 'Unhealthy' }
|
||||
}
|
||||
},
|
||||
refetchInterval: 30000, // Refetch every 30 seconds
|
||||
retry: 1,
|
||||
staleTime: 20000,
|
||||
})
|
||||
|
||||
const getHealthStatus = () => {
|
||||
if (isLoading) return { color: 'badge-ghost', label: '...', status: 'Checking...' }
|
||||
if (error) return { color: 'badge-error', label: '●', status: 'Health Check Failed' }
|
||||
|
||||
// Check status field (could be "Healthy", "Degraded", "Unhealthy")
|
||||
const status = healthData?.status || 'Unknown'
|
||||
const statusLower = status.toLowerCase()
|
||||
|
||||
// Also check entries if status is not directly available
|
||||
let overallStatus = statusLower
|
||||
if (healthData?.entries) {
|
||||
const entries = Object.values(healthData.entries)
|
||||
const hasUnhealthy = entries.some((entry: any) => entry?.status?.toLowerCase() === 'unhealthy')
|
||||
const hasDegraded = entries.some((entry: any) => entry?.status?.toLowerCase() === 'degraded')
|
||||
|
||||
if (hasUnhealthy) {
|
||||
overallStatus = 'unhealthy'
|
||||
} else if (hasDegraded) {
|
||||
overallStatus = 'degraded'
|
||||
} else if (entries.length > 0) {
|
||||
overallStatus = 'healthy'
|
||||
}
|
||||
}
|
||||
|
||||
switch (overallStatus) {
|
||||
case 'healthy':
|
||||
return { color: 'badge-success', label: '●', status: 'API Status: Healthy' }
|
||||
case 'degraded':
|
||||
return { color: 'badge-warning', label: '●', status: 'API Status: Degraded' }
|
||||
case 'unhealthy':
|
||||
return { color: 'badge-error', label: '●', status: 'API Status: Unhealthy' }
|
||||
default:
|
||||
return { color: 'badge-ghost', label: '?', status: 'API Status: Unknown' }
|
||||
}
|
||||
}
|
||||
|
||||
const healthStatus = getHealthStatus()
|
||||
|
||||
return (
|
||||
<div className="tooltip tooltip-bottom" data-tip={healthStatus.status}>
|
||||
<div className={`badge ${healthStatus.color} badge-xs cursor-help p-0.5`}>
|
||||
<span className="text-[8px] leading-none">{healthStatus.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Global Loader Component
|
||||
const GlobalLoader = () => {
|
||||
const isFetching = useIsFetching()
|
||||
return isFetching ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader size="xs"></Loader>
|
||||
<span className="text-xs text-base-content/70 hidden lg:inline">Loading...</span>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
@@ -279,7 +355,7 @@ export default function NavBar() {
|
||||
{/* Navbar */}
|
||||
<div className="navbar bg-base-300 shadow-lg border-b border-base-content/10 w-full relative z-50">
|
||||
{/* Navbar Start - Mobile Menu Button and Logo */}
|
||||
<div className="navbar-start">
|
||||
<div className="navbar-start flex items-center gap-2">
|
||||
<button
|
||||
className="btn btn-ghost lg:hidden"
|
||||
onClick={toggleSidebar}
|
||||
@@ -298,6 +374,7 @@ export default function NavBar() {
|
||||
<Link className="btn btn-ghost text-xl" to="/">
|
||||
<img src={Logo} className="h-8 w-8 object-contain" alt="logo" />
|
||||
</Link>
|
||||
<ApiHealthBadge />
|
||||
</div>
|
||||
|
||||
{/* Navbar Center - Desktop Navigation */}
|
||||
@@ -326,7 +403,6 @@ export default function NavBar() {
|
||||
<div className="navbar-end gap-2">
|
||||
<GlobalLoader />
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<span className="text-xs opacity-70 hidden xl:inline">Environment:</span>
|
||||
<EnvironmentDropdown isInSidebar={false} />
|
||||
</div>
|
||||
{/* Show environment badge on mobile */}
|
||||
@@ -394,9 +470,6 @@ export default function NavBar() {
|
||||
{authenticated && (
|
||||
<li>
|
||||
<div className="px-4 py-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm opacity-70">Environment:</span>
|
||||
</div>
|
||||
<EnvironmentDropdown isInSidebar={true} />
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -14,12 +14,15 @@ interface CurrentPlatformData {
|
||||
lastUpdated?: Date | string
|
||||
}
|
||||
|
||||
type MetricType = 'agents' | 'strategies' | 'volume' | 'pnl' | 'openInterest' | 'positionCount'
|
||||
|
||||
interface PlatformLineChartProps {
|
||||
dailySnapshots: DailySnapshot[]
|
||||
currentData?: CurrentPlatformData
|
||||
title?: string
|
||||
height?: number
|
||||
width?: number
|
||||
metrics?: MetricType[] // Filter which metrics to display
|
||||
}
|
||||
|
||||
function PlatformLineChart({
|
||||
@@ -27,7 +30,8 @@ function PlatformLineChart({
|
||||
currentData,
|
||||
title = "Platform Metrics Over Time",
|
||||
height = 400,
|
||||
width = 800
|
||||
width = 800,
|
||||
metrics = ['agents', 'strategies', 'volume', 'pnl', 'openInterest', 'positionCount'] // Default: show all
|
||||
}: PlatformLineChartProps) {
|
||||
const theme = useTheme().themeProperty()
|
||||
|
||||
@@ -78,13 +82,15 @@ function PlatformLineChart({
|
||||
return `$${value.toFixed(0)}`
|
||||
}
|
||||
|
||||
const plotData = [
|
||||
const allPlotData = [
|
||||
{
|
||||
key: 'agents' as MetricType,
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Total Agents',
|
||||
x: dates,
|
||||
y: agents,
|
||||
yaxis: 'y' as const,
|
||||
line: {
|
||||
color: theme.primary,
|
||||
width: 2
|
||||
@@ -98,11 +104,13 @@ function PlatformLineChart({
|
||||
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
|
||||
},
|
||||
{
|
||||
key: 'strategies' as MetricType,
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Active Strategies',
|
||||
x: dates,
|
||||
y: strategies,
|
||||
yaxis: 'y' as const,
|
||||
line: {
|
||||
color: theme.success,
|
||||
width: 2
|
||||
@@ -116,12 +124,13 @@ function PlatformLineChart({
|
||||
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
|
||||
},
|
||||
{
|
||||
key: 'volume' as MetricType,
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Total Volume',
|
||||
x: dates,
|
||||
y: volume,
|
||||
yaxis: 'y2',
|
||||
yaxis: 'y2' as const,
|
||||
line: {
|
||||
color: theme.info,
|
||||
width: 2
|
||||
@@ -135,12 +144,13 @@ function PlatformLineChart({
|
||||
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
|
||||
},
|
||||
{
|
||||
key: 'pnl' as MetricType,
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Platform PnL',
|
||||
x: dates,
|
||||
y: pnl,
|
||||
yaxis: 'y2',
|
||||
yaxis: 'y2' as const,
|
||||
line: {
|
||||
color: pnl.some(p => p >= 0) ? theme.success : theme.error,
|
||||
width: 2
|
||||
@@ -154,12 +164,13 @@ function PlatformLineChart({
|
||||
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
|
||||
},
|
||||
{
|
||||
key: 'openInterest' as MetricType,
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Open Interest',
|
||||
x: dates,
|
||||
y: openInterest,
|
||||
yaxis: 'y2',
|
||||
yaxis: 'y2' as const,
|
||||
line: {
|
||||
color: theme.warning,
|
||||
width: 2
|
||||
@@ -173,11 +184,13 @@ function PlatformLineChart({
|
||||
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
|
||||
},
|
||||
{
|
||||
key: 'positionCount' as MetricType,
|
||||
type: 'scatter' as const,
|
||||
mode: 'lines+markers' as const,
|
||||
name: 'Position Count',
|
||||
x: dates,
|
||||
y: positionCount,
|
||||
yaxis: 'y' as const,
|
||||
line: {
|
||||
color: theme.secondary,
|
||||
width: 2
|
||||
@@ -192,6 +205,11 @@ function PlatformLineChart({
|
||||
}
|
||||
]
|
||||
|
||||
// Filter plot data based on metrics prop and remove key property
|
||||
const plotData = allPlotData
|
||||
.filter(item => metrics.includes(item.key))
|
||||
.map(({ key, ...rest }) => rest)
|
||||
|
||||
const layout = {
|
||||
title: {
|
||||
text: title,
|
||||
@@ -214,7 +232,7 @@ function PlatformLineChart({
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'Count',
|
||||
text: plotData.some(d => d.yaxis === 'y') ? 'Count' : '',
|
||||
font: {
|
||||
color: theme['base-content'] || '#ffffff'
|
||||
}
|
||||
@@ -223,11 +241,12 @@ function PlatformLineChart({
|
||||
gridcolor: theme.neutral || '#333333',
|
||||
showgrid: true,
|
||||
zeroline: false,
|
||||
side: 'left' as const
|
||||
side: 'left' as const,
|
||||
visible: plotData.some(d => d.yaxis === 'y')
|
||||
},
|
||||
yaxis2: {
|
||||
title: {
|
||||
text: 'Amount ($)',
|
||||
text: plotData.some(d => d.yaxis === 'y2') ? 'Amount ($)' : '',
|
||||
font: {
|
||||
color: theme['base-content'] || '#ffffff'
|
||||
}
|
||||
@@ -238,7 +257,8 @@ function PlatformLineChart({
|
||||
zeroline: false,
|
||||
side: 'right' as const,
|
||||
overlaying: 'y' as const,
|
||||
tickformat: '$,.0f'
|
||||
tickformat: '$,.0f',
|
||||
visible: plotData.some(d => d.yaxis === 'y2')
|
||||
},
|
||||
legend: {
|
||||
font: {
|
||||
|
||||
@@ -13,43 +13,44 @@ import PlatformSummary from './platformSummary'
|
||||
|
||||
const tabs: ITabsType = [
|
||||
{
|
||||
Component: Monitoring,
|
||||
Component: PlatformSummary,
|
||||
index: 1,
|
||||
label: 'Platform Summary',
|
||||
},
|
||||
{
|
||||
Component: Monitoring,
|
||||
index: 2,
|
||||
label: 'Monitoring',
|
||||
},
|
||||
{
|
||||
Component: Analytics,
|
||||
index: 2,
|
||||
index: 3,
|
||||
label: 'Analytics',
|
||||
},
|
||||
{
|
||||
Component: BestAgents,
|
||||
index: 3,
|
||||
index: 4,
|
||||
label: 'Best Agents',
|
||||
},
|
||||
{
|
||||
Component: AgentSearch,
|
||||
index: 4,
|
||||
index: 5,
|
||||
label: 'Agent Search',
|
||||
},
|
||||
{
|
||||
Component: AgentIndex,
|
||||
index: 5,
|
||||
index: 6,
|
||||
label: 'Agent Index',
|
||||
},
|
||||
{
|
||||
Component: AgentStrategy,
|
||||
index: 6,
|
||||
label: 'Agent Strategy',
|
||||
},
|
||||
{
|
||||
Component: PlatformSummary,
|
||||
index: 7,
|
||||
label: 'Platform Summary',
|
||||
label: 'Agent Strategy',
|
||||
},
|
||||
]
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
// Platform Summary is now the first tab (index 1)
|
||||
const [selectedTab, setSelectedTab] = useState<number>(tabs[0].index)
|
||||
|
||||
useEffect(() => {}, [])
|
||||
|
||||
@@ -77,19 +77,19 @@ function PlatformSummary({index}: { index: number }) {
|
||||
const formatChangeIndicator = (change: number) => {
|
||||
if (change > 0) {
|
||||
return (
|
||||
<span className="text-green-500">
|
||||
<span className="text-success">
|
||||
+{change} Today
|
||||
</span>
|
||||
)
|
||||
} else if (change < 0) {
|
||||
return (
|
||||
<span className="text-red-500">
|
||||
<span className="text-error">
|
||||
{change} Today
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="text-gray-500">
|
||||
<span className="text-base-content/60">
|
||||
No change
|
||||
</span>
|
||||
)
|
||||
@@ -130,37 +130,20 @@ function PlatformSummary({index}: { index: number }) {
|
||||
return (
|
||||
<div className="p-6 bg-base-100 min-h-screen">
|
||||
{/* Subtle refetching indicator */}
|
||||
{isFetching && data && (
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<div className="bg-blue-500 text-white px-3 py-1 rounded-full text-sm flex items-center gap-2">
|
||||
<div className="loading loading-spinner loading-xs"></div>
|
||||
Updating...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-2">
|
||||
{formatNumber(platformData?.totalActiveStrategies || 0)} Strategies Deployed
|
||||
</h1>
|
||||
<div className="text-lg">
|
||||
{platformData && formatChangeIndicator(changesToday.strategiesChange)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Stats Grid */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{/* Total Volume Traded */}
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Volume Traded</h3>
|
||||
<div className="text-3xl font-bold text-white mb-1">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70">Total Volume Traded</h3>
|
||||
<div className="text-3xl font-bold text-base-content mb-1">
|
||||
{formatCurrency(platformData?.totalPlatformVolume || 0)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm ${changesToday.volumeChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm ${changesToday.volumeChange >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{changesToday.volumeChange >= 0 ? '+' : ''}{formatCurrency(changesToday.volumeChange)} Today
|
||||
<span className="ml-2 text-gray-400">
|
||||
<span className="ml-2 text-base-content/60">
|
||||
({formatPercentageChange(platformData?.totalPlatformVolume || 0, changesToday.volumeChange)})
|
||||
</span>
|
||||
</div>
|
||||
@@ -178,7 +161,7 @@ function PlatformSummary({index}: { index: number }) {
|
||||
return (
|
||||
<div
|
||||
key={`historical-${index}`}
|
||||
className="flex-1 bg-green-500 rounded-sm opacity-60 hover:opacity-80 transition-opacity"
|
||||
className="flex-1 bg-success rounded-sm opacity-60 hover:opacity-80 transition-opacity"
|
||||
style={{ height: `${Math.max(height, 4)}%` }}
|
||||
title={`${new Date(snapshot.date || 0).toLocaleDateString()}: ${formatCurrency(snapshot.totalVolume || 0)}`}
|
||||
/>
|
||||
@@ -186,7 +169,7 @@ function PlatformSummary({index}: { index: number }) {
|
||||
})}
|
||||
{/* Current data bar */}
|
||||
<div
|
||||
className="flex-1 bg-blue-500 rounded-sm opacity-80 hover:opacity-100 transition-opacity border-2 border-blue-300"
|
||||
className="flex-1 bg-primary rounded-sm opacity-80 hover:opacity-100 transition-opacity border-2 border-primary/50"
|
||||
style={{
|
||||
height: `${(() => {
|
||||
const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0)
|
||||
@@ -198,193 +181,230 @@ function PlatformSummary({index}: { index: number }) {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-8 bg-green-500 rounded opacity-20"></div>
|
||||
<div className="w-full h-8 bg-success rounded opacity-20"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top 3 Most Profitable */}
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-2xl">🤑</span>
|
||||
<h3 className="text-lg font-semibold text-gray-400">Top 3 Most Profitable</h3>
|
||||
<h3 className="card-title text-base-content/70">Top 3 Most Profitable</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-white">
|
||||
<div className="avatar placeholder">
|
||||
<div className="bg-primary text-primary-content rounded-full w-8">
|
||||
<span className="text-xs font-bold">
|
||||
{strategy.strategyName?.charAt(0) || 'S'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-medium">
|
||||
<div className="text-sm text-base-content font-medium">
|
||||
{strategy.strategyName || '[Strategy Name]'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">📧</div>
|
||||
<div className="text-xs text-base-content/60">📧</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-bold ${strategy.netPnL && strategy.netPnL >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm font-bold ${strategy.netPnL && strategy.netPnL >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{strategy.netPnL && strategy.netPnL >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
|
||||
</div>
|
||||
</div>
|
||||
)) || (
|
||||
<div className="text-gray-500 text-sm">No profitable strategies found</div>
|
||||
<div className="text-base-content/60 text-sm">No profitable strategies found</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top 3 Rising (by ROI) */}
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-2xl">📈</span>
|
||||
<h3 className="text-lg font-semibold text-gray-400">Top 3 by ROI</h3>
|
||||
<h3 className="card-title text-base-content/70">Top 3 by ROI</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-white">
|
||||
<div className="avatar placeholder">
|
||||
<div className="bg-success text-success-content rounded-full w-8">
|
||||
<span className="text-xs font-bold">
|
||||
{strategy.strategyName?.charAt(0) || 'S'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-medium">
|
||||
<div className="text-sm text-base-content font-medium">
|
||||
{strategy.strategyName || '[Strategy Name]'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
<div className="text-xs text-base-content/60">
|
||||
Vol: {formatCurrency(strategy.volume || 0)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div
|
||||
className={`text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm font-bold ${(strategy.roi || 0) >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}%
|
||||
</div>
|
||||
<div
|
||||
className={`text-xs ${(strategy.netPnL || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||
className={`text-xs ${(strategy.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{(strategy.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)) || (
|
||||
<div className="text-gray-500 text-sm">No ROI data available</div>
|
||||
<div className="text-base-content/60 text-sm">No ROI data available</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top 3 Agents by PnL */}
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-2xl">👥</span>
|
||||
<h3 className="text-lg font-semibold text-gray-400">Top 3 Agents by PnL</h3>
|
||||
<h3 className="card-title text-base-content/70">Top 3 Agents by PnL</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{topAgentsByPnL?.slice(0, 3).map((agent, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-white">
|
||||
<div className="avatar placeholder">
|
||||
<div className="bg-secondary text-secondary-content rounded-full w-8">
|
||||
<span className="text-xs font-bold">
|
||||
{agent.agentName?.charAt(0) || 'A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-white font-medium">
|
||||
<div className="text-sm text-base-content font-medium">
|
||||
{agent.agentName || '[Agent Name]'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
<div className="text-xs text-base-content/60">
|
||||
{agent.activeStrategiesCount || 0} strategies
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div
|
||||
className={`text-sm font-bold ${(agent.netPnL || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm font-bold ${(agent.netPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{(agent.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(agent.netPnL || 0)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
<div className="text-xs text-base-content/60">
|
||||
{(agent.totalROI || 0).toFixed(2)}% ROI
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)) || (
|
||||
<div className="text-gray-500 text-sm">No agent data available</div>
|
||||
<div className="text-base-content/60 text-sm">No agent data available</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Platform Summary Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-6 mb-8">
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Agents</h3>
|
||||
<div className="text-3xl font-bold text-white mb-1">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-6 mb-8">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Total Agents</h3>
|
||||
<div className="text-3xl font-bold text-base-content mb-1">
|
||||
{formatNumber(platformData?.totalAgents || 0)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm ${changesToday.agentsChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm ${changesToday.agentsChange >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{formatChangeIndicator(changesToday.agentsChange)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Active Strategies</h3>
|
||||
<div className="text-3xl font-bold text-white mb-1">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Strategies Deployed</h3>
|
||||
<div className="text-3xl font-bold text-base-content mb-1">
|
||||
{formatNumber(platformData?.totalActiveStrategies || 0)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm ${changesToday.strategiesChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm ${changesToday.strategiesChange >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{formatChangeIndicator(changesToday.strategiesChange)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Platform PnL</h3>
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Active Strategies</h3>
|
||||
<div className="text-3xl font-bold text-base-content mb-1">
|
||||
{formatNumber(platformData?.totalActiveStrategies || 0)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-3xl font-bold ${(platformData?.totalPlatformPnL || 0) >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm ${changesToday.strategiesChange >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{formatChangeIndicator(changesToday.strategiesChange)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Total Platform PnL</h3>
|
||||
<div
|
||||
className={`text-3xl font-bold ${(platformData?.totalPlatformPnL || 0) >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{(platformData?.totalPlatformPnL || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.totalPlatformPnL || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 mb-1">
|
||||
<div className="text-sm text-base-content/60 mb-1">
|
||||
Fees: {formatCurrency(platformData?.totalPlatformFees || 0)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm ${changesToday.pnLChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm ${changesToday.pnLChange >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{changesToday.pnLChange >= 0 ? '+' : ''}{formatCurrency(changesToday.pnLChange)} Today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Open Interest</h3>
|
||||
<div className="text-3xl font-bold text-white mb-1">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Open Interest</h3>
|
||||
<div className="text-3xl font-bold text-base-content mb-1">
|
||||
{formatCurrency(platformData?.openInterest || 0)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm ${changesToday.openInterestChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm ${changesToday.openInterestChange >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{changesToday.openInterestChange >= 0 ? '+' : ''}{formatCurrency(changesToday.openInterestChange)} Today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Total Positions</h3>
|
||||
<div className="text-3xl font-bold text-white mb-1">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Total Positions</h3>
|
||||
<div className="text-3xl font-bold text-base-content mb-1">
|
||||
{formatNumber(platformData?.totalPositionCount || 0)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm ${changesToday.positionCountChange >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
className={`text-sm ${changesToday.positionCountChange >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{formatChangeIndicator(changesToday.positionCountChange)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Long Positions</h3>
|
||||
<div className="text-3xl font-bold text-green-400 mb-1">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Long Positions</h3>
|
||||
<div className="text-3xl font-bold text-success mb-1">
|
||||
{formatNumber(platformData?.positionCountByDirection?.Long || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
<div className="text-sm text-base-content/60">
|
||||
{(() => {
|
||||
const longCount = platformData?.positionCountByDirection?.Long || 0;
|
||||
const shortCount = platformData?.positionCountByDirection?.Short || 0;
|
||||
@@ -394,13 +414,15 @@ function PlatformSummary({index}: { index: number }) {
|
||||
})()} of total positions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-2">Short Positions</h3>
|
||||
<div className="text-3xl font-bold text-red-400 mb-1">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70 text-base">Short Positions</h3>
|
||||
<div className="text-3xl font-bold text-error mb-1">
|
||||
{formatNumber(platformData?.positionCountByDirection?.Short || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
<div className="text-sm text-base-content/60">
|
||||
{(() => {
|
||||
const longCount = platformData?.positionCountByDirection?.Long || 0;
|
||||
const shortCount = platformData?.positionCountByDirection?.Short || 0;
|
||||
@@ -411,35 +433,50 @@ function PlatformSummary({index}: { index: number }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
|
||||
</div>
|
||||
|
||||
{/* Platform Metrics Chart */}
|
||||
<div className="mb-8">
|
||||
{/* Platform Metrics Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Trading Metrics Chart */}
|
||||
<PlatformLineChart
|
||||
dailySnapshots={platformData?.dailySnapshots || []}
|
||||
currentData={{
|
||||
totalAgents: platformData?.totalAgents,
|
||||
totalActiveStrategies: platformData?.totalActiveStrategies,
|
||||
totalPlatformVolume: platformData?.totalPlatformVolume,
|
||||
totalPlatformPnL: platformData?.totalPlatformPnL,
|
||||
openInterest: platformData?.openInterest,
|
||||
totalPositionCount: platformData?.totalPositionCount,
|
||||
lastUpdated: platformData?.lastUpdated
|
||||
}}
|
||||
title="Platform Metrics Over Time"
|
||||
title="Trading Metrics"
|
||||
height={400}
|
||||
metrics={['volume', 'pnl', 'openInterest', 'positionCount']}
|
||||
/>
|
||||
|
||||
{/* User-Centric Metrics Chart */}
|
||||
<PlatformLineChart
|
||||
dailySnapshots={platformData?.dailySnapshots || []}
|
||||
currentData={{
|
||||
totalAgents: platformData?.totalAgents,
|
||||
totalActiveStrategies: platformData?.totalActiveStrategies,
|
||||
lastUpdated: platformData?.lastUpdated
|
||||
}}
|
||||
title="User-Centric Metrics"
|
||||
height={400}
|
||||
metrics={['agents', 'strategies']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Volume and Positions by Asset */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Volume by Asset */}
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-4">Volume by Asset</h3>
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70">Volume by Asset</h3>
|
||||
<div className="space-y-3 max-h-80 overflow-y-auto">
|
||||
{platformData?.volumeByAsset && Object.keys(platformData.volumeByAsset).length > 0 ? (
|
||||
Object.entries(platformData.volumeByAsset)
|
||||
@@ -448,30 +485,33 @@ function PlatformSummary({index}: { index: number }) {
|
||||
.map(([asset, volume]) => (
|
||||
<div key={asset} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-white">
|
||||
<div className="avatar placeholder">
|
||||
<div className="bg-primary text-primary-content rounded-full w-8">
|
||||
<span className="text-xs font-bold">
|
||||
{asset.substring(0, 2)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-white font-medium">{asset}</span>
|
||||
</div>
|
||||
<span className="text-base-content font-medium">{asset}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-white font-semibold">
|
||||
<div className="text-base-content font-semibold">
|
||||
{formatCurrency(volume)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm">No volume data available</div>
|
||||
<div className="text-base-content/60 text-sm">No volume data available</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions by Asset */}
|
||||
<div className="bg-base-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-400 mb-4">Positions by Asset</h3>
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-base-content/70">Positions by Asset</h3>
|
||||
<div className="space-y-3 max-h-80 overflow-y-auto">
|
||||
{platformData?.positionCountByAsset && Object.keys(platformData.positionCountByAsset).length > 0 ? (
|
||||
Object.entries(platformData.positionCountByAsset)
|
||||
@@ -480,19 +520,20 @@ function PlatformSummary({index}: { index: number }) {
|
||||
.map(([asset, count]) => (
|
||||
<div key={asset} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-white">
|
||||
<div className="avatar placeholder">
|
||||
<div className="bg-secondary text-secondary-content rounded-full w-8">
|
||||
<span className="text-xs font-bold">
|
||||
{asset.substring(0, 2)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-white font-medium">{asset}</span>
|
||||
</div>
|
||||
<span className="text-base-content font-medium">{asset}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-white font-semibold">
|
||||
<div className="text-base-content font-semibold">
|
||||
{formatNumber(count)} positions
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
<div className="text-xs text-base-content/60">
|
||||
{platformData?.totalPositionCount ?
|
||||
(count / platformData.totalPositionCount * 100).toFixed(1) : 0}% of
|
||||
total
|
||||
@@ -501,20 +542,22 @@ function PlatformSummary({index}: { index: number }) {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm">No position data available</div>
|
||||
<div className="text-base-content/60 text-sm">No position data available</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Freshness Indicator */}
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between text-sm text-gray-400">
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center justify-between text-sm text-base-content/60">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Last updated: {platformData?.lastUpdated ? new Date(platformData.lastUpdated).toLocaleString() : 'Unknown'}</span>
|
||||
{isFetching && (
|
||||
<div className="flex items-center gap-1 text-blue-400">
|
||||
<div className="flex items-center gap-1 text-primary">
|
||||
<div className="loading loading-spinner loading-xs"></div>
|
||||
<span>Refreshing...</span>
|
||||
</div>
|
||||
@@ -545,6 +588,7 @@ function PlatformSummary({index}: { index: number }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user