Skip to content

Commit

Permalink
[APR] refactor calculatepoolstats from round to date (#320)
Browse files Browse the repository at this point in the history
* Refactor getBALPriceByRound to DateRange

* Refactor tokenYield to recieve start and end

* Refactor calculatePoolStats to remove Rounds

* Update fetchDataForPoolId.ts

* Remove Selected shape for HistoricalChart

* Removed dates params from HistoricalChart

* Formatting

* Update apps/balancer-tools/src/app/apr/(utils)/calculatePoolStats.ts

Co-authored-by: José Ribeiro <[email protected]>

* Refactor getBALPriceForDateRange to recieve timestamp

* Refactor unixtimestamp as arg

* Fix getBALPriceForDateRange

---------

Co-authored-by: José Ribeiro <[email protected]>
  • Loading branch information
Rawallon and ribeirojose authored Sep 22, 2023
1 parent 0fae013 commit b2782b5
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 178 deletions.
100 changes: 64 additions & 36 deletions apps/balancer-tools/src/app/apr/(utils)/calculatePoolStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { Pool } from "#/lib/balancer/gauges";
import { withCache } from "#/lib/cache";
import { pools } from "#/lib/gql/server";

import { getWeeksBetweenDates } from "../api/(utils)/date";
import { PoolStatsData, PoolTokens, tokenAPR } from "../api/route";
import { getBALPriceByRound, getTokenPriceByDate } from "./getBALPriceByRound";
import {
getBALPriceForDateRange,
getTokenPriceByDate,
} from "./getBALPriceForDateRange";
import { getPoolRelativeWeight } from "./getRelativeWeight";
import { Round } from "./rounds";
import { getPoolTokensAprForDate } from "./tokenYield";
import { getPoolTokensAprForDate as getPoolTokensAprForDateRange } from "./tokenYield";
import { PoolTypeEnum } from "./types";

export interface calculatePoolData extends Omit<PoolStatsData, "apr"> {
Expand All @@ -32,7 +35,7 @@ const WEEKS_IN_YEAR = 52;
const SECONDS_IN_DAY = 86400;
const SECONDS_IN_YEAR = 365 * SECONDS_IN_DAY;

const fetchPoolAveragesInRange = withCache(
const fetchPoolAveragesForDateRange = withCache(
async function fetchPoolAveragesInRangeFn(
poolId: string,
network: string,
Expand All @@ -41,7 +44,7 @@ const fetchPoolAveragesInRange = withCache(
): Promise<[number, number, string, { symbol: string; balance: string }[]]> {
const res = await pools.gql(network).poolSnapshotInRange({
poolId,
from: from - SECONDS_IN_DAY,
from,
to,
});

Expand All @@ -52,7 +55,6 @@ const fetchPoolAveragesInRange = withCache(
if (res.poolSnapshots.length === 0) {
return [0, 0, "", []];
}

const avgLiquidity =
res.poolSnapshots.reduce(
(acc, snapshot) => acc + parseFloat(snapshot.liquidity),
Expand Down Expand Up @@ -81,15 +83,15 @@ const fetchPoolAveragesInRange = withCache(
);

async function calculateTokensStats(
round: Round,
endAtTimestamp: number,
poolTokenData: PoolTokens[],
poolNetwork: string,
tokenBalance: { symbol: string; balance: string }[],
) {
const tokensPrices = await Promise.all(
poolTokenData.map(async (token) => {
const tokenPrice = await getTokenPriceByDate(
round.endDate,
endAtTimestamp * 1000,
token.address,
parseInt(poolNetwork),
);
Expand Down Expand Up @@ -124,58 +126,53 @@ async function calculateTokensStats(
return Promise.all(tokenPromises);
}

// TODO: #BAL-873 - Refactor this logic
export async function calculatePoolStats({
round,
startAtTimestamp,
endAtTimestamp,
poolId,
}: {
round: Round;
startAtTimestamp: number;
endAtTimestamp: number;
poolId: string;
}): Promise<calculatePoolData> {
const pool = new Pool(poolId);
const network = String(pool.network ?? 1);

const [
balPriceUSD,
[tvl, volume, symbol, tokenBalance],
votingShare,
[feeAPR, collectedFeesUSD],
tokensAPR,
] = await Promise.all([
getBALPriceByRound(round.startDate, round.endDate),
fetchPoolAveragesInRange(
getBALPriceForDateRange(startAtTimestamp, endAtTimestamp),
fetchPoolAveragesForDateRange(
poolId,
network,
round.startDate.getTime() / 1000,
round.endDate.getTime() / 1000,
),
getPoolRelativeWeight(poolId, round.endDate.getTime() / 1000),
getFeeApr(
poolId,
network,
round.startDate.getTime() / 1000,
round.endDate.getTime() / 1000,
startAtTimestamp,
endAtTimestamp,
),
getPoolRelativeWeight(poolId, endAtTimestamp),
getFeeAprForDateRange(poolId, network, startAtTimestamp, endAtTimestamp),
//TODO: on #BAL-795 use another strategy for cache using the poolId
getPoolTokensAprForDate(
getPoolTokensAprForDateRange(
network,
poolId as Address,
//Currently, this is calculating the APR on the last day of the round.
//This should be changed on #BAL-799
round.activeRound
? Math.round(new Date().getTime() / 1000)
: round.endDate.getTime() / 1000,
startAtTimestamp,
endAtTimestamp,
),
]);

const tokens = await calculateTokensStats(
round,
endAtTimestamp,
pool.tokens,
network,
tokenBalance,
);

const apr = calculateRoundAPR(
round,
const apr = calculateAPRForDateRange(
startAtTimestamp,
endAtTimestamp,
votingShare,
tvl,
balPriceUSD,
Expand All @@ -191,7 +188,6 @@ export async function calculatePoolStats({
}

return {
roundId: Number(round.value),
poolId,
apr,
balPriceUSD,
Expand All @@ -206,15 +202,47 @@ export async function calculatePoolStats({
};
}

function calculateRoundAPR(
round: Round,
function calculateAPRForDateRange(
startAtTimestamp: number,
endAtTimestamp: number,
votingShare: number,
tvl: number,
balPriceUSD: number,
feeAPR: number,
tokensAPR: tokenAPR[],
) {
const emissions = balEmissions.weekly(round.endDate.getTime() / 1000);
const weeksApart = getWeeksBetweenDates(startAtTimestamp, endAtTimestamp);
let emissions;
if (weeksApart >= 1) {
const weekArray = Array.from({ length: weeksApart }, (_, index) => {
const weekStartDate = new Date(startAtTimestamp * 1000);
weekStartDate.setDate(weekStartDate.getDate() + index * 7);
const weekEndDate = new Date(weekStartDate);
weekEndDate.setDate(weekStartDate.getDate() + 6);
return { weekNumber: index + 1, weekStartDate, weekEndDate };
});

// Calculate the total balance emissions and count of weeks
const { totalBalanceEmissions, weekCount } = weekArray.reduce(
(acc, week) => {
const weeklyBalanceEmissions = balEmissions.weekly(
week.weekStartDate.getTime() / 1000,
);
return {
totalBalanceEmissions:
acc.totalBalanceEmissions + weeklyBalanceEmissions,
weekCount: acc.weekCount + 1,
};
},
{ totalBalanceEmissions: 0, weekCount: 0 },
);

// TODO: #BAL-876 - Fix calculation for big changes on year change
emissions = totalBalanceEmissions / weekCount;
} else {
emissions = balEmissions.weekly(endAtTimestamp);
}

const vebalAPR =
balPriceUSD && tvl && votingShare
? ((WEEKS_IN_YEAR * (emissions * votingShare * balPriceUSD)) / tvl) * 100
Expand All @@ -240,7 +268,7 @@ function calculateRoundAPR(
};
}

const getFeeApr = withCache(async function getFeeAprFn(
const getFeeAprForDateRange = withCache(async function getFeeAprFn(
poolId: string,
network: string,
from: number,
Expand Down
60 changes: 0 additions & 60 deletions apps/balancer-tools/src/app/apr/(utils)/getBALPriceByRound.ts

This file was deleted.

60 changes: 60 additions & 0 deletions apps/balancer-tools/src/app/apr/(utils)/getBALPriceForDateRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { networkFor } from "@bleu-balancer-tools/utils";

import { withCache } from "#/lib/cache";
import { DefiLlamaAPI } from "#/lib/coingecko";

import { calculateDaysBetween } from "../api/(utils)/date";

const BAL_TOKEN_ADDRESS = "0xba100000625a3754423978a60c9317c58a424e3d";
const BAL_TOKEN_NETWORK = 1;

/**
* Calculates the average of an array of numbers.
*/
const calculateAverage = (arr: number[]) =>
arr.reduce((sum, val) => sum + val, 0) / arr.length;

export const getBALPriceForDateRange = withCache(
async function getBALPriceByRoundFn(
startAtTimestamp: number,
endAtTimestamp: number,
) {
const numberOfDays = calculateDaysBetween(
startAtTimestamp * 1000,
endAtTimestamp * 1000,
);
const pricePromises = Array.from({ length: numberOfDays }, (_) => {
return getTokenPriceByDate(
endAtTimestamp * 1000,
BAL_TOKEN_ADDRESS,
BAL_TOKEN_NETWORK,
);
});
try {
const prices = await Promise.all(pricePromises);
return calculateAverage(prices);
} catch (error) {
// TODO: BAL-782 - Add sentry here
// eslint-disable-next-line no-console
console.error(
`Error fetching BAL price between ${startAtTimestamp} and ${endAtTimestamp}`,
);
throw error;
}
},
);

export const getTokenPriceByDate = withCache(async function getTokenPriceByDate(
dateTimestamp: number,
tokenAddress: string,
tokenNetwork: number,
) {
const token = `${networkFor(tokenNetwork).toLowerCase()}:${tokenAddress}`;
const relevantDateForPrice = Math.min(Date.now(), dateTimestamp);
const response = await new DefiLlamaAPI().getHistoricalPrice(
new Date(relevantDateForPrice),
[token],
);

return response.coins[token]?.price;
});
7 changes: 4 additions & 3 deletions apps/balancer-tools/src/app/apr/(utils)/tokenYield.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export const getPoolTokensAprForDate = withCache(
async function getPoolTokensAprForDateFn(
chain: string,
poolId: Address,
date: number,
startAt: number,
endAt: number,
) {
const rateProviders = await getPoolTokensRateProviders(chain, poolId);

Expand All @@ -46,8 +47,8 @@ export const getPoolTokensAprForDate = withCache(
symbol,
yield: await getAPRFromRateProviderInterval(
rateProviderAddress as Address,
date - SECONDS_IN_DAY,
date,
startAt,
endAt,
chainName,
),
}),
Expand Down
23 changes: 21 additions & 2 deletions apps/balancer-tools/src/app/apr/api/(utils)/date.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
const MILLISECONDS_IN_A_WEEK = 7 * MILLISECONDS_IN_DAY;

export function formatDateToMMDDYYYY(date: Date): string {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
Expand All @@ -6,16 +9,32 @@ export function formatDateToMMDDYYYY(date: Date): string {
}

export const generateDateRange = (startDate: Date, endDate: Date) => {
const dayMilliseconds = 24 * 60 * 60 * 1000;
const dateRange = [];

for (
let currentDate = startDate;
currentDate <= endDate;
currentDate = new Date(currentDate.getTime() + dayMilliseconds)
currentDate = new Date(currentDate.getTime() + MILLISECONDS_IN_DAY)
) {
dateRange.push(currentDate);
}

return dateRange;
};

/**
* Calculates the number of days between two dates, inclusive of both start and end dates.
*/
export const calculateDaysBetween = (startDate: number, endDate: number) =>
Math.floor((endDate - startDate) / MILLISECONDS_IN_DAY) + 1;

export function getWeeksBetweenDates(
startDateTimestamp: number,
endDateTimestamp: number,
) {
const timeDifferenceInMilliseconds = Math.abs(
startDateTimestamp - endDateTimestamp,
);
// Calculate the number of weeks apart
return Math.floor(timeDifferenceInMilliseconds / MILLISECONDS_IN_A_WEEK);
}
Loading

0 comments on commit b2782b5

Please sign in to comment.