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

478 lines
15 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import isEmpty from 'lodash/isEmpty'
import { useTranslation } from 'next-i18next'
import { LineChart, XAxis, YAxis, Line, Tooltip } from 'recharts'
import { SaveIcon } from '@heroicons/react/outline'
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 { QuestionMarkCircleIcon } from '@heroicons/react/outline'
import { useTheme } from 'next-themes'
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)
}
// Each line added to the graph will use one of these colors
const COLORS = {
All: '#ff7c43',
USDC: '#ffa600',
SRM: '#8dd3c7',
SOL: '#A288E3',
RAY: '#4AB839',
MSOL: '#fb8072',
MNGO: '#80b1d3',
LUNA: '#fdb462',
AVAX: '#b3de69',
BNB: '#FF47A6',
FTT: '#A38560',
BTC: '#bc80bd',
ETH: '#05C793',
ADA: '#3F8EFC',
GMT: '#CBA74A',
}
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',
// '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 { 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']
},
}
const metric: (values: []) => void = metrics[chartToShow]
const 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),
}
})
// Normalise chart to start from 0 (except for account value)
if (parseInt(performanceRange) !== 90 && chartToShow !== 'account-value') {
const startValues = Object.assign({}, stats[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 fetchHourlyPerformanceStats = async () => {
setLoading(true)
const response = await 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')}`
)
const parsedResponse = await response.json()
let entries: any = Object.entries(parsedResponse)
entries = entries
.map(([key, value]) => [key, Object.entries(value)])
.reverse()
const uniqueSymbols = [
...new Set(
([] as string[]).concat(
['All'],
...entries.map(([_, tokens]) => tokens.map(([token, _]) => token))
)
),
]
setUniqueSymbols(uniqueSymbols)
setFilteredSymbols(uniqueSymbols)
setHourlyPerformanceStats(entries)
setLoading(false)
}
fetchHourlyPerformanceStats()
}, [mangoAccountPk, performanceRange])
useEffect(() => {
calculateChartData(chartToShow)
}, [hourlyPerformanceStats])
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') {
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" />
}
}
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-4 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>
{!isEmpty(hourlyPerformanceStats) && !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) => (
<Line
key={`${v}${i}`}
type="monotone"
dataKey={`${v}`}
stroke={`${COLORS[v]}`}
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>
) : null
) : 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-4 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">
{filteredSymbols.map((s) => (
<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 hover:border-th-fgd-3 hover:text-th-fgd-3 focus:border-th-fgd-3 focus:text-th-fgd-3 focus:outline-none'
}`}
onClick={() => toggleOption(s)}
style={
selectedSymbols.includes(s)
? { borderColor: COLORS[s], color: COLORS[s] }
: {}
}
key={s}
>
{renderSymbolIcon(s)}
{s}
</button>
))}
</div>
</div>
</>
) : (
<div>{t('connect-wallet')}</div>
)}
</>
)
}
export default AccountPerformance