add average rates chart to stats

This commit is contained in:
saml33 2024-03-07 12:22:26 +11:00
parent adc8b02fda
commit 6cb89c79ea
10 changed files with 1034 additions and 0 deletions

View File

@ -0,0 +1,68 @@
import { DownTriangle, UpTriangle } from './DirectionTriangles'
import FormatNumericValue from './FormatNumericValue'
const Change = ({
change,
decimals,
prefix,
size,
suffix,
}: {
change: number | typeof NaN
decimals?: number
prefix?: string
size?: 'small' | 'large'
suffix?: string
}) => {
return !isNaN(change) ? (
<div className="flex items-center space-x-1.5">
{change > 0 ? (
<div className="mt-[1px]">
<UpTriangle size={size} />
</div>
) : change < 0 ? (
<div className="mt-[1px]">
<DownTriangle size={size} />
</div>
) : null}
<p
className={`font-mono font-normal ${
size === 'small'
? 'text-xs'
: size === 'large'
? 'text-base'
: 'text-sm'
} ${
change > 0
? 'text-th-up'
: change < 0
? 'text-th-down'
: 'text-th-fgd-2'
}`}
>
{prefix ? prefix : ''}
<FormatNumericValue
value={isNaN(change) ? '0.00' : Math.abs(change)}
decimals={decimals ? decimals : 2}
/>
{suffix ? suffix : ''}
</p>
</div>
) : (
<p
className={`font-mono font-normal ${
size === 'small'
? 'text-xs'
: size === 'large'
? 'text-base'
: 'text-sm'
}`}
>
{prefix ? prefix : ''}
<FormatNumericValue value="0.00" decimals={decimals ? decimals : 2} />
{suffix ? suffix : ''}
</p>
)
}
export default Change

View File

@ -0,0 +1,53 @@
import { FunctionComponent } from 'react'
interface ChartRangeButtonsProps {
activeValue: string
className?: string
onChange: (x: string) => void
values: Array<string>
names?: Array<string>
}
const ChartRangeButtons: FunctionComponent<ChartRangeButtonsProps> = ({
activeValue,
className,
values,
onChange,
names,
}) => {
return (
<div className="relative flex">
{activeValue && values.includes(activeValue) ? (
<div
className="absolute left-0 top-0 h-full rounded-md bg-th-bkg-2"
style={{
transform: `translateX(${
values.findIndex((v) => v === activeValue) * 100
}%)`,
width: `${100 / values.length}%`,
}}
/>
) : null}
{values.map((v, i) => (
<button
className={`${className} relative h-6 cursor-pointer rounded-md px-3 text-center text-xs focus-visible:bg-th-bkg-2 focus-visible:text-th-fgd-1
${
v === activeValue
? `text-th-fgd-1`
: `text-th-fgd-4 md:hover:text-th-active`
}
`}
key={`${v}${i}`}
onClick={() => onChange(v)}
style={{
width: `${100 / values.length}%`,
}}
>
{names ? names[i] : v}
</button>
))}
</div>
)
}
export default ChartRangeButtons

View File

@ -0,0 +1,27 @@
type ContentBoxProps = {
children: React.ReactNode
className?: string
showBackground?: boolean
hideBorder?: boolean
hidePadding?: boolean
}
const ContentBox = ({
children,
className = '',
showBackground = false,
hideBorder = false,
hidePadding = false,
}: ContentBoxProps) => {
return (
<div
className={`${hideBorder ? '' : 'border border-th-bkg-3'} ${
showBackground ? 'bg-th-bkg-2' : ''
} ${hidePadding ? '' : 'p-6'} ${className}`}
>
{children}
</div>
)
}
export default ContentBox

View File

@ -0,0 +1,610 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FunctionComponent, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip as RechartsTooltip,
ResponsiveContainer,
ReferenceLine,
BarChart,
Bar,
Cell,
Label,
} from 'recharts'
import ContentBox from './ContentBox'
import SheenLoader from './SheenLoader'
import { ArrowsRightLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
import { FadeInFadeOut } from './Transitions'
import ChartRangeButtons from './ChartRangeButtons'
import Change from './Change'
import { DAILY_MILLISECONDS } from 'utils/constants'
import { AxisDomain } from 'recharts/types/util/types'
import { useTranslation } from 'next-i18next'
import FormatNumericValue from './FormatNumericValue'
import { ContentType } from 'recharts/types/component/Tooltip'
import Tooltip from './Tooltip'
dayjs.extend(relativeTime)
const titleClasses = 'mb-0.5 text-base'
interface DetailedAreaOrBarChartProps {
changeAsPercent?: boolean
chartType?: 'area' | 'bar'
customTooltip?: ContentType<number, string>
data: any[] | undefined
daysToShow?: string
domain?: AxisDomain
heightClass?: string
hideChange?: boolean
hideAxis?: boolean
loaderHeightClass?: string
loading?: boolean
prefix?: string
setDaysToShow?: (x: string) => void
small?: boolean
suffix?: string
tickFormat?: (x: number) => string
title?: string
tooltipContent?: string
xKey: string
formatXKeyHeading?: (k: string | number) => string
xAxisLabel?: string
xAxisType?: 'number' | 'category' | undefined
yDecimals?: number
yKey: string
showZeroLine?: boolean
tooltipDateFormat?: string
}
export const formatDateAxis = (date: string, days: number) => {
if (days === 1) {
return dayjs(date).format('h:mma')
} else {
return dayjs(date).format('D MMM')
}
}
const DetailedAreaOrBarChart: FunctionComponent<
DetailedAreaOrBarChartProps
> = ({
changeAsPercent,
chartType = 'area',
customTooltip,
data,
daysToShow = '1',
domain,
heightClass,
hideChange,
hideAxis,
loaderHeightClass,
loading,
prefix = '',
setDaysToShow,
showZeroLine,
small,
suffix = '',
tickFormat,
title,
tooltipContent,
tooltipDateFormat,
xKey,
formatXKeyHeading,
xAxisLabel,
xAxisType,
yDecimals,
yKey,
}) => {
const { t } = useTranslation('common')
const [mouseData, setMouseData] = useState<any>(null)
const [showChangePercentage, setShowChangePercentage] = useState(
changeAsPercent || false,
)
const handleMouseMove = (coords: any) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
const flipGradientCoords = useMemo(() => {
if (!data || !data.length) return
return data[0][yKey] <= 0 && data[data.length - 1][yKey] < 0
}, [data, yKey])
const filteredData = useMemo(() => {
if (!data || !data.length) return []
if (xAxisType === 'number') return data
const start = Number(daysToShow) * DAILY_MILLISECONDS
const filtered = data.filter((d: any) => {
const dataTime = new Date(d[xKey]).getTime()
const now = new Date().getTime()
const limit = now - start
return dataTime >= limit
})
return filtered
}, [data, daysToShow, xAxisType, xKey])
const calculateChartChange = () => {
if (filteredData.length) {
let firstValue = filteredData[0][yKey]
if (xAxisType === 'number') {
const minValue = filteredData.reduce(
(min, current) => (current[xKey] < min[xKey] ? current : min),
filteredData[0],
)
firstValue = minValue[yKey]
}
if (mouseData) {
const index = filteredData.findIndex(
(d: any) => d[xKey] === mouseData[xKey],
)
const currentValue = filteredData[index]?.[yKey]
const change =
index >= 0
? showChangePercentage
? ((currentValue - firstValue) / Math.abs(firstValue)) * 100
: currentValue - firstValue
: 0
return isNaN(change) ? 0 : change
} else {
const currentValue = filteredData[filteredData.length - 1][yKey]
return showChangePercentage
? ((currentValue - firstValue) / Math.abs(firstValue)) * 100
: currentValue - firstValue
}
}
return 0
}
return (
<FadeInFadeOut show={true}>
<ContentBox hideBorder hidePadding>
{loading ? (
<SheenLoader className="flex flex-1">
<div
className={`${
loaderHeightClass ? loaderHeightClass : 'h-96'
} w-full rounded-lg bg-th-bkg-2`}
/>
</SheenLoader>
) : (
<div className="relative">
{setDaysToShow ? (
<div className="mb-4 sm:absolute sm:-top-1 sm:right-0 sm:mb-0 sm:flex sm:justify-end">
<ChartRangeButtons
activeValue={daysToShow}
names={['24H', '7D', '30D']}
values={['1', '7', '30']}
onChange={(v) => setDaysToShow(v)}
/>
</div>
) : null}
{title ? (
tooltipContent ? (
<Tooltip content={tooltipContent}>
<p
className={`${titleClasses}
tooltip-underline`}
>
{title}
</p>
</Tooltip>
) : (
<p className={titleClasses}>{title}</p>
)
) : null}
{filteredData.length ? (
<>
<div className="flex items-start justify-between">
<div className="flex flex-col md:flex-row md:items-start md:space-x-6">
<div>
{mouseData ? (
<div>
<div
className={`flex items-end ${
small ? 'h-8 text-2xl' : 'mb-1 text-4xl'
} font-display text-th-fgd-1`}
>
<span className="tabular-nums">
{mouseData[yKey] < 0 ? '-' : ''}
{prefix}
<FormatNumericValue
value={Math.abs(mouseData[yKey])}
decimals={yDecimals}
/>
{suffix}
</span>
{!hideChange ? (
<div
className={`ml-3 flex items-center ${
small ? 'mb-[3px]' : 'mb-0.5'
}`}
>
<Change
change={calculateChartChange()}
decimals={
!showChangePercentage ? yDecimals : 2
}
prefix={!showChangePercentage ? prefix : ''}
suffix={!showChangePercentage ? suffix : '%'}
/>
{changeAsPercent ? (
<ToggleChangeTypeButton
changeType={showChangePercentage}
setChangeType={setShowChangePercentage}
/>
) : null}
</div>
) : null}
</div>
<p
className={`${
small ? 'text-xs' : 'text-sm'
} text-th-fgd-4`}
>
{formatXKeyHeading
? formatXKeyHeading(mouseData[xKey])
: dayjs(mouseData[xKey]).format(
tooltipDateFormat
? tooltipDateFormat
: 'DD MMM YY, h:mma',
)}
</p>
</div>
) : (
<div>
<div
className={`flex items-end ${
small ? 'h-8 text-2xl' : 'mb-1 text-4xl'
} font-display text-th-fgd-1`}
>
<span>
{filteredData[filteredData.length - 1][yKey] < 0
? '-'
: ''}
{prefix}
<span className="tabular-nums">
<FormatNumericValue
value={
data
? Math.abs(data[data.length - 1][yKey])
: 0
}
decimals={yDecimals}
/>
</span>
{suffix}
</span>
{!hideChange ? (
<div
className={`ml-3 flex items-center ${
small ? 'mb-[3px]' : 'mb-0.5'
}`}
>
<Change
change={calculateChartChange()}
decimals={
!showChangePercentage ? yDecimals : 2
}
prefix={!showChangePercentage ? prefix : ''}
suffix={!showChangePercentage ? suffix : '%'}
/>
{changeAsPercent ? (
<ToggleChangeTypeButton
changeType={showChangePercentage}
setChangeType={setShowChangePercentage}
/>
) : null}
</div>
) : null}
</div>
<p
className={`${
small ? 'text-xs' : 'text-sm'
} text-th-fgd-4`}
>
{formatXKeyHeading
? formatXKeyHeading(
filteredData[filteredData.length - 1][xKey],
)
: dayjs(
filteredData[filteredData.length - 1][xKey],
).format(
tooltipDateFormat
? tooltipDateFormat
: 'DD MMM YY, h:mma',
)}
</p>
</div>
)}
</div>
</div>
</div>
<div
className={`-mt-1 ${
heightClass ? heightClass : 'h-96'
} w-auto`}
>
<div className="-mx-6 mt-6 h-full">
<ResponsiveContainer width="100%" height="100%">
{chartType === 'area' ? (
<AreaChart
data={filteredData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<RechartsTooltip
cursor={{
strokeOpacity: 0.09,
}}
content={customTooltip ? customTooltip : <></>}
/>
<defs>
<linearGradient
id={`gradientArea-${title?.replace(
/[^a-zA-Z]/g,
'',
)}`}
x1="0"
y1={flipGradientCoords ? '1' : '0'}
x2="0"
y2={flipGradientCoords ? '0' : '1'}
>
<stop
offset="0%"
stopColor={
calculateChartChange() >= 0
? 'var(--success)'
: 'var(--error)'
}
stopOpacity={0.15}
/>
<stop
offset="99%"
stopColor={
calculateChartChange() >= 0
? 'var(--success)'
: 'var(--error)'
}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey={yKey}
stroke={
isNaN(calculateChartChange())
? 'var(--fgd-4)'
: calculateChartChange() >= 0
? 'var(--success)'
: 'var(--error)'
}
strokeWidth={1.5}
fill={`url(#gradientArea-${title?.replace(
/[^a-zA-Z]/g,
'',
)})`}
/>
<XAxis
axisLine={false}
dataKey={xKey}
hide={hideAxis}
minTickGap={20}
padding={{ left: 20, right: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
xAxisType !== 'number'
? (d) => formatDateAxis(d, parseInt(daysToShow))
: undefined
}
type={xAxisType}
>
{xAxisLabel ? (
<Label
value={xAxisLabel}
offset={-2}
position="insideBottom"
fontSize={10}
fill="var(--fgd-3)"
/>
) : null}
</XAxis>
<YAxis
axisLine={false}
dataKey={yKey}
hide={hideAxis}
minTickGap={20}
type="number"
domain={
domain
? domain
: ([dataMin, dataMax]) => {
const difference = dataMax - dataMin
if (difference < 0.01) {
return [dataMin - 0.001, dataMax + 0.001]
} else if (difference < 0.1) {
return [dataMin - 0.01, dataMax + 0.01]
} else if (difference < 1) {
return [dataMin - 0.1, dataMax + 0.11]
} else if (difference < 10) {
return [dataMin - 1, dataMax + 1]
} else {
return [dataMin, dataMax]
}
}
}
padding={{ top: 20, bottom: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickFormatter={
tickFormat ? (v) => tickFormat(v) : undefined
}
tickLine={false}
/>
{showZeroLine ? (
<ReferenceLine
y={0}
stroke="var(--fgd-4)"
strokeDasharray="2 2"
/>
) : null}
</AreaChart>
) : (
<BarChart
data={filteredData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<RechartsTooltip
cursor={{
fill: 'var(--bkg-2)',
opacity: 0.5,
}}
content={customTooltip ? customTooltip : <></>}
/>
<defs>
<linearGradient
id="greenGradientBar"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor="var(--success)"
stopOpacity={1}
/>
<stop
offset="100%"
stopColor="var(--success)"
stopOpacity={0.7}
/>
</linearGradient>
<linearGradient
id="redGradientBar"
x1="0"
y1="1"
x2="0"
y2="0"
>
<stop
offset="0%"
stopColor="var(--error)"
stopOpacity={1}
/>
<stop
offset="100%"
stopColor="var(--error)"
stopOpacity={0.7}
/>
</linearGradient>
</defs>
<Bar dataKey={yKey}>
{filteredData.map((entry, index) => {
return (
<Cell
key={`cell-${index}`}
fill={
entry[yKey] > 0
? 'url(#greenGradientBar)'
: 'url(#redGradientBar)'
}
/>
)
})}
</Bar>
<XAxis
dataKey={xKey}
axisLine={false}
dy={10}
minTickGap={20}
padding={{ left: 20, right: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) =>
formatDateAxis(v, parseInt(daysToShow))
}
/>
<YAxis
dataKey={yKey}
interval="preserveStartEnd"
axisLine={false}
dx={-10}
padding={{ top: 20, bottom: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
tickFormat ? (v) => tickFormat(v) : undefined
}
type="number"
/>
<ReferenceLine y={0} stroke="var(--bkg-4)" />
</BarChart>
)}
</ResponsiveContainer>
</div>
</div>
</>
) : (
<div
className={`flex ${
heightClass ? heightClass : 'h-96'
} mt-4 items-center justify-center rounded-lg border border-th-bkg-3 p-8 text-th-fgd-3`}
>
<div>
<NoSymbolIcon className="mx-auto mb-1 h-6 w-6 text-th-fgd-4" />
<p className="text-th-fgd-3">{t('no-data')}</p>
</div>
</div>
)}
</div>
)}
</ContentBox>
</FadeInFadeOut>
)
}
export default DetailedAreaOrBarChart
const ToggleChangeTypeButton = ({
changeType,
setChangeType,
}: {
changeType: boolean
setChangeType: (isPercent: boolean) => void
}) => {
return (
<button
className="ml-2 flex h-4 w-4 items-center justify-center text-th-fgd-3 focus:outline-none md:hover:text-th-active"
onClick={() => setChangeType(!changeType)}
>
<ArrowsRightLeftIcon className="h-3.5 w-3.5" />
</button>
)
}

View File

@ -0,0 +1,19 @@
export const UpTriangle = ({ size }: { size?: 'small' | 'large' }) => (
<div
className={`h-0 w-0 ${
size === 'small'
? 'border-b-[6.92px] border-l-[4px] border-r-[4px]'
: 'border-b-[8.65px] border-l-[5px] border-r-[5px]'
} border-b-th-up border-l-transparent border-r-transparent`}
/>
)
export const DownTriangle = ({ size }: { size?: 'small' | 'large' }) => (
<div
className={`h-0 w-0 ${
size === 'small'
? 'border-l-[4px] border-r-[4px] border-t-[6.92px]'
: 'border-l-[5px] border-r-[5px] border-t-[8.65px]'
} border-l-transparent border-r-transparent border-t-th-down`}
/>
)

View File

@ -0,0 +1,57 @@
import { useQuery } from '@tanstack/react-query'
import useMangoGroup from 'hooks/useMangoGroup'
import { useMemo, useState } from 'react'
import { fetchTokenStatsData } from 'utils/stats'
import TokenRatesChart from './TokenRatesChart'
const HistoricalStats = () => {
const { group } = useMangoGroup()
const [depositDaysToShow, setDepositDaysToShow] = useState('30')
const { data: historicalStats, isLoading: loadingHistoricalStats } = useQuery(
['historical-stats'],
() => fetchTokenStatsData(group),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!group,
},
)
const usdcStats = useMemo(() => {
if (!historicalStats?.length) return []
return historicalStats
.filter((token) => token.symbol === 'USDC')
.sort(
(a, b) =>
new Date(a.date_hour).getTime() - new Date(b.date_hour).getTime(),
)
}, [historicalStats])
const chartData = useMemo(() => {
if (!usdcStats?.length) return []
const statsToFormat = [...usdcStats]
for (const stat of statsToFormat) {
const APY_Daily_Compound = Math.pow(1 + stat.deposit_rate / 365, 365) - 1
stat.deposit_apy = APY_Daily_Compound
}
return statsToFormat
}, [usdcStats])
return (
<div className="px-6 pt-8">
<TokenRatesChart
data={chartData}
dataKey="deposit_apy"
daysToShow={depositDaysToShow}
loading={loadingHistoricalStats}
setDaysToShow={setDepositDaysToShow}
title={`USDC Deposit Rates (APR)`}
/>
</div>
)
}
export default HistoricalStats

View File

@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
import { STAKEABLE_TOKENS } from 'utils/constants'
import { formatCurrencyValue } from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
import HistoricalStats from './HistoricalStats'
const StatsPage = () => {
const { group } = useMangoGroup()
@ -138,6 +139,7 @@ const StatsPage = () => {
})}
</div>
)}
<HistoricalStats />
</div>
)
}

View File

@ -0,0 +1,112 @@
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { TokenStatsItem } from 'types'
interface GroupedDataItem extends TokenStatsItem {
intervalStartMillis: number
}
const groupByHourlyInterval = (
data: TokenStatsItem[],
dataKey: 'borrow_apr' | 'deposit_apy',
intervalDurationHours: number,
) => {
const intervalMillis = intervalDurationHours * 60 * 60 * 1000
const groupedData = []
let currentGroup: GroupedDataItem | null = null
let itemsInCurrentGroup = 0
for (let i = 0; i < data.length; i++) {
const obj = data[i]
const date = new Date(obj.date_hour)
const intervalStartMillis =
Math.floor(date.getTime() / intervalMillis) * intervalMillis
if (
!currentGroup ||
currentGroup.intervalStartMillis !== intervalStartMillis
) {
if (currentGroup) {
// calculate the average for the previous group
currentGroup[dataKey] /= itemsInCurrentGroup
groupedData.push(currentGroup)
}
currentGroup = {
...obj,
intervalStartMillis: intervalStartMillis,
}
// initialize the sum for the new group
currentGroup[dataKey] = obj[dataKey] * 100
itemsInCurrentGroup = 1
} else {
// add the value to the sum for the current group
currentGroup[dataKey] += obj[dataKey] * 100
itemsInCurrentGroup++
}
}
// calculate the average for the last group (if it exists)
if (currentGroup) {
currentGroup[dataKey] /= itemsInCurrentGroup
groupedData.push(currentGroup)
}
return groupedData
}
const TokenRatesChart = ({
data,
dataKey,
daysToShow,
loading,
setDaysToShow,
title,
}: {
data: TokenStatsItem[]
dataKey: 'deposit_apy' | 'borrow_apr'
daysToShow: string | undefined
loading: boolean
setDaysToShow: (x: string) => void
title: string
}) => {
const { t } = useTranslation('stats')
const [interval, intervalString] = useMemo(() => {
if (daysToShow === '30') {
return [24, 'Daily']
} else if (daysToShow === '7') {
return [6, 'Six-hourly']
} else {
return [1, 'Hourly']
}
}, [daysToShow])
const chartData = useMemo(() => {
if (!data || !data.length) return []
const groupedData = groupByHourlyInterval(data, dataKey, interval)
return groupedData
}, [data, dataKey, interval])
return (
<DetailedAreaOrBarChart
chartType="bar"
data={chartData}
daysToShow={daysToShow}
setDaysToShow={setDaysToShow}
heightClass="h-64"
loaderHeightClass="h-[334px]"
loading={loading}
small
hideChange
suffix="%"
tickFormat={(x) => `${x.toFixed(2)}%`}
title={`${t(intervalString)} ${title}`}
xKey="date_hour"
yKey={dataKey}
/>
)
}
export default TokenRatesChart

View File

@ -364,6 +364,7 @@ export interface TokenStatsItem {
collected_fees: number
date_hour: string
deposit_apr: number
deposit_apy: number
deposit_rate: number
mango_group: string
price: number

85
utils/stats.ts Normal file
View File

@ -0,0 +1,85 @@
import { Group } from '@blockworks-foundation/mango-v4'
import { MangoTokenStatsItem, TokenStatsItem } from 'types'
import { MANGO_DATA_API_URL } from 'utils/constants'
export const fetchTokenStatsData = async (group: Group | undefined) => {
if (!group) return []
const response = await fetch(
`${MANGO_DATA_API_URL}/token-historical-stats?mango-group=${group?.publicKey.toString()}`,
)
if (!response.ok) {
throw new Error('Network response was not ok')
}
const rawData = await response.json()
const [data] = processTokenStatsData(rawData, group)
return data
}
export const processTokenStatsData = (
data: TokenStatsItem[] | unknown,
group: Group,
) => {
const mangoStatsMap: Record<string, MangoTokenStatsItem> = {}
if (!Array.isArray(data)) return []
data.forEach((c) => {
const bank = group.banksMapByTokenIndex.get(c.token_index)?.[0]
if (!bank) return
const date: string = c.date_hour
const uiPrice = bank.uiPrice
if (!mangoStatsMap[date]) {
mangoStatsMap[date] = {
date,
depositValue: 0,
borrowValue: 0,
feesCollected: 0,
}
}
mangoStatsMap[date].depositValue += Math.floor(c.total_deposits * c.price)
mangoStatsMap[date].borrowValue += Math.floor(c.total_borrows * c.price)
mangoStatsMap[date].feesCollected += c.collected_fees * uiPrice
})
// add most recent value, using most recent datapoint to patch difficult to compute stats
for (const banks of group.banksMapByTokenIndex.values()) {
const bank = banks[0]
const now = new Date().toISOString()
const filtered = data.filter(
(x: TokenStatsItem) => bank.tokenIndex === x?.token_index,
)
if (!filtered || filtered.length === 0) {
continue
}
const previous = filtered.reduce((max, cur) =>
max.date_hour > cur.date_hour ? max : cur,
)
const APY_Daily_Compound =
Math.pow(1 + bank.getDepositRateUi() / 365, 365) - 1
const tokenStatsItem: TokenStatsItem = {
borrow_apr: previous.borrow_apr,
borrow_rate: bank.getBorrowRateUi() / 100,
collected_fees: previous.collected_fees,
date_hour: now,
deposit_apr: previous.deposit_apr,
deposit_apy: APY_Daily_Compound,
deposit_rate: bank.getDepositRateUi() / 100,
mango_group: bank.group.toBase58(),
price: bank.uiPrice,
symbol: bank.name,
token_index: bank.tokenIndex,
total_borrows: bank.uiBorrows(),
total_deposits: bank.uiDeposits(),
}
data.push(tokenStatsItem)
}
const mangoStats: MangoTokenStatsItem[] = Object.values(mangoStatsMap)
mangoStats.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
)
return [data, mangoStats]
}