From 83d13bde740c1c7a48e1cbc37329739231d19c77 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sat, 8 Nov 2025 04:29:50 +0700 Subject: [PATCH] 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. --- .../components/mollecules/NavBar/NavBar.tsx | 123 +++- .../PlatformLineChart/PlatformLineChart.tsx | 38 +- .../src/pages/dashboardPage/dashboard.tsx | 23 +- .../pages/dashboardPage/platformSummary.tsx | 682 ++++++++++-------- 4 files changed, 502 insertions(+), 364 deletions(-) diff --git a/src/Managing.WebApp/src/components/mollecules/NavBar/NavBar.tsx b/src/Managing.WebApp/src/components/mollecules/NavBar/NavBar.tsx index 238844f9..5dc838ce 100644 --- a/src/Managing.WebApp/src/components/mollecules/NavBar/NavBar.tsx +++ b/src/Managing.WebApp/src/components/mollecules/NavBar/NavBar.tsx @@ -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 +} + 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({ + 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 ( +
+
+ {healthStatus.label} +
+
+ ) +} + // Global Loader Component const GlobalLoader = () => { const isFetching = useIsFetching() return isFetching ? (
- Loading...
) : null } @@ -278,27 +354,28 @@ export default function NavBar() { <> {/* Navbar */}
- {/* Navbar Start - Mobile Menu Button and Logo */} -
- - - logo - -
+ + + + + + logo + + +
{/* Navbar Center - Desktop Navigation */}
@@ -326,7 +403,6 @@ export default function NavBar() {
- Environment:
{/* Show environment badge on mobile */} @@ -394,9 +470,6 @@ export default function NavBar() { {authenticated && (
  • -
    - Environment: -
  • diff --git a/src/Managing.WebApp/src/components/mollecules/PlatformLineChart/PlatformLineChart.tsx b/src/Managing.WebApp/src/components/mollecules/PlatformLineChart/PlatformLineChart.tsx index 6760fee5..eb834be6 100644 --- a/src/Managing.WebApp/src/components/mollecules/PlatformLineChart/PlatformLineChart.tsx +++ b/src/Managing.WebApp/src/components/mollecules/PlatformLineChart/PlatformLineChart.tsx @@ -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 ? '
    Current Data' : '') + '' }, { + 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 ? '
    Current Data' : '') + '' }, { + 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 ? '
    Current Data' : '') + '' }, { + 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 ? '
    Current Data' : '') + '' }, { + 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 ? '
    Current Data' : '') + '' }, { + 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: { diff --git a/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx b/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx index e3cfa619..e9e47066 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx @@ -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(tabs[0].index) useEffect(() => {}, []) diff --git a/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx b/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx index 853aced2..39798215 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx @@ -77,19 +77,19 @@ function PlatformSummary({index}: { index: number }) { const formatChangeIndicator = (change: number) => { if (change > 0) { return ( - + +{change} Today ) } else if (change < 0) { return ( - + {change} Today ) } return ( - + No change ) @@ -130,284 +130,307 @@ function PlatformSummary({index}: { index: number }) { return (
    {/* Subtle refetching indicator */} - {isFetching && data && ( -
    -
    -
    - Updating... -
    -
    - )} - - {/* Header */} -
    -

    - {formatNumber(platformData?.totalActiveStrategies || 0)} Strategies Deployed -

    -
    - {platformData && formatChangeIndicator(changesToday.strategiesChange)} -
    -
    {/* Main Stats Grid */}
    {/* Total Volume Traded */} -
    -

    Total Volume Traded

    -
    - {formatCurrency(platformData?.totalPlatformVolume || 0)} -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {changesToday.volumeChange >= 0 ? '+' : ''}{formatCurrency(changesToday.volumeChange)} Today - +
    +
    +

    Total Volume Traded

    +
    + {formatCurrency(platformData?.totalPlatformVolume || 0)} +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {changesToday.volumeChange >= 0 ? '+' : ''}{formatCurrency(changesToday.volumeChange)} Today + ({formatPercentageChange(platformData?.totalPlatformVolume || 0, changesToday.volumeChange)}) -
    - {/* Volume chart using daily snapshots */} -
    - {platformData?.dailySnapshots && platformData.dailySnapshots.length > 0 ? ( - <> - {/* Historical data bars */} - {[...platformData.dailySnapshots] - .sort((a, b) => new Date(a.date || 0).getTime() - new Date(b.date || 0).getTime()) - .slice(-7) // Last 7 days - .map((snapshot, index) => { - const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0) - const height = maxVolume > 0 ? ((snapshot.totalVolume || 0) / maxVolume) * 100 : 0 - return ( -
    - ) - })} - {/* Current data bar */} -
    { +
    + {/* Volume chart using daily snapshots */} +
    + {platformData?.dailySnapshots && platformData.dailySnapshots.length > 0 ? ( + <> + {/* Historical data bars */} + {[...platformData.dailySnapshots] + .sort((a, b) => new Date(a.date || 0).getTime() - new Date(b.date || 0).getTime()) + .slice(-7) // Last 7 days + .map((snapshot, index) => { const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0) - const height = maxVolume > 0 ? ((platformData?.totalPlatformVolume || 0) / maxVolume) * 100 : 0 - return Math.max(height, 4) - })()}%` - }} - title={`Current Total: ${formatCurrency(platformData?.totalPlatformVolume || 0)}`} - /> - - ) : ( -
    - )} + const height = maxVolume > 0 ? ((snapshot.totalVolume || 0) / maxVolume) * 100 : 0 + return ( +
    + ) + })} + {/* Current data bar */} +
    { + const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0) + const height = maxVolume > 0 ? ((platformData?.totalPlatformVolume || 0) / maxVolume) * 100 : 0 + return Math.max(height, 4) + })()}%` + }} + title={`Current Total: ${formatCurrency(platformData?.totalPlatformVolume || 0)}`} + /> + + ) : ( +
    + )} +
    {/* Top 3 Most Profitable */} -
    -
    - 🤑 -

    Top 3 Most Profitable

    -
    -
    - {topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => ( -
    -
    -
    - - {strategy.strategyName?.charAt(0) || 'S'} - -
    -
    -
    - {strategy.strategyName || '[Strategy Name]'} +
    +
    +
    + 🤑 +

    Top 3 Most Profitable

    +
    +
    + {topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => ( +
    +
    +
    +
    + + {strategy.strategyName?.charAt(0) || 'S'} + +
    -
    📧
    +
    +
    + {strategy.strategyName || '[Strategy Name]'} +
    +
    📧
    +
    +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {strategy.netPnL && strategy.netPnL >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {strategy.netPnL && strategy.netPnL >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)} -
    -
    - )) || ( -
    No profitable strategies found
    - )} + )) || ( +
    No profitable strategies found
    + )} +
    {/* Top 3 Rising (by ROI) */} -
    -
    - 📈 -

    Top 3 by ROI

    -
    -
    - {topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => ( -
    -
    -
    - - {strategy.strategyName?.charAt(0) || 'S'} - -
    -
    -
    - {strategy.strategyName || '[Strategy Name]'} +
    +
    +
    + 📈 +

    Top 3 by ROI

    +
    +
    + {topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => ( +
    +
    +
    +
    + + {strategy.strategyName?.charAt(0) || 'S'} + +
    -
    - Vol: {formatCurrency(strategy.volume || 0)} +
    +
    + {strategy.strategyName || '[Strategy Name]'} +
    +
    + Vol: {formatCurrency(strategy.volume || 0)} +
    +
    +
    +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}% +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {(strategy.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)}
    -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {(strategy.roi || 0) >= 0 ? '+' : ''}{strategy.roi?.toFixed(2) || 0}% -
    -
    = 0 ? 'text-green-400' : 'text-red-400'}`}> - {(strategy.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(strategy.netPnL || 0)} -
    -
    -
    - )) || ( -
    No ROI data available
    - )} + )) || ( +
    No ROI data available
    + )} +
    {/* Top 3 Agents by PnL */} -
    -
    - 👥 -

    Top 3 Agents by PnL

    -
    -
    - {topAgentsByPnL?.slice(0, 3).map((agent, index) => ( -
    -
    -
    - - {agent.agentName?.charAt(0) || 'A'} - -
    -
    -
    - {agent.agentName || '[Agent Name]'} +
    +
    +
    + 👥 +

    Top 3 Agents by PnL

    +
    +
    + {topAgentsByPnL?.slice(0, 3).map((agent, index) => ( +
    +
    +
    +
    + + {agent.agentName?.charAt(0) || 'A'} + +
    -
    - {agent.activeStrategiesCount || 0} strategies +
    +
    + {agent.agentName || '[Agent Name]'} +
    +
    + {agent.activeStrategiesCount || 0} strategies +
    +
    +
    +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {(agent.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(agent.netPnL || 0)} +
    +
    + {(agent.totalROI || 0).toFixed(2)}% ROI
    -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {(agent.netPnL || 0) >= 0 ? '+' : ''}{formatCurrency(agent.netPnL || 0)} -
    -
    - {(agent.totalROI || 0).toFixed(2)}% ROI -
    -
    -
    - )) || ( -
    No agent data available
    - )} + )) || ( +
    No agent data available
    + )} +
    {/* Platform Summary Stats */} -
    -
    -

    Total Agents

    -
    - {formatNumber(platformData?.totalAgents || 0)} -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {formatChangeIndicator(changesToday.agentsChange)} +
    +
    +
    +

    Total Agents

    +
    + {formatNumber(platformData?.totalAgents || 0)} +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {formatChangeIndicator(changesToday.agentsChange)} +
    -
    -

    Active Strategies

    -
    - {formatNumber(platformData?.totalActiveStrategies || 0)} -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {formatChangeIndicator(changesToday.strategiesChange)} +
    +
    +

    Strategies Deployed

    +
    + {formatNumber(platformData?.totalActiveStrategies || 0)} +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {formatChangeIndicator(changesToday.strategiesChange)} +
    -
    -

    Total Platform PnL

    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {(platformData?.totalPlatformPnL || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.totalPlatformPnL || 0)} -
    -
    - Fees: {formatCurrency(platformData?.totalPlatformFees || 0)} -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {changesToday.pnLChange >= 0 ? '+' : ''}{formatCurrency(changesToday.pnLChange)} Today +
    +
    +

    Active Strategies

    +
    + {formatNumber(platformData?.totalActiveStrategies || 0)} +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {formatChangeIndicator(changesToday.strategiesChange)} +
    -
    -

    Open Interest

    -
    - {formatCurrency(platformData?.openInterest || 0)} -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {changesToday.openInterestChange >= 0 ? '+' : ''}{formatCurrency(changesToday.openInterestChange)} Today +
    +
    +

    Total Platform PnL

    +
    = 0 ? 'text-success' : 'text-error'}`}> + {(platformData?.totalPlatformPnL || 0) >= 0 ? '+' : ''}{formatCurrency(platformData?.totalPlatformPnL || 0)} +
    +
    + Fees: {formatCurrency(platformData?.totalPlatformFees || 0)} +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {changesToday.pnLChange >= 0 ? '+' : ''}{formatCurrency(changesToday.pnLChange)} Today +
    -
    -

    Total Positions

    -
    - {formatNumber(platformData?.totalPositionCount || 0)} -
    -
    = 0 ? 'text-green-500' : 'text-red-500'}`}> - {formatChangeIndicator(changesToday.positionCountChange)} +
    +
    +

    Open Interest

    +
    + {formatCurrency(platformData?.openInterest || 0)} +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {changesToday.openInterestChange >= 0 ? '+' : ''}{formatCurrency(changesToday.openInterestChange)} Today +
    -
    -

    Long Positions

    -
    - {formatNumber(platformData?.positionCountByDirection?.Long || 0)} -
    -
    - {(() => { - const longCount = platformData?.positionCountByDirection?.Long || 0; - const shortCount = platformData?.positionCountByDirection?.Short || 0; - const total = longCount + shortCount; - if (total === 0) return '0%'; - return ((longCount / total) * 100).toFixed(1) + '%'; - })()} of total positions +
    +
    +

    Total Positions

    +
    + {formatNumber(platformData?.totalPositionCount || 0)} +
    +
    = 0 ? 'text-success' : 'text-error'}`}> + {formatChangeIndicator(changesToday.positionCountChange)} +
    -
    -

    Short Positions

    -
    - {formatNumber(platformData?.positionCountByDirection?.Short || 0)} +
    +
    +

    Long Positions

    +
    + {formatNumber(platformData?.positionCountByDirection?.Long || 0)} +
    +
    + {(() => { + const longCount = platformData?.positionCountByDirection?.Long || 0; + const shortCount = platformData?.positionCountByDirection?.Short || 0; + const total = longCount + shortCount; + if (total === 0) return '0%'; + return ((longCount / total) * 100).toFixed(1) + '%'; + })()} of total positions +
    -
    - {(() => { - const longCount = platformData?.positionCountByDirection?.Long || 0; - const shortCount = platformData?.positionCountByDirection?.Short || 0; - const total = longCount + shortCount; - if (total === 0) return '0%'; - return ((shortCount / total) * 100).toFixed(1) + '%'; - })()} of total positions +
    + +
    +
    +

    Short Positions

    +
    + {formatNumber(platformData?.positionCountByDirection?.Short || 0)} +
    +
    + {(() => { + const longCount = platformData?.positionCountByDirection?.Long || 0; + const shortCount = platformData?.positionCountByDirection?.Short || 0; + const total = longCount + shortCount; + if (total === 0) return '0%'; + return ((shortCount / total) * 100).toFixed(1) + '%'; + })()} of total positions +
    @@ -417,131 +440,152 @@ function PlatformSummary({index}: { index: number }) {
    - {/* Platform Metrics Chart */} -
    + {/* Platform Metrics Charts */} +
    + {/* Trading Metrics Chart */} + + {/* User-Centric Metrics Chart */} +
    {/* Volume and Positions by Asset */}
    {/* Volume by Asset */} -
    -

    Volume by Asset

    -
    - {platformData?.volumeByAsset && Object.keys(platformData.volumeByAsset).length > 0 ? ( - Object.entries(platformData.volumeByAsset) - .sort(([, a], [, b]) => b - a) - .slice(0, 10) - .map(([asset, volume]) => ( -
    -
    -
    - - {asset.substring(0, 2)} - +
    +
    +

    Volume by Asset

    +
    + {platformData?.volumeByAsset && Object.keys(platformData.volumeByAsset).length > 0 ? ( + Object.entries(platformData.volumeByAsset) + .sort(([, a], [, b]) => b - a) + .slice(0, 10) + .map(([asset, volume]) => ( +
    +
    +
    +
    + + {asset.substring(0, 2)} + +
    +
    + {asset}
    - {asset} -
    -
    -
    - {formatCurrency(volume)} +
    +
    + {formatCurrency(volume)} +
    -
    - )) - ) : ( -
    No volume data available
    - )} + )) + ) : ( +
    No volume data available
    + )} +
    {/* Positions by Asset */} -
    -

    Positions by Asset

    -
    - {platformData?.positionCountByAsset && Object.keys(platformData.positionCountByAsset).length > 0 ? ( - Object.entries(platformData.positionCountByAsset) - .sort(([, a], [, b]) => b - a) - .slice(0, 10) - .map(([asset, count]) => ( -
    -
    -
    - - {asset.substring(0, 2)} - +
    +
    +

    Positions by Asset

    +
    + {platformData?.positionCountByAsset && Object.keys(platformData.positionCountByAsset).length > 0 ? ( + Object.entries(platformData.positionCountByAsset) + .sort(([, a], [, b]) => b - a) + .slice(0, 10) + .map(([asset, count]) => ( +
    +
    +
    +
    + + {asset.substring(0, 2)} + +
    +
    + {asset}
    - {asset} -
    -
    -
    - {formatNumber(count)} positions -
    -
    - {platformData?.totalPositionCount ? - (count / platformData.totalPositionCount * 100).toFixed(1) : 0}% of - total +
    +
    + {formatNumber(count)} positions +
    +
    + {platformData?.totalPositionCount ? + (count / platformData.totalPositionCount * 100).toFixed(1) : 0}% of + total +
    -
    - )) - ) : ( -
    No position data available
    - )} + )) + ) : ( +
    No position data available
    + )} +
    {/* Data Freshness Indicator */} -
    -
    -
    -
    - Last updated: {platformData?.lastUpdated ? new Date(platformData.lastUpdated).toLocaleString() : 'Unknown'} - {isFetching && ( -
    -
    - Refreshing... -
    - )} +
    +
    +
    +
    +
    + Last updated: {platformData?.lastUpdated ? new Date(platformData.lastUpdated).toLocaleString() : 'Unknown'} + {isFetching && ( +
    +
    + Refreshing... +
    + )} +
    +
    - + Last snapshot: {platformData?.lastSnapshot ? new Date(platformData.lastSnapshot).toLocaleString() : 'Unknown'}
    - Last snapshot: {platformData?.lastSnapshot ? new Date(platformData.lastSnapshot).toLocaleString() : 'Unknown'}