Skip to content

Commit

Permalink
feat: track investor side profit (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
filo87 authored Jul 15, 2024
1 parent 31e1f1a commit 7b38112
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 1 deletion.
14 changes: 14 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ type InvestorTransaction @entity {
currencyAmount: BigInt
tokenPrice: BigInt
transactionFee: BigInt

realizedProfitFifo: BigInt
}

enum AssetTransactionType {
Expand Down Expand Up @@ -402,6 +404,18 @@ type TrancheBalance @entity {

sumClaimedTrancheTokens: BigInt!
sumClaimedCurrency: BigInt!

unrealizedProfit: BigInt!
}

type InvestorPosition @entity {
id: ID! #address - poolId - trancheId - hash
account: Account! @index
pool: Pool! @index
tranche: Tranche! @index
timestamp: Date!
holdingQuantity: BigInt!
purchasePrice: BigInt!
}

enum AssetStatus {
Expand Down
14 changes: 14 additions & 0 deletions src/mappings/handlers/blockHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
import { AssetPositionService } from '../services/assetPositionService'
import { EpochService } from '../services/epochService'
import { SnapshotPeriodService } from '../services/snapshotPeriodService'
import { TrancheBalanceService } from '../services/trancheBalanceService'
import { InvestorPositionService } from '../services/investorPositionService'

const timekeeper = TimekeeperService.init()

Expand Down Expand Up @@ -74,6 +76,18 @@ async function _handleBlock(block: SubstrateBlock): Promise<void> {
await tranche.computeYieldAnnualized('yield30DaysAnnualized', period.start, daysAgo30)
await tranche.computeYieldAnnualized('yield90DaysAnnualized', period.start, daysAgo90)
await tranche.save()

// Compute TrancheBalances Unrealized Profit
const trancheBalances = await TrancheBalanceService.getByTrancheId(tranche.id) as TrancheBalanceService[]
for (const trancheBalance of trancheBalances) {
const unrealizedProfit = await InvestorPositionService.computeUnrealizedProfitAtPrice(
trancheBalance.accountId,
tranche.id,
tranche.tokenPrice
)
await trancheBalance.updateUnrealizedProfit(unrealizedProfit)
await trancheBalance.save()
}
}
// Asset operations
const activeLoanData = await pool.getPortfolio()
Expand Down
16 changes: 16 additions & 0 deletions src/mappings/handlers/evmHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CurrencyBalanceService } from '../services/currencyBalanceService'
import type { Provider } from '@ethersproject/providers'
import { TrancheBalanceService } from '../services/trancheBalanceService'
import { escrows, userEscrows } from '../../config'
import { InvestorPositionService } from '../services/investorPositionService'

const _ethApi = api as unknown as Provider
//const networkPromise = typeof ethApi.getNetwork === 'function' ? ethApi.getNetwork() : null
Expand Down Expand Up @@ -125,9 +126,24 @@ async function _handleEvmTransfer(event: TransferLog): Promise<void> {
// Handle Transfer In and Out
if (isFromUserAddress && isToUserAddress) {
const txIn = InvestorTransactionService.transferIn({ ...orderData, address: toAccount.id })
await InvestorPositionService.buy(
txIn.accountId,
txIn.trancheId,
txIn.hash,
txIn.timestamp,
txIn.tokenAmount,
txIn.tokenPrice
)
await txIn.save()

const txOut = InvestorTransactionService.transferOut({ ...orderData, address: fromAccount.id })
const profit = await InvestorPositionService.sellFifo(
txOut.accountId,
txOut.trancheId,
txOut.tokenAmount,
txOut.tokenPrice
)
await txOut.setRealizedProfitFifo(profit)
await txOut.save()
}
}
16 changes: 16 additions & 0 deletions src/mappings/handlers/ormlTokensHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InvestorTransactionService } from '../services/investorTransactionServi
import { PoolService } from '../services/poolService'
import { TrancheService } from '../services/trancheService'
import { BlockchainService, LOCAL_CHAIN_ID } from '../services/blockchainService'
import { InvestorPositionService } from '../services/investorPositionService'

export const handleTokenTransfer = errorHandler(_handleTokenTransfer)
async function _handleTokenTransfer(event: SubstrateEvent<TokensTransferEvent>): Promise<void> {
Expand Down Expand Up @@ -64,10 +65,25 @@ async function _handleTokenTransfer(event: SubstrateEvent<TokensTransferEvent>):
// CREATE 2 TRANSFERS FOR FROM AND TO ADDRESS
// with from create TRANSFER_OUT
const txOut = InvestorTransactionService.transferOut({ ...orderData, address: fromAccount.id })
const profit = await InvestorPositionService.sellFifo(
txOut.accountId,
txOut.trancheId,
txOut.tokenAmount,
txOut.tokenPrice
)
await txOut.setRealizedProfitFifo(profit)
await txOut.save()

// with to create TRANSFER_IN
const txIn = InvestorTransactionService.transferIn({ ...orderData, address: toAccount.id })
await InvestorPositionService.buy(
txIn.accountId,
txIn.trancheId,
txIn.hash,
txIn.timestamp,
txIn.tokenAmount,
txIn.tokenPrice
)
await txIn.save()
}

Expand Down
15 changes: 14 additions & 1 deletion src/mappings/handlers/poolsHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { AssetService, ONCHAIN_CASH_ASSET_ID } from '../services/assetService'
import { AssetTransactionData, AssetTransactionService } from '../services/assetTransactionService'
import { substrateStateSnapshotter } from '../../helpers/stateSnapshot'
import { Pool, PoolSnapshot } from '../../types'
import { InvestorPositionService } from '../services/investorPositionService'

export const handlePoolCreated = errorHandler(_handlePoolCreated)
async function _handlePoolCreated(event: SubstrateEvent<PoolCreatedEvent>): Promise<void> {
Expand Down Expand Up @@ -227,6 +228,15 @@ async function _handleEpochExecuted(event: SubstrateEvent<EpochClosedExecutedEve
await it.save()
await oo.updateUnfulfilledInvest(it.currencyAmount)
await trancheBalance.investExecute(it.currencyAmount, it.tokenAmount)

await InvestorPositionService.buy(
it.accountId,
it.trancheId,
it.hash,
it.timestamp,
it.tokenAmount,
it.tokenPrice
)
}

if (oo.redeemAmount > BigInt(0) && epochState.redeemFulfillmentPercentage > BigInt(0)) {
Expand All @@ -235,9 +245,12 @@ async function _handleEpochExecuted(event: SubstrateEvent<EpochClosedExecutedEve
amount: oo.redeemAmount,
fulfillmentPercentage: epochState.redeemFulfillmentPercentage,
})
await it.save()
await oo.updateUnfulfilledRedeem(it.tokenAmount)
await trancheBalance.redeemExecute(it.tokenAmount, it.currencyAmount)

const profit = await InvestorPositionService.sellFifo(it.accountId, it.trancheId, it.tokenAmount, it.tokenPrice)
await it.setRealizedProfitFifo(profit)
await it.save()
}

await trancheBalance.save()
Expand Down
85 changes: 85 additions & 0 deletions src/mappings/services/investorPositionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { InvestorPosition } from '../../types/models/InvestorPosition'
import { WAD } from '../../config'
import { nToBigInt, bnToBn, BN } from '@polkadot/util'

export class InvestorPositionService extends InvestorPosition {
static init(accountId: string, trancheId: string, hash: string, timestamp: Date, quantity: bigint, price: bigint) {
const [ poolId ] = trancheId.split('-')
const id = `${accountId}-${trancheId}-${hash}`
logger.info(
`Initialising new InvestorPosition with Id ${id} ` +
`holdingQuantity: ${quantity.toString(10)} price: ${price.toString(10)}`
)
return new this(id, accountId, poolId, trancheId, timestamp, quantity, price)
}

static buy(accountId: string, trancheId: string, hash: string, timestamp: Date, quantity: bigint, price: bigint) {
if (quantity > BigInt(0)) {
return this.init(accountId, trancheId, hash, timestamp, quantity, price).save()
} else {
logger.warn(`Skipping tranche position ${trancheId}-${hash} as quantity is 0`)
return Promise.resolve()
}
}

static async sellFifo(accountId: string, trancheId: string, sellingQuantity: bigint, sellingPrice: bigint) {
logger.info(
`Selling positions for ${trancheId} ` +
`sellingQuantity: ${sellingQuantity.toString(10)} sellingPrice: ${sellingPrice.toString(10)}`
)
if (sellingQuantity <= BigInt(0)) return BigInt(0)
const positions = await this.getByFields([['accountId', '=', accountId], ['trancheId', '=', trancheId]])
positions.sort((a, b) => b.timestamp.valueOf() - a.timestamp.valueOf())

const sellPositions: [InvestorPosition: InvestorPosition, sellQuantity: bigint][] = []
let remainingQuantity = sellingQuantity
while (remainingQuantity > BigInt(0)) {
const currentPosition = positions.pop()
if (!currentPosition) throw new Error(`No positions to sell for tranche ${trancheId}`)
if (remainingQuantity > currentPosition.holdingQuantity) {
const soldQuantity = currentPosition.holdingQuantity
currentPosition['holdingQuantity'] = BigInt(0)
sellPositions.push([currentPosition, soldQuantity])
remainingQuantity -= soldQuantity
} else {
const soldQuantity = remainingQuantity
currentPosition['holdingQuantity'] -= soldQuantity
sellPositions.push([currentPosition, soldQuantity])
remainingQuantity -= soldQuantity
}
}

const profitFromSale = nToBigInt(bnToBn(sellingPrice).mul(bnToBn(sellingQuantity)).div(WAD))
const costOfBuy = nToBigInt(
sellPositions.reduce<BN>(
(totalCost, line) => totalCost.add(bnToBn(line[0].purchasePrice).mul(bnToBn(line[1]).div(WAD))),
new BN(0)
)
)

const dbUpdates = sellPositions.map((line) =>
line[0].holdingQuantity > BigInt(0) ? line[0].save() : this.remove(line[0].id)
)

await Promise.all(dbUpdates)
return profitFromSale - costOfBuy
}

static async computeUnrealizedProfitAtPrice(accountId: string, trancheId: string, sellingPrice: bigint) {
if (!sellingPrice || sellingPrice <= BigInt(0)) return BigInt(0)
logger.info(`Computing unrealizedProfit at price ${sellingPrice} for tranche ${trancheId}`)
const sellingPositions = await this.getByFields([['accountId', '=', accountId], ['trancheId', '=', trancheId]])
const sellingQuantity = sellingPositions.reduce<bigint>(
(result, position) => result + position.holdingQuantity,
BigInt(0)
)
const profitFromSale = nToBigInt(bnToBn(sellingPrice).mul(bnToBn(sellingQuantity)).div(WAD))
const costOfBuy = nToBigInt(
sellingPositions.reduce<BN>(
(totalCost, line) => totalCost.add(bnToBn(line.purchasePrice).mul(bnToBn(line.holdingQuantity).div(WAD))),
new BN(0)
)
)
return profitFromSale - costOfBuy
}
}
6 changes: 6 additions & 0 deletions src/mappings/services/investorTransactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export class InvestorTransactionService extends InvestorTransaction {
tx.currencyAmount = currencyTypes.includes(type) ? data.amount : this.computeCurrencyAmount(data)
tx.tokenAmount = tokenTypes.includes(type) ? data.amount : this.computeTokenAmount(data)

tx.realizedProfitFifo = BigInt(0)

return tx
}

Expand Down Expand Up @@ -175,4 +177,8 @@ export class InvestorTransactionService extends InvestorTransaction {
static computeFulfilledAmount(data: InvestorTransactionData) {
return nToBigInt(bnToBn(data.amount).mul(bnToBn(data.fulfillmentPercentage)).div(WAD))
}

public setRealizedProfitFifo(profit: bigint) {
this.realizedProfitFifo = profit
}
}
9 changes: 9 additions & 0 deletions src/mappings/services/trancheBalanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class TrancheBalanceService extends TrancheBalance {
BigInt(0),
BigInt(0),
BigInt(0),
BigInt(0),
BigInt(0)
)
return trancheBalance
Expand Down Expand Up @@ -83,4 +84,12 @@ export class TrancheBalanceService extends TrancheBalance {
this.claimableCurrency -= currencyAmount
this.sumClaimedCurrency += currencyAmount
}

public updateUnrealizedProfit(unrealizedProfit: bigint) {
logger.info(
`Updating unrealizedProfit for trancheBalance: ${this.id}-${this.poolId}-${this.trancheId}` +
` to: ${unrealizedProfit}`
)
this.unrealizedProfit = unrealizedProfit
}
}

0 comments on commit 7b38112

Please sign in to comment.