@tracer-protocol/pools-js
Version:
Javascript library for interacting with Tracer's Perpetual Pools
434 lines (360 loc) • 18.3 kB
JavaScript
"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;