diff --git a/src/Managing.WebApp/src/components/mollecules/PlatformLineChart/PlatformLineChart.tsx b/src/Managing.WebApp/src/components/mollecules/PlatformLineChart/PlatformLineChart.tsx new file mode 100644 index 00000000..6760fee5 --- /dev/null +++ b/src/Managing.WebApp/src/components/mollecules/PlatformLineChart/PlatformLineChart.tsx @@ -0,0 +1,303 @@ +import React from 'react' +import Plot from 'react-plotly.js' +import useTheme from '../../../hooks/useTheme' +import {DailySnapshot} from '../../../generated/ManagingApiTypes' +import {Config} from 'plotly.js' + +interface CurrentPlatformData { + totalAgents?: number + totalActiveStrategies?: number + totalPlatformVolume?: number + totalPlatformPnL?: number + openInterest?: number + totalPositionCount?: number + lastUpdated?: Date | string +} + +interface PlatformLineChartProps { + dailySnapshots: DailySnapshot[] + currentData?: CurrentPlatformData + title?: string + height?: number + width?: number +} + +function PlatformLineChart({ + dailySnapshots, + currentData, + title = "Platform Metrics Over Time", + height = 400, + width = 800 +}: PlatformLineChartProps) { + const theme = useTheme().themeProperty() + + // Create current data snapshot if currentData is provided + const currentSnapshot: DailySnapshot | null = currentData ? { + date: currentData.lastUpdated ? + (currentData.lastUpdated instanceof Date ? currentData.lastUpdated : new Date(currentData.lastUpdated)) : + new Date(), + totalAgents: currentData.totalAgents, + totalStrategies: currentData.totalActiveStrategies, + totalVolume: currentData.totalPlatformVolume, + totalPnL: currentData.totalPlatformPnL, + totalOpenInterest: currentData.openInterest, + totalPositionCount: currentData.totalPositionCount + } : null + + // Combine historical snapshots with current data + const allSnapshots = currentSnapshot ? [...dailySnapshots, currentSnapshot] : dailySnapshots + + // Sort snapshots by date + const sortedSnapshots = [...allSnapshots].sort((a, b) => { + const dateA = a.date instanceof Date ? a.date : (a.date ? new Date(a.date) : new Date()) + const dateB = b.date instanceof Date ? b.date : (b.date ? new Date(b.date) : new Date()) + return dateA.getTime() - dateB.getTime() + }) + + // Prepare data for the chart + const dates = sortedSnapshots.map(snapshot => + snapshot.date instanceof Date ? snapshot.date : (snapshot.date ? new Date(snapshot.date) : new Date()) + ) + const agents = sortedSnapshots.map(snapshot => snapshot.totalAgents || 0) + const strategies = sortedSnapshots.map(snapshot => snapshot.totalStrategies || 0) + const volume = sortedSnapshots.map(snapshot => snapshot.totalVolume || 0) + const pnl = sortedSnapshots.map(snapshot => snapshot.totalPnL || 0) + const openInterest = sortedSnapshots.map(snapshot => snapshot.totalOpenInterest || 0) + const positionCount = sortedSnapshots.map(snapshot => snapshot.totalPositionCount || 0) + + // Determine if the last point is current data (to highlight it) + const isCurrentDataPoint = (index: number) => currentSnapshot && index === sortedSnapshots.length - 1 + + // Format currency values for display + const formatCurrency = (value: number) => { + if (value >= 1000000) { + return `$${(value / 1000000).toFixed(1)}M` + } else if (value >= 1000) { + return `$${(value / 1000).toFixed(1)}K` + } + return `$${value.toFixed(0)}` + } + + const plotData = [ + { + type: 'scatter' as const, + mode: 'lines+markers' as const, + name: 'Total Agents', + x: dates, + y: agents, + line: { + color: theme.primary, + width: 2 + }, + marker: { + size: agents.map((_, index) => isCurrentDataPoint(index) ? 10 : 6), + color: agents.map((_, index) => isCurrentDataPoint(index) ? theme.warning : theme.primary), + symbol: agents.map((_, index) => isCurrentDataPoint(index) ? 'star' : 'circle') + }, + hovertemplate: 'Agents
%{x}
Count: %{y}' + + (currentSnapshot ? '
Current Data' : '') + '' + }, + { + type: 'scatter' as const, + mode: 'lines+markers' as const, + name: 'Active Strategies', + x: dates, + y: strategies, + line: { + color: theme.success, + width: 2 + }, + marker: { + size: strategies.map((_, index) => isCurrentDataPoint(index) ? 10 : 6), + color: strategies.map((_, index) => isCurrentDataPoint(index) ? theme.warning : theme.success), + symbol: strategies.map((_, index) => isCurrentDataPoint(index) ? 'star' : 'circle') + }, + hovertemplate: 'Strategies
%{x}
Count: %{y}' + + (currentSnapshot ? '
Current Data' : '') + '' + }, + { + type: 'scatter' as const, + mode: 'lines+markers' as const, + name: 'Total Volume', + x: dates, + y: volume, + yaxis: 'y2', + line: { + color: theme.info, + width: 2 + }, + marker: { + size: volume.map((_, index) => isCurrentDataPoint(index) ? 10 : 6), + color: volume.map((_, index) => isCurrentDataPoint(index) ? theme.warning : theme.info), + symbol: volume.map((_, index) => isCurrentDataPoint(index) ? 'star' : 'circle') + }, + hovertemplate: 'Volume
%{x}
Amount: ' + formatCurrency(volume[dates.indexOf(new Date('%{x}'))] || 0) + + (currentSnapshot ? '
Current Data' : '') + '' + }, + { + type: 'scatter' as const, + mode: 'lines+markers' as const, + name: 'Platform PnL', + x: dates, + y: pnl, + yaxis: 'y2', + line: { + color: pnl.some(p => p >= 0) ? theme.success : theme.error, + width: 2 + }, + marker: { + size: pnl.map((_, index) => isCurrentDataPoint(index) ? 10 : 6), + color: pnl.map((_, index) => isCurrentDataPoint(index) ? theme.warning : (pnl[index] >= 0 ? theme.success : theme.error)), + symbol: pnl.map((_, index) => isCurrentDataPoint(index) ? 'star' : 'circle') + }, + hovertemplate: 'PnL
%{x}
Amount: ' + formatCurrency(pnl[dates.indexOf(new Date('%{x}'))] || 0) + + (currentSnapshot ? '
Current Data' : '') + '' + }, + { + type: 'scatter' as const, + mode: 'lines+markers' as const, + name: 'Open Interest', + x: dates, + y: openInterest, + yaxis: 'y2', + line: { + color: theme.warning, + width: 2 + }, + marker: { + size: openInterest.map((_, index) => isCurrentDataPoint(index) ? 10 : 6), + color: openInterest.map((_, index) => isCurrentDataPoint(index) ? theme.error : theme.warning), + symbol: openInterest.map((_, index) => isCurrentDataPoint(index) ? 'star' : 'circle') + }, + hovertemplate: 'Open Interest
%{x}
Amount: ' + formatCurrency(openInterest[dates.indexOf(new Date('%{x}'))] || 0) + + (currentSnapshot ? '
Current Data' : '') + '' + }, + { + type: 'scatter' as const, + mode: 'lines+markers' as const, + name: 'Position Count', + x: dates, + y: positionCount, + line: { + color: theme.secondary, + width: 2 + }, + marker: { + size: positionCount.map((_, index) => isCurrentDataPoint(index) ? 10 : 6), + color: positionCount.map((_, index) => isCurrentDataPoint(index) ? theme.warning : theme.secondary), + symbol: positionCount.map((_, index) => isCurrentDataPoint(index) ? 'star' : 'circle') + }, + hovertemplate: 'Positions
%{x}
Count: %{y}' + + (currentSnapshot ? '
Current Data' : '') + '' + } + ] + + const layout = { + title: { + text: title, + font: { + color: theme['base-content'] || '#ffffff', + size: 16 + } + }, + xaxis: { + title: { + text: 'Date', + font: { + color: theme['base-content'] || '#ffffff' + } + }, + color: theme['base-content'] || '#ffffff', + gridcolor: theme.neutral || '#333333', + showgrid: true, + zeroline: false + }, + yaxis: { + title: { + text: 'Count', + font: { + color: theme['base-content'] || '#ffffff' + } + }, + color: theme['base-content'] || '#ffffff', + gridcolor: theme.neutral || '#333333', + showgrid: true, + zeroline: false, + side: 'left' as const + }, + yaxis2: { + title: { + text: 'Amount ($)', + font: { + color: theme['base-content'] || '#ffffff' + } + }, + color: theme['base-content'] || '#ffffff', + gridcolor: theme.neutral || '#333333', + showgrid: false, + zeroline: false, + side: 'right' as const, + overlaying: 'y' as const, + tickformat: '$,.0f' + }, + legend: { + font: { + color: theme['base-content'] || '#ffffff' + }, + bgcolor: 'rgba(0,0,0,0)', + bordercolor: 'rgba(0,0,0,0)' + }, + margin: { + l: 60, + r: 60, + t: 60, + b: 60, + pad: 4 + }, + paper_bgcolor: 'rgba(0,0,0,0)', + plot_bgcolor: 'rgba(0,0,0,0)', + autosize: true, + height: height, + hovermode: 'x unified' as const, + hoverlabel: { + bgcolor: theme['base-200'] || '#1a1a1a', + bordercolor: theme.primary || '#54B5F9', + font: { + color: theme['base-content'] || '#ffffff', + size: 12 + } + } + } + + const config: Partial = { + displayModeBar: true, + displaylogo: false, + modeBarButtonsToRemove: ['pan2d', 'lasso2d', 'select2d', 'autoScale2d', 'resetScale2d'] as any, + responsive: true + } + + if (sortedSnapshots.length === 0) { + return ( +
+
+
📊
+
No historical data available
+
+
+ ) + } + + return ( +
+ +
+ ) +} + +export default PlatformLineChart diff --git a/src/Managing.WebApp/src/components/mollecules/index.tsx b/src/Managing.WebApp/src/components/mollecules/index.tsx index 537405ac..eced19e7 100644 --- a/src/Managing.WebApp/src/components/mollecules/index.tsx +++ b/src/Managing.WebApp/src/components/mollecules/index.tsx @@ -14,3 +14,4 @@ export { default as SelectColumnFilter } from './Table/SelectColumnFilter' export { default as Card } from './Card/Card' export { default as ConfigDisplayModal } from './ConfigDisplayModal/ConfigDisplayModal' export { default as IndicatorsDisplay } from './IndicatorsDisplay/IndicatorsDisplay' +export { default as PlatformLineChart } from './PlatformLineChart/PlatformLineChart' diff --git a/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx b/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx index eebef7c3..6676eb28 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/platformSummary.tsx @@ -2,6 +2,7 @@ import React, {useMemo} from 'react' import {useQuery, useQueryClient} from '@tanstack/react-query' import useApiUrlStore from '../../app/store/apiStore' import {fetchPlatformData} from '../../services/platformService' +import {PlatformLineChart} from '../../components/mollecules' function PlatformSummary({index}: { index: number }) { const {apiUrl} = useApiUrlStore() @@ -166,21 +167,36 @@ function PlatformSummary({index}: { index: number }) { {/* Volume chart using daily snapshots */}
{platformData?.dailySnapshots && platformData.dailySnapshots.length > 0 ? ( - [...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]) - const height = maxVolume > 0 ? ((snapshot.totalVolume || 0) / maxVolume) * 100 : 0 - return ( -
- ) - }) + <> + {/* 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 */} +
{ + 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)}`} + /> + ) : (
)} @@ -372,9 +388,13 @@ function PlatformSummary({index}: { index: number }) { {formatNumber(platformData?.positionCountByDirection?.Long || 0)}
- {platformData?.totalPositionCount ? - ((platformData.positionCountByDirection?.Long || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}% - of total + {(() => { + 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
@@ -384,13 +404,35 @@ function PlatformSummary({index}: { index: number }) { {formatNumber(platformData?.positionCountByDirection?.Short || 0)}
- {platformData?.totalPositionCount ? - ((platformData.positionCountByDirection?.Short || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}% - of total + {(() => { + 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
+ {/* Platform Metrics Chart */} +
+ +
+ {/* Volume and Positions by Asset */}
{/* Volume by Asset */}