Skip to content

Commit

Permalink
Fix APRs
Browse files Browse the repository at this point in the history
  • Loading branch information
prevostc committed Mar 24, 2024
1 parent 0a84637 commit a62f35b
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 31 deletions.
18 changes: 11 additions & 7 deletions src/utils/apr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ export class AprState {
}

public addTransaction(collectedAmountUSD: BigDecimal, collectTimestamp: BigInt, totalValueLocked: BigDecimal): void {
if (collectedAmountUSD.equals(ZERO_BD)) {
return
}
const entry = new AprStateEntry(collectedAmountUSD, collectTimestamp, totalValueLocked)

// check if the entry is in the right strict order
Expand Down Expand Up @@ -81,11 +78,14 @@ export class AprCalc {
}
if (state.collects.length === 1) {
const entry = state.collects[0]
if (entry.totalValueLocked.equals(ZERO_BD)) {
return ZERO_BD
}
return entry.collectedAmount.div(entry.totalValueLocked)
}

// for each time slice, get the time weighted tvl and time weighted collected amount
let weightedYieldRate = ZERO_BD
let agg = ZERO_BD
for (let idx = 1; idx < state.collects.length; idx++) {
const prev = state.collects[idx - 1]
const curr = state.collects[idx]
Expand All @@ -97,14 +97,18 @@ export class AprCalc {
.toBigDecimal()
.div(curr.collectTimestamp.minus(prev.collectTimestamp).toBigDecimal())
const sliceCollectedUSD = curr.collectedAmount.times(slicePercentSpan)
const sliceTvl = prev.totalValueLocked
const sliceSize = curr.collectTimestamp.minus(sliceStart).toBigDecimal()

if (!curr.totalValueLocked.equals(ZERO_BD)) {
weightedYieldRate = weightedYieldRate.plus(sliceCollectedUSD.div(curr.totalValueLocked).times(sliceSize))
if (!sliceTvl.equals(ZERO_BD)) {
// compute how much each $ is contributing to the yield for this slice
const sliceAgg = sliceCollectedUSD.div(sliceTvl).times(sliceSize)
agg = agg.plus(sliceAgg)
}
}

const elapsedPeriod = bigIntMin(now.minus(state.collects[0].collectTimestamp), period)
const yieldRate = weightedYieldRate.div(elapsedPeriod.toBigDecimal())
const yieldRate = agg.div(elapsedPeriod.toBigDecimal())
const periodsInYear = YEAR.div(elapsedPeriod)
const annualized = yieldRate.times(periodsInYear.toBigDecimal())
return annualized
Expand Down
93 changes: 69 additions & 24 deletions tests/utils/apr.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, test, describe } from "matchstick-as/assembly/index"
import { BigDecimal, BigInt } from "@graphprotocol/graph-ts"
import { BigDecimal, BigInt, log } from "@graphprotocol/graph-ts"
import { AprCalc, AprState } from "../../src/utils/apr"
import { ZERO_BD } from "../../src/utils/decimal"
import { DAY, WEEK } from "../../src/utils/time"
Expand Down Expand Up @@ -69,12 +69,38 @@ describe("AprCalc", () => {
assert.assertTrue(res.equals(ZERO_BD))
})

test("do not crash when TVL is zero now", () => {
let aprState = AprState.deserialize(new Array<BigDecimal>())
const now = DAY

aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(0), BigDecimal.fromString("1000"))
aprState.addTransaction(BigDecimal.fromString("200"), BigInt.fromI32(10000), BigDecimal.fromString("2000"))
aprState.addTransaction(BigDecimal.fromString("300"), DAY, BigDecimal.fromString("0"))
const res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("56.862268518"), BigDecimal.fromString("0.0001"))
})

test("should evict old entries", () => {
let aprState = AprState.deserialize(new Array<BigDecimal>())

aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(100), BigDecimal.fromString("1000"))
aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(200), BigDecimal.fromString("2000"))
aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(69382300), BigDecimal.fromString("3000"))
aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(69382400), BigDecimal.fromString("4000"))
aprState = AprCalc.evictOldEntries(DAY, aprState, BigInt.fromI32(69382400))

assert.assertTrue(aprState.collects.length === 3)
})

