UNPKG

@tracer-protocol/pools-js

Version:

Javascript library for interacting with Tracer's Perpetual Pools

434 lines (360 loc) 18.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getExpectedExecutionTimestamp = exports.calcTokenPrice = exports.calcSkew = exports.calcRebalanceRate = exports.calcRatio = exports.calcPoolStatePreview = exports.calcPercentageLossTransfer = exports.calcNotionalValue = exports.calcNextValueTransfer = exports.calcEffectiveShortGain = exports.calcEffectiveLongGain = exports.calcDirection = exports.calcBptTokenSpotPrice = exports.calcBptTokenPrice = exports.calcAPY = void 0; var _bignumber = require("bignumber.js"); const UP = 1; const DOWN = 2; const NO_CHANGE = 3; /** * Calculates the effective multiplier returns for the longs. This amount varies depending on the skew between the long and short balances. * Gain and loss refer to whether the pool is receiving tokens from the other pool or transferring tokens to the opposite pool. * If there is more balance in the long pool than the short pools, you would expect the short pool to have * `effectiveLongGain > leverage`. * Both sides of the pool will always have an effectiveLoss lower limit at leverage, ie you can never have `effectiveLoss < leverage`. * @param longBalance quote balance of the long pool in quote units (eg USD) * @param shortBalance quote balance of the short pool in quote units (eg USD) * @param leverage pool leverage * @returns the effective winning returns to the long pool on next rebalance */ const calcEffectiveLongGain = (shortBalance, longBalance, leverage) => new _bignumber.BigNumber(1).div(calcSkew(shortBalance, longBalance)).times(leverage); /** * Calculates the effective gains multiplier for the shorts. This amount varies depending on the skew between the long and short balances. * Gain and loss refer to whether the pool is receiving tokens from the other pool or transferring tokens to the opposite pool. * If there is more balance in the long pool than the short pools, you would expect the short pool to have * `effectiveLongGain > leverage`. * The pools effective losses will always have a lower limit at leverage, ie you can never have `effectiveLoss < leverage`. * @param longBalance quote balance of the long pool in USD * @param shortBalance quote balance of the short pool in USD * @param leverage pool leverage * @returns the effective gains to the short pool on next rebalance */ exports.calcEffectiveLongGain = calcEffectiveLongGain; const calcEffectiveShortGain = (shortBalance, longBalance, leverage) => calcSkew(shortBalance, longBalance).times(leverage); // weekly -> 52 exports.calcEffectiveShortGain = calcEffectiveShortGain; const COMPOUND_FREQUENCY = 52; /** * Calculates the compounding gains * @param apr annual percentage rate as a decimal * eg 1 is a 100% apr * @returns annual percentage yield coumpounded weekly */ const calcAPY = apr => { _bignumber.BigNumber.config({ POW_PRECISION: 10 }); const apy = apr.div(COMPOUND_FREQUENCY).plus(1).pow(COMPOUND_FREQUENCY).minus(1); _bignumber.BigNumber.config({ POW_PRECISION: 0 }); return apy; }; /** * Calculates the percentage the losing pool must transfer to the winning pool on next upKeep. * JS implementation of https://github.com/tracer-protocol/perpetual-pools-contracts/blob/pools-v2/contracts/libraries/PoolSwapLibrary.sol#L348-L362 * @param oldPrice old market price * @param newPrice new market price * @param leverage pool leverage * @returns the percentage the losing pool must transfer * 2 / (1 + e^(-2 * leverage * (1 - (oldPrice / newPrice)))) - 1 */ exports.calcAPY = calcAPY; const calcPercentageLossTransfer = (oldPrice, newPrice, leverage) => { const ONE = new _bignumber.BigNumber(1); const TWO = new _bignumber.BigNumber(2); const priceRatio = newPrice >= oldPrice ? oldPrice.div(newPrice) : newPrice.div(oldPrice); return TWO.div(ONE.plus(Math.exp(TWO.negated().times(leverage).times(ONE.minus(priceRatio)).toNumber()))).minus(1); }; /** * Calculates the notional value of tokens * @param tokenPrice current price of tokens * @param numTokens number of tokens * @returns notional value of the tokens */ exports.calcPercentageLossTransfer = calcPercentageLossTransfer; const calcNotionalValue = (tokenPrice, numTokens) => { return tokenPrice.times(numTokens); }; /** * Calculates the ratio of the old price to the new price * @param oldPrice old market price * @param newPrice new market price */ exports.calcNotionalValue = calcNotionalValue; const calcRatio = (oldPrice, newPrice) => { if (oldPrice.eq(0)) return new _bignumber.BigNumber(0); return newPrice.div(oldPrice); }; exports.calcRatio = calcRatio; const calcSkew = (shortBalance, longBalance) => { if (shortBalance.eq(0) || longBalance.eq(0)) { // This isnt a fully accurate return since // at shortBalance 0 there there will be incentive to short // and vice versa for longs return new _bignumber.BigNumber(1); } return longBalance.div(shortBalance); }; exports.calcSkew = calcSkew; const calcRebalanceRate = (shortBalance, longBalance) => { return calcSkew(shortBalance, longBalance).minus(1); }; /** * Calcualtes the direction of the price movement * @param newPrice new market price * @param oldPrice old market price * @return DOWN (2) if oldPrice > newPrice, NO_CHANGE (3) if newPrice = oldPrice, or UP (1) if newPrice > oldPrice */ exports.calcRebalanceRate = calcRebalanceRate; const calcDirection = (oldPrice, newPrice) => { // newPrice.div(oldPrice); const priceRatio = calcRatio(oldPrice, newPrice); if (priceRatio.gt(1)) { // number go up return new _bignumber.BigNumber(UP); } else if (priceRatio.eq(1)) { return new _bignumber.BigNumber(NO_CHANGE); } else { // priceRatio.lt(1) return new _bignumber.BigNumber(DOWN); } }; /** * Calculate the pool tokens price * Since totalQuoteValue will generally be in USD the returned amount * will also be in USD */ exports.calcDirection = calcDirection; const calcTokenPrice = (totalQuoteValue, tokenSupply) => { // if supply is 0 priceRatio is 1/1 if (tokenSupply.eq(0) || totalQuoteValue.eq(0)) { return new _bignumber.BigNumber(1); } return totalQuoteValue.div(tokenSupply); }; /** * Calculates how much value will be transferred between the pools * * @param oldPrice old market price * @param newPrice new market price * @param leverage pool leverage * @param longBalance quote balance of the long pool in USD * @param shortBalance quote balance of the short pool in USD * * returns an object containing longValueTransfer and shortValueTransfer */ exports.calcTokenPrice = calcTokenPrice; const calcNextValueTransfer = (oldPrice, newPrice, leverage, longBalance, shortBalance) => { const direction = calcDirection(oldPrice, newPrice); const percentageLossTransfer = calcPercentageLossTransfer(oldPrice, newPrice, leverage); let gain; if (direction.eq(UP)) { // long wins gain = percentageLossTransfer.times(shortBalance); // long gains and short loses longs gain return { longValueTransfer: gain, shortValueTransfer: gain.negated() }; } else if (direction.eq(DOWN)) { // short wins gain = percentageLossTransfer.times(longBalance).abs(); return { longValueTransfer: gain.negated(), shortValueTransfer: gain }; } // else no value transfer return { longValueTransfer: new _bignumber.BigNumber(0), shortValueTransfer: new _bignumber.BigNumber(0) }; }; /** * Calculates the Balancer LP token price given a list of tokens included in the pool * @param tokens list of tokens included in the balancer pool * @returns 0 if no tokens are given, if the tokens have no USDC value or if the stakingToken supply is 0 * otherwise returns the price of the LP token. */ exports.calcNextValueTransfer = calcNextValueTransfer; const calcBptTokenPrice = (stakingTokenSupply, tokens) => { if (!tokens) { return new _bignumber.BigNumber(0); } let balancerPoolUSDValue = new _bignumber.BigNumber(0); for (const token of tokens) { const tokenUSDValue = calcNotionalValue(token.usdPrice, token.reserves); balancerPoolUSDValue = balancerPoolUSDValue.plus(tokenUSDValue); } if (balancerPoolUSDValue.eq(0) || stakingTokenSupply.eq(0)) { return new _bignumber.BigNumber(0); } return balancerPoolUSDValue.div(stakingTokenSupply); }; /** * Calculates the trade price between two Balancer tokens. * This price is dependent on the reserves deposited on each side * within the Balancer pool, as well as the weighting of each. * @param sellingToken weight and balance of token that is being sold * @param buyingToken weight and balance of token that is being bought * @param swapFee percentage swap fee in decimals * @returns */ exports.calcBptTokenPrice = calcBptTokenPrice; const calcBptTokenSpotPrice = (sellingToken, buyingToken, swapFee) => { if (sellingToken.weight.eq(0) || buyingToken.weight.eq(0)) return new _bignumber.BigNumber(0); if (sellingToken.balance.eq(0) || buyingToken.balance.eq(0)) { console.error("Selling token balance zero"); return new _bignumber.BigNumber(0); } const numerator = sellingToken.balance.div(sellingToken.weight); const denominator = buyingToken.balance.div(buyingToken.weight); const swapFeeMultiplier = new _bignumber.BigNumber(1).div(new _bignumber.BigNumber(1).minus(swapFee)); return numerator.div(denominator).times(swapFeeMultiplier); }; /** * Calculate the expected execution given a commit timestamp, the frontRunningInterval, updateInterval and lastUpdate. * This is just an estimate as there is a slight delay between possibleExecution and finalExecutionTimestamp * See https://github.com/tracer-protocol/pools-js/blob/updated-calc/src/utils/calculations.ts#L280-L332 * for a long clearer sudo codish written version */ exports.calcBptTokenSpotPrice = calcBptTokenSpotPrice; const getExpectedExecutionTimestamp = (frontRunningInterval, updateInterval, lastUpdate, commitCreated) => { const nextRebalance = lastUpdate + updateInterval; const timeSinceCommit = lastUpdate - commitCreated; let updateIntervalsPassed = timeSinceCommit > 0 ? Math.ceil(timeSinceCommit / updateInterval) : 0; if (updateIntervalsPassed === 1) { updateIntervalsPassed = 0; } // for frontRunningInterval <= updateInterval this will be 1 // anything else will give us how many intervals we need to wait // such that waitingTime >= frontRunningInterval let updateIntervalsInFrontRunningInterval = Math.ceil(frontRunningInterval / updateInterval); // if numberOfUpdateInteravalsToWait is 1 then frontRunningInterval <= updateInterval // for frontRunningInterval < updateInterval // set numberOfWaitIntervals to 0 since there is potential it CAN be included next updateInterval // for frontRunningInterval === updateInterval // the commit will be appropriately caught by the condition // (potentialExecutionTime - commitCreated) < frontRunningInterval // = nextRebalance - commitCreated < frontRunningInterval // = lastUpdate + updateInterval - commitCreated < frontRunningInterval // = lastUpdate + updateInterval < frontRunningInterval + commitCreated // = lastUpdate + updateInterval < updateInterval + commitCreated // = lastUpdate < commitCreated // and will always be included in the following updateInterval unless lastUpdate < commitCreated if (frontRunningInterval <= updateInterval) { updateIntervalsInFrontRunningInterval = 0; } const potentialExecutionTime = nextRebalance + (updateIntervalsInFrontRunningInterval - updateIntervalsPassed) * updateInterval; // only possible if frontRunningInterval < updateInterval if (potentialExecutionTime - commitCreated < frontRunningInterval) { // commit was created during frontRunningInterval return potentialExecutionTime + updateInterval; // commit will be executed in the following updateInterval } else { return potentialExecutionTime; } }; /** * calculates the expected state of the pool after applying the given pending commits to the given pool state * @param object containing current pool state * @returns the expected state of the pool */ exports.getExpectedExecutionTimestamp = getExpectedExecutionTimestamp; const calcPoolStatePreview = previewInputs => { const { leverage, fee, longBalance, shortBalance, longTokenSupply, shortTokenSupply, pendingLongTokenBurn, pendingShortTokenBurn, lastOraclePrice, currentOraclePrice, pendingCommits, oraclePriceTransformer } = previewInputs; let expectedLongBalance = longBalance; let expectedShortBalance = shortBalance; // tokens are burned on commit, so they are reflected in token supply immediately // add the pending burns to the starting supplies // as pending commits are executed, the running supply will be reduced based on burns let expectedLongSupply = longTokenSupply.plus(pendingLongTokenBurn); let expectedShortSupply = shortTokenSupply.plus(pendingShortTokenBurn); let totalNetPendingLong = new _bignumber.BigNumber(0); let totalNetPendingShort = new _bignumber.BigNumber(0); let expectedLongTokenPrice = expectedLongBalance.div(expectedLongSupply); let expectedShortTokenPrice = expectedShortBalance.div(expectedShortSupply); let movingOraclePriceBefore = lastOraclePrice; let movingOraclePriceAfter = lastOraclePrice; let expectedPendingLongTokenBurn = pendingLongTokenBurn; let expectedPendingShortTokenBurn = pendingShortTokenBurn; // each pendingCommit is the summation of all commits for each upkeep in pendingCommits for (const pendingCommit of pendingCommits) { // subtract fees each upkeep expectedLongBalance = expectedLongBalance.minus(fee.times(expectedLongBalance)); expectedShortBalance = expectedShortBalance.minus(fee.times(expectedShortBalance)); const { longBurnPoolTokens, longBurnShortMintPoolTokens, longMintSettlement, shortBurnPoolTokens, shortBurnLongMintPoolTokens, shortMintSettlement } = pendingCommit; // apply price transformations to emulate underlying oracle wrapper implementation movingOraclePriceBefore = movingOraclePriceAfter; movingOraclePriceAfter = oraclePriceTransformer(movingOraclePriceBefore, currentOraclePrice); // calc value transfer each upkeep const { longValueTransfer, shortValueTransfer } = calcNextValueTransfer(movingOraclePriceBefore, movingOraclePriceAfter, new _bignumber.BigNumber(leverage), expectedLongBalance, expectedShortBalance); // balances immediately before commits executed expectedLongBalance = expectedLongBalance.plus(longValueTransfer); expectedShortBalance = expectedShortBalance.plus(shortValueTransfer); const totalLongBurn = longBurnPoolTokens.plus(longBurnShortMintPoolTokens); const totalShortBurn = shortBurnPoolTokens.plus(shortBurnLongMintPoolTokens); expectedPendingLongTokenBurn = expectedPendingLongTokenBurn.minus(totalLongBurn); expectedPendingShortTokenBurn = expectedPendingShortTokenBurn.minus(totalShortBurn); // current balance + expected value transfer / expected supply // if either side has no token supply, any amount no matter how small will buy the whole side const longTokenPriceDenominator = expectedLongSupply.plus(totalLongBurn); expectedLongTokenPrice = longTokenPriceDenominator.lte(0) ? expectedLongBalance : expectedLongBalance.div(longTokenPriceDenominator); const shortTokenPriceDenominator = expectedShortSupply.plus(totalShortBurn); expectedShortTokenPrice = shortTokenPriceDenominator.lte(0) ? expectedShortBalance : expectedShortBalance.div(shortTokenPriceDenominator); const totalLongMint = longMintSettlement.plus(shortBurnLongMintPoolTokens.times(expectedShortTokenPrice)); const totalShortMint = shortMintSettlement.plus(longBurnShortMintPoolTokens.times(expectedLongTokenPrice)); const netPendingLongBalance = totalLongMint.minus(totalLongBurn.times(expectedLongTokenPrice)); const netPendingShortBalance = totalShortMint.minus(totalShortBurn.times(expectedShortTokenPrice)); totalNetPendingLong = totalNetPendingLong.plus(netPendingLongBalance); totalNetPendingShort = totalNetPendingShort.plus(netPendingShortBalance); expectedLongBalance = expectedLongBalance.plus(netPendingLongBalance); expectedShortBalance = expectedShortBalance.plus(netPendingShortBalance); expectedLongSupply = expectedLongSupply.minus(totalLongBurn).plus(totalLongMint.div(expectedLongTokenPrice)); expectedShortSupply = expectedShortSupply.minus(totalShortBurn).plus(totalShortMint.div(expectedShortTokenPrice)); } const expectedSkew = expectedShortBalance.eq(0) || expectedLongBalance.eq(0) ? new _bignumber.BigNumber(1) : expectedLongBalance.div(expectedShortBalance); const effectiveCurrentLongSupply = longTokenSupply.plus(pendingLongTokenBurn); const effectiveCurrentShortSupply = shortTokenSupply.plus(pendingShortTokenBurn); const currentLongTokenPrice = longBalance.div(effectiveCurrentLongSupply.eq(0) ? 1 : effectiveCurrentLongSupply); const currentShortTokenPrice = shortBalance.div(effectiveCurrentShortSupply.eq(0) ? 1 : effectiveCurrentShortSupply); return { timestamp: Math.floor(Date.now() / 1000), currentSkew: longBalance.eq(0) || shortBalance.eq(0) ? new _bignumber.BigNumber(1) : longBalance.div(shortBalance), currentLongBalance: longBalance, currentLongSupply: effectiveCurrentLongSupply, currentShortBalance: shortBalance, currentShortSupply: effectiveCurrentShortSupply, currentLongTokenPrice, currentShortTokenPrice, currentPendingLongTokenBurn: pendingLongTokenBurn, currentPendingShortTokenBurn: pendingShortTokenBurn, expectedSkew, expectedLongBalance, expectedLongSupply, expectedShortBalance, expectedShortSupply, totalNetPendingLong, totalNetPendingShort, expectedLongTokenPrice, expectedShortTokenPrice, expectedPendingLongTokenBurn, expectedPendingShortTokenBurn, lastOraclePrice: lastOraclePrice, expectedOraclePrice: movingOraclePriceAfter, pendingCommits }; }; exports.calcPoolStatePreview = calcPoolStatePreview;