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 ConfigDisplayModal } from './ConfigDisplayModal/ConfigDisplayModal'
|
||||
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 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 */}
|
||||
<div className="mt-4 h-16 flex items-end gap-1">
|
||||
{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 (
|
||||
<div
|
||||
key={index}
|
||||
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)}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
<>
|
||||
{/* 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 (
|
||||
<div
|
||||
key={`historical-${index}`}
|
||||
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>
|
||||
)}
|
||||
@@ -372,9 +388,13 @@ function PlatformSummary({index}: { index: number }) {
|
||||
{formatNumber(platformData?.positionCountByDirection?.Long || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -384,13 +404,35 @@ function PlatformSummary({index}: { index: number }) {
|
||||
{formatNumber(platformData?.positionCountByDirection?.Short || 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{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
|
||||
</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 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Volume by Asset */}
|
||||
|
||||
Reference in New Issue
Block a user