diff --git a/src/utils/apr.ts b/src/utils/apr.ts index c34534c..9ae9902 100644 --- a/src/utils/apr.ts +++ b/src/utils/apr.ts @@ -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 @@ -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] @@ -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 diff --git a/tests/utils/apr.test.ts b/tests/utils/apr.test.ts index e0f5569..ca734bf 100644 --- a/tests/utils/apr.test.ts +++ b/tests/utils/apr.test.ts @@ -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" @@ -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()) + 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()) + + 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()) 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")) }) @@ -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")) }) @@ -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")) }) @@ -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")) }) @@ -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", () => { @@ -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()) - 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()) - - 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")) }) })