Commit 2953d4cb authored by Noah Zinsmeister's avatar Noah Zinsmeister

Rate -> Fraction

make new Fraction class more robust / generic

tweak tsconfig
parent 8ecdc265
......@@ -19,13 +19,6 @@
"start": "tsdx watch",
"test": "tsdx test"
},
"devDependencies": {
"@types/big.js": "^4.0.5",
"@types/jest": "^24.0.25",
"tsdx": "^0.12.1",
"tslib": "^1.10.0",
"typescript": "^3.7.4"
},
"dependencies": {
"@ethersproject/address": "^5.0.0-beta.134",
"big.js": "^5.2.2",
......@@ -33,6 +26,13 @@
"tiny-invariant": "^1.0.6",
"toformat": "^2.0.0"
},
"devDependencies": {
"@types/big.js": "^4.0.5",
"@types/jest": "^24.0.25",
"tsdx": "^0.12.1",
"tslib": "^1.10.0",
"typescript": "^3.7.5"
},
"engines": {
"node": ">=10"
},
......
......@@ -18,9 +18,11 @@ export enum TradeType {
// exports for internal consumption
export const ZERO = BigInt(0)
export const ONE = BigInt(1)
export const TEN = BigInt(10)
export const _1000 = BigInt(1000)
export const _100 = BigInt(100)
export const _997 = BigInt(997)
export const _1000 = BigInt(1000)
export enum SolidityType {
uint8,
......
import invariant from 'tiny-invariant'
import { ZERO, ONE, TEN, _100 } from '../constants'
import { BigintIsh } from '../types'
import { parseBigintIsh } from '../utils/parseInputs'
import { formatSignificant, formatFixed } from '../utils/formatOutputs'
import { Route } from './route'
export class Fraction {
public readonly numerator: bigint
public readonly denominator: bigint
constructor(numerator: BigintIsh, denominator: BigintIsh = ONE) {
this.numerator = parseBigintIsh(numerator)
this.denominator = parseBigintIsh(denominator)
}
// warning: this can truncate!
get quotient() {
return this.numerator / this.denominator
}
public invert(): Fraction {
return new Fraction(this.denominator, this.numerator)
}
public multiply(other: Fraction): Fraction {
return new Fraction(this.numerator * other.numerator, this.denominator * other.denominator)
}
public formatSignificant(significantDigits: number, ...rest: any[]): string {
return formatSignificant(this.numerator, this.denominator, significantDigits, ...rest)
}
public formatFixed(decimalPlaces: number, ...rest: any[]): string {
return formatFixed(this.numerator, this.denominator, decimalPlaces, ...rest)
}
}
export class Price {
public readonly price: Fraction // normalized
public readonly scalar: Fraction // used to convert back to raw balances
static fromRoute(route: Route): Price {
const prices: Fraction[] = route.exchanges.map((exchange, i) => {
const input = route.path[i]
const baseIndex = input.address === exchange.pair[0].address ? 0 : 1
const quoteIndex = input.address === exchange.pair[0].address ? 1 : 0
return new Fraction(
exchange.balances[quoteIndex] * TEN ** BigInt(exchange.pair[baseIndex].decimals),
exchange.balances[baseIndex] * TEN ** BigInt(exchange.pair[quoteIndex].decimals)
)
})
const price = prices.reduce((accumulator, currentValue) => accumulator.multiply(currentValue), new Fraction(ONE))
const scalar = new Fraction(TEN ** BigInt(route.output.decimals), TEN ** BigInt(route.input.decimals))
return new Price(price, scalar)
}
constructor(price: Fraction, scalar: Fraction) {
this.price = price
this.scalar = scalar
}
public invert(): Price {
return new Price(this.price.invert(), this.scalar.invert())
}
public quote(amount: BigintIsh): bigint {
const amountParsed = parseBigintIsh(amount)
invariant(amountParsed > ZERO, `${amountParsed} isn't positive.`)
return this.price.multiply(this.scalar).multiply(new Fraction(amount)).quotient
}
public formatSignificant(significantDigits = 6, ...rest: any[]) {
return this.price.formatSignificant(significantDigits, ...rest)
}
public formatFixed(decimalPlaces = 6, ...rest: any[]) {
return this.price.formatFixed(decimalPlaces, ...rest)
}
}
export class Percent {
public readonly percent: Fraction
constructor(percent: Fraction) {
this.percent = percent
}
public formatSignificant(significantDigits = 5, ...rest: any[]) {
return this.percent.multiply(new Fraction(_100)).formatSignificant(significantDigits, ...rest)
}
public formatFixed(decimalPlaces = 2, ...rest: any[]) {
return this.percent.multiply(new Fraction(_100)).formatFixed(decimalPlaces, ...rest)
}
}
......@@ -2,3 +2,5 @@ export * from './token'
export * from './exchange'
export * from './route'
export * from './trade'
export * from './fractions'
import invariant from 'tiny-invariant'
import { TEN } from '../constants'
import { Fraction, BigintIsh } from '../types'
import { parseBigintIsh } from '../utils/parseInputs'
import { formatSignificant, formatFixed } from '../utils/formatOutputs'
import { Route } from './route'
export class Rate {
public readonly rate: Fraction
constructor(rate: Fraction) {
this.rate = rate
}
public formatSignificant(significantDigits = 6, invert = false, ...rest: any[]): string {
const fraction = (invert ? this.rate.slice().reverse() : this.rate) as Fraction
return formatSignificant(fraction, significantDigits, ...rest)
}
public formatFixed(decimalPlaces = 6, invert = false, ...rest: any[]): string {
const fraction = (invert ? this.rate.slice().reverse() : this.rate) as Fraction
return formatFixed(fraction, decimalPlaces, ...rest)
}
}
export class Price extends Rate {
public readonly scalar: Fraction // used to convert back to balances
static fromRoute(route: Route): Price {
const rates: Fraction[] = route.exchanges.map((exchange, i) => {
const input = route.path[i]
const baseIndex = input.address === exchange.pair[0].address ? 0 : 1
const quoteIndex = input.address === exchange.pair[0].address ? 1 : 0
return [
exchange.balances[quoteIndex] * TEN ** BigInt(exchange.pair[baseIndex].decimals),
exchange.balances[baseIndex] * TEN ** BigInt(exchange.pair[quoteIndex].decimals)
]
})
const rate: Fraction = [
rates.map(rate => rate[0]).reduce((accumulator, currentValue) => accumulator * currentValue, BigInt(1)),
rates.map(rate => rate[1]).reduce((accumulator, currentValue) => accumulator * currentValue, BigInt(1))
]
const scalar: Fraction = [TEN ** BigInt(route.output.decimals), TEN ** BigInt(route.input.decimals)]
return new Price(rate, scalar)
}
constructor(rate: Fraction, scalar: Fraction) {
super(rate)
this.scalar = scalar
}
public quote(amount: BigintIsh, invert = false): bigint {
const amountParsed = parseBigintIsh(amount)
invariant(amountParsed > 0, `${amountParsed} isn't positive.`)
const [numeratorRate, denominatorRate] = invert ? this.rate.slice().reverse() : this.rate
const [numeratorScalar, denominatorScalar] = invert
? (this.scalar as Fraction).slice().reverse()
: (this.scalar as Fraction)
return (amountParsed * numeratorRate * numeratorScalar) / (denominatorRate * denominatorScalar)
}
}
export class Percent extends Rate {
public formatSignificant(significantDigits = 6, ...rest: any[]): string {
return formatSignificant([this.rate[0] * BigInt(100), this.rate[1]], significantDigits, ...rest)
}
public formatFixed(decimalPlaces = 6, ...rest: any[]): string {
return formatFixed([this.rate[0] * BigInt(100), this.rate[1]], decimalPlaces, ...rest)
}
public formatSignificantRaw(...args: any[]) {
return super.formatSignificant(...args)
}
public formatFixedRaw(...args: any[]) {
return super.formatFixed(...args)
}
}
......@@ -2,7 +2,7 @@ import invariant from 'tiny-invariant'
import { Token } from './token'
import { Exchange } from './exchange'
import { Price } from './rate'
import { Price } from './fractions'
export class Route {
public readonly exchanges: Exchange[]
......
import invariant from 'tiny-invariant'
import { _997, _1000, SolidityType, TradeType } from '../constants'
import { BigintIsh, Fraction } from '../types'
import { ZERO, ONE, _997, _1000, SolidityType, TradeType } from '../constants'
import { BigintIsh } from '../types'
import { parseBigintIsh } from '../utils/parseInputs'
import { validateSolidityTypeInstance } from '../utils/validateInputs'
import { Exchange } from './exchange'
import { Route } from './route'
import { Price, Percent } from './rate'
import { Fraction, Price, Percent } from './fractions'
function getOutputAmount(inputAmount: bigint, inputReserve: bigint, outputReserve: bigint): bigint {
invariant(inputAmount > 0, `${inputAmount} is not positive.`)
invariant(inputReserve > 0, `${inputReserve} is not positive.`)
invariant(outputReserve > 0, `${outputReserve} is not positive.`)
invariant(inputAmount > ZERO, `${inputAmount} is not positive.`)
invariant(inputReserve > ZERO, `${inputReserve} is not positive.`)
invariant(outputReserve > ZERO, `${outputReserve} is not positive.`)
const inputAmountWithFee = inputAmount * _997
const numerator = inputAmountWithFee * outputReserve
const denominator = inputReserve * _1000 + inputAmountWithFee
......@@ -24,26 +24,27 @@ function getInputAmount(outputAmount: bigint, inputReserve: bigint, outputReserv
invariant(outputReserve > 0, `${outputReserve} is not positive.`)
const numerator = inputReserve * outputAmount * _1000
const denominator = (outputReserve - outputAmount) * _997
return numerator / denominator + BigInt(1)
return numerator / denominator + ONE
}
function getSlippage(inputAmount: bigint, midPrice: Price, outputAmount: bigint): Percent {
const exactQuote: Fraction = [
inputAmount * midPrice.rate[0] * midPrice.scalar[0],
midPrice.rate[1] * midPrice.scalar[1]
]
const normalizedNumerator: Fraction = [outputAmount * exactQuote[1] - exactQuote[0], exactQuote[1]]
const invertedDenominator = exactQuote.slice().reverse() as Fraction
return new Percent([normalizedNumerator[0] * invertedDenominator[0], normalizedNumerator[1] * invertedDenominator[1]])
const exactQuote = midPrice.price.multiply(midPrice.scalar).multiply(new Fraction(inputAmount))
const normalizedNumerator = new Fraction(
outputAmount * exactQuote.denominator - exactQuote.numerator,
exactQuote.denominator
)
const invertedDenominator = exactQuote.invert()
return new Percent(normalizedNumerator.multiply(invertedDenominator))
}
function getPercentChange(referenceRate: Price, newRate: Price): Percent {
const normalizedNumerator: Fraction = [
newRate.rate[0] * referenceRate.rate[1] - referenceRate.rate[0] * newRate.rate[1],
referenceRate.rate[1] * newRate.rate[1]
]
const invertedDenominator = referenceRate.rate.slice().reverse() as Fraction
return new Percent([normalizedNumerator[0] * invertedDenominator[0], normalizedNumerator[1] * invertedDenominator[1]])
const normalizedNumerator = new Fraction(
newRate.price.numerator * referenceRate.price.denominator -
referenceRate.price.numerator * newRate.price.denominator,
referenceRate.price.denominator * newRate.price.denominator
)
const invertedDenominator = referenceRate.price.invert()
return new Percent(normalizedNumerator.multiply(invertedDenominator))
}
export class Trade {
......@@ -111,14 +112,13 @@ export class Trade {
this.inputAmount = inputAmount
this.outputAmount = outputAmount
this.tradeType = tradeType
const executionPrice = new Price(
[outputAmount * route.midPrice.scalar[1], inputAmount * route.midPrice.scalar[0]],
this.executionPrice = new Price(
new Fraction(outputAmount, inputAmount).multiply(route.midPrice.scalar.invert()),
route.midPrice.scalar
)
this.executionPrice = executionPrice
this.slippage = getSlippage(inputAmount, route.midPrice, outputAmount)
const nextMidPrice = Price.fromRoute(new Route(nextExchanges, route.input))
this.nextMidPrice = nextMidPrice
this.slippage = getSlippage(inputAmount, route.midPrice, outputAmount)
this.midPricePercentChange = getPercentChange(route.midPrice, nextMidPrice)
}
}
export * from './types'
export { ChainId, WETH, TradeType } from './constants'
export * from './entities'
export type BigintIsh = bigint | string
export type Fraction = [bigint, bigint]
......@@ -3,13 +3,12 @@ import _Decimal from 'decimal.js-light'
import _Big, { RoundingMode } from 'big.js'
import toFormat from 'toformat'
import { Fraction } from '../types'
const Decimal = toFormat(_Decimal)
const Big = toFormat(_Big)
export function formatSignificant(
[numerator, denominator]: Fraction,
numerator: bigint,
denominator: bigint,
significantDigits: number,
format: object = { groupSeparator: '' },
roundingMode?: number
......@@ -23,7 +22,8 @@ export function formatSignificant(
}
export function formatFixed(
[numerator, denominator]: Fraction,
numerator: bigint,
denominator: bigint,
decimalPlaces: number,
format: object = { groupSeparator: '' },
roundingMode?: RoundingMode
......
......@@ -66,7 +66,7 @@ describe('entities', () => {
expect(route.midPrice.quote(decimalize(1, route.input.decimals))).toEqual(
decimalize(1234, route.output.decimals)
)
expect(route.midPrice.quote(decimalize(1234, route.output.decimals), true)).toEqual(
expect(route.midPrice.invert().quote(decimalize(1234, route.output.decimals))).toEqual(
decimalize(1, route.input.decimals)
)
......@@ -75,26 +75,26 @@ describe('entities', () => {
expect(route.midPrice.formatSignificant(3)).toEqual('1230')
expect(route.midPrice.formatSignificant(4)).toEqual('1234')
expect(route.midPrice.formatSignificant(5)).toEqual('1234')
expect(route.midPrice.formatSignificant(4, undefined, { groupSeparator: ',' })).toEqual('1,234')
expect(route.midPrice.formatSignificant(1, true)).toEqual('0.0008')
expect(route.midPrice.formatSignificant(2, true)).toEqual('0.00081')
expect(route.midPrice.formatSignificant(3, true)).toEqual('0.00081')
expect(route.midPrice.formatSignificant(4, true)).toEqual('0.0008104')
expect(route.midPrice.formatSignificant(4, true, undefined, 1)).toEqual('0.0008103')
expect(route.midPrice.formatSignificant(5, true)).toEqual('0.00081037')
expect(route.midPrice.formatSignificant(4, { groupSeparator: ',' })).toEqual('1,234')
expect(route.midPrice.invert().formatSignificant(1)).toEqual('0.0008')
expect(route.midPrice.invert().formatSignificant(2)).toEqual('0.00081')
expect(route.midPrice.invert().formatSignificant(3)).toEqual('0.00081')
expect(route.midPrice.invert().formatSignificant(4)).toEqual('0.0008104')
expect(route.midPrice.invert().formatSignificant(4, undefined, 1)).toEqual('0.0008103')
expect(route.midPrice.invert().formatSignificant(5)).toEqual('0.00081037')
expect(route.midPrice.formatFixed(0)).toEqual('1234')
expect(route.midPrice.formatFixed(1)).toEqual('1234.0')
expect(route.midPrice.formatFixed(2)).toEqual('1234.00')
expect(route.midPrice.formatFixed(2, undefined, { groupSeparator: ',' })).toEqual('1,234.00')
expect(route.midPrice.formatFixed(0, true)).toEqual('0')
expect(route.midPrice.formatFixed(1, true)).toEqual('0.0')
expect(route.midPrice.formatFixed(4, true)).toEqual('0.0008')
expect(route.midPrice.formatFixed(5, true)).toEqual('0.00081')
expect(route.midPrice.formatFixed(6, true)).toEqual('0.000810')
expect(route.midPrice.formatFixed(7, true)).toEqual('0.0008104')
expect(route.midPrice.formatFixed(7, true, undefined, 0)).toEqual('0.0008103')
expect(route.midPrice.formatFixed(8, true)).toEqual('0.00081037')
expect(route.midPrice.formatFixed(2, { groupSeparator: ',' })).toEqual('1,234.00')
expect(route.midPrice.invert().formatFixed(0)).toEqual('0')
expect(route.midPrice.invert().formatFixed(1)).toEqual('0.0')
expect(route.midPrice.invert().formatFixed(4)).toEqual('0.0008')
expect(route.midPrice.invert().formatFixed(5)).toEqual('0.00081')
expect(route.midPrice.invert().formatFixed(6)).toEqual('0.000810')
expect(route.midPrice.invert().formatFixed(7)).toEqual('0.0008104')
expect(route.midPrice.invert().formatFixed(7, undefined, 0)).toEqual('0.0008103')
expect(route.midPrice.invert().formatFixed(8)).toEqual('0.00081037')
})
describe('Trade', () => {
......@@ -109,18 +109,16 @@ describe('entities', () => {
expect(trade.outputAmount).toEqual(BigInt('1662497915624478906'))
expect(trade.executionPrice.formatSignificant(18)).toEqual('1.66249791562447891')
expect(trade.executionPrice.formatSignificant(18, true)).toEqual('0.601504513540621866')
expect(trade.executionPrice.invert().formatSignificant(18)).toEqual('0.601504513540621866')
expect(trade.executionPrice.quote(inputAmount)).toEqual(trade.outputAmount)
expect(trade.executionPrice.quote(trade.outputAmount, true)).toEqual(inputAmount)
expect(trade.executionPrice.invert().quote(trade.outputAmount)).toEqual(inputAmount)
expect(trade.nextMidPrice.formatSignificant(18)).toEqual('1.38958368072925352')
expect(trade.nextMidPrice.formatSignificant(18, true)).toEqual('0.71964')
expect(trade.nextMidPrice.invert().formatSignificant(18)).toEqual('0.71964')
expect(trade.slippage.formatSignificant(18)).toEqual('-16.8751042187760547')
expect(trade.slippage.formatSignificantRaw(18)).toEqual('-0.168751042187760547')
expect(trade.midPricePercentChange.formatSignificant(18)).toEqual('-30.5208159635373242')
expect(trade.midPricePercentChange.formatSignificantRaw(18)).toEqual('-0.305208159635373242')
})
it('TradeType.EXACT_OUTPUT', () => {
......@@ -135,18 +133,16 @@ describe('entities', () => {
// TODO think about inverse execution price?
expect(trade.executionPrice.formatSignificant(18)).toEqual('1.66249791562447891')
expect(trade.executionPrice.formatSignificant(18, true)).toEqual('0.601504513540621866')
expect(trade.executionPrice.invert().formatSignificant(18)).toEqual('0.601504513540621866')
expect(trade.executionPrice.quote(trade.inputAmount)).toEqual(outputAmount)
expect(trade.executionPrice.quote(outputAmount, true)).toEqual(trade.inputAmount)
expect(trade.executionPrice.invert().quote(outputAmount)).toEqual(trade.inputAmount)
expect(trade.nextMidPrice.formatSignificant(18)).toEqual('1.38958368072925352')
expect(trade.nextMidPrice.formatSignificant(18, true)).toEqual('0.71964')
expect(trade.nextMidPrice.invert().formatSignificant(18)).toEqual('0.71964')
expect(trade.slippage.formatSignificant(18)).toEqual('-16.8751042187760547')
expect(trade.slippage.formatSignificantRaw(18)).toEqual('-0.168751042187760547')
expect(trade.midPricePercentChange.formatSignificant(18)).toEqual('-30.5208159635373242')
expect(trade.midPricePercentChange.formatSignificantRaw(18)).toEqual('-0.305208159635373242')
})
it('minimum TradeType.EXACT_INPUT', () => {
......@@ -167,9 +163,6 @@ describe('entities', () => {
expect(trade.slippage.formatSignificant(18)).toEqual(
tokens[1].decimals === 9 ? '-0.300000099400899902' : '-0.3000000000000001'
)
expect(trade.slippage.formatSignificantRaw(18)).toEqual(
tokens[1].decimals === 9 ? '-0.00300000099400899902' : '-0.003000000000000001'
)
}
})
})
......
{
"include": ["src", "types", "test"],
"compilerOptions": {
"target": "es2016",
"target": "es2020",
"module": "esnext",
"lib": ["dom", "esnext", "esnext.bigint"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
......@@ -24,7 +23,6 @@
"paths": {
"*": ["src/*", "node_modules/*"]
},
"jsx": "react",
"esModuleInterop": true
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment