Merge pull request #198 from blockworks-foundation/health-contributions

health contributions
This commit is contained in:
saml33 2023-07-18 21:01:43 +10:00 committed by GitHub
commit 0f3f20d51b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1417 additions and 149 deletions

View File

@ -4,18 +4,18 @@ import { formatYAxis } from 'utils/formatting'
import { HourlyFundingChartData, PerformanceDataItem } from 'types' import { HourlyFundingChartData, PerformanceDataItem } from 'types'
import { ContentType } from 'recharts/types/component/Tooltip' import { ContentType } from 'recharts/types/component/Tooltip'
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart' import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
import { ChartToShow } from './AccountPage' import { ViewToShow } from './AccountPage'
import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid' import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
const CHART_TABS: ChartToShow[] = [ const CHART_TABS: ViewToShow[] = [
'account-value', 'account-value',
'pnl', 'pnl',
'cumulative-interest-value', 'cumulative-interest-value',
] ]
const AccountChart = ({ const AccountChart = ({
chartToShow, chartName,
setChartToShow, handleViewChange,
customTooltip, customTooltip,
data, data,
hideChart, hideChart,
@ -23,8 +23,8 @@ const AccountChart = ({
yDecimals, yDecimals,
yKey, yKey,
}: { }: {
chartToShow: ChartToShow chartName: ViewToShow
setChartToShow: (chart: ChartToShow) => void handleViewChange: (view: ViewToShow) => void
customTooltip?: ContentType<number, string> customTooltip?: ContentType<number, string>
data: PerformanceDataItem[] | HourlyFundingChartData[] | undefined data: PerformanceDataItem[] | HourlyFundingChartData[] | undefined
hideChart: () => void hideChart: () => void
@ -37,7 +37,7 @@ const AccountChart = ({
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!data || !data.length) return [] if (!data || !data.length) return []
if (chartToShow === 'cumulative-interest-value') { if (chartName === 'cumulative-interest-value') {
return data.map((d) => ({ return data.map((d) => ({
interest_value: interest_value:
d.borrow_interest_cumulative_usd * -1 + d.borrow_interest_cumulative_usd * -1 +
@ -46,7 +46,7 @@ const AccountChart = ({
})) }))
} }
return data return data
}, [data, chartToShow]) }, [data, chartName])
return ( return (
<> <>
@ -61,11 +61,11 @@ const AccountChart = ({
{CHART_TABS.map((tab) => ( {CHART_TABS.map((tab) => (
<button <button
className={`whitespace-nowrap rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${ className={`whitespace-nowrap rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
chartToShow === tab chartName === tab
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active' ? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
: 'text-th-fgd-3 md:hover:text-th-fgd-2' : 'text-th-fgd-3 md:hover:text-th-fgd-2'
}`} }`}
onClick={() => setChartToShow(tab)} onClick={() => handleViewChange(tab)}
key={tab} key={tab}
> >
{t(tab)} {t(tab)}

View File

@ -3,7 +3,7 @@ import useMangoAccount from 'hooks/useMangoAccount'
import useMangoGroup from 'hooks/useMangoGroup' import useMangoGroup from 'hooks/useMangoGroup'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { ChartToShow } from './AccountPage' import { ViewToShow } from './AccountPage'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { fetchFundingTotals, fetchVolumeTotals } from 'utils/account' import { fetchFundingTotals, fetchVolumeTotals } from 'utils/account'
import Tooltip from '@components/shared/Tooltip' import Tooltip from '@components/shared/Tooltip'
@ -25,13 +25,13 @@ const AccountHeroStats = ({
accountValue, accountValue,
rollingDailyData, rollingDailyData,
setShowPnlHistory, setShowPnlHistory,
setChartToShow, handleViewChange,
}: { }: {
accountPnl: number accountPnl: number
accountValue: number accountValue: number
rollingDailyData: PerformanceDataItem[] rollingDailyData: PerformanceDataItem[]
setShowPnlHistory: (show: boolean) => void setShowPnlHistory: (show: boolean) => void
setChartToShow: (view: ChartToShow) => void handleViewChange: (view: ViewToShow) => void
}) => { }) => {
const { t } = useTranslation(['common', 'account']) const { t } = useTranslation(['common', 'account'])
const { group } = useMangoGroup() const { group } = useMangoGroup()
@ -162,71 +162,78 @@ const AccountHeroStats = ({
return volume return volume
}, [hourlyVolumeData]) }, [hourlyVolumeData])
const handleChartToShow = (
viewName:
| 'pnl'
| 'cumulative-interest-value'
| 'hourly-funding'
| 'hourly-volume'
) => {
setChartToShow(viewName)
}
const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData
return ( return (
<> <>
<div className="grid grid-cols-6 border-b border-th-bkg-3"> <div className="grid grid-cols-6 border-b border-th-bkg-3">
<div className="col-span-6 border-t border-th-bkg-3 py-3 px-6 md:col-span-3 lg:col-span-2 lg:border-t-0 xl:col-span-1"> <div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-t-0 xl:col-span-1">
<div id="account-step-four"> <div id="account-step-four">
<Tooltip <div className="flex justify-between">
maxWidth="20rem" <Tooltip
placement="top-start" maxWidth="20rem"
delay={100} placement="top-start"
content={ delay={100}
<div className="flex-col space-y-2 text-sm"> content={
<p className="text-xs"> <div className="flex-col space-y-2 text-sm">
Health describes how close your account is to liquidation. <p className="text-xs">
The lower your account health is the more likely you are to Health describes how close your account is to liquidation.
get liquidated when prices fluctuate. The lower your account health is the more likely you are
</p> to get liquidated when prices fluctuate.
{maintHealth < 100 && mangoAccountAddress ? ( </p>
<> {maintHealth < 100 && mangoAccountAddress ? (
<p className="text-xs font-bold text-th-fgd-1"> <>
Your account health is {maintHealth}% <p className="text-xs font-bold text-th-fgd-1">
</p> Your account health is {maintHealth}%
<p className="text-xs"> </p>
<span className="font-bold text-th-fgd-1"> <p className="text-xs">
Scenario: <span className="font-bold text-th-fgd-1">
</span>{' '} Scenario:
If the prices of all your liabilities increase by{' '} </span>{' '}
{maintHealth}%, even for just a moment, some of your If the prices of all your liabilities increase by{' '}
liabilities will be liquidated. {maintHealth}%, even for just a moment, some of your
</p> liabilities will be liquidated.
<p className="text-xs"> </p>
<span className="font-bold text-th-fgd-1"> <p className="text-xs">
Scenario: <span className="font-bold text-th-fgd-1">
</span>{' '} Scenario:
If the value of your total collateral decreases by{' '} </span>{' '}
{( If the value of your total collateral decreases by{' '}
(1 - 1 / ((maintHealth || 0) / 100 + 1)) * {(
100 (1 - 1 / ((maintHealth || 0) / 100 + 1)) *
).toFixed(2)} 100
% , some of your liabilities will be liquidated. ).toFixed(2)}
</p> % , some of your liabilities will be liquidated.
<p className="text-xs"> </p>
These are examples. A combination of events can also <p className="text-xs">
lead to liquidation. These are examples. A combination of events can also
</p> lead to liquidation.
</> </p>
) : null} </>
</div> ) : null}
} </div>
> }
<p className="tooltip-underline text-sm font-normal text-th-fgd-3 xl:text-base"> >
{t('health')} <p className="tooltip-underline text-sm font-normal text-th-fgd-3 xl:text-base">
</p> {t('health')}
</Tooltip> </p>
</Tooltip>
{mangoAccountAddress ? (
<Tooltip
className="hidden md:block"
content={t('account:health-contributions')}
delay={100}
>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => handleViewChange('health-contributions')}
>
<ChartBarIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
<div className="mt-1 mb-0.5 flex items-center space-x-3"> <div className="mt-1 mb-0.5 flex items-center space-x-3">
<p className="text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl"> <p className="text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
{maintHealth}% {maintHealth}%
@ -298,7 +305,7 @@ const AccountHeroStats = ({
</span> </span>
</div> </div>
</div> </div>
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 px-6 md:col-span-3 lg:col-span-2 lg:border-l lg:border-t-0 xl:col-span-1"> <div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-l lg:border-t-0 xl:col-span-1">
<div <div
id="account-step-seven" id="account-step-seven"
className="flex w-full flex-col items-start" className="flex w-full flex-col items-start"
@ -323,7 +330,7 @@ const AccountHeroStats = ({
<IconButton <IconButton
className="text-th-fgd-3" className="text-th-fgd-3"
hideBg hideBg
onClick={() => handleChartToShow('pnl')} onClick={() => handleViewChange('pnl')}
> >
<ChartBarIcon className="h-5 w-5" /> <ChartBarIcon className="h-5 w-5" />
</IconButton> </IconButton>
@ -372,7 +379,7 @@ const AccountHeroStats = ({
<IconButton <IconButton
className="text-th-fgd-3" className="text-th-fgd-3"
hideBg hideBg
onClick={() => handleChartToShow('hourly-volume')} onClick={() => handleViewChange('hourly-volume')}
> >
<ChartBarIcon className="h-5 w-5" /> <ChartBarIcon className="h-5 w-5" />
</IconButton> </IconButton>
@ -429,7 +436,7 @@ const AccountHeroStats = ({
className="text-th-fgd-3" className="text-th-fgd-3"
hideBg hideBg
onClick={() => onClick={() =>
handleChartToShow('cumulative-interest-value') handleViewChange('cumulative-interest-value')
} }
> >
<ChartBarIcon className="h-5 w-5" /> <ChartBarIcon className="h-5 w-5" />
@ -471,7 +478,7 @@ const AccountHeroStats = ({
<IconButton <IconButton
className="text-th-fgd-3" className="text-th-fgd-3"
hideBg hideBg
onClick={() => handleChartToShow('hourly-funding')} onClick={() => handleViewChange('hourly-funding')}
> >
<ChartBarIcon className="h-5 w-5" /> <ChartBarIcon className="h-5 w-5" />
</IconButton> </IconButton>

View File

@ -1,12 +1,11 @@
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import AccountActions from './AccountActions' import AccountActions from './AccountActions'
import AccountTabs from './AccountTabs' import AccountTabs from './AccountTabs'
import AccountChart from './AccountChart' import AccountChart from './AccountChart'
import useMangoAccount from '../../hooks/useMangoAccount' import useMangoAccount from '../../hooks/useMangoAccount'
import useLocalStorageState from 'hooks/useLocalStorageState' import useLocalStorageState from 'hooks/useLocalStorageState'
// import AccountOnboardingTour from '@components/tours/AccountOnboardingTour'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useViewport } from 'hooks/useViewport' import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme' import { breakpoints } from 'utils/theme'
@ -18,39 +17,44 @@ import VolumeChart from './VolumeChart'
import AccountHeroStats from './AccountHeroStats' import AccountHeroStats from './AccountHeroStats'
import AccountValue from './AccountValue' import AccountValue from './AccountValue'
import useAccountPerformanceData from 'hooks/useAccountPerformanceData' import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats' import HealthContributions from './HealthContributions'
import { PerformanceDataItem } from 'types'
import { useRouter } from 'next/router'
import { useWallet } from '@solana/wallet-adapter-react'
const TABS = ['account-value', 'account:assets-liabilities'] const TABS = ['account-value', 'account:assets-liabilities']
export type ChartToShow = export type ViewToShow =
| '' | ''
| 'account-value' | 'account-value'
| 'cumulative-interest-value' | 'cumulative-interest-value'
| 'pnl' | 'pnl'
| 'hourly-funding' | 'hourly-funding'
| 'hourly-volume' | 'hourly-volume'
| 'health-contributions'
const AccountPage = () => { const AccountPage = () => {
const { t } = useTranslation(['common', 'account']) const { t } = useTranslation(['common', 'account'])
const { group } = useMangoGroup() const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount() const { mangoAccount } = useMangoAccount()
const [chartToShow, setChartToShow] = useState<ChartToShow>('')
const [showPnlHistory, setShowPnlHistory] = useState<boolean>(false) const [showPnlHistory, setShowPnlHistory] = useState<boolean>(false)
const { width } = useViewport() const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false const isMobile = width ? width < breakpoints.md : false
// const tourSettings = mangoStore((s) => s.settings.tours)
// const [isOnBoarded] = useLocalStorageState(IS_ONBOARDED_KEY)
const [activeTab, setActiveTab] = useLocalStorageState( const [activeTab, setActiveTab] = useLocalStorageState(
'accountHeroKey-0.1', 'accountHeroKey-0.1',
'account-value' 'account-value'
) )
const { performanceData, rollingDailyData } = useAccountPerformanceData() const { performanceData, rollingDailyData } = useAccountPerformanceData()
const { hourlyVolumeData, loadingHourlyVolume } = const router = useRouter()
useAccountHourlyVolumeStats() const { view } = router.query
const handleHideChart = () => { const handleViewChange = useCallback(
setChartToShow('') (view: ViewToShow) => {
} const query = { ...router.query, ['view']: view }
router.push({ pathname: router.pathname, query })
},
[router]
)
const handleCloseDailyPnlModal = () => { const handleCloseDailyPnlModal = () => {
setShowPnlHistory(false) setShowPnlHistory(false)
@ -94,7 +98,7 @@ const AccountPage = () => {
] ]
}, [accountPnl, accountValue, performanceData]) }, [accountPnl, accountValue, performanceData])
return !chartToShow ? ( return !view ? (
<> <>
<div className="flex flex-col border-b-0 border-th-bkg-3 px-6 py-4 lg:flex-row lg:items-center lg:justify-between lg:border-b"> <div className="flex flex-col border-b-0 border-th-bkg-3 px-6 py-4 lg:flex-row lg:items-center lg:justify-between lg:border-b">
<div> <div>
@ -119,7 +123,7 @@ const AccountPage = () => {
accountValue={accountValue} accountValue={accountValue}
latestAccountData={latestAccountData} latestAccountData={latestAccountData}
rollingDailyData={rollingDailyData} rollingDailyData={rollingDailyData}
setChartToShow={setChartToShow} handleViewChange={handleViewChange}
/> />
) : null} ) : null}
{activeTab === 'account:assets-liabilities' ? ( {activeTab === 'account:assets-liabilities' ? (
@ -135,13 +139,10 @@ const AccountPage = () => {
accountPnl={accountPnl} accountPnl={accountPnl}
accountValue={accountValue} accountValue={accountValue}
rollingDailyData={rollingDailyData} rollingDailyData={rollingDailyData}
setChartToShow={setChartToShow}
setShowPnlHistory={setShowPnlHistory} setShowPnlHistory={setShowPnlHistory}
handleViewChange={handleViewChange}
/> />
<AccountTabs /> <AccountTabs />
{/* {!tourSettings?.account_tour_seen && isOnBoarded && connected ? (
<AccountOnboardingTour />
) : null} */}
{showPnlHistory ? ( {showPnlHistory ? (
<PnlHistoryModal <PnlHistoryModal
pnlChangeToday={pnlChangeToday} pnlChangeToday={pnlChangeToday}
@ -151,42 +152,76 @@ const AccountPage = () => {
) : null} ) : null}
</> </>
) : ( ) : (
<> <AccountView
{chartToShow === 'account-value' ? ( view={view as ViewToShow}
<AccountChart latestAccountData={latestAccountData}
chartToShow="account-value" handleViewChange={handleViewChange}
setChartToShow={setChartToShow} />
data={performanceData?.concat(latestAccountData)}
hideChart={handleHideChart}
yKey="account_equity"
/>
) : chartToShow === 'pnl' ? (
<AccountChart
chartToShow="pnl"
setChartToShow={setChartToShow}
data={performanceData?.concat(latestAccountData)}
hideChart={handleHideChart}
yKey="pnl"
/>
) : chartToShow === 'hourly-funding' ? (
<FundingChart hideChart={handleHideChart} />
) : chartToShow === 'hourly-volume' ? (
<VolumeChart
chartData={hourlyVolumeData}
hideChart={handleHideChart}
loading={loadingHourlyVolume}
/>
) : (
<AccountChart
chartToShow="cumulative-interest-value"
setChartToShow={setChartToShow}
data={performanceData}
hideChart={handleHideChart}
yKey="interest_value"
/>
)}
</>
) )
} }
export default AccountPage export default AccountPage
const AccountView = ({
view,
handleViewChange,
latestAccountData,
}: {
view: ViewToShow
latestAccountData: PerformanceDataItem[]
handleViewChange: (view: ViewToShow) => void
}) => {
const router = useRouter()
const { connected } = useWallet()
const { address } = router.query
const { performanceData } = useAccountPerformanceData()
const handleHideChart = useCallback(() => {
if (address && !connected) {
router.push(`/?address=${address}`, undefined, { shallow: true })
} else {
router.push('/', undefined, { shallow: true })
}
}, [address, router, connected])
switch (view) {
case 'account-value':
return (
<AccountChart
chartName="account-value"
handleViewChange={handleViewChange}
data={performanceData?.concat(latestAccountData)}
hideChart={handleHideChart}
yKey="account_equity"
/>
)
case 'pnl':
return (
<AccountChart
chartName="pnl"
handleViewChange={handleViewChange}
data={performanceData?.concat(latestAccountData)}
hideChart={handleHideChart}
yKey="pnl"
/>
)
case 'cumulative-interest-value':
return (
<AccountChart
chartName="cumulative-interest-value"
handleViewChange={handleViewChange}
data={performanceData?.concat(latestAccountData)}
hideChart={handleHideChart}
yKey="interest_value"
/>
)
case 'hourly-funding':
return <FundingChart hideChart={handleHideChart} />
case 'hourly-volume':
return <VolumeChart hideChart={handleHideChart} />
case 'health-contributions':
return <HealthContributions hideView={handleHideChart} />
default:
return null
}
}

View File

@ -19,19 +19,19 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { useViewport } from 'hooks/useViewport' import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme' import { breakpoints } from 'utils/theme'
import { ChartToShow } from './AccountPage' import { ViewToShow } from './AccountPage'
import useAccountPerformanceData from 'hooks/useAccountPerformanceData' import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
const AccountValue = ({ const AccountValue = ({
accountValue, accountValue,
latestAccountData, latestAccountData,
rollingDailyData, rollingDailyData,
setChartToShow, handleViewChange,
}: { }: {
accountValue: number accountValue: number
latestAccountData: PerformanceDataItem[] latestAccountData: PerformanceDataItem[]
rollingDailyData: PerformanceDataItem[] rollingDailyData: PerformanceDataItem[]
setChartToShow: (chart: ChartToShow) => void handleViewChange: (view: ViewToShow) => void
}) => { }) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { theme } = useTheme() const { theme } = useTheme()
@ -62,7 +62,7 @@ const AccountValue = ({
} }
const handleShowAccountValueChart = () => { const handleShowAccountValueChart = () => {
setChartToShow('account-value') handleViewChange('account-value')
setShowExpandChart(false) setShowExpandChart(false)
} }

View File

@ -0,0 +1,338 @@
import HealthContributionsChart from './HealthContributionsChart'
import useMangoGroup from 'hooks/useMangoGroup'
import useMangoAccount from 'hooks/useMangoAccount'
import { useMemo, useState } from 'react'
import { HealthType } from '@blockworks-foundation/mango-v4'
import {
ArrowLeftIcon,
NoSymbolIcon,
QuestionMarkCircleIcon,
} from '@heroicons/react/20/solid'
import Tooltip from '@components/shared/Tooltip'
import TokenLogo from '@components/shared/TokenLogo'
import { useTranslation } from 'next-i18next'
import MarketLogos from '@components/trade/MarketLogos'
import mangoStore from '@store/mangoStore'
import TokensHealthTable from './TokensHealthTable'
import MarketsHealthTable from './MarketsHealthTable'
import { HealthContribution, PerpMarketContribution } from 'types'
const HealthContributions = ({ hideView }: { hideView: () => void }) => {
const { t } = useTranslation(['common', 'account', 'trade'])
const { group } = useMangoGroup()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const [initActiveIndex, setInitActiveIndex] = useState<number | undefined>(
undefined
)
const [maintActiveIndex, setMaintActiveIndex] = useState<number | undefined>(
undefined
)
const [initHealthContributions, maintHealthContributions] = useMemo(() => {
if (!group || !mangoAccount) return [[], []]
const initAssets = mangoAccount.getHealthContributionPerAssetUi(
group,
HealthType.init
)
const initContributions = []
for (const item of initAssets) {
const contribution = item.contribution
if (item.asset === 'USDC') {
const hasPerp =
!!item.contributionDetails?.perpMarketContributions.find(
(perp: PerpMarketContribution) => Math.abs(perp.contributionUi) > 0
)
initContributions.push({
...item,
contribution: Math.abs(contribution),
hasPerp: hasPerp,
isAsset: contribution > 0 ? true : false,
})
if (item.contributionDetails) {
for (const perpMarket of item.contributionDetails
.perpMarketContributions) {
const contribution = Math.abs(perpMarket.contributionUi)
if (contribution > 0) {
initContributions.push({
asset: perpMarket.market,
contribution: contribution,
isAsset: perpMarket.contributionUi > 0 ? true : false,
})
}
}
}
} else {
initContributions.push({
...item,
isAsset: contribution > 0 ? true : false,
contribution: Math.abs(contribution),
})
}
}
const maintAssets = mangoAccount.getHealthContributionPerAssetUi(
group,
HealthType.maint
)
const maintContributions = []
for (const item of maintAssets) {
const contribution = item.contribution
if (item.asset === 'USDC') {
const hasPerp =
!!item.contributionDetails?.perpMarketContributions.find(
(perp: PerpMarketContribution) => Math.abs(perp.contributionUi) > 0
)
maintContributions.push({
...item,
hasPerp: hasPerp,
isAsset: contribution > 0 ? true : false,
contribution: Math.abs(contribution),
})
if (item.contributionDetails) {
for (const perpMarket of item.contributionDetails
.perpMarketContributions) {
const contribution = Math.abs(perpMarket.contributionUi)
if (contribution > 0) {
maintContributions.push({
asset: perpMarket.market,
contribution: contribution,
isAsset: perpMarket.contributionUi > 0 ? true : false,
})
}
}
}
} else {
maintContributions.push({
...item,
isAsset: contribution > 0 ? true : false,
contribution: Math.abs(contribution),
})
}
}
return [initContributions, maintContributions]
}, [group, mangoAccount])
const [initHealthMarkets, initHealthTokens] = useMemo(() => {
if (!initHealthContributions.length) return [[], []]
const splitData = initHealthContributions.reduce(
(
acc: { market: HealthContribution[]; token: HealthContribution[] },
obj: HealthContribution
) => {
const isPerp = obj.asset.includes('PERP')
const isSpotMarket = obj.asset.includes('/')
if (isSpotMarket) {
acc.market.push(obj)
}
if (!isPerp && !isSpotMarket) {
acc.token.push(obj)
}
return acc
},
{ market: [], token: [] }
)
return [splitData.market, splitData.token]
}, [initHealthContributions])
const [maintHealthMarkets, maintHealthTokens] = useMemo(() => {
if (!maintHealthContributions.length) return [[], []]
const splitData = maintHealthContributions.reduce(
(
acc: { market: HealthContribution[]; token: HealthContribution[] },
obj: HealthContribution
) => {
const isPerp = obj.asset.includes('PERP')
const isSpotMarket = obj.asset.includes('/')
if (isSpotMarket) {
acc.market.push(obj)
}
if (!isPerp && !isSpotMarket) {
acc.token.push(obj)
}
return acc
},
{ market: [], token: [] }
)
const markets = splitData.market.filter((d) => d.contribution > 0)
const tokens = splitData.token
return [markets, tokens]
}, [maintHealthContributions])
const handleLegendClick = (item: HealthContribution) => {
const maintIndex = maintChartData.findIndex((d) => d.asset === item.asset)
const initIndex = initChartData.findIndex((d) => d.asset === item.asset)
setMaintActiveIndex(maintIndex)
setInitActiveIndex(initIndex)
}
const handleLegendMouseEnter = (item: HealthContribution) => {
const maintIndex = maintChartData.findIndex((d) => d.asset === item.asset)
const initIndex = initChartData.findIndex((d) => d.asset === item.asset)
setMaintActiveIndex(maintIndex)
setInitActiveIndex(initIndex)
}
const handleLegendMouseLeave = () => {
setInitActiveIndex(undefined)
setMaintActiveIndex(undefined)
}
const renderLegendLogo = (asset: string) => {
const group = mangoStore.getState().group
if (!group)
return <QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
const isSpotMarket = asset.includes('/')
const isPerpMarket = asset.includes('PERP')
const isMarket = isSpotMarket || isPerpMarket
if (isMarket) {
let market
if (isSpotMarket) {
market = group.getSerum3MarketByName(asset)
} else {
market = group.getPerpMarketByName(asset)
}
return market ? (
<MarketLogos market={market} size="small" />
) : (
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
)
} else {
const bank = group.banksMapByName.get(asset)?.[0]
return bank ? (
<div className="mr-1.5">
<TokenLogo bank={bank} size={16} />
</div>
) : (
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
)
}
}
const initChartData = useMemo(() => {
if (!initHealthContributions.length) return []
return initHealthContributions
.filter((cont) => {
if (cont.asset.includes('PERP')) {
return
} else if (cont.asset.includes('/')) {
return cont.contribution > 0.01
} else return cont
})
.sort((a, b) => {
const aMultiplier = a.isAsset ? 1 : -1
const bMultiplier = b.isAsset ? 1 : -1
return b.contribution * bMultiplier - a.contribution * aMultiplier
})
}, [initHealthContributions])
const maintChartData = useMemo(() => {
if (!maintHealthContributions.length) return []
return maintHealthContributions
.filter((cont) => {
if (cont.asset.includes('PERP')) {
return
} else if (cont.asset.includes('/')) {
return cont.contribution > 0.01
} else return cont
})
.sort((a, b) => {
const aMultiplier = a.isAsset ? 1 : -1
const bMultiplier = b.isAsset ? 1 : -1
return b.contribution * bMultiplier - a.contribution * aMultiplier
})
}, [maintHealthContributions])
return group ? (
<>
<div className="hide-scroll flex h-14 items-center space-x-4 overflow-x-auto border-b border-th-bkg-3">
<button
className="flex h-14 w-14 flex-shrink-0 items-center justify-center border-r border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
onClick={hideView}
>
<ArrowLeftIcon className="h-5 w-5" />
</button>
<h2 className="text-lg">{t('account:health-contributions')}</h2>
</div>
{mangoAccountAddress ? (
<>
<div className="mx-auto grid max-w-[1140px] grid-cols-2 gap-6 p-6 sm:gap-8">
<div className="col-span-1 flex h-full flex-col items-center">
<Tooltip content={t('account:tooltip-init-health')}>
<h3 className="tooltip-underline text-xs sm:text-base">
{t('account:init-health-contributions')}
</h3>
</Tooltip>
<HealthContributionsChart
data={initChartData}
activeIndex={initActiveIndex}
setActiveIndex={setInitActiveIndex}
/>
</div>
<div className="col-span-1 flex flex-col items-center">
<Tooltip content={t('account:tooltip-maint-health')}>
<h3 className="tooltip-underline text-xs sm:text-base">
{t('account:maint-health-contributions')}
</h3>
</Tooltip>
<HealthContributionsChart
data={maintChartData}
activeIndex={maintActiveIndex}
setActiveIndex={setMaintActiveIndex}
/>
</div>
<div className="col-span-2 mx-auto flex max-w-[600px] flex-wrap justify-center space-x-4">
{[...maintChartData]
.sort((a, b) => b.contribution - a.contribution)
.map((d, i) => {
return (
<div
key={d.asset + i}
className={`default-transition flex h-7 cursor-pointer items-center md:hover:text-th-active`}
onClick={() => handleLegendClick(d)}
onMouseEnter={() => handleLegendMouseEnter(d)}
onMouseLeave={handleLegendMouseLeave}
>
{renderLegendLogo(d.asset)}
<span className={`default-transition`}>{d.asset}</span>
</div>
)
})}
</div>
</div>
{maintHealthTokens.length ? (
<div className="border-t border-th-bkg-3 pt-6">
<h2 className="mb-1 px-6 text-lg">{t('tokens')}</h2>
<TokensHealthTable
initTokens={initHealthTokens}
maintTokens={maintHealthTokens}
handleLegendClick={handleLegendClick}
handleLegendMouseEnter={handleLegendMouseEnter}
handleLegendMouseLeave={handleLegendMouseLeave}
/>
</div>
) : null}
{maintHealthMarkets.length ? (
<div className="pt-6">
<h2 className="mb-1 px-6 text-lg">{t('markets')}</h2>
<MarketsHealthTable
initMarkets={initHealthMarkets}
maintMarkets={maintHealthMarkets}
handleLegendClick={handleLegendClick}
handleLegendMouseEnter={handleLegendMouseEnter}
handleLegendMouseLeave={handleLegendMouseLeave}
/>
</div>
) : null}
</>
) : (
<div className="mx-6 mt-6 flex flex-col items-center rounded-lg border border-th-bkg-3 p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('account:no-data')}</p>
</div>
)}
</>
) : null
}
export default HealthContributions

View File

@ -0,0 +1,148 @@
import { useTranslation } from 'next-i18next'
import { useTheme } from 'next-themes'
import {
Cell,
Pie,
PieChart,
ResponsiveContainer,
Sector,
SectorProps,
} from 'recharts'
import { COLORS } from 'styles/colors'
import { useMemo } from 'react'
import { formatCurrencyValue } from 'utils/numbers'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import { HealthContribution } from 'types'
const HealthContributionsChart = ({
data,
activeIndex,
setActiveIndex,
}: {
data: HealthContribution[]
activeIndex: number | undefined
setActiveIndex: (i: number | undefined) => void
}) => {
const { t } = useTranslation(['common', 'account'])
const { theme } = useTheme()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const handleClick = (index: number) => {
setActiveIndex(index)
}
const handleMouseEnter = (data: HealthContribution, index: number) => {
setActiveIndex(index)
}
const handleMouseLeave = () => {
setActiveIndex(undefined)
}
const pieSizes = isMobile
? { size: 160, outerRadius: 80, innerRadius: 64 }
: { size: 240, outerRadius: 120, innerRadius: 96 }
const { size, outerRadius, innerRadius } = pieSizes
const [chartHeroAsset, chartHeroValue] = useMemo(() => {
if (!data.length) return [undefined, undefined]
if (activeIndex === undefined) {
const value = data.reduce((a, c) => {
const assetOrLiabMultiplier = c.isAsset ? 1 : -1
return a + c.contribution * assetOrLiabMultiplier
}, 0)
return [t('total'), value]
} else {
const asset = data[activeIndex]
const assetOrLiabMultiplier = asset.isAsset ? 1 : -1
const value = asset.contribution * assetOrLiabMultiplier
return [asset.asset, value]
}
}, [activeIndex, data])
const renderActiveShape = ({
cx,
cy,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
}: SectorProps) => {
return (
<g>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius! + 4}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
</g>
)
}
return data.length ? (
<div className="mt-4 flex h-full w-full flex-col items-center">
<div className="relative h-[168px] w-[168px] sm:h-[248px] sm:w-[248px]">
<ResponsiveContainer height="100%" width="100%">
<PieChart width={size} height={size}>
<Pie
cursor="pointer"
data={data}
dataKey="contribution"
cx="50%"
cy="50%"
outerRadius={outerRadius}
innerRadius={innerRadius}
minAngle={2}
startAngle={90}
endAngle={450}
activeIndex={activeIndex}
activeShape={renderActiveShape}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{data.map((entry, index) => {
const fillColor = entry.isAsset
? COLORS.UP[theme]
: COLORS.DOWN[theme]
let opacity
if (entry.isAsset) {
opacity = 1 - index * 0.1
} else {
opacity = 1 - Math.abs((index - (data.length - 1)) * 0.1)
}
return (
<Cell
key={`cell-${index}`}
fill={fillColor}
opacity={opacity}
stroke="none"
/>
)
})}
</Pie>
</PieChart>
</ResponsiveContainer>
{chartHeroValue !== undefined ? (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
<p className="text-xs sm:text-sm">{chartHeroAsset}</p>
<span className="text-base font-bold sm:text-xl">
{formatCurrencyValue(chartHeroValue, 2)}
</span>
</div>
) : null}
</div>
</div>
) : null
}
export default HealthContributionsChart

View File

@ -0,0 +1,276 @@
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import useMangoGroup from 'hooks/useMangoGroup'
import useMangoAccount from 'hooks/useMangoAccount'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import { MouseEventHandler } from 'react'
import MarketLogos from '@components/trade/MarketLogos'
import { HealthContribution } from 'types'
const MarketsHealthTable = ({
initMarkets,
maintMarkets,
handleLegendClick,
handleLegendMouseEnter,
handleLegendMouseLeave,
}: {
initMarkets: HealthContribution[]
maintMarkets: HealthContribution[]
handleLegendClick: (cont: HealthContribution) => void
handleLegendMouseEnter: (cont: HealthContribution) => void
handleLegendMouseLeave: MouseEventHandler
}) => {
const { t } = useTranslation(['common', 'account', 'trade'])
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
return group && mangoAccount ? (
!isMobile ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-init-health')}>
<span className="tooltip-underline">
{t('account:init-health-contribution')}
</span>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-maint-health')}>
<span className="tooltip-underline">
{t('account:maint-health-contribution')}
</span>
</Tooltip>
</div>
</Th>
</TrHead>
</thead>
<tbody>
{maintMarkets
.sort((a, b) => b.contribution - a.contribution)
.map((cont) => {
const { asset, contribution, isAsset } = cont
const market = group.getSerum3MarketByName(asset)
const bank = group.banksMapByTokenIndex.get(
market.baseTokenIndex
)?.[0]
let initAssetWeight = 0
let initLiabWeight = 0
let maintAssetWeight = 0
let maintLiabWeight = 0
if (bank) {
initAssetWeight = bank
.scaledInitAssetWeight(bank.price)
.toNumber()
initLiabWeight = bank
.scaledInitLiabWeight(bank.price)
.toNumber()
maintAssetWeight = bank.maintAssetWeight.toNumber()
maintLiabWeight = bank.maintLiabWeight.toNumber()
}
const assetOrLiabMultiplier = isAsset ? 1 : -1
const initContribution =
(initMarkets.find((cont) => cont.asset === asset)
?.contribution || 0) * assetOrLiabMultiplier
const maintContribution = contribution * assetOrLiabMultiplier
return (
<TrBody
key={asset}
className="cursor-pointer md:hover:bg-th-bkg-2"
onClick={() => handleLegendClick(cont)}
onMouseEnter={() => handleLegendMouseEnter(cont)}
onMouseLeave={handleLegendMouseLeave}
>
<Td>
<div className="flex items-center">
<MarketLogos market={market} />
<p className="font-body">{asset}</p>
</div>
</Td>
<Td>
<div className="text-right">
<p>
<FormatNumericValue
value={initContribution}
decimals={2}
isUsd
/>
</p>
<p className="text-th-fgd-3">
{initContribution > 0
? initAssetWeight.toFixed(2)
: initContribution < 0
? initLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</Td>
<Td>
<div className="text-right">
<p>
<FormatNumericValue
value={maintContribution}
decimals={2}
isUsd
/>
</p>
<p className="text-th-fgd-3">
{maintContribution > 0
? maintAssetWeight.toFixed(2)
: maintContribution < 0
? maintLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="mt-3 border-y border-th-bkg-3">
{maintMarkets
.sort((a, b) => b.contribution - a.contribution)
.map((cont) => {
const { asset, contribution, isAsset } = cont
const market = group.getSerum3MarketByName(asset)
const bank = group.banksMapByTokenIndex.get(
market.baseTokenIndex
)?.[0]
let initAssetWeight = 0
let initLiabWeight = 0
let maintAssetWeight = 0
let maintLiabWeight = 0
if (bank) {
initAssetWeight = bank
.scaledInitAssetWeight(bank.price)
.toNumber()
initLiabWeight = bank.scaledInitLiabWeight(bank.price).toNumber()
maintAssetWeight = bank.maintAssetWeight.toNumber()
maintLiabWeight = bank.maintLiabWeight.toNumber()
}
const assetOrLiabMultiplier = isAsset ? 1 : -1
const initContribution =
(initMarkets.find((cont) => cont.asset === asset)?.contribution ||
0) * assetOrLiabMultiplier
const maintContribution = contribution * assetOrLiabMultiplier
return (
<Disclosure key={asset}>
{({ open }) => (
<>
<Disclosure.Button
className={`w-full border-t border-th-bkg-3 p-4 text-left first:border-t-0 focus:outline-none`}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<MarketLogos market={market} />
<div>
<p className="text-th-fgd-1">{asset}</p>
</div>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</div>
</Disclosure.Button>
<Transition
enter="transition ease-in duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
>
<Disclosure.Panel>
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4 pb-4">
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
<Tooltip
content={t('account:tooltip-init-health')}
>
<span className="tooltip-underline">
{t('account:init-health-contribution')}
</span>
</Tooltip>
</p>
<p className="font-mono text-th-fgd-2">
<FormatNumericValue
value={initContribution}
decimals={2}
isUsd
/>
</p>
<p className="font-mono text-th-fgd-3">
{initContribution > 0
? initAssetWeight.toFixed(2)
: initContribution < 0
? initLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
<Tooltip
content={t('account:tooltip-maint-health')}
>
<span className="tooltip-underline">
{t('account:maint-health-contribution')}
</span>
</Tooltip>
</p>
<p className="font-mono text-th-fgd-2">
<FormatNumericValue
value={maintContribution}
decimals={2}
isUsd
/>
</p>
<p className="font-mono text-th-fgd-3">
{maintContribution > 0
? maintAssetWeight.toFixed(2)
: maintContribution < 0
? maintLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</div>
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
)
})}
</div>
)
) : null
}
export default MarketsHealthTable

View File

@ -0,0 +1,406 @@
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import TokenLogo from '@components/shared/TokenLogo'
import Tooltip from '@components/shared/Tooltip'
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import useMangoGroup from 'hooks/useMangoGroup'
import useMangoAccount from 'hooks/useMangoAccount'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import { MouseEventHandler } from 'react'
import { ContributionDetails, HealthContribution } from 'types'
const TokensHealthTable = ({
initTokens,
maintTokens,
handleLegendClick,
handleLegendMouseEnter,
handleLegendMouseLeave,
}: {
initTokens: HealthContribution[]
maintTokens: HealthContribution[]
handleLegendClick: (cont: HealthContribution) => void
handleLegendMouseEnter: (cont: HealthContribution) => void
handleLegendMouseLeave: MouseEventHandler
}) => {
const { t } = useTranslation(['common', 'account', 'trade'])
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
return group && mangoAccount ? (
!isMobile ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('token')}</Th>
<Th className="text-right">{t('trade:notional')}</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-init-health')}>
<span className="tooltip-underline">
{t('account:init-health-contribution')}
</span>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-maint-health')}>
<span className="tooltip-underline">
{t('account:maint-health-contribution')}
</span>
</Tooltip>
</div>
</Th>
</TrHead>
</thead>
<tbody>
{maintTokens
.sort((a, b) => b.contribution - a.contribution)
.map((cont) => {
const {
asset,
contribution,
contributionDetails,
isAsset,
hasPerp,
} = cont
const bank = group.banksMapByName.get(asset)?.[0]
let initAssetWeight = 0
let initLiabWeight = 0
let maintAssetWeight = 0
let maintLiabWeight = 0
let balance = 0
if (bank) {
initAssetWeight = bank
.scaledInitAssetWeight(bank.price)
.toNumber()
initLiabWeight = bank
.scaledInitLiabWeight(bank.price)
.toNumber()
maintAssetWeight = bank.maintAssetWeight.toNumber()
maintLiabWeight = bank.maintLiabWeight.toNumber()
balance = mangoAccount.getTokenBalanceUi(bank)
}
const assetOrLiabMultiplier = isAsset ? 1 : -1
const initToken = initTokens.find((cont) => cont.asset === asset)
const initContribution =
(initToken?.contribution || 0) * assetOrLiabMultiplier
const maintContribution = contribution * assetOrLiabMultiplier
return (
<TrBody
key={asset}
className="cursor-pointer md:hover:bg-th-bkg-2"
onClick={() => handleLegendClick(cont)}
onMouseEnter={() => handleLegendMouseEnter(cont)}
onMouseLeave={handleLegendMouseLeave}
>
<Td>
<div className="flex items-center">
<div className="mr-2.5 flex flex-shrink-0 items-center">
<TokenLogo bank={bank} />
</div>
<p className="font-body">{asset}</p>
</div>
</Td>
<Td className="text-right">
{bank ? (
<p>
<FormatNumericValue
value={balance * bank.uiPrice}
decimals={2}
isUsd
/>{' '}
<span className={`block text-th-fgd-4`}>
<FormatNumericValue
value={balance}
decimals={bank.mintDecimals}
/>
</span>
</p>
) : (
''
)}
</Td>
<Td>
<div className="flex flex-col items-end text-right">
<Tooltip
className={!hasPerp ? 'hidden' : ''}
content={
<UsdcTooltipContent
contributions={initToken?.contributionDetails}
/>
}
>
<p className={hasPerp ? 'tooltip-underline' : ''}>
<FormatNumericValue
value={initContribution}
decimals={2}
isUsd
/>
</p>
</Tooltip>
<p className="text-th-fgd-3">
{initContribution > 0
? initAssetWeight.toFixed(2)
: initContribution < 0
? initLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end text-right">
<Tooltip
className={!hasPerp ? 'hidden' : ''}
content={
<UsdcTooltipContent
contributions={contributionDetails}
/>
}
>
<p className={hasPerp ? 'tooltip-underline' : ''}>
<FormatNumericValue
value={maintContribution}
decimals={2}
isUsd
/>
</p>
</Tooltip>
<p className="text-th-fgd-3">
{maintContribution > 0
? maintAssetWeight.toFixed(2)
: maintContribution < 0
? maintLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="mt-3 border-y border-th-bkg-3">
{maintTokens
.sort((a, b) => b.contribution - a.contribution)
.map((cont) => {
const {
asset,
contribution,
contributionDetails,
isAsset,
hasPerp,
} = cont
const bank = group.banksMapByName.get(asset)?.[0]
let initAssetWeight = 0
let initLiabWeight = 0
let maintAssetWeight = 0
let maintLiabWeight = 0
let balance = 0
if (bank) {
initAssetWeight = bank
.scaledInitAssetWeight(bank.price)
.toNumber()
initLiabWeight = bank.scaledInitLiabWeight(bank.price).toNumber()
maintAssetWeight = bank.maintAssetWeight.toNumber()
maintLiabWeight = bank.maintLiabWeight.toNumber()
balance = mangoAccount.getTokenBalanceUi(bank)
}
const assetOrLiabMultiplier = isAsset ? 1 : -1
const initToken = initTokens.find((cont) => cont.asset === asset)
const initContribution =
(initToken?.contribution || 0) * assetOrLiabMultiplier
const maintContribution = contribution * assetOrLiabMultiplier
return (
<Disclosure key={asset}>
{({ open }) => (
<>
<Disclosure.Button
className={`w-full border-t border-th-bkg-3 p-4 text-left first:border-t-0 focus:outline-none`}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="mr-2.5">
<TokenLogo bank={bank} />
</div>
<div>
<p className="text-th-fgd-1">{asset}</p>
</div>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</div>
</Disclosure.Button>
<Transition
enter="transition ease-in duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
>
<Disclosure.Panel>
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4 pb-4">
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('trade:notional')}
</p>
<p>
{bank ? (
<span className="font-mono text-th-fgd-2">
<FormatNumericValue
value={balance * bank.uiPrice}
decimals={2}
isUsd
/>{' '}
<span className={`block text-th-fgd-4`}>
<FormatNumericValue
value={balance}
decimals={bank.mintDecimals}
/>
</span>
</span>
) : (
''
)}
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('account:init-health-contribution')}
</p>
<Tooltip
className={!hasPerp ? 'hidden' : ''}
content={
<UsdcTooltipContent
contributions={initToken?.contributionDetails}
/>
}
>
<p
className={`font-mono text-th-fgd-2 ${
hasPerp ? 'tooltip-underline' : ''
}`}
>
<FormatNumericValue
value={initContribution}
decimals={2}
isUsd
/>
</p>
</Tooltip>
<p className="font-mono text-th-fgd-3">
{initContribution > 0
? initAssetWeight.toFixed(2)
: initContribution < 0
? initLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('account:maint-health-contribution')}
</p>
<Tooltip
className={!hasPerp ? 'hidden' : ''}
content={
<UsdcTooltipContent
contributions={contributionDetails}
/>
}
>
<p
className={`font-mono text-th-fgd-2 ${
hasPerp ? 'tooltip-underline' : ''
}`}
>
<FormatNumericValue
value={maintContribution}
decimals={2}
isUsd
/>
</p>
</Tooltip>
<p className="font-mono text-th-fgd-3">
{maintContribution > 0
? maintAssetWeight.toFixed(2)
: maintContribution < 0
? maintLiabWeight.toFixed(2)
: 0}
x
</p>
</div>
</div>
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
)
})}
</div>
)
) : null
}
export default TokensHealthTable
const UsdcTooltipContent = ({
contributions,
}: {
contributions: ContributionDetails | undefined
}) => {
const { t } = useTranslation('common')
if (!contributions) return null
const { perpMarketContributions, spotUi } = contributions
return (
<>
<div className="space-y-1">
<div className="flex justify-between">
<p className="mr-3">{t('spot')}</p>
<span className="font-mono text-th-fgd-2">
<FormatNumericValue value={spotUi} decimals={2} isUsd />
</span>
</div>
{perpMarketContributions
.filter((cont) => Math.abs(cont.contributionUi) > 0.01)
.map((perp) => (
<div className="flex justify-between" key={perp.market}>
<p className="mr-3">{perp.market}</p>
<span className="font-mono text-th-fgd-2">
<FormatNumericValue
value={perp.contributionUi}
decimals={2}
isUsd
/>
</span>
</div>
))}
</div>
</>
)
}

View File

@ -24,18 +24,15 @@ import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
import { FadeInFadeOut } from '@components/shared/Transitions' import { FadeInFadeOut } from '@components/shared/Transitions'
import ContentBox from '@components/shared/ContentBox' import ContentBox from '@components/shared/ContentBox'
import SheenLoader from '@components/shared/SheenLoader' import SheenLoader from '@components/shared/SheenLoader'
import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats'
import useMangoAccount from 'hooks/useMangoAccount'
import { DAILY_MILLISECONDS } from 'utils/constants' import { DAILY_MILLISECONDS } from 'utils/constants'
const VolumeChart = ({ const VolumeChart = ({ hideChart }: { hideChart: () => void }) => {
chartData,
hideChart,
loading,
}: {
chartData: FormattedHourlyAccountVolumeData[] | undefined
hideChart: () => void
loading: boolean
}) => {
const { t } = useTranslation(['account', 'common', 'stats']) const { t } = useTranslation(['account', 'common', 'stats'])
const { mangoAccountAddress } = useMangoAccount()
const { hourlyVolumeData: chartData, loadingHourlyVolume: loading } =
useAccountHourlyVolumeStats()
const [daysToShow, setDaysToShow] = useState('30') const [daysToShow, setDaysToShow] = useState('30')
const { theme } = useTheme() const { theme } = useTheme()
@ -160,8 +157,8 @@ const VolumeChart = ({
onChange={(v) => setDaysToShow(v)} onChange={(v) => setDaysToShow(v)}
/> />
</div> </div>
{loading ? ( {loading && mangoAccountAddress ? (
<SheenLoader className="flex flex-1"> <SheenLoader className="mt-6 flex flex-1">
<div <div
className={`h-[calc(100vh-166px)] w-full rounded-lg bg-th-bkg-2`} className={`h-[calc(100vh-166px)] w-full rounded-lg bg-th-bkg-2`}
/> />

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReactNode, forwardRef } from 'react' import { MouseEventHandler, ReactNode, forwardRef } from 'react'
export const Table = ({ export const Table = ({
children, children,
@ -43,15 +44,19 @@ interface TrBodyProps {
children: ReactNode children: ReactNode
className?: string className?: string
onClick?: () => void onClick?: () => void
onMouseEnter?: (x: any) => void
onMouseLeave?: MouseEventHandler
} }
export const TrBody = forwardRef<HTMLTableRowElement, TrBodyProps>( export const TrBody = forwardRef<HTMLTableRowElement, TrBodyProps>(
(props, ref) => { (props, ref) => {
const { children, className, onClick } = props const { children, className, onClick, onMouseEnter, onMouseLeave } = props
return ( return (
<tr <tr
className={`border-y border-th-bkg-3 ${className}`} className={`border-y border-th-bkg-3 ${className}`}
onClick={onClick} onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={ref} ref={ref}
> >
{children} {children}

View File

@ -26,8 +26,6 @@ export default function useAccountHourlyVolumeStats() {
return { return {
hourlyVolumeData, hourlyVolumeData,
loadingHourlyVolumeData,
fetchingHourlyVolumeData,
loadingHourlyVolume, loadingHourlyVolume,
} }
} }

View File

@ -5,14 +5,21 @@
"daily-volume": "24h Volume", "daily-volume": "24h Volume",
"export": "Export {{dataType}}", "export": "Export {{dataType}}",
"funding-chart": "Funding Chart", "funding-chart": "Funding Chart",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
"liabilities": "Liabilities", "liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume", "lifetime-volume": "Lifetime Trade Volume",
"maint-health-contribution": "Maint Health Contribution",
"maint-health-contributions": "Maint Health Contributions",
"no-data": "No data to display", "no-data": "No data to display",
"no-pnl-history": "No PnL History", "no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart", "pnl-chart": "PnL Chart",
"pnl-history": "PnL History", "pnl-history": "PnL History",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw", "tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value", "tooltip-leverage": "Total assets value divided by account equity value",
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
"tooltip-pnl": "The amount your account has profited or lost", "tooltip-pnl": "The amount your account has profited or lost",
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)", "tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid", "tooltip-total-funding": "The sum of perp position funding earned and paid",

View File

@ -101,6 +101,7 @@
"mango": "Mango", "mango": "Mango",
"mango-stats": "Mango Stats", "mango-stats": "Mango Stats",
"market": "Market", "market": "Market",
"markets": "Markets",
"max": "Max", "max": "Max",
"max-borrow": "Max Borrow", "max-borrow": "Max Borrow",
"more": "More", "more": "More",

View File

@ -5,14 +5,21 @@
"daily-volume": "24h Volume", "daily-volume": "24h Volume",
"export": "Export {{dataType}}", "export": "Export {{dataType}}",
"funding-chart": "Funding Chart", "funding-chart": "Funding Chart",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
"liabilities": "Liabilities", "liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume", "lifetime-volume": "Lifetime Trade Volume",
"maint-health-contribution": "Maint Health Contribution",
"maint-health-contributions": "Maint Health Contributions",
"no-data": "No data to display", "no-data": "No data to display",
"no-pnl-history": "No PnL History", "no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart", "pnl-chart": "PnL Chart",
"pnl-history": "PnL History", "pnl-history": "PnL History",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw", "tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value", "tooltip-leverage": "Total assets value divided by account equity value",
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
"tooltip-pnl": "The amount your account has profited or lost", "tooltip-pnl": "The amount your account has profited or lost",
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)", "tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid", "tooltip-total-funding": "The sum of perp position funding earned and paid",

View File

@ -101,6 +101,7 @@
"mango": "Mango", "mango": "Mango",
"mango-stats": "Mango Stats", "mango-stats": "Mango Stats",
"market": "Market", "market": "Market",
"markets": "Markets",
"max": "Max", "max": "Max",
"max-borrow": "Max Borrow", "max-borrow": "Max Borrow",
"more": "More", "more": "More",

View File

@ -5,14 +5,21 @@
"daily-volume": "24h Volume", "daily-volume": "24h Volume",
"export": "Export {{dataType}}", "export": "Export {{dataType}}",
"funding-chart": "Funding Chart", "funding-chart": "Funding Chart",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
"liabilities": "Liabilities", "liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume", "lifetime-volume": "Lifetime Trade Volume",
"maint-health-contribution": "Maint Health Contribution",
"maint-health-contributions": "Maint Health Contributions",
"no-data": "No data to display", "no-data": "No data to display",
"no-pnl-history": "No PnL History", "no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart", "pnl-chart": "PnL Chart",
"pnl-history": "PnL History", "pnl-history": "PnL History",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw", "tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value", "tooltip-leverage": "Total assets value divided by account equity value",
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
"tooltip-pnl": "The amount your account has profited or lost", "tooltip-pnl": "The amount your account has profited or lost",
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)", "tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid", "tooltip-total-funding": "The sum of perp position funding earned and paid",

View File

@ -101,6 +101,7 @@
"mango": "Mango", "mango": "Mango",
"mango-stats": "Mango Stats", "mango-stats": "Mango Stats",
"market": "Market", "market": "Market",
"markets": "Markets",
"max": "Max", "max": "Max",
"max-borrow": "Max Borrow", "max-borrow": "Max Borrow",
"more": "More", "more": "More",

View File

@ -5,14 +5,21 @@
"daily-volume": "24h Volume", "daily-volume": "24h Volume",
"export": "Export {{dataType}}", "export": "Export {{dataType}}",
"funding-chart": "Funding Chart", "funding-chart": "Funding Chart",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
"liabilities": "Liabilities", "liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume", "lifetime-volume": "Lifetime Trade Volume",
"maint-health-contribution": "Maint Health Contribution",
"maint-health-contributions": "Maint Health Contributions",
"no-data": "No data to display", "no-data": "No data to display",
"no-pnl-history": "No PnL History", "no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart", "pnl-chart": "PnL Chart",
"pnl-history": "PnL History", "pnl-history": "PnL History",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw", "tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value", "tooltip-leverage": "Total assets value divided by account equity value",
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
"tooltip-pnl": "The amount your account has profited or lost", "tooltip-pnl": "The amount your account has profited or lost",
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)", "tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid", "tooltip-total-funding": "The sum of perp position funding earned and paid",

View File

@ -101,6 +101,7 @@
"mango": "Mango", "mango": "Mango",
"mango-stats": "Mango统计", "mango-stats": "Mango统计",
"market": "市场", "market": "市场",
"markets": "Markets",
"max": "最多", "max": "最多",
"max-borrow": "最多借贷", "max-borrow": "最多借贷",
"more": "更多", "more": "更多",

View File

@ -5,14 +5,21 @@
"daily-volume": "24h Volume", "daily-volume": "24h Volume",
"export": "Export {{dataType}}", "export": "Export {{dataType}}",
"funding-chart": "Funding Chart", "funding-chart": "Funding Chart",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
"liabilities": "Liabilities", "liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume", "lifetime-volume": "Lifetime Trade Volume",
"maint-health-contribution": "Maint Health Contribution",
"maint-health-contributions": "Maint Health Contributions",
"no-data": "No data to display", "no-data": "No data to display",
"no-pnl-history": "No PnL History", "no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart", "pnl-chart": "PnL Chart",
"pnl-history": "PnL History", "pnl-history": "PnL History",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw", "tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value", "tooltip-leverage": "Total assets value divided by account equity value",
"tooltip-maint-health": "The contribution an asset gives to your maintenance account health. If your maintenance health reaches 0 your account will be liquidated.",
"tooltip-pnl": "The amount your account has profited or lost", "tooltip-pnl": "The amount your account has profited or lost",
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)", "tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid", "tooltip-total-funding": "The sum of perp position funding earned and paid",

View File

@ -101,6 +101,7 @@
"mango": "Mango", "mango": "Mango",
"mango-stats": "Mango統計", "mango-stats": "Mango統計",
"market": "市場", "market": "市場",
"markets": "Markets",
"max": "最多", "max": "最多",
"max-borrow": "最多借貸", "max-borrow": "最多借貸",
"more": "更多", "more": "更多",

View File

@ -413,3 +413,21 @@ export type TickerData = {
target_volume: string target_volume: string
ticker_id: string ticker_id: string
} }
export interface HealthContribution {
asset: string
contribution: number
contributionDetails?: ContributionDetails
hasPerp?: boolean
isAsset: boolean
}
export interface PerpMarketContribution {
market: string
contributionUi: number
}
export interface ContributionDetails {
perpMarketContributions: PerpMarketContribution[]
spotUi: number
}