Skip to content

Instantly share code, notes, and snippets.

@jeffywu
Created February 15, 2021 17:17
Show Gist options
  • Save jeffywu/0bc3cb934adf14511ba2fa04b675fd74 to your computer and use it in GitHub Desktop.
Save jeffywu/0bc3cb934adf14511ba2fa04b675fd74 to your computer and use it in GitHub Desktop.
Notional Market Calculation
import {BigNumber} from 'ethers/utils';
import {Decimal} from 'decimal.js';
import {CashMarket} from './typechain/CashMarket';
import {WeiPerEther} from 'ethers/constants';
import {getNowSeconds, toBigNumber} from './utils';
import {CashGroup} from './cashGroup';
import {GraphClient, MarketQueryResult} from './graphClient';
import {ApolloQueryResult} from '@apollo/client';
/** Mimic ABDK rounding settings */
Decimal.set({
precision: 18,
rounding: Decimal.ROUND_CEIL,
});
const MAX64 = new Decimal(9.223372036854775807);
/**
* Used internally for annualizing rates
* @ignore
*/
export const SECONDS_IN_YEAR = 31536000;
/** Describes the market for a maturity for calculating rates */
export interface MarketData {
/** Total current cash available in the market */
totalCurrentCash: BigNumber;
/** Total fCash available in the market */
totalfCash: BigNumber;
/** Total liquidity tokens in the market */
totalLiquidity: BigNumber;
/** Current rate anchor for the market */
rateAnchor: ExchangeRate;
/** Rate scalar for the market */
rateScalar: number;
/** The last implied rate that the market traded at */
lastImpliedRate: ImpliedPeriodRate;
}
/** Global contract parameters for calculating rates */
export interface RateParams {
/** Decimal places of precision for the rate (defaults to 1e9) */
ratePrecision: number;
/** Liquidity fee paid as a part of the exchange rate */
liquidityFee: number;
/** Decimal places of precision for balances (defaults to 1e18) */
balancePrecision: BigNumber;
liquidityHaircut: BigNumber;
globalRateAnchor: number;
globalRateScalar: number;
maxTradeSize: BigNumber;
}
/** Type alias for rates that are annualized */
type ImpliedAnnualRate = number;
/** Type alias for rates that are period-ized */
type ImpliedPeriodRate = number;
/** Spot exchange rate */
type ExchangeRate = number;
/**
* Provides methods for calculating various rates for a market. These are replications of the rate calculation methods on the Fixed Rate Pool
* contract itself for use in user interfaces where lower latency calculations are important. The ultimate source of truth for rate calculations
* will always be the smart contract.
*
* The three relevant types of rates are:
* - Exchange Rate: the amount of current cash to be exchanged for a unit of fCash (i.e. `current * exchangeRate = future`)
* - Implied Period Rate: the interest rate implied by the exchange rate over the length of the period that the fCash will mature. (i.e. `(exchangeRate - 1) * (maturityLength / timeToMaturity)`)
* - Implied Annual Rate: the implied period rate on an annualized basis
*/
export class Market {
/**
* Formats rates as a string with a given precision.
*
* @category Formatting [Static]
* @param rate rate to format
* @param ratePrecision the rate Precision (default: 1e9)
* @param precision amount of decimals to return (default: 4)
* @return formatted rate string
*/
public static formatImpliedRate(rate: number, ratePrecision = 1e9, precision = 4) {
if (rate === undefined || rate === 0) return 'No Rate';
return ((rate / ratePrecision) * 100).toFixed(precision) + '%';
}
/**
* Formats rates as a string with a given precision.
*
* @category Formatting
* @param rate rate to format
* @param precision the number of decimals places to supply (default: 3)
* @return formatted rate string
* @return the rate formatted as a string
*/
public formatImpliedRate(rate: number, precision = 3): string {
return Market.formatImpliedRate(rate, this.rateParams.ratePrecision, precision);
}
/**
* Formats rates as a string with a given precision.
*
* @category Formatting [Static]
* @param rate rate to format
* @param ratePrecision the rate Precision (default: 1e9)
* @param precision amount of decimals to return (default: 4)
* @return formatted rate string
*/
public static formatExchangeRate(rate: number, ratePrecision = 1e9, precision = 4) {
if (rate === undefined || rate === 0) return 'No Rate';
return (((rate - ratePrecision) / ratePrecision) * 100).toFixed(precision) + '%';
}
/**
* Formats rates as a string with a given precision.
*
* @category Formatting
* @param rate rate to format
* @param precision the number of decimals places to supply (default: 4)
* @return formatted rate string
* @return the rate formatted as a string
*/
public formatExchangeRate(rate: number, precision = 4): string {
return Market.formatExchangeRate(rate, this.rateParams.ratePrecision, precision);
}
/**
* Converts an implied period rate to an implied annual rate
*
* @category Formatting [Static]
* @param impliedPeriodRate the implied period rate to annualize
* @param maturityLength the size of the period of the simple rate
* @param avgBlockTimeMs average block time for the network
* @param ratePrecision rate Precision (default: 1e9)
* @return implied annualized rate
*/
public static periodToAnnualRate(impliedPeriodRate: ImpliedPeriodRate, maturityLength: number): ImpliedAnnualRate {
if (impliedPeriodRate === undefined) return 0;
if (maturityLength == 0) return 0;
const multiplier = SECONDS_IN_YEAR / maturityLength;
return Math.trunc(impliedPeriodRate * multiplier);
}
/**
* Converts an implied period rate to an implied annual rate
*
* @category Formatting
* @param impliedPeriodRate
* @return implied annualized rate
*/
public periodToAnnualRate(impliedPeriodRate?: ImpliedPeriodRate): ImpliedAnnualRate {
if (impliedPeriodRate === undefined) return 0;
if (this.cashGroup.maturityLength == 0) return 0;
return Market.periodToAnnualRate(impliedPeriodRate, this.cashGroup.maturityLength);
}
/**
* Convert an implied annual rate to an implied period rate.
*
* @category Formatting [Static]
* @param annualizedRate the annualized rate to convert to an implied period rate
* @param maturityLength period length of the market
* @param avgBlockTimeMs average block time for the network
* @param ratePrecision rate Precision (default: 1e9)
* @return implied annual rate
*/
public static annualToPeriodRate(annualizedRate: ImpliedAnnualRate, maturityLength: number): ImpliedPeriodRate {
if (annualizedRate === undefined) return 0;
if (maturityLength == 0) return 0;
const multiplier = SECONDS_IN_YEAR / maturityLength;
return Math.trunc(annualizedRate / multiplier);
}
/**
* Convert an implied annual rate to an implied period rate.
*
* @category Formatting
* @param annualizedRate the annualized rate to convert to an implied period rate
* @return implied annual rate
*/
public annualToPeriodRate(annualizedRate: ImpliedAnnualRate): number {
return Market.annualToPeriodRate(annualizedRate, this.cashGroup.maturityLength);
}
/**
* Converts an exchange rate to an implied period rate
*
* @category Formatting [Static]
* @param exchangeRate
* @param blockTime block time the exchange occurs on
* @param maturityLength period length of the market
* @param maturity block height when the market will mature
* @param ratePrecision (default: 1e9)
* @return implied period rate
*/
public static exchangeToPeriodRate(
exchangeRate: ExchangeRate,
blockTime: number,
maturityLength: number,
maturity: number,
ratePrecision = 1e9,
): ImpliedPeriodRate {
const timeToMaturity = maturity - blockTime;
if (timeToMaturity < 0) {
throw new RangeError('timeToMaturity < 0, cannot convert an exchange rate in a matured period.');
}
return Math.trunc(((exchangeRate - ratePrecision) * maturityLength) / timeToMaturity);
}
/**
* Converts an exchange rate to an implied period rate
*
* @category Formatting
* @param exchangeRate exchange rate
* @param blockTime block time the exchange occurs on
* @return implied period rate
*/
public exchangeToPeriodRate(exchangeRate: ExchangeRate, blockTime: number): ImpliedPeriodRate {
return Market.exchangeToPeriodRate(exchangeRate, blockTime, this.cashGroup.maturityLength, this.maturity);
}
/**
* Converts a period rate to an exchange rate.
*
* @param impliedPeriodRate
* @param blockTime
* @param maturityLength
* @param maturity
* @param ratePrecision
*/
public static periodToExchangeRate(
impliedPeriodRate: ImpliedPeriodRate,
blockTime: number,
maturityLength: number,
maturity: number,
ratePrecision = 1e9,
): ExchangeRate {
const timeToMaturity = maturity - blockTime;
if (timeToMaturity < 0) {
throw new RangeError('timeToMaturity < 0, cannot convert an exchange rate in a matured period.');
}
return Math.trunc((impliedPeriodRate * timeToMaturity) / maturityLength) + ratePrecision;
}
/**
* Converts a period rate to an exchange rate.
*
* @param impliedPeriodRate
* @param blockTime
*/
public periodToExchangeRate(impliedPeriodRate: ImpliedPeriodRate, blockTime: number): ExchangeRate {
return Market.periodToExchangeRate(impliedPeriodRate, blockTime, this.cashGroup.maturityLength, this.maturity);
}
public fCashFromExchangeRate(exchangeRate: ExchangeRate, cash: BigNumber): BigNumber {
return Market.fCashFromExchangeRate(exchangeRate, cash, this.rateParams.ratePrecision);
}
public static fCashFromExchangeRate(exchangeRate: ExchangeRate, cash: BigNumber, ratePrecision = 1e9): BigNumber {
return cash.mul(exchangeRate).div(ratePrecision);
}
public cashFromExchangeRate(exchangeRate: ExchangeRate, fCash: BigNumber): BigNumber {
return Market.cashFromExchangeRate(exchangeRate, fCash, this.rateParams.ratePrecision);
}
public static cashFromExchangeRate(exchangeRate: ExchangeRate, fCash: BigNumber, ratePrecision = 1e9): BigNumber {
return fCash.mul(ratePrecision).div(exchangeRate);
}
/**
* Returns the implied rate for the loan over the period
*
* @category Formatting [Static]
* @param fCashAmount
* @param cashAmount
* @param maturityLength period length of the market
* @param blockTime block time the exchange occurs on
* @param ratePrecision (default: 1e9)
* @return implied period rate
*/
public static impliedPeriodRate(
fCashAmount: BigNumber,
cashAmount: BigNumber,
maturityLength: number,
maturity: number,
blockTime: number,
ratePrecision = 1e9,
): ImpliedPeriodRate {
const timeToMaturity = maturity - blockTime;
return fCashAmount
.mul(ratePrecision)
.div(cashAmount)
.sub(ratePrecision)
.mul(maturityLength)
.div(timeToMaturity)
.toNumber();
}
/**
* Returns the implied rate for the loan over the period
*
* @param fCashAmount
* @param cashAmount
* @param blockTime block time the exchange occurs on
* @return implied period rate
*/
public impliedPeriodRate(fCashAmount: BigNumber, cashAmount: BigNumber, blockTime: number): ImpliedPeriodRate {
return Market.impliedPeriodRate(
fCashAmount,
cashAmount,
this.cashGroup.maturityLength,
this.maturity,
blockTime,
this.rateParams.ratePrecision,
);
}
/**
* Returns the spot exchange rate between two amounts.
*
* @category Formatting [Static]
* @param fCashAmount
* @param cashAmount
* @param ratePrecision
* @return exchange rate been fCash and current amount
*/
public static exchangeRate(fCashAmount: BigNumber, cashAmount: BigNumber, ratePrecision: number): ExchangeRate {
return fCashAmount.mul(ratePrecision).div(cashAmount).toNumber();
}
/**
* Returns the spot exchange rate between two amounts.
*
* @category Formatting
* @param fCashAmount
* @param cashAmount
* @return exchange rate been fCash and current amount
*/
public exchangeRate(fCashAmount: BigNumber, cashAmount: BigNumber): ExchangeRate {
return Market.exchangeRate(fCashAmount, cashAmount, this.rateParams.ratePrecision);
}
/**
* Returns the current market rate. Async because it fetches the current block time.
*
* @category Calculation
* @returns exchange rate at `blockTime`
*/
public marketImpliedRateNow() {
const nowSeconds = getNowSeconds();
return this.exchangeToPeriodRate(this.marketExchangeRate(nowSeconds), nowSeconds);
}
/**
* Returns the current market rate. Async because it fetches the current block time.
*
* @category Calculation
* @returns exchange rate at `blockTime`
*/
public marketImpliedRate(blockTime: number) {
return this.exchangeToPeriodRate(this.marketExchangeRate(blockTime), blockTime);
}
/**
* Returns the current market rate. Async because it fetches the current block time.
*
* @category Calculation
* @returns exchange rate at `blockTime`
*/
public marketExchangeRateNow() {
const nowSeconds = getNowSeconds();
return this.marketExchangeRate(nowSeconds);
}
/**
* Returns the current market rate.
*
* @category Calculation
* @param blockTime block time where the exchange will occur
* @returns exchange rate at `blockTime`
*/
public marketExchangeRate(blockTime: number) {
const anchor = this.calculateAnchor(blockTime);
return this.calculateExchangeRate(
new BigNumber(0),
this.market.totalCurrentCash,
this.market.totalfCash,
anchor,
blockTime,
);
}
/**
* Calculates the amount of current cash that can be borrowed after selling the specified amount of fCash.
*
* @category Calculation
* @param fCashAmount amount of fCash to sell
* @param blockTime block time where the exchange will occur
* @returns the amount of current cash this will purchase
*/
public getfCashToCurrentCashInput(fCashAmount: BigNumber, blockTime: number) {
const anchor = this.calculateAnchor(blockTime);
const feeRate = this.calculateFeeRate(blockTime);
const tradeRate =
this.calculateExchangeRate(fCashAmount, this.market.totalCurrentCash, this.market.totalfCash, anchor, blockTime) +
feeRate;
return fCashAmount.mul(this.rateParams.ratePrecision).div(tradeRate);
}
/**
* Calculates the amount of fCash that must be sold in order to borrow the amount of current cash specified.
*
* @category Calculation
* @param cashAmount amount of current cash to purchase
* @param blockTime block time where the exchange will occur
* @returns the amount of fCash that must be sold
*/
public getfCashToCurrentCashOutput(cashAmount: BigNumber, blockTime: number) {
return this.iterateRates(cashAmount, blockTime, true);
}
/**
* Calculates the amount of fCash that will be purchased if `cashAmount` is lent.
*
* @category Calculation
* @param cashAmount amount of current cash to sell
* @param blockTime block time where the exchange will occur
* @returns the amount of fCash this will purchase
*/
public getCurrentCashTofCashInput(cashAmount: BigNumber, blockTime: number) {
return this.iterateRates(cashAmount, blockTime, false);
}
/**
* Calculates the amount of current cash that must be lent in order to purchase `fCashAmount` at maturity.
*
* @category Calculation
* @param fCashAmount amount of fCash to purchase
* @param blockTime block time where the exchange will occur
* @returns the amount of current cash that must be sold
*/
public getCurrentCashTofCashOutput(fCashAmount: BigNumber, blockTime: number) {
const anchor = this.calculateAnchor(blockTime);
const feeRate = this.calculateFeeRate(blockTime);
const tradeRate =
this.calculateExchangeRate(
fCashAmount.mul(-1),
this.market.totalCurrentCash,
this.market.totalfCash,
anchor,
blockTime,
) - feeRate;
if (tradeRate < this.rateParams.ratePrecision) {
throw new Error('Cannot lend at negative interest rates');
}
return fCashAmount.mul(this.rateParams.ratePrecision).div(tradeRate);
}
public getLiquidityTokenClaims(tokens: BigNumber, shouldHaircut = true) {
const nowSeconds = getNowSeconds();
const cashClaim = this.market.totalCurrentCash.mul(tokens).div(this.market.totalLiquidity);
const fCashClaim = this.market.totalfCash.mul(tokens).div(this.market.totalLiquidity);
if (this.maturity <= nowSeconds || !shouldHaircut) {
return {
cashClaim,
fCashClaim,
};
} else {
return {
cashClaim: cashClaim.mul(this.rateParams.liquidityHaircut).div(WeiPerEther),
fCashClaim: fCashClaim.mul(this.rateParams.liquidityHaircut).div(WeiPerEther),
};
}
}
public getTokensMinted(cashAmount: BigNumber) {
if (this.market.totalLiquidity.isZero()) {
return cashAmount;
}
return this.market.totalLiquidity.mul(cashAmount).div(this.market.totalCurrentCash);
}
public getPoolShare(tokens: BigNumber, calculatePostAdd = false) {
if (this.market.totalLiquidity.isZero()) {
return '100%';
}
if (calculatePostAdd) {
const divisor = this.market.totalLiquidity.add(tokens);
return (tokens.mul(new BigNumber(100000)).div(divisor).toNumber() / 1000).toFixed(3) + '%';
}
return (tokens.mul(new BigNumber(100000)).div(this.market.totalLiquidity).toNumber() / 1000).toFixed(3) + '%';
}
public getfCashForLiquidity(cashAmount: BigNumber) {
return this.market.totalfCash.mul(cashAmount).div(this.market.totalCurrentCash);
}
public getImpliedRatePostLiquidity(cashAmount: BigNumber, blockTime: number, fCash?: BigNumber) {
if (fCash == null) {
fCash = this.market.totalfCash.mul(cashAmount).div(this.market.totalCurrentCash);
}
const newTotalCollateral = this.market.totalCurrentCash.add(cashAmount);
const newTotalfCash = this.market.totalfCash.add(fCash);
const anchor = this.calculateAnchor(blockTime, newTotalCollateral, newTotalfCash);
return this.exchangeToPeriodRate(
this.calculateExchangeRate(new BigNumber(0), newTotalCollateral, newTotalfCash, anchor, blockTime),
blockTime,
);
}
public getPresentValue(fCashValue: BigNumber): BigNumber {
const exchangeRate = this.marketExchangeRateNow();
return fCashValue.mul(this.rateParams.ratePrecision).div(exchangeRate);
}
/**
* @category Formatting
*/
public toJSON() {
return {
totalCurrentCash: this.market.totalCurrentCash.toString(),
totalfCash: this.market.totalfCash.toString(),
totalLiquidity: this.market.totalLiquidity.toString(),
maturity: this.maturity,
};
}
private timeToMaturity(blockTime: number) {
return this.maturity - blockTime;
}
private calculateAnchor(
blockTime: number,
totalCurrentCash = this.market.totalCurrentCash,
totalfCash = this.market.totalfCash,
) {
const newImpliedRate = this.exchangeToPeriodRate(
this.calculateExchangeRate(new BigNumber(0), totalCurrentCash, totalfCash, this.market.rateAnchor, blockTime),
blockTime,
);
let rateDifference = 0;
if (this.market.lastImpliedRate != 0) {
rateDifference = Math.trunc(
((newImpliedRate - this.market.lastImpliedRate) * this.timeToMaturity(blockTime)) /
this.cashGroup.maturityLength,
);
}
let rateAnchor: number;
if (this.market.rateAnchor == 0) {
rateAnchor = new BigNumber(this.rateParams.globalRateAnchor)
.sub(this.rateParams.ratePrecision)
.mul(this.timeToMaturity(blockTime))
.div(SECONDS_IN_YEAR)
.add(this.rateParams.ratePrecision)
.toNumber();
} else {
rateAnchor = this.market.rateAnchor;
}
return rateAnchor - rateDifference;
}
private calculateScalar(blockTime: number) {
let rateScalar = this.rateParams.globalRateScalar;
if (this.market.rateScalar != 0) {
rateScalar = this.market.rateScalar;
}
return Math.trunc((rateScalar * this.cashGroup.maturityLength) / this.timeToMaturity(blockTime));
}
private calculateFeeRate(blockTime: number): number {
return Math.trunc((this.rateParams.liquidityFee * this.timeToMaturity(blockTime)) / this.cashGroup.maturityLength);
}
/**
* Returns the exchange rate given the total amount of fCash and cash. Uses
* mathjs internally.
*
* @param _totalCurrentCash
* @param _totalfCash
* @return a bignumber of the exchange rate
*/
private calculateExchangeRate(
_fCashAmount: BigNumber,
_totalCurrentCash: BigNumber,
_totalfCash: BigNumber,
anchor: number,
blockTime: number,
): ExchangeRate {
const fCashAmount = new Decimal(_fCashAmount.toString());
const totalCurrentCash = new Decimal(_totalCurrentCash.toString());
const totalfCash = new Decimal(_totalfCash.toString());
const one = new Decimal(1);
// p = (totalFC + FC) / (totalFC + totalC)
const proportion = totalfCash.add(fCashAmount).div(totalfCash.add(totalCurrentCash));
if (proportion.lt(0) || proportion.gt(1)) {
throw new RangeError(
`Trade size of ${fCashAmount} out of bounds for market ${this.maturity}, proportion = ${proportion}`,
);
}
const scalar = this.calculateScalar(blockTime);
const ratio = proportion.div(one.sub(proportion));
if (ratio.greaterThan(MAX64)) {
throw new RangeError(`Trade size of ${fCashAmount} out of bounds for market ${this.maturity}, ratio = ${ratio}`);
}
// exchangeRate = ln(p / (1 - p)) / rateScalar + rateAnchor
const logPortion = Decimal.ln(ratio).mul(this.rateParams.ratePrecision).round(); // We round here instead of trunc() because of the ABDK behavior
const exchangeRate = logPortion.div(scalar).trunc().add(anchor);
return parseInt(exchangeRate.toFixed(0));
}
private iterateRates(cashAmount: BigNumber, blockTime: number, isBuy: boolean) {
const marketRate = this.marketExchangeRate(blockTime);
let fCashAmount = cashAmount.mul(marketRate).div(this.rateParams.ratePrecision);
let currentEstimate = isBuy
? this.getfCashToCurrentCashInput(fCashAmount, blockTime)
: this.getCurrentCashTofCashOutput(fCashAmount, blockTime);
let diff = cashAmount.sub(currentEstimate);
while (!diff.eq(0)) {
let fcDiff = diff.mul(marketRate).div(this.rateParams.ratePrecision);
if (fcDiff.eq(0)) {
// The minimum of the diff has to be 1 if this has not converged.
fcDiff = new BigNumber(1);
}
fCashAmount = fCashAmount.add(fcDiff);
currentEstimate = isBuy
? this.getfCashToCurrentCashInput(fCashAmount, blockTime)
: this.getCurrentCashTofCashOutput(fCashAmount, blockTime);
diff = cashAmount.sub(currentEstimate);
}
return fCashAmount;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment