Skip to content

Commit

Permalink
Fix daily moving avg (#4)
Browse files Browse the repository at this point in the history
* Add a daily moving average utility

* Update 30D avg on deposit and withdraw

* Ignore test run file
  • Loading branch information
prevostc authored Apr 10, 2024
1 parent 8e6ab41 commit 7be0401
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 31 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ node_modules

subgraph.yaml
src/config.ts

tests/.latest.json
10 changes: 4 additions & 6 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -649,12 +649,11 @@ type Investor @entity {
"Current position value in USD as the sum of all the investor ongoing positions"
totalPositionValueUSD: BigDecimal!

"Internal state to calculate the moving averages of the total position value"
averageDailyTotalPositionValueUSDState: [BigDecimal!]!
"Sum of the investor total positions value at the end of the day for the last 30 days in USD divided by the number of active days"
averageDailyTotalPositionValueUSD30D: BigDecimal!

"The last daily total position values in USD for the last 30 days. Most recent last."
last30DailyTotalPositionValuesUSD: [BigDecimal!]!

"The transactions count that the investor was the sender of"
cumulativeInteractionsCount: Int!
"All deposits of the investor"
Expand Down Expand Up @@ -765,12 +764,11 @@ type InvestorPosition @entity {
"Current position value in USD"
positionValueUSD: BigDecimal!

"Internal state to calculate the moving averages of the position value"
averageDailyPositionValueUSDState: [BigDecimal!]!
"Sum of the daily position value for the last 30 days in USD divided by the number of active days"
averageDailyPositionValueUSD30D: BigDecimal!

"The last daily position values in USD for the last 30 days. Most recent last."
last30DailyPositionValuesUSD: [BigDecimal!]!

"The compounded amount of the first token"
cumulativeCompoundedAmount0: BigDecimal!
"The compounded amount of the second token"
Expand Down
36 changes: 17 additions & 19 deletions src/clock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Address, BigDecimal, Bytes, ethereum, log } from "@graphprotocol/graph-ts"
import { Address, BigDecimal, BigInt, Bytes, ethereum, log } from "@graphprotocol/graph-ts"
import { ClockTick, Investor } from "../generated/schema"
import { DAY, MINUTES_15, SNAPSHOT_PERIODS } from "./utils/time"
import { getClockTick } from "./entity/clock"
Expand All @@ -10,6 +10,7 @@ import { getBeefyCLStrategy, getBeefyCLVaultSnapshot, isVaultRunning } from "./e
import { getInvestorPositionSnapshot } from "./entity/position"
import { getInvestorSnapshot } from "./entity/investor"
import { BeefyVaultConcLiq as BeefyCLVaultContract } from "../generated/templates/BeefyCLVault/BeefyVaultConcLiq"
import { DailyAvgCalc, DailyAvgState } from "./utils/daily-avg"

export function handleClockTick(block: ethereum.Block): void {
const timestamp = block.timestamp
Expand Down Expand Up @@ -143,17 +144,15 @@ function updateDataOnClockTick(tick: ClockTick, isNewDay: boolean): void {
position.underlyingBalance0USD = position.underlyingBalance0.times(token0PriceInUSD)
position.underlyingBalance1USD = position.underlyingBalance1.times(token1PriceInUSD)
position.positionValueUSD = position.underlyingBalance0USD.plus(position.underlyingBalance1USD)
let state = DailyAvgState.deserialize(position.averageDailyPositionValueUSDState)
if (isNewDay) {
let last30DailyPositionValuesUSD = position.last30DailyPositionValuesUSD // required by thegraph
last30DailyPositionValuesUSD.push(position.positionValueUSD) // most recent value last
while (last30DailyPositionValuesUSD.length > 30) {
last30DailyPositionValuesUSD.shift() // remove oldest value
}
position.last30DailyPositionValuesUSD = last30DailyPositionValuesUSD
position.averageDailyPositionValueUSD30D = last30DailyPositionValuesUSD
.reduce<BigDecimal>((acc, val) => acc.plus(val), ZERO_BD)
.div(BigDecimal.fromString(last30DailyPositionValuesUSD.length.toString()))
state.addValue(position.positionValueUSD)
state.resetPendingValue()
} else {
state.updatePendingValueTimestamp(tick.timestamp)
}
position.averageDailyPositionValueUSD30D = DailyAvgCalc.avg(DAY.times(BigInt.fromU32(30)), state)
position.averageDailyPositionValueUSDState = state.serialize()
position.save()
// update position snapshot
for (let k = 0; k < periods.length; k++) {
Expand Down Expand Up @@ -201,17 +200,16 @@ function updateDataOnClockTick(tick: ClockTick, isNewDay: boolean): void {
// @ts-ignore
const tvl: BigDecimal = investorTVL.get(investorIdStr)
investor.totalPositionValueUSD = tvl

let state = DailyAvgState.deserialize(investor.averageDailyTotalPositionValueUSDState)
if (isNewDay) {
let last30DailyTotalPositionValuesUSD = investor.last30DailyTotalPositionValuesUSD
last30DailyTotalPositionValuesUSD.push(tvl) // most recent value last
while (last30DailyTotalPositionValuesUSD.length > 30) {
last30DailyTotalPositionValuesUSD.shift() // remove oldest value
}
investor.last30DailyTotalPositionValuesUSD = last30DailyTotalPositionValuesUSD
investor.averageDailyTotalPositionValueUSD30D = last30DailyTotalPositionValuesUSD
.reduce<BigDecimal>((acc, val) => acc.plus(val), ZERO_BD)
.div(BigDecimal.fromString(last30DailyTotalPositionValuesUSD.length.toString()))
state.addValue(tvl)
state.resetPendingValue()
} else {
state.updatePendingValueTimestamp(tick.timestamp)
}
investor.averageDailyTotalPositionValueUSD30D = DailyAvgCalc.avg(DAY.times(BigInt.fromU32(30)), state)
investor.averageDailyTotalPositionValueUSDState = state.serialize()
investor.save()
for (let j = 0; j < periods.length; j++) {
const period = periods[j]
Expand Down
2 changes: 1 addition & 1 deletion src/entity/investor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function getInvestor(accountAddress: Bytes): Investor {
investor.lastInteractionAt = ZERO_BI
investor.totalPositionValueUSD = ZERO_BD
investor.averageDailyTotalPositionValueUSD30D = ZERO_BD
investor.last30DailyTotalPositionValuesUSD = new Array<BigDecimal>()
investor.averageDailyTotalPositionValueUSDState = new Array<BigDecimal>()
investor.cumulativeInteractionsCount = 0
investor.cumulativeDepositCount = 0
investor.cumulativeWithdrawCount = 0
Expand Down
2 changes: 1 addition & 1 deletion src/entity/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function getInvestorPosition(vault: BeefyCLVault, investor: Investor): In
position.underlyingBalance1USD = ZERO_BD
position.positionValueUSD = ZERO_BD
position.averageDailyPositionValueUSD30D = ZERO_BD
position.last30DailyPositionValuesUSD = new Array<BigDecimal>()
position.averageDailyPositionValueUSDState = new Array<BigDecimal>()
position.cumulativeCompoundedAmount0 = ZERO_BD
position.cumulativeCompoundedAmount1 = ZERO_BD
position.cumulativeCompoundedAmount0USD = ZERO_BD
Expand Down
101 changes: 101 additions & 0 deletions src/utils/daily-avg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { BigDecimal, BigInt, log } from "@graphprotocol/graph-ts"
import { ZERO_BD, ZERO_BI } from "./decimal"
import { DAY } from "./time"

export class DailyAvgState {
constructor(
public pendingValue: BigDecimal,
public pendingValueTimestamp: BigInt,
// a circular buffer of the last n values
// a closed value is a value that is not updated anymore
public closedValues: Array<BigDecimal>,
) {}

serialize(): Array<BigDecimal> {
let res = new Array<BigDecimal>()
res.push(this.pendingValue)
res.push(this.pendingValueTimestamp.toBigDecimal())
for (let idx = 0; idx < this.closedValues.length; idx++) {
let entry = this.closedValues[idx]
res.push(entry)
}
return res
}

static deserialize(data: Array<BigDecimal>): DailyAvgState {
let values = new Array<BigDecimal>()
if (data.length === 0) {
return new DailyAvgState(ZERO_BD, ZERO_BI, values)
}

let pendingValue = data.shift() as BigDecimal
let pendingValueTimestamp = BigInt.fromString((data.shift() as BigDecimal).truncate(0).toString())

while (data.length > 0) {
let value = data.shift() as BigDecimal
values.push(value)
}
return new DailyAvgState(pendingValue, pendingValueTimestamp, values)
}

public addValue(entry: BigDecimal): void {
this.closedValues.push(entry)
}

public setPendingValue(value: BigDecimal, timestamp: BigInt): void {
this.pendingValue = value
this.pendingValueTimestamp = timestamp
}

public resetPendingValue(): void {
this.pendingValue = ZERO_BD
this.pendingValueTimestamp = ZERO_BI
}

public updatePendingValueTimestamp(timestamp: BigInt): void {
this.pendingValueTimestamp = timestamp
}
}

export class DailyAvgCalc {
public static avg(entriesToUse: BigInt, state: DailyAvgState): BigDecimal {
// first, eliminate the entries that are not in the period anymore
state = DailyAvgCalc.evictOldEntries(entriesToUse, state)

// we need at least 1 entry to compute the apr
if (state.closedValues.length === 0) {
return state.pendingValue
}

const closedSeconds = DAY.times(BigInt.fromI32(state.closedValues.length))
const pendingSeconds = state.pendingValueTimestamp.mod(DAY)
let res = ZERO_BD
for (let idx = 0; idx < state.closedValues.length; idx++) {
const entry = state.closedValues[idx]
res = res.plus(entry.times(DAY.toBigDecimal()))
}
res = res.plus(state.pendingValue.times(pendingSeconds.toBigDecimal()))
return res.div(closedSeconds.plus(pendingSeconds).toBigDecimal())
}

/**
* Evict entries that do not belong to the period anymore
*/
public static evictOldEntries(entriesToUse: BigInt, state: DailyAvgState): DailyAvgState {
if (entriesToUse.lt(ZERO_BI) || entriesToUse.equals(ZERO_BI)) {
log.error("DailyAvgCalc: entriesToUse cannot be negative or zero, got {}", [entriesToUse.toString()])
throw new Error("DailyAvgCalc: entriesToUse cannot be negative or zero")
}

let lastEntryIdx = state.closedValues.length - 1
let firstEntryIdx = lastEntryIdx - entriesToUse.toI32() + 1
let firstEntryIdxClamped = Math.max(firstEntryIdx, 0) as i32

const newEntries = new Array<BigDecimal>()
for (let idx = firstEntryIdxClamped; idx <= lastEntryIdx; idx++) {
newEntries.push(state.closedValues[idx])
}

return new DailyAvgState(state.pendingValue, state.pendingValueTimestamp, newEntries)
}
}
1 change: 1 addition & 0 deletions src/utils/time.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BigInt } from "@graphprotocol/graph-ts"

export const MINUTES_15 = BigInt.fromI32(60 * 15)
export const HOUR = BigInt.fromI32(60 * 60)
export const DAY = BigInt.fromI32(60 * 60 * 24)
export const WEEK = BigInt.fromI32(60 * 60 * 24 * 7)
export const YEAR = BigInt.fromI32(60 * 60 * 24 * 365)
Expand Down
9 changes: 9 additions & 0 deletions src/vault-interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { InvestorPositionInteraction } from "../generated/schema"
import { getEventIdentifier } from "./utils/event"
import { SHARE_TOKEN_MINT_ADDRESS } from "./config"
import { isBoostAddress } from "./entity/boost"
import { DailyAvgCalc, DailyAvgState } from "./utils/daily-avg"

export function handleVaultDeposit(event: DepositEvent): void {
updateUserPosition(event, event.params.user, true, false)
Expand Down Expand Up @@ -223,6 +224,10 @@ function updateUserPosition(
position.initialUnderlyingBalance1USD = position.initialUnderlyingBalance1USD.times(mult)
position.initialPositionValueUSD = position.initialPositionValueUSD.times(mult)
}
let dailyAvgState = DailyAvgState.deserialize(position.averageDailyPositionValueUSDState)
dailyAvgState.setPendingValue(position.positionValueUSD, event.block.timestamp)
position.averageDailyPositionValueUSD30D = DailyAvgCalc.avg(BigInt.fromI32(30), dailyAvgState)
position.averageDailyPositionValueUSDState = dailyAvgState.serialize()
position.save()
for (let i = 0; i < periods.length; i++) {
log.debug("updateUserPosition: updating investor position snapshot of investor {} for vault {} and period {}", [
Expand Down Expand Up @@ -352,6 +357,10 @@ function updateUserPosition(
investor.cumulativeInteractionsCount += 1
if (!isTransfer && isDeposit) investor.cumulativeDepositCount += 1
if (!isTransfer && !isDeposit) investor.cumulativeWithdrawCount += 1
dailyAvgState = DailyAvgState.deserialize(investor.averageDailyTotalPositionValueUSDState)
dailyAvgState.setPendingValue(investor.totalPositionValueUSD, event.block.timestamp)
investor.averageDailyTotalPositionValueUSD30D = DailyAvgCalc.avg(BigInt.fromI32(30), dailyAvgState)
investor.averageDailyTotalPositionValueUSDState = dailyAvgState.serialize()
investor.save()
for (let i = 0; i < periods.length; i++) {
log.debug("updateUserPosition: updating investor snapshot for investor {} and period {}", [
Expand Down
4 changes: 0 additions & 4 deletions tests/.latest.json

This file was deleted.

Loading

0 comments on commit 7be0401

Please sign in to comment.