test("should compute apr properly with one entry", () => {
let aprState = AprState.deserialize(new Array<BigDecimal>())
const now = BigInt.fromI32(100)

aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(100), BigDecimal.fromString("1000"))
const res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("0.1"), BigDecimal.fromString("0.0001"))
})
Expand All @@ -87,6 +113,7 @@ describe("AprCalc", () => {
aprState.addTransaction(BigDecimal.fromString("10"), BigInt.fromI32(0), BigDecimal.fromString("1000"))
aprState.addTransaction(BigDecimal.fromString("10"), DAY, BigDecimal.fromString("1000"))
const res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("3.65"), BigDecimal.fromString("0.0001"))
})
Expand All @@ -99,6 +126,7 @@ describe("AprCalc", () => {
aprState.addTransaction(BigDecimal.fromString("10"), BigInt.fromI32(0), BigDecimal.fromString("1000"))
aprState.addTransaction(BigDecimal.fromString("10"), DAY, BigDecimal.fromString("1000"))
const res = AprCalc.calculateLastApr(WEEK, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("3.65"), BigDecimal.fromString("0.0001"))
})
Expand All @@ -111,6 +139,7 @@ describe("AprCalc", () => {
aprState.addTransaction(BigDecimal.fromString("20"), BigInt.fromI32(10000), BigDecimal.fromString("1000"))
aprState.addTransaction(BigDecimal.fromString("30"), DAY, BigDecimal.fromString("1000"))
const res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("10.527546"), BigDecimal.fromString("0.0001"))
})
Expand All @@ -123,8 +152,9 @@ describe("AprCalc", () => {
aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(10000), BigDecimal.fromString("2000"))
aprState.addTransaction(BigDecimal.fromString("100"), DAY, BigDecimal.fromString("3000"))
const res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("12.870756172"), BigDecimal.fromString("0.0001"))
assertIsCloseTo(res, BigDecimal.fromString("20.362268518"), BigDecimal.fromString("0.0001"))
})

test("should compute apr when yield and total value locked changes", () => {
Expand All @@ -135,32 +165,47 @@ describe("AprCalc", () => {
aprState.addTransaction(BigDecimal.fromString("200"), BigInt.fromI32(10000), BigDecimal.fromString("2000"))
aprState.addTransaction(BigDecimal.fromString("300"), DAY, BigDecimal.fromString("3000"))
const res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("36.5"), BigDecimal.fromString("0.0001"))
assertIsCloseTo(res, BigDecimal.fromString("56.86226"), BigDecimal.fromString("0.0001"))
})

test("do not crash when TVL is zero now", () => {
test("should compute apr when the day is not over yet", () => {
let aprState = AprState.deserialize(new Array<BigDecimal>())
const now = DAY

aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(0), BigDecimal.fromString("1000"))
aprState.addTransaction(BigDecimal.fromString("200"), BigInt.fromI32(10000), BigDecimal.fromString("2000"))
aprState.addTransaction(BigDecimal.fromString("300"), DAY, BigDecimal.fromString("0"))
const res = AprCalc.calculateLastApr(DAY, aprState, now)

assertIsCloseTo(res, BigDecimal.fromString("4.2245370"), BigDecimal.fromString("0.0001"))
})

test("should evict old entries", () => {
let aprState = AprState.deserialize(new Array<BigDecimal>())

aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(100), BigDecimal.fromString("1000"))
aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(200), BigDecimal.fromString("2000"))
aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(69382300), BigDecimal.fromString("3000"))
aprState.addTransaction(BigDecimal.fromString("100"), BigInt.fromI32(69382400), BigDecimal.fromString("4000"))
aprState = AprCalc.evictOldEntries(DAY, aprState, BigInt.fromI32(69382400))

assert.assertTrue(aprState.collects.length === 3)
// using 6 decimals
const one = BigDecimal.fromString("1000000")

// whatever$ at 00:00, tvl of $100
// => 0% apr for the first hour
let now = BigInt.fromI32(0)
log.debug("\n\n======= now: {}\n", [now.toString()])
aprState.addTransaction(BigDecimal.fromString("0"), now, one.times(BigDecimal.fromString("100")))
assert.assertTrue(AprCalc.evictOldEntries(DAY, aprState, now).collects.length === 1)
let res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])
assertIsCloseTo(res, ZERO_BD, BigDecimal.fromString("0.0001"))

// 2: 1$ at 01:00, tvl of $100 => +1% for the first hour
// => APR_24H is 1% * 24 * 365 => 8760%
now = BigInt.fromI32(60 * 60)
log.debug("\n\n======= now: {}\n", [now.toString()])
aprState.addTransaction(one, now, one.times(BigDecimal.fromString("100")))
assert.assertTrue(AprCalc.evictOldEntries(DAY, aprState, now).collects.length === 2)
res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])
assertIsCloseTo(res, BigDecimal.fromString("87.60"), BigDecimal.fromString("0.0001"))

// 3: deposit of $100 at 12:00, claiming 10$ => +10% for 11h
// => Avg % per hour is (10% * 11 + 1% * 1) / 12 => +9.25% on average over for 12h
// => APR_24h is 9.25% * 2 * 365 : 67.525%
now = BigInt.fromI32(12 * 60 * 60)
log.debug("\n\n======= now: {}\n", [now.toString()])
aprState.addTransaction(one.times(BigDecimal.fromString("10")), now, one.times(BigDecimal.fromString("200")))
assert.assertTrue(AprCalc.evictOldEntries(DAY, aprState, now).collects.length === 3)
res = AprCalc.calculateLastApr(DAY, aprState, now)
log.debug("res: {}", [res.toString()])

assertIsCloseTo(res, BigDecimal.fromString("67.525"), BigDecimal.fromString("0.0001"))
})
})

Expand Down

0 comments on commit a62f35b

Please sign in to comment.