mango-ui-v3/components/account_page/AccountPerformancePerToken.tsx

546 lines
18 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import { useTranslation } from 'next-i18next'
import { LineChart, XAxis, YAxis, Line, Tooltip } from 'recharts'
import useMangoStore from '../../stores/useMangoStore'
import { numberCompactFormatter } from '../../utils/'
import { exportDataToCSV } from '../../utils/export'
import Button from '../Button'
import useDimensions from 'react-cool-dimensions'
import Select from 'components/Select'
import Checkbox from 'components/Checkbox'
import ButtonGroup from 'components/ButtonGroup'
import * as MonoIcons from '../icons'
import { SaveIcon, QuestionMarkCircleIcon } from '@heroicons/react/solid'
import { useTheme } from 'next-themes'
import { CHART_COLORS } from './LongShortChart'
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
export const handleDustTicks = (v) => {
return v < 0.000001
? v === 0
? 0
: v.toExponential()
: numberCompactFormatter.format(v)
}
const HEADERS = [
'time',
'symbol',
'spot_value',
'perp_value',
'open_orders_value',
'transfer_balance',
'perp_spot_transfers_balance',
'mngo_rewards_value',
'mngo_rewards_quantity',
'long_funding',
'short_funding',
'long_funding_cumulative',
'short_funding_cumulative',
// 'deposit_interest',
// 'borrow_interest',
// 'deposit_interest_cumulative',
// 'borrow_interest_cumulative',
// 'price',
]
const DATA_CATEGORIES = [
'account-value',
'account-pnl',
'perp-pnl',
'maker-volume',
'taker-volume',
// 'interest-cumulative',
'funding-cumulative',
'mngo-rewards',
]
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 performanceRangePresetValues = performanceRangePresets.map((x) => x.value)
const AccountPerformance = () => {
const { t } = useTranslation(['common', 'account-performance'])
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const [hourlyPerformanceStats, setHourlyPerformanceStats] = useState<any>([])
const [uniqueSymbols, setUniqueSymbols] = useState<string[]>([])
const [filteredSymbols, setFilteredSymbols] = useState<string[]>([])
const [chartData, setChartData] = useState([])
const [loading, setLoading] = useState(false)
const [selectedSymbols, setSelectedSymbols] = useState(['All'])
const [chartToShow, setChartToShow] = useState('account-value')
const [selectAll, setSelectAll] = useState(false)
const [performanceRange, setPerformanceRange] = useState('30')
const [volumeData, setVolumeData] = useState<any>([])
const [volumeSymbols, setVolumeSymbols] = useState<string[]>([])
const { observe, width, height } = useDimensions()
const { theme } = useTheme()
const mangoAccountPk = useMemo(() => {
if (mangoAccount) {
return mangoAccount.publicKey.toString()
}
}, [mangoAccount])
const exportPerformanceDataToCSV = () => {
const dataToExport = hourlyPerformanceStats
.map(([time, tokenObjects]) => {
return tokenObjects.map(([token, values]) => {
return { time: time, symbol: token, ...values }
})
})
.flat()
const title = `${
mangoAccount?.name || mangoAccount?.publicKey
}-Performance-${new Date().toLocaleDateString()}`
exportDataToCSV(dataToExport, title, HEADERS, t)
}
const calculateChartData = (chartToShow) => {
const metrics = {
'account-value': (values) => {
return (
values['spot_value'] +
values['perp_value'] +
values['open_orders_value']
)
},
'account-pnl': (values) => {
return (
values['spot_value'] +
values['perp_value'] +
values['open_orders_value'] -
values['transfer_balance']
)
},
'perp-pnl': (values) => {
return (
values['perp_value'] +
values['perp_spot_transfers_balance'] +
values['mngo_rewards_value']
)
},
'perp-pnl-ex-rewards': (values) => {
return values['perp_value'] + values['perp_spot_transfers_balance']
},
// 'interest-cumulative': (values) => {
// return (
// (values['deposit_interest_cumulative'] +
// values['borrow_interest_cumulative']) *
// values['price']
// )
// },
'funding-cumulative': (values) => {
return (
values['long_funding_cumulative'] + values['short_funding_cumulative']
)
},
'mngo-rewards': (values) => {
return values['mngo_rewards_value']
},
'maker-volume': (values) => {
return values['maker_cumulative']
},
'taker-volume': (values) => {
return values['taker_cumulative']
},
}
const metric: (values: []) => void = metrics[chartToShow]
let stats
if (chartToShow !== 'maker-volume' && chartToShow !== 'taker-volume') {
stats = hourlyPerformanceStats.map(([time, tokenObjects]) => {
return {
time: time,
...Object.fromEntries(
tokenObjects.map(([token, values]) => [token, metric(values)])
),
All: tokenObjects
.map(([_, values]) => metric(values))
.reduce((a, b) => a + b, 0),
}
})
} else {
stats = volumeData.map(([time, tokenObjects]) => {
return {
time: time,
...Object.fromEntries(
tokenObjects.map(([token, values]) => [token, metric(values)])
),
All: tokenObjects
.map(([_, values]) => metric(values))
.reduce((a, b) => a + b, 0),
}
})
}
// Normalise chart to start from 0 (except for account value)
if (parseInt(performanceRange) !== 90 && chartToShow !== 'account-value') {
const startValues = Object.assign({}, stats[0])
// Initialize symbol not present at the start to 0
uniqueSymbols
.filter((e) => !(e in startValues))
.map((f) => (startValues[f] = 0))
for (let i = 0; i < stats.length; i++) {
for (const key in stats[i]) {
if (key !== 'time') {
stats[i][key] = stats[i][key] - startValues[key]
}
}
}
}
setChartData(stats)
setChartToShow(chartToShow)
}
useEffect(() => {
const fetchStats = async () => {
setLoading(true)
const promises = [
fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/account-performance-per-token?mango-account=${mangoAccountPk}&start-date=${dayjs()
.subtract(parseInt(performanceRange), 'day')
.format('YYYY-MM-DD')}`
),
fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/volumes-by-mango-account?mango-account=${mangoAccountPk}&start-date=${dayjs()
.subtract(parseInt(performanceRange), 'day')
.format('YYYY-MM-DD')}`
),
]
const data = await Promise.all(promises)
const performanceData = await data[0].json()
const volumeData = await data[1].json()
let performanceEntries: any = Object.entries(performanceData)
performanceEntries = performanceEntries
.map(([key, value]) => [key, Object.entries(value)])
.reverse()
let volumeEntries: any = Object.entries(volumeData)
volumeEntries = volumeEntries.map(([key, value]) => [
key,
Object.entries(value),
])
const uniqueSymbols = [
...new Set(
([] as string[]).concat(
['All'],
...performanceEntries.map(([_, tokens]) =>
tokens.map(([token, _]) => token)
)
)
),
]
const uniqueVolumeSymbols = [
...new Set(
([] as string[]).concat(
['All'],
...volumeEntries.map(([_, tokens]) =>
tokens.map(([token, _]) => token)
)
)
),
]
setUniqueSymbols(uniqueSymbols)
setFilteredSymbols(uniqueSymbols)
setVolumeSymbols(uniqueVolumeSymbols)
setHourlyPerformanceStats(performanceEntries)
setVolumeData(volumeEntries)
setLoading(false)
}
fetchStats()
}, [mangoAccountPk, performanceRange])
useEffect(() => {
calculateChartData(chartToShow)
}, [hourlyPerformanceStats, volumeData])
useEffect(() => {
if (
['perp-pnl', 'mngo-rewards', 'funding-cumulative'].includes(chartToShow)
) {
setFilteredSymbols(uniqueSymbols.filter((s) => s !== 'USDC'))
if (selectedSymbols.includes('USDC')) {
setSelectedSymbols(selectedSymbols.filter((s) => s !== 'USDC'))
}
} else {
if (selectAll) {
setSelectedSymbols(uniqueSymbols)
}
setFilteredSymbols(uniqueSymbols)
}
}, [chartToShow, selectedSymbols, hourlyPerformanceStats])
const toggleOption = (v) => {
selectedSymbols.includes(v)
? setSelectedSymbols(selectedSymbols.filter((item) => item !== v))
: setSelectedSymbols([...selectedSymbols, v])
}
const handleSelectAll = () => {
if (!selectAll) {
setSelectedSymbols([...uniqueSymbols])
setSelectAll(true)
} else {
setSelectedSymbols([])
setSelectAll(false)
}
}
const renderTooltip = (props) => {
const { payload } = props
return payload ? (
<div className="space-y-1.5 rounded-md bg-th-bkg-1 p-3">
<p className="text-xs">
{dayjs(payload[0]?.payload.time).format('ddd D MMM YYYY')}
</p>
{payload.map((entry, index) => {
return (
<div
className="flex w-32 items-center justify-between text-xs"
key={`item-${index}`}
>
<p className="mb-0 text-xs" style={{ color: entry.color }}>
{entry.name}
</p>
<p className="mb-0 text-xs" style={{ color: entry.color }}>
{numberCompacter.format(entry.value)}
</p>
</div>
)
})}
</div>
) : null
}
const numberCompacter = Intl.NumberFormat('en', {
style: 'currency',
currency: 'USD',
notation: 'compact',
maximumFractionDigits: 2,
})
const renderSymbolIcon = (s) => {
if (s === 'All') return
if (chartToShow !== 'maker-volume' && chartToShow !== 'taker-volume') {
const iconName = `${s.slice(0, 1)}${s.slice(1, 4).toLowerCase()}MonoIcon`
const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon
return <SymbolIcon className="mr-1.5 h-3.5 w-auto" />
} else {
const iconName = `${s.slice(0, 1)}${s.slice(1, -5).toLowerCase()}MonoIcon`
const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon
return <SymbolIcon className="mr-1.5 h-3.5 w-auto" />
}
}
const symbols = useMemo(() => {
if (chartToShow !== 'maker-volume' && chartToShow !== 'taker-volume') {
return filteredSymbols
}
return volumeSymbols
}, [chartToShow])
return (
<>
<div className="flex items-center justify-between pb-4">
<h2>{t('account-performance')}</h2>
<Button
className={`float-right h-8 pt-0 pb-0 pl-3 pr-3 text-xs`}
onClick={exportPerformanceDataToCSV}
>
<div className={`flex items-center whitespace-nowrap`}>
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-data')}
</div>
</Button>
</div>
<div className="hidden pb-3 sm:block">
<ButtonGroup
activeValue={chartToShow}
className="min-h-[32px]"
onChange={(cat) => calculateChartData(cat)}
values={DATA_CATEGORIES}
names={DATA_CATEGORIES.map((val) => t(`account-performance:${val}`))}
/>
</div>
<Select
value={t(`account-performance:${chartToShow}`)}
onChange={(cat) => calculateChartData(cat)}
className="mb-3 sm:hidden"
>
{DATA_CATEGORIES.map((cat) => (
<Select.Option key={cat} value={cat}>
<div className="flex w-full items-center justify-between">
{t(`account-performance:${cat}`)}
</div>
</Select.Option>
))}
</Select>
{mangoAccount ? (
<>
<div
className="h-[540px] w-full rounded-lg rounded-b-none border border-th-bkg-3 p-6 pb-24 sm:pb-16"
ref={observe}
>
<div className="flex flex-col pb-4 sm:flex-row sm:items-center sm:justify-between">
<h3 className="mb-4 sm:mb-0">{`${t(
`account-performance:${chartToShow}`
)}`}</h3>
<div className="w-full sm:ml-auto sm:w-56">
<ButtonGroup
activeValue={performanceRange}
className="h-8"
onChange={(p) => setPerformanceRange(p)}
values={performanceRangePresetValues}
names={performanceRangePresetLabels}
/>
</div>
</div>
{!loading ? (
chartData.length > 0 && selectedSymbols.length > 0 ? (
<LineChart
width={width}
height={height}
data={chartData}
margin={{ top: 5, left: 16, right: 8, bottom: 5 }}
>
<XAxis
dataKey="time"
axisLine={false}
hide={chartData.length > 0 ? false : true}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => dayjs(v).format('D MMM')}
/>
<YAxis
type="number"
domain={['dataMin', 'dataMax']}
axisLine={false}
dx={-10}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => numberCompacter.format(v)}
/>
<Tooltip content={renderTooltip} cursor={false} />
{selectedSymbols.map((v, i) => {
const symbol =
v.includes('/') || v.includes('-') ? v.slice(0, -5) : v
return (
<Line
key={`${v}${i}`}
type="monotone"
dataKey={`${v}`}
stroke={`${CHART_COLORS(theme)[symbol]}`}
dot={false}
/>
)
})}
</LineChart>
) : selectedSymbols.length === 0 ? (
<div className="flex h-full w-full items-center justify-center p-4">
<p className="mb-0">
{t('account-performance:select-an-asset')}
</p>
</div>
) : (
<div className="flex h-full w-full items-center justify-center p-4">
<p className="mb-0">{t('account-performance:no-data')}</p>
</div>
)
) : loading ? (
<div
style={{ height: height - 16 }}
className="w-full animate-pulse rounded-md bg-th-bkg-3"
/>
) : null}
</div>
<div className="-mt-[1px] rounded-b-lg border border-th-bkg-3 py-3 px-6">
<div className="mb-2 flex items-center justify-between">
<p className="mb-0 font-bold">{t('assets')}</p>
<Checkbox
halfState={
selectedSymbols.length !== 0 &&
filteredSymbols.length !== selectedSymbols.length
}
checked={selectAll}
onChange={handleSelectAll}
>
{t('select-all')}
</Checkbox>
</div>
<div className="-ml-1 flex flex-wrap">
{symbols.map((s) => {
const symbol =
s.includes('/') || s.includes('-') ? s.slice(0, -5) : s
return (
<button
className={`default-transition m-1 flex items-center rounded-full border py-1 px-2 text-xs font-bold ${
selectedSymbols.includes(s)
? ''
: 'border-th-fgd-4 text-th-fgd-4 focus:border-th-fgd-3 focus:text-th-fgd-3 focus:outline-none md:hover:border-th-fgd-3 md:hover:text-th-fgd-3'
}`}
onClick={() => toggleOption(s)}
style={
selectedSymbols.includes(s)
? {
borderColor: CHART_COLORS(theme)[symbol],
color: CHART_COLORS(theme)[symbol],
}
: {}
}
key={s}
>
{renderSymbolIcon(s)}
{s == 'All' ? t(`account-performance:all`) : s}
</button>
)
})}
</div>
</div>
</>
) : (
<div>{t('connect-wallet')}</div>
)}
</>
)
}
export default AccountPerformance