Skip to content

Commit

Permalink
fix: profit and loss report fixes (#58)
Browse files Browse the repository at this point in the history
* Handle missing metadata hash for profitAndLoss

* Add tests to retrieve quarterly and yearly data

* [bot] New pkg version: 0.0.0-alpha.11

* Fix processor tests

* Throw errors for other reports requiring metadata

* Add more tests

---------

Co-authored-by: GitHub Actions <actions@github.com>
  • Loading branch information
sophialittlejohn and actions-user authored Jan 28, 2025
1 parent fb52d09 commit f75d455
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 19 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@centrifuge/sdk",
"version": "0.0.0-alpha.10",
"version": "0.0.0-alpha.11",
"description": "",
"homepage": "https://github.com/centrifuge/sdk/tree/main#readme",
"author": "",
Expand Down
12 changes: 12 additions & 0 deletions src/IndexerQueries/poolSnapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type PoolSnapshot = {
poolCurrency: {
decimals: number
}
tranches: string[]
}

export type SubqueryPoolSnapshot = {
Expand Down Expand Up @@ -67,6 +68,11 @@ export type SubqueryPoolSnapshot = {
currency: {
decimals: number
}
tranches: {
nodes: {
id: string
}[]
}
}
}[]
}
Expand Down Expand Up @@ -108,6 +114,11 @@ query($filter: PoolSnapshotFilter) {
currency {
decimals
}
tranches {
nodes {
id
}
}
}
}
}
Expand Down Expand Up @@ -152,6 +163,7 @@ export function poolSnapshotsPostProcess(data: SubqueryPoolSnapshot): PoolSnapsh
sumInterestAccruedByPeriod: new Currency(state.sumInterestAccruedByPeriod, poolCurrencyDecimals),
sumRealizedProfitFifoByPeriod: new Currency(state.sumRealizedProfitFifoByPeriod, poolCurrencyDecimals),
sumUnrealizedProfitByPeriod: new Currency(state.sumUnrealizedProfitByPeriod, poolCurrencyDecimals),
tranches: state.pool.tranches.nodes.map((tranche) => tranche.id),
}

