Skip to content

Commit

Permalink
YTM in asset details (#2116)
Browse files Browse the repository at this point in the history
* Rename face value and add YTM to tx table

* Add weighted YTM to asset detail

* Fix math

* Show YTM only for external assets

* Exclude repaid transactions from YTM and weighted YTM average
  • Loading branch information
sophialittlejohn authored May 17, 2024
1 parent 0fc1034 commit 31dd5da
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 44 deletions.
132 changes: 90 additions & 42 deletions centrifuge-app/src/pages/Loan/TransactionTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import Decimal from 'decimal.js-light'
import { useMemo } from 'react'
import { Column, DataTable } from '../../components/DataTable'
import { Dec } from '../../utils/Decimal'
import { formatDate } from '../../utils/date'
import { formatBalance } from '../../utils/formatting'
import { daysBetween, formatDate } from '../../utils/date'
import { formatBalance, formatPercentage } from '../../utils/formatting'

type Props = {
transactions: AssetTransaction[]
Expand All @@ -16,6 +16,8 @@ type Props = {
loanType: 'external' | 'internal'
pricing: PricingInfo
poolType: 'publicCredit' | 'privateCredit' | undefined
maturityDate: Date
originationDate: Date | undefined
}

type Row = {
Expand All @@ -24,11 +26,21 @@ type Row = {
quantity: CurrencyBalance | null
transactionDate: string
settlePrice: CurrencyBalance | null
faceFlow: Decimal | null
faceValue: Decimal | null
position: Decimal
yieldToMaturity: Decimal | null
}

export const TransactionTable = ({ transactions, currency, loanType, decimals, pricing, poolType }: Props) => {
export const TransactionTable = ({
transactions,
currency,
loanType,
decimals,
pricing,
poolType,
maturityDate,
originationDate,
}: Props) => {
const assetTransactions = useMemo(() => {
const sortedTransactions = transactions.sort((a, b) => {
if (a.timestamp > b.timestamp) {
Expand All @@ -46,46 +58,62 @@ export const TransactionTable = ({ transactions, currency, loanType, decimals, p
return 0
})

return sortedTransactions.map((transaction, index, array) => ({
type: transaction.type,
amount: transaction.amount,
quantity: transaction.quantity ? new CurrencyBalance(transaction.quantity, 18) : null,
transactionDate: transaction.timestamp,
settlePrice: transaction.settlementPrice
? new CurrencyBalance(new BN(transaction.settlementPrice), decimals)
: null,
faceFlow:
return sortedTransactions.map((transaction, index, array) => {
const termDays = originationDate
? daysBetween(originationDate, maturityDate)
: daysBetween(new Date(), maturityDate)
const yearsBetweenDates = termDays / 365

const faceValue =
transaction.quantity && (pricing as ExternalPricingInfo).notional
? new CurrencyBalance(transaction.quantity, 18)
.toDecimal()
.mul((pricing as ExternalPricingInfo).notional.toDecimal())
: null

return {
type: transaction.type,
amount: transaction.amount,
quantity: transaction.quantity ? new CurrencyBalance(transaction.quantity, 18) : null,
transactionDate: transaction.timestamp,
yieldToMaturity:
transaction.amount && faceValue && transaction.type !== 'REPAID'
? Dec(2)
.mul(faceValue?.sub(transaction.amount.toDecimal()))
.div(Dec(yearsBetweenDates).mul(faceValue.add(transaction.amount.toDecimal())))
.mul(100)
: null,
settlePrice: transaction.settlementPrice
? new CurrencyBalance(new BN(transaction.settlementPrice), decimals)
: null,
position: array.slice(0, index + 1).reduce((sum, trx) => {
if (trx.type === 'BORROWED') {
sum = sum.add(
trx.quantity
? new CurrencyBalance(trx.quantity, 18)
.toDecimal()
.mul((pricing as ExternalPricingInfo).notional.toDecimal())
: trx.amount
? trx.amount.toDecimal()
: Dec(0)
)
}
if (trx.type === 'REPAID') {
sum = sum.sub(
trx.quantity
? new CurrencyBalance(trx.quantity, 18)
.toDecimal()
.mul((pricing as ExternalPricingInfo).notional.toDecimal())
: trx.amount
? trx.amount.toDecimal()
: Dec(0)
)
}
return sum
}, Dec(0)),
}))
faceValue,
position: array.slice(0, index + 1).reduce((sum, trx) => {
if (trx.type === 'BORROWED') {
sum = sum.add(
trx.quantity
? new CurrencyBalance(trx.quantity, 18)
.toDecimal()
.mul((pricing as ExternalPricingInfo).notional.toDecimal())
: trx.amount
? trx.amount.toDecimal()
: Dec(0)
)
}
if (trx.type === 'REPAID') {
sum = sum.sub(
trx.quantity
? new CurrencyBalance(trx.quantity, 18)
.toDecimal()
.mul((pricing as ExternalPricingInfo).notional.toDecimal())
: trx.amount
? trx.amount.toDecimal()
: Dec(0)
)
}
return sum
}, Dec(0)),
}
})
}, [transactions, decimals, pricing])

const getStatusChipType = (type: AssetTransactionType) => {
Expand Down Expand Up @@ -127,10 +155,10 @@ export const TransactionTable = ({ transactions, currency, loanType, decimals, p
? [
{
align: 'left',
header: `Face flow (${currency})`,
header: `Face value (${currency})`,
cell: (row: Row) =>
row.faceFlow
? `${row.type === 'REPAID' ? '-' : ''}${formatBalance(row.faceFlow, undefined, 2, 2)}`
row.faceValue
? `${row.type === 'REPAID' ? '-' : ''}${formatBalance(row.faceValue, undefined, 2, 2)}`
: '-',
},
{
Expand All @@ -143,6 +171,16 @@ export const TransactionTable = ({ transactions, currency, loanType, decimals, p
header: `Settle price (${currency})`,
cell: (row: Row) => (row.settlePrice ? formatBalance(row.settlePrice, undefined, 6, 2) : '-'),
},
...(loanType === 'external'
? [
{
align: 'left',
header: `YTM`,
cell: (row: Row) =>
!row.yieldToMaturity || row.yieldToMaturity?.lt(0) ? '-' : formatPercentage(row.yieldToMaturity),
},
]
: []),
{
align: 'left',
header: `Net cash flow (${currency})`,
Expand Down Expand Up @@ -170,6 +208,16 @@ export const TransactionTable = ({ transactions, currency, loanType, decimals, p
? `${row.type === 'REPAID' ? '-' : ''}${formatBalance(row.position, undefined, 2, 2)}`
: '-',
},
...(loanType === 'external'
? [
{
align: 'left',
header: `YTM`,
cell: (row: Row) =>
!row.yieldToMaturity || row.yieldToMaturity?.lt(0) ? '-' : formatPercentage(row.yieldToMaturity),
},
]
: []),
]),
] as Column[]
}, [])
Expand Down
66 changes: 64 additions & 2 deletions centrifuge-app/src/pages/Loan/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CurrencyBalance, Loan as LoanType, Pool, PricingInfo, TinlakeLoan } from '@centrifuge/centrifuge-js'
import {
CurrencyBalance,
ExternalPricingInfo,
Loan as LoanType,
Pool,
PricingInfo,
TinlakeLoan,
} from '@centrifuge/centrifuge-js'
import {
Box,
Button,
Expand All @@ -25,9 +32,10 @@ import { RouterLinkButton } from '../../components/RouterLinkButton'
import { Tooltips } from '../../components/Tooltips'
import { nftMetadataSchema } from '../../schemas'
import { LoanTemplate } from '../../types'
import { Dec } from '../../utils/Decimal'
import { copyToClipboard } from '../../utils/copyToClipboard'
import { daysBetween, formatDate, isValidDate } from '../../utils/date'
import { formatBalance, truncateText } from '../../utils/formatting'
import { formatBalance, formatPercentage, truncateText } from '../../utils/formatting'
import { useLoan, useNftDocumentId } from '../../utils/useLoans'
import { useMetadata } from '../../utils/useMetadata'
import { useCentNFT } from '../../utils/useNFTs'
Expand Down Expand Up @@ -139,6 +147,52 @@ function Loan() {
return 0
}, [originationDate, loan?.pricing.maturityDate])

const weightedYTM = React.useMemo(() => {
if (
loan?.pricing &&
'valuationMethod' in loan.pricing &&
loan.pricing.valuationMethod === 'oracle' &&
loan.pricing.interestRate.isZero()
) {
const termDays = originationDate
? daysBetween(originationDate, loan?.pricing.maturityDate)
: daysBetween(new Date(), loan?.pricing.maturityDate)
const yearsBetweenDates = termDays / 365

return borrowerAssetTransactions
?.filter((tx) => tx.type !== 'REPAID')
.reduce((prev, curr) => {
const faceValue =
curr.quantity && (loan.pricing as ExternalPricingInfo).notional
? new CurrencyBalance(curr.quantity, 18)
.toDecimal()
.mul((loan.pricing as ExternalPricingInfo).notional.toDecimal())
: null

const yieldToMaturity =
curr.amount && faceValue
? Dec(2)
.mul(faceValue?.sub(curr.amount.toDecimal()))
.div(Dec(yearsBetweenDates).mul(faceValue.add(curr.amount.toDecimal())))
.mul(100)
: null
return yieldToMaturity?.mul(curr.quantity!).add(prev) || prev
}, Dec(0))
}
return null
}, [loan, borrowerAssetTransactions])

const averageWeightedYTM = React.useMemo(() => {
if (borrowerAssetTransactions?.length && weightedYTM) {
const sum = borrowerAssetTransactions
.filter((tx) => tx.type !== 'REPAID')
.reduce((prev, curr) => {
return curr.quantity ? Dec(curr.quantity).add(prev) : prev
}, Dec(0))
return sum.isZero() ? Dec(0) : weightedYTM.div(sum)
}
}, [weightedYTM])

return (
<Stack>
<Box mt={2} ml={2}>
Expand Down Expand Up @@ -194,6 +248,12 @@ function Loan() {
)}`,
},
],
...(loan.pricing.maturityDate &&
'valuationMethod' in loan.pricing &&
loan.pricing.valuationMethod === 'oracle' &&
averageWeightedYTM
? [{ label: 'Average YTM', value: formatPercentage(averageWeightedYTM) }]
: []),
]}
/>

Expand Down Expand Up @@ -272,6 +332,8 @@ function Loan() {
poolType={poolMetadata?.pool?.asset.class as 'publicCredit' | 'privateCredit' | undefined}
decimals={pool.currency.decimals}
pricing={loan.pricing as PricingInfo}
maturityDate={new Date(loan.pricing.maturityDate)}
originationDate={originationDate ? new Date(originationDate) : undefined}
/>
</PageSection>
) : null}
Expand Down

0 comments on commit 31dd5da

Please sign in to comment.