Add chart to the front

This commit is contained in:
2025-10-03 04:33:07 +07:00
parent 8771f58414
commit dd08450bbb
3 changed files with 367 additions and 21 deletions

View File

@@ -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: '<b>Agents</b><br>%{x}<br>Count: %{y}' +
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
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: '<b>Strategies</b><br>%{x}<br>Count: %{y}' +
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
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: '<b>Volume</b><br>%{x}<br>Amount: ' + formatCurrency(volume[dates.indexOf(new Date('%{x}'))] || 0) +
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
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: '<b>PnL</b><br>%{x}<br>Amount: ' + formatCurrency(pnl[dates.indexOf(new Date('%{x}'))] || 0) +
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
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: '<b>Open Interest</b><br>%{x}<br>Amount: ' + formatCurrency(openInterest[dates.indexOf(new Date('%{x}'))] || 0) +
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
},
{
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: '<b>Positions</b><br>%{x}<br>Count: %{y}' +
(currentSnapshot ? '<br><i>Current Data</i>' : '') + '<extra></extra>'
}
]
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<Config> = {
displayModeBar: true,
displaylogo: false,
modeBarButtonsToRemove: ['pan2d', 'lasso2d', 'select2d', 'autoScale2d', 'resetScale2d'] as any,
responsive: true
}
if (sortedSnapshots.length === 0) {
return (
<div className="flex items-center justify-center h-64 bg-base-200 rounded-lg">
<div className="text-center">
<div className="text-gray-400 mb-2">📊</div>
<div className="text-gray-400">No historical data available</div>
</div>
</div>
)
}
return (
<div className="bg-base-200 rounded-lg p-4">
<Plot
data={plotData}
layout={layout}
config={config}
style={{ width: '100%', height: `${height}px` }}
useResizeHandler={true}
/>
</div>
)
}
export default PlatformLineChart

View File

@@ -14,3 +14,4 @@ export { default as SelectColumnFilter } from './Table/SelectColumnFilter'
export { default as Card } from './Card/Card' export { default as Card } from './Card/Card'
export { default as ConfigDisplayModal } from './ConfigDisplayModal/ConfigDisplayModal' export { default as ConfigDisplayModal } from './ConfigDisplayModal/ConfigDisplayModal'
export { default as IndicatorsDisplay } from './IndicatorsDisplay/IndicatorsDisplay' export { default as IndicatorsDisplay } from './IndicatorsDisplay/IndicatorsDisplay'
export { default as PlatformLineChart } from './PlatformLineChart/PlatformLineChart'

View File

@@ -2,6 +2,7 @@ import React, {useMemo} from 'react'
import {useQuery, useQueryClient} from '@tanstack/react-query' import {useQuery, useQueryClient} from '@tanstack/react-query'
import useApiUrlStore from '../../app/store/apiStore' import useApiUrlStore from '../../app/store/apiStore'
import {fetchPlatformData} from '../../services/platformService' import {fetchPlatformData} from '../../services/platformService'
import {PlatformLineChart} from '../../components/mollecules'
function PlatformSummary({index}: { index: number }) { function PlatformSummary({index}: { index: number }) {
const {apiUrl} = useApiUrlStore() const {apiUrl} = useApiUrlStore()
@@ -166,21 +167,36 @@ function PlatformSummary({index}: { index: number }) {
{/* Volume chart using daily snapshots */} {/* Volume chart using daily snapshots */}
<div className="mt-4 h-16 flex items-end gap-1"> <div className="mt-4 h-16 flex items-end gap-1">
{platformData?.dailySnapshots && platformData.dailySnapshots.length > 0 ? ( {platformData?.dailySnapshots && platformData.dailySnapshots.length > 0 ? (
[...platformData.dailySnapshots] <>
.sort((a, b) => new Date(a.date || 0).getTime() - new Date(b.date || 0).getTime()) {/* Historical data bars */}
.slice(-7) // Last 7 days {[...platformData.dailySnapshots]
.map((snapshot, index) => { .sort((a, b) => new Date(a.date || 0).getTime() - new Date(b.date || 0).getTime())
const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0]) .slice(-7) // Last 7 days
const height = maxVolume > 0 ? ((snapshot.totalVolume || 0) / maxVolume) * 100 : 0 .map((snapshot, index) => {
return ( const maxVolume = Math.max(...platformData.dailySnapshots?.map(s => s.totalVolume || 0) || [0], platformData?.totalPlatformVolume || 0)
<div const height = maxVolume > 0 ? ((snapshot.totalVolume || 0) / maxVolume) * 100 : 0
key={index} return (
className="flex-1 bg-green-500 rounded-sm opacity-60 hover:opacity-80 transition-opacity" <div
style={{ height: `${Math.max(height, 4)}%` }} key={`historical-${index}`}
title={`${new Date(snapshot.date || 0).toLocaleDateString()}: ${formatCurrency(snapshot.totalVolume || 0)}`} className="flex-1 bg-green-500 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)}`}
}) />
)
})}
{/* Current data bar */}
<div
className="flex-1 bg-blue-500 rounded-sm opacity-80 hover:opacity-100 transition-opacity border-2 border-blue-300"
style={{
height: `${(() => {
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)}`}
/>
</>
) : ( ) : (
<div className="w-full h-8 bg-green-500 rounded opacity-20"></div> <div className="w-full h-8 bg-green-500 rounded opacity-20"></div>
)} )}
@@ -372,9 +388,13 @@ function PlatformSummary({index}: { index: number }) {
{formatNumber(platformData?.positionCountByDirection?.Long || 0)} {formatNumber(platformData?.positionCountByDirection?.Long || 0)}
</div> </div>
<div className="text-sm text-gray-400"> <div className="text-sm text-gray-400">
{platformData?.totalPositionCount ? {(() => {
((platformData.positionCountByDirection?.Long || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}% const longCount = platformData?.positionCountByDirection?.Long || 0;
of total const shortCount = platformData?.positionCountByDirection?.Short || 0;
const total = longCount + shortCount;
if (total === 0) return '0%';
return ((longCount / total) * 100).toFixed(1) + '%';
})()} of total positions
</div> </div>
</div> </div>
@@ -384,13 +404,35 @@ function PlatformSummary({index}: { index: number }) {
{formatNumber(platformData?.positionCountByDirection?.Short || 0)} {formatNumber(platformData?.positionCountByDirection?.Short || 0)}
</div> </div>
<div className="text-sm text-gray-400"> <div className="text-sm text-gray-400">
{platformData?.totalPositionCount ? {(() => {
((platformData.positionCountByDirection?.Short || 0) / platformData.totalPositionCount * 100).toFixed(1) : 0}% const longCount = platformData?.positionCountByDirection?.Long || 0;
of total const shortCount = platformData?.positionCountByDirection?.Short || 0;
const total = longCount + shortCount;
if (total === 0) return '0%';
return ((shortCount / total) * 100).toFixed(1) + '%';
})()} of total positions
</div> </div>
</div> </div>
</div> </div>
{/* Platform Metrics Chart */}
<div className="mb-8">
<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"
height={400}
/>
</div>
{/* Volume and Positions by Asset */} {/* Volume and Positions by Asset */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Volume by Asset */} {/* Volume by Asset */}