Skip to content

Commit

Permalink
Centrifuge App: Portfolio asset allocation (#1641)
Browse files Browse the repository at this point in the history
  • Loading branch information
onnovisser authored and gpmayorga committed Nov 23, 2023
1 parent 99dd40f commit 62dcceb
Show file tree
Hide file tree
Showing 12 changed files with 278 additions and 53 deletions.
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

0 comments on commit 62dcceb

Please sign in to comment.