Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Centrifuge App: Portfolio asset allocation #1641

Merged
merged 11 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 82 additions & 15 deletions centrifuge-app/src/components/Portfolio/AssetAllocation.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,93 @@
import { useBalances } from '@centrifuge/centrifuge-react'
import { Box, Text } from '@centrifuge/fabric'
import { Box, Shelf, Stack, Text } from '@centrifuge/fabric'
import Decimal from 'decimal.js-light'
import * as React from 'react'
import { useAddress } from '../../utils/useAddress'
import { useTheme } from 'styled-components'
import { Dec } from '../../utils/Decimal'
import { formatBalanceAbbreviated } from '../../utils/formatting'
import { usePoolMetadataMulti, usePools } from '../../utils/usePools'
import { LabelValueStack } from '../LabelValueStack'
import { AssetClassChart } from './AssetClassChart'

export function AssetAllocation() {
const address = useAddress()
const assetClassLabels = {
privateCredit: 'Private Credit',
publicCredit: 'Public Credit',
}
type AssetClass = 'publicCredit' | 'privateCredit'

export function AssetAllocation({ address }: { address: string }) {
const balances = useBalances(address)
const pools = usePools()
const theme = useTheme()
const poolIds = new Set(balances?.tranches.map((t) => t.poolId))
const filteredPools = pools?.filter((p) => poolIds.has(p.id)) ?? []
const metas = usePoolMetadataMulti(filteredPools)
const assetClasses = [...new Set(metas.map((m) => m.data?.pool?.asset?.class as string).filter(Boolean))]
const valueByClass: Record<string, Decimal> = Object.fromEntries(assetClasses.map((item) => [item, Dec(0)]))
let total = Dec(0)
balances?.tranches.forEach((balance) => {
const poolIndex = filteredPools.findIndex((p) => p.id === balance.poolId)
const price =
filteredPools[poolIndex]?.tranches.find((t) => t.id === balance.trancheId)?.tokenPrice?.toDecimal() ?? Dec(0)
const asset = metas[poolIndex].data?.pool?.asset?.class
const value = balance.balance.toDecimal().mul(price)
total = total.add(value)
valueByClass[asset!] = valueByClass[asset!]?.add(value)
})

const shades = [600, 800, 200, 400]
const shares = assetClasses
.map((item, index) => {
const nextShade = shades[index % shades.length]
return {
name: assetClassLabels[item as AssetClass] ?? item,
value: valueByClass[item].toNumber(),
color: theme.colors.accentScale[nextShade],
labelColor: nextShade >= 500 ? 'white' : 'black',
}
})
.sort((a, b) => (b.value > a.value ? 1 : a === b ? 0 : -1))

return !!balances?.tranches && !!balances?.tranches.length ? (
<Box as="article">
<Stack gap={2}>
<Text as="h2" variant="heading2">
Allocation
</Text>
<Box as="ul" role="list">
{balances?.tranches.map((tranche, index) => (
<Box as="li" key={`${tranche.trancheId}${index}`}>
<Box>
<Text>Asset Class</Text>{' '}
</Box>
</Box>
))}
</Box>
</Box>
<Shelf gap={8}>
<AssetClassChart data={shares} currency="USD" total={total.toNumber()} />
<Shelf as="ul" alignSelf="stretch" alignItems="stretch" flex={1} gap={6}>
{shares.map((cell, i) => (
<>
{i > 0 && <Box width="1px" backgroundColor="borderSecondary" />}
<LabelValueStack
label={
<Box position="relative" ml={22}>
{cell.name}
<div
style={{
width: 12,
height: 12,
borderRadius: '50%',
backgroundColor: cell.color,
position: 'absolute',
top: '50%',
right: 'calc(100% + 10px)',
transform: 'translateY(-50%)',
}}
/>
</Box>
}
value={
<Box ml={22}>
<Text variant="heading2">{formatBalanceAbbreviated(cell.value, 'USD')}</Text>
</Box>
}
key={i}
/>
</>
))}
</Shelf>
</Shelf>
</Stack>
) : null
}
52 changes: 52 additions & 0 deletions centrifuge-app/src/components/Portfolio/AssetClassChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react'
import { Cell, Pie, PieChart as RechartsPieChart, Tooltip, TooltipProps } from 'recharts'
import { formatBalanceAbbreviated, formatPercentage } from '../../utils/formatting'
import { TooltipContainer, TooltipEntry, TooltipTitle } from '../Charts/CustomChartElements'

type PieChartProps = {
data: { name: string; value: number; color?: string }[]
currency: string
total: number
}

export function AssetClassChart({ data, currency, total }: PieChartProps) {
return (
<RechartsPieChart width={150} height={150} style={{ fontFamily: 'Inter' }}>
<Pie
data={data}
cx="50%"
cy="50%"
outerRadius={72}
innerRadius={36}
dataKey="value"
nameKey="name"
startAngle={-270}
>
{data.map((item, index) => (
<Cell
key={`cell-${item.color}-${index}`}
fill={item?.color || 'transparent'}
stroke={item?.color! || 'transparent'}
/>
))}
</Pie>
<Tooltip content={<TooltipContent currency={currency} total={total} />} />
</RechartsPieChart>
)
}

function TooltipContent({ payload, currency, total }: TooltipProps<any, any> & { currency: string; total: number }) {
if (payload && payload.length > 0) {
return (
<TooltipContainer>
<TooltipTitle>{payload[0].name}</TooltipTitle>
{payload.map(({ payload, name, value }) => (
<TooltipEntry name={formatBalanceAbbreviated(value, currency)} color={payload.color} key={name}>
{formatPercentage((value / total) * 100)}
</TooltipEntry>
))}
</TooltipContainer>
)
}
return null
}
10 changes: 7 additions & 3 deletions centrifuge-app/src/components/Report/PoolBalance.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Pool } from '@centrifuge/centrifuge-js/dist/modules/pools'
import { Text } from '@centrifuge/fabric'
import * as React from 'react'
import { Dec } from '../../utils/Decimal'
import { formatBalanceAbbreviated } from '../../utils/formatting'
import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl'
import { useDailyPoolStates, useMonthlyPoolStates } from '../../utils/usePools'
Expand Down Expand Up @@ -86,7 +87,7 @@ export function PoolBalance({ pool }: { pool: Pool }) {
name: `\u00A0 \u00A0 ${token.currency.name.split(' ').at(-1)} tranche`,
value:
poolStates?.map((state) =>
state.tranches[token.id].price
state.tranches[token.id]?.price
? formatBalanceAbbreviated(state.tranches[token.id].price?.toFloat()!, pool.currency.symbol)
: '1.000'
) || [],
Expand All @@ -110,7 +111,10 @@ export function PoolBalance({ pool }: { pool: Pool }) {
name: `\u00A0 \u00A0 ${token.currency.name.split(' ').at(-1)} tranche`,
value:
poolStates?.map((state) =>
formatBalanceAbbreviated(state.tranches[token.id].fulfilledInvestOrders.toDecimal(), pool.currency.symbol)
formatBalanceAbbreviated(
state.tranches[token.id]?.fulfilledInvestOrders.toDecimal() ?? Dec(0),
pool.currency.symbol
)
) || [],
heading: false,
})) || [],
Expand All @@ -129,7 +133,7 @@ export function PoolBalance({ pool }: { pool: Pool }) {
value:
poolStates?.map((state) =>
formatBalanceAbbreviated(
state.tranches[token.id].fulfilledRedeemOrders.toDecimal(),
state.tranches[token.id]?.fulfilledRedeemOrders.toDecimal() ?? Dec(0),
pool.currency.symbol
)
) || [],
Expand Down
27 changes: 13 additions & 14 deletions centrifuge-app/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ type EnvironmentConfig = {
}
defaultTheme: 'light' | 'dark'
baseCurrency: 'USD'
assetClasses: string[]
defaultAssetClass: string
assetClasses: Record<'publicCredit' | 'privateCredit', string[]>
poolCreationType: TransactionOptions['createType']
useDocumentNfts: boolean
defaultPodUrl: string
Expand All @@ -86,8 +85,7 @@ const ALTAIR: EnvironmentConfig = {
},
defaultTheme: 'dark',
baseCurrency: 'USD',
assetClasses: ['Art NFTs'],
defaultAssetClass: 'Art NFTs',
assetClasses: { privateCredit: ['Art NFTs'], publicCredit: [] },
poolCreationType,
useDocumentNfts: true,
defaultPodUrl,
Expand All @@ -103,16 +101,17 @@ const CENTRIFUGE: EnvironmentConfig = {
},
defaultTheme: 'light',
baseCurrency: 'USD',
assetClasses: [
'Consumer Credit',
'Corporate Credit',
'Commercial Real Estate',
'Residential Real Estate',
'Project Finance',
'Trade Finance',
'US Treasuries',
],
defaultAssetClass: 'Consumer Credit',
assetClasses: {
privateCredit: [
'Consumer Credit',
'Corporate Credit',
'Commercial Real Estate',
'Residential Real Estate',
'Project Finance',
'Trade Finance',
],
publicCredit: ['Corporate Bonds', 'US Treasuries'],
},
poolCreationType,
useDocumentNfts: true,
defaultPodUrl,
Expand Down
43 changes: 37 additions & 6 deletions centrifuge-app/src/pages/IssuerCreatePool/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ import { TrancheSection } from './TrancheInput'
import { useStoredIssuer } from './useStoredIssuer'
import { validate } from './validate'

const ASSET_CLASSES = config.assetClasses.map((label) => ({
label,
value: label,
const assetClassLabels = {
privateCredit: 'Private Credit',
publicCredit: 'Public Credit',
}
type AssetClass = 'publicCredit' | 'privateCredit'

const ASSET_CLASSES = Object.keys(config.assetClasses).map((key) => ({
label: assetClassLabels[key as AssetClass],
value: key,
}))
const DEFAULT_ASSET_CLASS = config.defaultAssetClass

export const IssuerCreatePoolPage: React.FC = () => {
return (
Expand Down Expand Up @@ -98,7 +103,8 @@ export type CreatePoolValues = Omit<
const initialValues: CreatePoolValues = {
poolIcon: null,
poolName: '',
assetClass: DEFAULT_ASSET_CLASS,
assetClass: 'privateCredit',
subAssetClass: '',
currency: '',
maxReserve: '',
epochHours: 23, // in hours
Expand Down Expand Up @@ -453,6 +459,12 @@ function CreatePoolForm() {
.add(collectionDeposit.toDecimal())
const deposit = createDeposit.add(proxyDeposit.toDecimal())

const subAssetClasses =
config.assetClasses[form.values.assetClass]?.map((label) => ({
label,
value: label,
})) ?? []

return (
<>
<PreimageHashDialog
Expand Down Expand Up @@ -534,7 +546,10 @@ function CreatePoolForm() {
<Select
name="assetClass"
label={<Tooltips type="assetClass" label="Asset class*" variant="secondary" />}
onChange={(event) => form.setFieldValue('assetClass', event.target.value)}
onChange={(event) => {
form.setFieldValue('assetClass', event.target.value)
form.setFieldValue('subAssetClass', '', false)
}}
onBlur={field.onBlur}
errorMessage={meta.touched && meta.error ? meta.error : undefined}
value={field.value}
Expand All @@ -544,6 +559,22 @@ function CreatePoolForm() {
)}
</Field>
</Box>
<Box gridColumn="span 2">
<Field name="subAssetClass" validate={validate.subAssetClass}>
{({ field, meta, form }: FieldProps) => (
<Select
name="subAssetClass"
label="Secondary asset class"
onChange={(event) => form.setFieldValue('subAssetClass', event.target.value)}
onBlur={field.onBlur}
errorMessage={meta.touched && meta.error ? meta.error : undefined}
value={field.value}
options={subAssetClasses}
placeholder="Select..."
/>
)}
</Field>
</Box>
<Box gridColumn="span 2">
<Field name="currency" validate={validate.currency}>
{({ field, form, meta }: FieldProps) => (
Expand Down
1 change: 1 addition & 0 deletions centrifuge-app/src/pages/IssuerCreatePool/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const validate = {
poolName: combine(required(), maxLength(100)),
poolIcon: combine(required(), mimeType('image/svg+xml', 'Icon must be an SVG file')),
assetClass: required(),
subAssetClass: required(),
maxReserve: combine(required(), nonNegativeNumber(), max(Number.MAX_SAFE_INTEGER)),
podEndpoint: pattern(/^https?:\/\/.{4,}/, 'Not a valid URL'),

Expand Down
Loading
Loading