wip: uni update apy calc (#441)

* uni update apy calc

* uni v3 add new apy as new field

* smol fix

* curve fix 3dpool

* yeti finance fix

* bug fix

* fix

* merge master
This commit is contained in:
slasher125
2022-11-28 09:14:29 +01:00
committed by GitHub
parent b8b64785b2
commit 96e7ed651d
4 changed files with 426 additions and 15 deletions

4
.gitignore vendored
View File

@@ -10,4 +10,6 @@ ccImages
*.csv
*.sql
scripts/*.json
src/adaptors/list.js
src/adaptors/list.js
Untitled.ipynb
.ipynb_checkpoints/

View File

@@ -0,0 +1,322 @@
// forked from uniswap.fish chads (see https://github.com/chunza2542/uniswap.fish)
const bn = require('bignumber.js');
const axios = require('axios');
interface Tick {
tickIdx: string;
liquidityNet: string;
price0: string;
price1: string;
}
bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 });
const Q96 = new bn(2).pow(96);
const getTickFromPrice = (
price: number,
token0Decimal: string,
token1Decimal: string
): number => {
const token0 = expandDecimals(price, Number(token0Decimal));
const token1 = expandDecimals(1, Number(token1Decimal));
const sqrtPrice = encodeSqrtPriceX96(token1).div(encodeSqrtPriceX96(token0));
return Math.log(sqrtPrice.toNumber()) / Math.log(Math.sqrt(1.0001));
};
const getPriceFromTick = (
tick: number,
token0Decimal: string,
token1Decimal: string
): number => {
const sqrtPrice = new bn(Math.pow(Math.sqrt(1.0001), tick)).multipliedBy(
new bn(2).pow(96)
);
const token0 = expandDecimals(1, Number(token0Decimal));
const token1 = expandDecimals(1, Number(token1Decimal));
const L2 = mulDiv(
encodeSqrtPriceX96(token0),
encodeSqrtPriceX96(token1),
Q96
);
const price = mulDiv(L2, Q96, sqrtPrice)
.div(new bn(2).pow(96))
.div(new bn(10).pow(token0Decimal))
.pow(2);
return price.toNumber();
};
// for calculation detail, please visit README.md (Section: Calculation Breakdown, No. 1)
interface TokensAmount {
amount0: number;
amount1: number;
}
const getTokensAmountFromDepositAmountUSD = (
P: number,
Pl: number,
Pu: number,
priceUSDX: number,
priceUSDY: number,
depositAmountUSD: number
): TokensAmount => {
const deltaL =
depositAmountUSD /
((Math.sqrt(P) - Math.sqrt(Pl)) * priceUSDY +
(1 / Math.sqrt(P) - 1 / Math.sqrt(Pu)) * priceUSDX);
let deltaY = deltaL * (Math.sqrt(P) - Math.sqrt(Pl));
if (deltaY * priceUSDY < 0) deltaY = 0;
if (deltaY * priceUSDY > depositAmountUSD)
deltaY = depositAmountUSD / priceUSDY;
let deltaX = deltaL * (1 / Math.sqrt(P) - 1 / Math.sqrt(Pu));
if (deltaX * priceUSDX < 0) deltaX = 0;
if (deltaX * priceUSDX > depositAmountUSD)
deltaX = depositAmountUSD / priceUSDX;
return { amount0: deltaX, amount1: deltaY };
};
// for calculation detail, please visit README.md (Section: Calculation Breakdown, No. 2)
const getLiquidityForAmount0 = (
sqrtRatioAX96: bn,
sqrtRatioBX96: bn,
amount0: bn
): bn => {
// amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower))
const intermediate = mulDiv(sqrtRatioBX96, sqrtRatioAX96, Q96);
return mulDiv(amount0, intermediate, sqrtRatioBX96.minus(sqrtRatioAX96));
};
const getLiquidityForAmount1 = (
sqrtRatioAX96: bn,
sqrtRatioBX96: bn,
amount1: bn
): bn => {
// amount1 / (sqrt(upper) - sqrt(lower))
return mulDiv(amount1, Q96, sqrtRatioBX96.minus(sqrtRatioAX96));
};
const getSqrtPriceX96 = (
price: number,
token0Decimal: number,
token1Decimal: number
): bn => {
const token0 = expandDecimals(price, token0Decimal);
const token1 = expandDecimals(1, token1Decimal);
return token0.div(token1).sqrt().multipliedBy(Q96);
};
const getLiquidityDelta = (
P: number,
lowerP: number,
upperP: number,
amount0: number,
amount1: number,
token0Decimal: number,
token1Decimal: number
): bn => {
const amt0 = expandDecimals(amount0, token1Decimal);
const amt1 = expandDecimals(amount1, token0Decimal);
const sqrtRatioX96 = getSqrtPriceX96(P, token0Decimal, token1Decimal);
const sqrtRatioAX96 = getSqrtPriceX96(lowerP, token0Decimal, token1Decimal);
const sqrtRatioBX96 = getSqrtPriceX96(upperP, token0Decimal, token1Decimal);
let liquidity: bn;
if (sqrtRatioX96.lte(sqrtRatioAX96)) {
liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amt0);
} else if (sqrtRatioX96.lt(sqrtRatioBX96)) {
const liquidity0 = getLiquidityForAmount0(
sqrtRatioX96,
sqrtRatioBX96,
amt0
);
const liquidity1 = getLiquidityForAmount1(
sqrtRatioAX96,
sqrtRatioX96,
amt1
);
liquidity = bn.min(liquidity0, liquidity1);
} else {
liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amt1);
}
return liquidity;
};
const estimateFee = (
liquidityDelta: bn,
liquidity: bn,
volume24H: number,
feeTier: string
): number => {
const feeTierPercentage = getFeeTierPercentage(feeTier);
const liquidityPercentage = liquidityDelta
.div(liquidity.plus(liquidityDelta))
.toNumber();
return feeTierPercentage * volume24H * liquidityPercentage;
};
const getLiquidityFromTick = (poolTicks: Tick[], tick: number): bn => {
// calculate a cumulative of liquidityNet from all ticks that poolTicks[i] <= tick
let liquidity: bn = new bn(0);
for (let i = 0; i < poolTicks.length - 1; ++i) {
liquidity = liquidity.plus(new bn(poolTicks[i].liquidityNet));
const lowerTick = Number(poolTicks[i].tickIdx);
const upperTick = Number(poolTicks[i + 1]?.tickIdx);
if (lowerTick <= tick && tick <= upperTick) {
break;
}
}
return liquidity;
};
// private helper functions
const encodeSqrtPriceX96 = (price: number | string | bn): bn => {
return new bn(price).sqrt().multipliedBy(Q96).integerValue(3);
};
const expandDecimals = (n: number | string | bn, exp: number): bn => {
return new bn(n).multipliedBy(new bn(10).pow(exp));
};
const mulDiv = (a: bn, b: bn, multiplier: bn) => {
return a.multipliedBy(b).div(multiplier);
};
const getFeeTierPercentage = (tier: string): number => {
if (tier === '100') return 0.01 / 100;
if (tier === '500') return 0.05 / 100;
if (tier === '3000') return 0.3 / 100;
if (tier === '10000') return 1 / 100;
return 0;
};
const getPoolTicks = async (
poolAddress: string,
url: string
): Promise<Tick[]> => {
const PAGE_SIZE = 3;
let result: Tick[] = [];
let page = 0;
while (true) {
const pool1 = await _getPoolTicksByPage(poolAddress, page, url);
const pool2 = await _getPoolTicksByPage(poolAddress, page + 1, url);
const pool3 = await _getPoolTicksByPage(poolAddress, page + 2, url);
result = [...result, ...pool1, ...pool2, ...pool3];
if (pool1.length === 0 || pool2.length === 0 || pool3.length === 0) {
break;
}
page += PAGE_SIZE;
}
return result;
};
const _getPoolTicksByPage = async (
poolAddress: string,
page: number,
url: string
): Promise<Tick[]> => {
let res;
try {
res = await _queryUniswap(
`{
ticks(first: 1000, skip: ${
page * 1000
}, where: { poolAddress: "${poolAddress}" }, orderBy: tickIdx) {
tickIdx
liquidityNet
price0
price1
}
}`,
url
);
} catch (e) {
console.log('_getPoolTicksByPage failed for', poolAddress);
return [];
}
return res === undefined ? [] : res.ticks;
};
const _queryUniswap = async (query: string, url: string): Promise<any> => {
const { data } = await axios({
url,
method: 'post',
data: {
query,
},
});
return data.data;
};
module.exports.EstimatedFees = async (
poolAddress,
priceAssumptionValue,
priceRangeValue,
currentPriceUSDToken1,
currentPriceUSDToken0,
depositAmountUSD,
decimalsToken0,
decimalsToken1,
feeTier,
url,
volume
) => {
const P = priceAssumptionValue;
let Pl = priceRangeValue[0];
let Pu = priceRangeValue[1];
const priceUSDX = currentPriceUSDToken1 || 1;
const priceUSDY = currentPriceUSDToken0 || 1;
const { amount0, amount1 } = getTokensAmountFromDepositAmountUSD(
P,
Pl,
Pu,
priceUSDX,
priceUSDY,
depositAmountUSD
);
const deltaL = getLiquidityDelta(
P,
Pl,
Pu,
amount0,
amount1,
Number(decimalsToken0 || 18),
Number(decimalsToken1 || 18)
);
let currentTick = getTickFromPrice(
P,
decimalsToken0 || '18',
decimalsToken1 || '18'
);
const poolTicks = await getPoolTicks(poolAddress, url);
if (!poolTicks.length) {
console.log(`No pool ticks found for ${poolAddress}`);
return { poolAddress, estimatedFee: 0 };
}
const L = getLiquidityFromTick(poolTicks, currentTick);
const estimatedFee =
P >= Pl && P <= Pu ? estimateFee(deltaL, L, volume, feeTier) : 0;
return { poolAddress, estimatedFee };
};

View File

@@ -3,7 +3,9 @@ const { request, gql } = require('graphql-request');
const superagent = require('superagent');
const utils = require('../utils');
const { EstimatedFees } = require('./estimateFee.ts');
const { checkStablecoin } = require('../../handlers/triggerEnrichment');
const { boundaries } = require('../../utils/exclude');
const baseUrl = 'https://api.thegraph.com/subgraphs/name';
const chains = {
@@ -57,6 +59,13 @@ const topLvl = async (
url,
]);
const [_, blockPrior7d] = await utils.getBlocks(
chainString,
timestamp,
[url],
604800
);
// pull data
let queryC = query;
let dataNow = await request(url, queryC.replace('<PLACEHOLDER>', block));
@@ -106,16 +115,90 @@ const topLvl = async (
// calculate tvl
dataNow = await utils.tvl(dataNow, chainString);
dataNow = dataNow.filter((p) => p.totalValueLockedUSD >= 1000);
// calculate apy
let data = dataNow.map((el) => utils.apy(el, dataPrior, [], version));
// check if stable pool (we use this info in triggerAdapter) for calculating tick ranges
return data.map((p) => {
// to reduce the nb of subgraph calls for tick range, we apply the lb db filter in here
dataNow = dataNow.filter(
(p) => p.totalValueLockedUSD >= boundaries.tvlUsdDB.lb
);
// add the symbol for the stablecoin (we need to distinguish btw stable and non stable pools
// so we apply the correct tick range)
dataNow = dataNow.map((p) => {
const symbol = utils.formatSymbol(`${p.token0.symbol}-${p.token1.symbol}`);
const stablecoin = checkStablecoin({ ...p, symbol }, stablecoins);
return {
...p,
symbol,
stablecoin,
};
});
const stablePool = checkStablecoin({ ...p, symbol }, stablecoins);
// for new v3 apy calc
const dataPrior7d = (
await request(url, queryPriorC.replace('<PLACEHOLDER>', blockPrior7d))
).pools;
// calc apy (note: old way of using 24h fees * 365 / tvl. keeping this for now) and will store the
// new apy calc as a separate field
// note re arbitrum: their subgraph is outdated (no tick data -> no uni v3 style apy calc)
dataNow = dataNow.map((el) => utils.apy(el, dataPrior, dataPrior7d, version));
if (chainString !== 'arbitrum') {
dataNow = dataNow.map((p) => ({
...p,
token1_in_token0: p.price1 / p.price0,
}));
// split up subgraph tick calls into n-batches
// (tick response can be in the thousands per pool)
const skip = 20;
let start = 0;
let stop = skip;
const pages = Math.floor(dataNow.length / skip);
// tick range
const pct = 0.3;
const pctStablePool = 0.001;
// assume an investment of 1e5 USD
const investmentAmount = 1e5;
let X = [];
for (let i = 0; i <= pages; i++) {
console.log(i);
let promises = dataNow.slice(start, stop).map((p) => {
const delta = p.stablecoin ? pctStablePool : pct;
const priceAssumption = p.stablecoin ? 1 : p.token1_in_token0;
return EstimatedFees(
p.id,
priceAssumption,
[p.token1_in_token0 * (1 - delta), p.token1_in_token0 * (1 + delta)],
p.price1,
p.price0,
investmentAmount,
p.token0.decimals,
p.token1.decimals,
p.feeTier,
url,
p.volumeUSD7d
);
});
X.push(await Promise.all(promises));
start += skip;
stop += skip;
}
const d = {};
X.flat().forEach((p) => {
d[p.poolAddress] = p.estimatedFee;
});
dataNow = dataNow.map((p) => ({
...p,
apy7d: ((d[p.id] * 52) / investmentAmount) * 100,
}));
}
return dataNow.map((p) => {
const poolMeta = `${p.feeTier / 1e4}%`;
const underlyingTokens = [p.token0.id, p.token1.id];
const token0 = underlyingTokens === undefined ? '' : underlyingTokens[0];
@@ -129,10 +212,11 @@ const topLvl = async (
pool: p.id,
chain: utils.formatChain(chainString),
project: 'uniswap-v3',
poolMeta: `${poolMeta}, stablePool=${stablePool}`,
symbol,
poolMeta: `${poolMeta}, stablePool=${p.stablecoin}`,
symbol: p.symbol,
tvlUsd: p.totalValueLockedUSD,
apyBase: p.apy1d,
apyBase7d: p.apy7d,
underlyingTokens,
url,
};
@@ -148,12 +232,13 @@ const main = async (timestamp = null) => {
if (!stablecoins.includes('eur')) stablecoins.push('eur');
if (!stablecoins.includes('3crv')) stablecoins.push('3crv');
const data = await Promise.all(
Object.entries(chains).map(([chain, url]) =>
topLvl(chain, url, query, queryPrior, 'v3', timestamp, stablecoins)
)
);
const data = [];
for (const [chain, url] of Object.entries(chains)) {
console.log(chain);
data.push(
await topLvl(chain, url, query, queryPrior, 'v3', timestamp, stablecoins)
);
}
return data.flat().filter((p) => utils.keepFinite(p));
};

View File

@@ -178,6 +178,8 @@ exports.tvl = async (dataNow, networkString) => {
}
el['totalValueLockedUSD'] = tvl;
el['price0'] = price0;
el['price1'] = price1;
}
return dataNowCopy;