Add chart to the front
This commit is contained in:
@@ -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
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user