if (currentSnapshot) {
Expand Down
43 changes: 34 additions & 9 deletions src/Reports/Processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ describe('Processor', () => {
it('should return empty array when no snapshots found', () => {
expect(processor.cashflow({ poolSnapshots: [], poolFeeSnapshots: {}, metadata: undefined })).to.deep.equal([])
})
it('should throw an error if no metadata is passed', () => {
let thrown = false
try {
processor.cashflow({
poolSnapshots: mockPoolSnapshots,
poolFeeSnapshots: mockPoolFeeSnapshots,
metadata: undefined,
})
} catch (e) {
thrown = true
}
expect(thrown).to.be.true
})

it('should aggregate values correctly when grouping by day', () => {
const result = processor.cashflow(
Expand Down Expand Up @@ -244,7 +257,7 @@ describe('Processor', () => {
sumUnrealizedProfitByPeriod: Currency.fromFloat(0.15, 6), // 0.15
},
{
...mockPoolSnapshots[0],
...mockPoolSnapshots[1],
id: 'pool-11',
timestamp: '2024-01-02T12:00:00Z',
sumInterestRepaidAmountByPeriod: Currency.fromFloat(0.1, 6),
Expand Down Expand Up @@ -283,14 +296,17 @@ describe('Processor', () => {
})

it('should handle undefined metadata', () => {
const result = processor.profitAndLoss({
poolSnapshots: mockPLPoolSnapshots,
poolFeeSnapshots: mockPLFeeSnapshots,
metadata: undefined,
})
expect(result).to.have.lengthOf(2)
const firstDay = result[0]
expect(firstDay?.subtype).to.equal('privateCredit') // should default to privateCredit
let thrown = false
try {
processor.profitAndLoss({
poolSnapshots: mockPLPoolSnapshots,
poolFeeSnapshots: mockPLFeeSnapshots,
metadata: undefined,
})
} catch (e) {
thrown = true
}
expect(thrown).to.be.true
})

it('should process private credit pool data correctly', () => {
Expand Down Expand Up @@ -621,6 +637,15 @@ describe('Processor', () => {
it('should return empty array when no snapshots found', () => {
expect(processor.assetList({ assetSnapshots: [], metadata: undefined })).to.deep.equal([])
})
it('should throw an error if no metadata is passed', () => {
let thrown = false
try {
processor.assetList({ assetSnapshots: mockAssetSnapshots, metadata: undefined })
} catch (e) {
thrown = true
}
expect(thrown).to.be.true
})
it('should process asset list correctly', () => {
const result = processor.assetList({ assetSnapshots: mockAssetSnapshots, metadata: mockPoolMetadata })
expect(result).to.have.lengthOf(2)
Expand Down
16 changes: 16 additions & 0 deletions src/Reports/Processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ export class Processor {
*/
cashflow(data: CashflowData, filter?: Omit<ReportFilter, 'to' | 'from'>): CashflowReport[] {
if (!data.poolSnapshots?.length) return []
if (!data.metadata) {
throw new Error(
'Provide the correct metadataHash to centrifuge.pool(<poolId>, <metadataHash>).reports.cashflow()'
)
}
const subtype = data.metadata?.pool.asset.class === 'Public credit' ? 'publicCredit' : 'privateCredit'
const items: CashflowReport[] = data.poolSnapshots.map((day) => {
const poolFees =
Expand Down Expand Up @@ -142,6 +147,12 @@ export class Processor {
*/
profitAndLoss(data: ProfitAndLossData, filter?: Omit<ReportFilter, 'to' | 'from'>): ProfitAndLossReport[] {
if (!data.poolSnapshots?.length) return []
// Check if the metadata tranches match the pool snapshots tranches to verify the correct metadataHash is provided
if (Object.keys(data.metadata?.tranches ?? {})[0] !== data.poolSnapshots[0]?.tranches[0]?.split('-')[1]) {
throw new Error(
'Provide the correct metadataHash to centrifuge.pool(<poolId>, <metadataHash>).reports.profitAndLoss()'
)
}
const items: ProfitAndLossReport[] = data.poolSnapshots.map((day) => {
const subtype = data.metadata?.pool.asset.class === 'Public credit' ? 'publicCredit' : 'privateCredit'
const profitAndLossFromAsset =
Expand Down Expand Up @@ -350,6 +361,11 @@ export class Processor {

assetList(data: AssetListData, filter?: Omit<AssetListReportFilter, 'to' | 'from'>): AssetListReport[] {
if (!data.assetSnapshots?.length) return []
if (!data.metadata) {
throw new Error(
'Provide the correct metadataHash to centrifuge.pool(<poolId>, <metadataHash>).reports.assetList()'
)
}
return data.assetSnapshots
.filter((snapshot) => {
if (snapshot.valuationMethod?.toLowerCase() === 'cash') return false
Expand Down
125 changes: 118 additions & 7 deletions src/Reports/Reports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai'
import { Centrifuge } from '../Centrifuge.js'
import { spy } from 'sinon'
import { Reports } from '../Reports/index.js'
import { ReportFilter } from '../types/reports.js'
import { ProfitAndLossReportPublicCredit, ReportFilter } from '../types/reports.js'
import { processor } from './Processor.js'

describe('Reports', () => {
Expand Down Expand Up @@ -143,7 +143,8 @@ describe('Reports', () => {
})
it('should retrieve 6 months worth of data and group by day, month, quarter and year', async () => {
const anemoyPoolId = '4139607887'
const pool = await centrifuge.pool(anemoyPoolId)
const anemoyMetadataHash = 'QmTjbzx4mX1A9vRFxzLDZszKQSTsFbH8YgnpfmTSfWx73G'
const pool = await centrifuge.pool(anemoyPoolId, anemoyMetadataHash)
const reports = new Reports(centrifuge, pool)
let filter: ReportFilter = {
from: '2024-01-01',
Expand Down Expand Up @@ -181,14 +182,121 @@ describe('Reports', () => {

describe('profit and loss report', () => {
it('should fetch profit and loss report', async () => {
const pool = await centrifuge.pool('1615768079')
const metadataHash = 'QmPfUr7DfUPd5ALfu96R5mbR2PhRfM94vboNFNCDgbj3S8'
const ns3PoolId = '1615768079'
const pool = await centrifuge.pool(ns3PoolId, metadataHash)
const report = await pool.reports.profitAndLoss({
from: '2024-11-02T22:11:29.776Z',
to: '2024-11-06T22:11:29.776Z',
groupBy: 'day',
})
expect(report.length).to.equal(4)
})
it('should throw an error if an incorrect metadataHash is provided', async () => {
const anemoyMetadataHash = 'QmXEdkcbnvvkFakMwxoy2UbxQzg4Q5nLXVZsHQDhDVRarC'
const ns3PoolId = '1615768079'
const pool = await centrifuge.pool(ns3PoolId, anemoyMetadataHash)
expect(
await pool.reports.profitAndLoss({
from: '2024-11-02T22:11:29.776Z',
to: '2024-11-06T22:11:29.776Z',
groupBy: 'day',
})
).to.be.throw
})
it('should provide the correct totalIncome for Anemoy on 01/01/2025', async () => {
const anemoyPoolId = '4139607887'
const metadataHash = 'QmXEdkcbnvvkFakMwxoy2UbxQzg4Q5nLXVZsHQDhDVRarC'
const pool = await centrifuge.pool(anemoyPoolId, metadataHash)
const report = await pool.reports.profitAndLoss({
from: '2025-01-01',
to: '2025-01-01',
groupBy: 'day',
})
expect((report[0] as ProfitAndLossReportPublicCredit)?.totalIncome.toString()).to.equal('4847257770')
})
it('should retrieve 6 months worth of data and group by day, month, quarter and year', async () => {
const anemoyPoolId = '4139607887'
const metadataHash = 'QmXEdkcbnvvkFakMwxoy2UbxQzg4Q5nLXVZsHQDhDVRarC'
const pool = await centrifuge.pool(anemoyPoolId, metadataHash)
const reports = new Reports(centrifuge, pool)
let filter: ReportFilter = {
from: '2024-01-01',
to: '2024-06-30',
groupBy: 'day',
}
const report = await reports.profitAndLoss(filter)
expect(report.length).to.equal(182)

filter = {
...filter,
groupBy: 'month',
}
const report2 = await reports.profitAndLoss(filter)
expect(report2.length).to.equal(6)
expect(report?.[report.length - 1]?.timestamp.slice(0, 10)).to.equal('2024-06-30')
expect(report?.[report.length - 1]?.subtype).to.equal('publicCredit') // real data
expect((report?.[report.length - 1] as ProfitAndLossReportPublicCredit)?.totalIncome).to.exist
expect((report?.[report.length - 1] as ProfitAndLossReportPublicCredit)?.totalIncome.toString()).to.equal('0') // real data

filter = {
...filter,
groupBy: 'quarter',
}
const report3 = await reports.profitAndLoss(filter)
expect(report3.length).to.equal(2)

filter = {
...filter,
groupBy: 'year',
}
const report4 = await reports.profitAndLoss(filter)
expect(report4.length).to.equal(1)
expect(report4?.[0]?.timestamp.slice(0, 10)).to.equal('2024-06-30')
})
it('should retrieve monthly data starting with Jan 2024', async () => {
const anemoyPoolId = '4139607887'
const metadataHash = 'QmXEdkcbnvvkFakMwxoy2UbxQzg4Q5nLXVZsHQDhDVRarC'
const pool = await centrifuge.pool(anemoyPoolId, metadataHash)
const report = await pool.reports.profitAndLoss({
from: '2024-01-11',
to: '2025-01-28',
groupBy: 'month',
})
expect(report.length).to.equal(13)
expect(report[0]?.timestamp.slice(0, 7)).to.equal('2024-01')
expect(report[report.length - 1]?.timestamp.slice(0, 7)).to.equal('2025-01')
expect(report[report.length - 2]?.timestamp.slice(0, 7)).to.equal('2024-12')
})
it('should retrieve quarterly data starting with Q3 2023', async () => {
const anemoyPoolId = '4139607887'
const metadataHash = 'QmXEdkcbnvvkFakMwxoy2UbxQzg4Q5nLXVZsHQDhDVRarC'
const pool = await centrifuge.pool(anemoyPoolId, metadataHash)
const report = await pool.reports.profitAndLoss({
from: '2023-01-01',
to: '2025-01-01',
groupBy: 'quarter',
})
expect(report.length).to.equal(7)
expect(report[0]?.timestamp.slice(0, 7)).to.equal('2023-09')
})
it('should retrieve yearly data starting in Sept 2023', async () => {
const anemoyPoolId = '4139607887'
const metadataHash = 'QmXEdkcbnvvkFakMwxoy2UbxQzg4Q5nLXVZsHQDhDVRarC'
const pool = await centrifuge.pool(anemoyPoolId, metadataHash)
const report = await pool.reports.profitAndLoss({
from: '2023-09-18',
to: '2025-01-01',
groupBy: 'year',
})
expect(report.length).to.equal(3)
expect(report[0]?.timestamp.slice(0, 10)).to.equal('2023-12-31')
expect(report[0]?.totalProfitAndLoss.toBigInt()).to.equal(0n)
expect(report[1]?.timestamp.slice(0, 10)).to.equal('2024-12-31')
expect(report[1]?.totalProfitAndLoss.toBigInt()).to.equal(170194942096n)
expect(report[2]?.timestamp.slice(0, 10)).to.equal('2025-01-01')
expect(report[2]?.totalProfitAndLoss.toBigInt()).to.equal(5003078970n)
})
})

describe('investor transactions report', () => {
Expand Down Expand Up @@ -341,17 +449,19 @@ describe('Reports', () => {
describe('asset list report', () => {
it('should fetch asset list report', async () => {
const anemoyPoolId = '4139607887'
const pool = await centrifuge.pool(anemoyPoolId)
const anemoyMetadataHash = 'QmTjbzx4mX1A9vRFxzLDZszKQSTsFbH8YgnpfmTSfWx73G'
const pool = await centrifuge.pool(anemoyPoolId, anemoyMetadataHash)
const report = await pool.reports.assetList({
from: '2024-01-01T22:11:29.776Z',
to: '2024-01-03T22:11:29.776Z',
})
expect(report.length).to.equal(4)
expect(report?.[0]?.subtype).to.equal('privateCredit')
expect(report?.[0]?.subtype).to.equal('publicCredit')
})
it('should filter by status ongoing', async () => {
const anemoyPoolId = '4139607887'
const pool = await centrifuge.pool(anemoyPoolId)
const anemoyMetadataHash = 'QmTjbzx4mX1A9vRFxzLDZszKQSTsFbH8YgnpfmTSfWx73G'
const pool = await centrifuge.pool(anemoyPoolId, anemoyMetadataHash)
const report = await pool.reports.assetList({
status: 'ongoing',
from: '2024-01-01T00:00:00.000Z',
Expand All @@ -361,7 +471,8 @@ describe('Reports', () => {
})
it('should filter by status overdue', async () => {
const anemoyPoolId = '4139607887'
const pool = await centrifuge.pool(anemoyPoolId)
const anemoyMetadataHash = 'QmTjbzx4mX1A9vRFxzLDZszKQSTsFbH8YgnpfmTSfWx73G'
const pool = await centrifuge.pool(anemoyPoolId, anemoyMetadataHash)
const report = await pool.reports.assetList({
status: 'overdue',
from: '2024-01-01T00:00:00.000Z',
Expand Down
4 changes: 2 additions & 2 deletions src/Reports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,13 @@ export class Reports extends Entity {
)
)
case 'investorTransactions':
return combineLatest([investorTransactions$, metadata$]).pipe(
return combineLatest([investorTransactions$]).pipe(
map(
([investorTransactions]) => processor.investorTransactions({ investorTransactions }, restFilter) as T[]
)
)
case 'assetTransactions':
return combineLatest([assetTransactions$, metadata$]).pipe(
return combineLatest([assetTransactions$]).pipe(
map(([assetTransactions]) => processor.assetTransactions({ assetTransactions }, restFilter) as T[])
)
case 'feeTransactions':
Expand Down
2 changes: 2 additions & 0 deletions src/tests/mocks/mockPoolSnapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const mockPoolSnapshots: PoolSnapshot[] = [
poolCurrency: {
decimals: 6,
},
tranches: ['pool1-0x6756e091ae798a8e51e12e27ee8facdf', 'pool1-0xda64aae939e4d3a981004619f1709d8f'],
},
{
id: 'pool-11',
Expand Down Expand Up @@ -61,5 +62,6 @@ export const mockPoolSnapshots: PoolSnapshot[] = [
poolCurrency: {
decimals: 6,
},
tranches: ['pool1-0x6756e091ae798a8e51e12e27ee8facdf', 'pool1-0xda64aae939e4d3a981004619f1709d8f'],
},
]

0 comments on commit f75d455

Please sign in to comment.