import { useState, useEffect, useMemo } from 'react' import { useTheme } from 'next-themes' import cloneDeep from 'lodash/cloneDeep' import dayjs from 'dayjs' import { AreaChart, Area, XAxis, YAxis, Tooltip as ChartTooltip, } from 'recharts' import { InformationCircleIcon, ScaleIcon } from '@heroicons/react/solid' import useDimensions from 'react-cool-dimensions' import { useTranslation } from 'next-i18next' import { ZERO_BN } from '@blockworks-foundation/mango-client' import ButtonGroup from '../ButtonGroup' import { formatUsdValue } from '../../utils' import { numberCompacter } from '../SwapTokenInfo' import Checkbox from '../Checkbox' import Tooltip from '../Tooltip' import useMangoStore, { PerpPosition } from 'stores/useMangoStore' import LongShortChart from './LongShortChart' import HealthHeart from 'components/HealthHeart' type AccountOverviewStats = { hourlyPerformanceStats: any[] performanceRange: '24hr' | '7d' | '30d' | '3m' } const defaultData = [ { account_equity: 0, pnl: 0, time: '2022-01-01T00:00:00.000Z' }, { account_equity: 0, pnl: 0, time: '2023-01-01T00:00:00.000Z' }, ] const performanceRangePresets = [ { label: '24h', value: 1 }, { label: '7d', value: 7 }, { label: '30d', value: 30 }, { label: '3m', value: 90 }, ] const performanceRangePresetLabels = performanceRangePresets.map((x) => x.label) const AccountOverviewStats = ({ hourlyPerformanceStats, accountValue }) => { const { theme } = useTheme() const { t } = useTranslation('common') const { observe, width, height } = useDimensions() const [chartToShow, setChartToShow] = useState('Value') const [chartData, setChartData] = useState([]) const [mouseData, setMouseData] = useState(null) const [performanceRange, setPerformanceRange] = useState('30d') const [showSpotPnl, setShowSpotPnl] = useState(true) const [showPerpPnl, setShowPerpPnl] = useState(true) const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache) const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances) const perpPositions = useMangoStore( (s) => s.selectedMangoAccount.perpPositions ) const maintHealthRatio = useMemo(() => { return mangoAccount && mangoGroup && mangoCache ? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint') : 100 }, [mangoAccount, mangoGroup, mangoCache]) const { longData, shortData, longExposure, shortExposure } = useMemo(() => { const longData: any = [] const shortData: any = [] if (!spotBalances || !perpPositions) { longData.push({ symbol: 'spacer', value: 1 }) shortData.push({ symbol: 'spacer', value: 1 }) // return {} } const DUST_THRESHOLD = 0.05 const netUnsettledPositionsValue = perpPositions.reduce( (a, c) => a + (c?.unsettledPnl ?? 0), 0 ) for (const { net, symbol, value } of spotBalances) { let amount = Number(net) let totValue = Number(value) if (symbol === 'USDC') { amount += netUnsettledPositionsValue totValue += netUnsettledPositionsValue } if (totValue > DUST_THRESHOLD) { longData.push({ asset: symbol, amount: amount, symbol: symbol, value: totValue, }) } if (-totValue > DUST_THRESHOLD) { shortData.push({ asset: symbol, amount: Math.abs(amount), symbol: symbol, value: Math.abs(totValue), }) } } for (const { marketConfig, basePosition, notionalSize, perpAccount, } of perpPositions.filter((p) => !!p) as PerpPosition[]) { if (notionalSize < DUST_THRESHOLD) continue if (perpAccount.basePosition.gt(ZERO_BN)) { longData.push({ asset: marketConfig.name, amount: basePosition, symbol: marketConfig.baseSymbol, value: notionalSize, }) } else { shortData.push({ asset: marketConfig.name, amount: Math.abs(basePosition), symbol: marketConfig.baseSymbol, value: notionalSize, }) } } const longExposure = longData.reduce((a, c) => a + c.value, 0) const shortExposure = shortData.reduce((a, c) => a + c.value, 0) if (shortExposure === 0) { shortData.push({ symbol: 'spacer', value: 1 }) } if (longExposure === 0) { longData.push({ symbol: 'spacer', value: 1 }) } const dif = longExposure - shortExposure if (dif > 0) { shortData.push({ symbol: 'spacer', value: dif }) } return { longData, shortData, longExposure, shortExposure } }, [spotBalances, perpPositions]) useEffect(() => { if (hourlyPerformanceStats.length > 0) { if (performanceRange === '3m') { setChartData(hourlyPerformanceStats.slice().reverse()) } if (performanceRange === '30d') { const start = new Date( // @ts-ignore dayjs().utc().hour(0).minute(0).subtract(29, 'day') ).getTime() const chartData = cloneDeep(hourlyPerformanceStats).filter( (d) => new Date(d.time).getTime() > start ) const pnlStart = chartData[chartData.length - 1].pnl const perpPnlStart = chartData[chartData.length - 1].perp_pnl for (let i = 0; i < chartData.length; i++) { if (i === chartData.length - 1) { chartData[i].pnl = 0 chartData[i].perp_pnl = 0 } else { chartData[i].pnl = chartData[i].pnl - pnlStart chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart } } setChartData(chartData.reverse()) } if (performanceRange === '7d') { const start = new Date( // @ts-ignore dayjs().utc().hour(0).minute(0).subtract(7, 'day') ).getTime() const chartData = cloneDeep(hourlyPerformanceStats).filter( (d) => new Date(d.time).getTime() > start ) const pnlStart = chartData[chartData.length - 1].pnl const perpPnlStart = chartData[chartData.length - 1].perp_pnl for (let i = 0; i < chartData.length; i++) { if (i === chartData.length - 1) { chartData[i].pnl = 0 chartData[i].perp_pnl = 0 } else { chartData[i].pnl = chartData[i].pnl - pnlStart chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart } } setChartData(chartData.reverse()) } if (performanceRange === '24h') { const start = new Date( // @ts-ignore dayjs().utc().hour(0).minute(0).subtract(1, 'day') ).getTime() const chartData = cloneDeep(hourlyPerformanceStats).filter( (d) => new Date(d.time).getTime() > start ) const pnlStart = chartData[chartData.length - 1].pnl const perpPnlStart = chartData[chartData.length - 1].perp_pnl for (let i = 0; i < chartData.length; i++) { if (i === chartData.length - 1) { chartData[i].pnl = 0 chartData[i].perp_pnl = 0 } else { chartData[i].pnl = chartData[i].pnl - pnlStart chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart } } setChartData(chartData.reverse()) } } else { setChartData([]) } }, [hourlyPerformanceStats, performanceRange]) useEffect(() => { if (chartData.length > 0) { for (const stat of chartData) { stat.spot_pnl = stat.pnl - stat.perp_pnl } } }, [chartData]) const handleMouseMove = (coords) => { if (coords.activePayload) { setMouseData(coords.activePayload[0].payload) } } const handleMouseLeave = () => { setMouseData(null) } const renderPnlChartTitle = () => { if (showPerpPnl && showSpotPnl) { return t('pnl') } if (!showSpotPnl) { return `${t('perp')} ${t('pnl')}` } if (!showPerpPnl) { return `${t('spot')} ${t('pnl')}` } } const formatDateAxis = (date) => { if (['3m', '30d'].includes(performanceRange)) { return dayjs(date).format('D MMM') } else if (performanceRange === '7d') { return dayjs(date).format('ddd, h:mma') } else { return dayjs(date).format('h:mma') } } const pnlChartDataKey = () => { if (!showPerpPnl && showSpotPnl) { return 'spot_pnl' } else if (!showSpotPnl && showPerpPnl) { return 'perp_pnl' } else { return 'pnl' } } const pnlChartColor = chartToShow === 'PnL' && chartData.length > 0 && chartData[chartData.length - 1][pnlChartDataKey()] > 0 ? theme === 'Mango' ? '#AFD803' : '#5EBF4D' : theme === 'Mango' ? '#F84638' : '#CC2929' return (
{chartToShow === 'Value' ? t('account-value') : renderPnlChartTitle()}{' '}
{mouseData ? ( <>
{formatUsdValue( mouseData[ chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity' ] )}
{dayjs(mouseData['time']).format('ddd MMM D YYYY, h:mma')}
) : chartData.length === 0 ? ( <>
{chartToShow === 'PnL' ? '--' : formatUsdValue(accountValue)}
{dayjs().format('ddd MMM D YYYY, h:mma')}
) : chartData.length > 0 ? ( <>
{chartToShow === 'PnL' ? formatUsdValue( chartData[chartData.length - 1][pnlChartDataKey()] ) : formatUsdValue(accountValue)}
{chartToShow === 'PnL' ? dayjs(chartData[chartData.length - 1]['time']).format( 'ddd MMM D YYYY, h:mma' ) : dayjs().format('ddd MMM D YYYY, h:mma')}
) : ( <>
)}
{t('tooltip-account-liquidated')}{' '} {t('learn-more')}
} >
{t('health')}
{maintHealthRatio < 100 ? maintHealthRatio.toFixed(2) : '>100'}%
{t('leverage')}
{mangoGroup && mangoCache ? (
{mangoAccount?.getLeverage(mangoGroup, mangoCache).toFixed(2)} x
) : null}
{t('assets')}
{mangoGroup && mangoCache ? (
{formatUsdValue(+longExposure)}
) : null}
{t('liabilities')}
{mangoGroup && mangoCache ? (
{formatUsdValue(+shortExposure)}
) : null}
setChartToShow(c)} values={['Value', 'PnL']} />
{chartToShow === 'PnL' && chartData.length ? (
setShowSpotPnl(e.target.checked)} > {t('include-spot')} setShowPerpPnl(e.target.checked)} > {t('include-perp')}
) : null}
setPerformanceRange(p)} values={performanceRangePresetLabels} />
{chartData.length > 0 ? (
} /> 0 ? 'url(#greenGradientArea)' : 'url(#redGradientArea)' : 'url(#defaultGradientArea)' } fillOpacity={0.3} /> numberCompacter.format(v)} /> formatDateAxis(v)} />
) : (

{t('no-chart')}

)}
) } export default AccountOverviewStats