Skip to content

Commit

Permalink
feat(spot): add balancer swap predictor (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
k-yang authored Jul 14, 2023
1 parent b81f02e commit c01dc68
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/nibijs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

- .

## v0.21.5

- Refactor stableswap class
- Add balancer class for predicting outputs

## v0.21.4

- Actually fix build
Expand Down
2 changes: 1 addition & 1 deletion packages/nibijs/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@nibiruchain/nibijs",
"description": "The TypeScript SDK for the Nibiru blockchain.",
"version": "0.21.4",
"version": "0.21.5",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
146 changes: 146 additions & 0 deletions packages/nibijs/src/balancer/balancer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { BigNumber } from "bignumber.js"

BigNumber.set({ ROUNDING_MODE: BigNumber.ROUND_FLOOR })

/**
* Swap contains the result of a swap
*
* @export
* @class Swap
* @property {BalancerPool} poolStart the pool before the swap
* @property {BalancerPool} poolEnd the pool after the swap
* @property {BigNumber} dxAmm the amount of x added to the pool
* @property {BigNumber} dxUser the amount of x removed from the pool
* @property {BigNumber} dyAmm the amount of y removed from the pool
* @property {BigNumber} dyUser the amount of y added to the pool
* @property {BigNumber} priceImpact the price impact of the swap
*/
export class Swap {
public poolStart: BalancerPool
public poolEnd: BalancerPool

public dxAmm: BigNumber
public dxUser: BigNumber
public dyAmm: BigNumber
public dyUser: BigNumber
public priceImpact: BigNumber

constructor(poolStart: BalancerPool, poolEnd: BalancerPool) {
this.poolStart = poolStart
this.poolEnd = poolEnd
this.dxAmm = poolEnd.x.minus(poolStart.x)
this.dyAmm = poolEnd.y.minus(poolStart.y)
this.dxUser = this.dxAmm.negated()
this.dyUser = this.dyAmm.negated()

const startPrice = poolStart.spotPrice()
const endPrice = poolEnd.spotPrice()
this.priceImpact = endPrice.minus(startPrice).dividedBy(startPrice)
}
}

/**
* Balancer contains the logic for exchanging tokens in a traditional xy=k AMM pool
*
* Constructor:
* @param {BigNumber} x
* @param {BigNumber} y
* @param {BigNumber} swapFee
*
* @export
* @class BalancerPool
* @property {BigNumber} x the amount of x in the pool
* @property {BigNumber} y the amount of y in the pool
* @property {BigNumber} swapFee the swap fee expressed as a ratio
*/
export class BalancerPool {
public x: BigNumber
public y: BigNumber
public swapFee: BigNumber

constructor(x: BigNumber, y: BigNumber, fee: BigNumber) {
this.swapFee = fee
this.x = x
this.y = y
}

k() {
return this.x.multipliedBy(this.y)
}

spotPrice() {
return this.y.dividedBy(this.x)
}

/**
* Calculates the result of adding x to the pool
*
* @param dxAmm the amount of x to add to the pool. Could be negative.
* @returns a Swap object representing the result of the swap
*/
swapX(dxAmm: BigNumber): Swap | undefined {
if (!this.x.plus(dxAmm).isPositive()) return undefined

let dxAmmEffective = dxAmm
if (dxAmm.isPositive()) {
dxAmmEffective = dxAmmEffective.multipliedBy(
BigNumber(1).minus(this.swapFee)
)
}

let dyAmmNeeded = this.k()
.dividedBy(this.x.plus(dxAmmEffective))
.minus(this.y)
if (dxAmm.isNegative()) {
// mutually exclusive from reducing dxAmmEffective
dyAmmNeeded = dyAmmNeeded.dividedBy(BigNumber(1).minus(this.swapFee))
}

const poolEnd = new BalancerPool(
this.x.plus(dxAmm),
this.y.plus(dyAmmNeeded),
this.swapFee
)

if (poolEnd.x.isLessThanOrEqualTo(0) || poolEnd.y.isLessThanOrEqualTo(0))
return undefined

return new Swap(this, poolEnd)
}

/**
* Calculates the result of adding y to the pool
*
* @param dyAmm the amount of y to add to the pool. Could be negative.
* @returns a Swap object representing the result of the swap
*/
swapY(dyAmm: BigNumber): Swap | undefined {
if (!this.y.plus(dyAmm).isPositive()) return undefined

let dyAmmEffective = dyAmm
if (dyAmm.isPositive()) {
dyAmmEffective = dyAmmEffective.multipliedBy(
BigNumber(1).minus(this.swapFee)
)
}

let dxAmmNeeded = this.k()
.dividedBy(this.y.plus(dyAmmEffective))
.minus(this.x)
if (dyAmm.isNegative()) {
// mutually exclusive from reducing dyAmmEffective
dxAmmNeeded = dxAmmNeeded.dividedBy(BigNumber(1).minus(this.swapFee))
}

const poolEnd = new BalancerPool(
this.x.plus(dxAmmNeeded),
this.y.plus(dyAmm),
this.swapFee
)

if (poolEnd.x.isLessThanOrEqualTo(0) || poolEnd.y.isLessThanOrEqualTo(0))
return undefined

return new Swap(this, poolEnd)
}
}
1 change: 1 addition & 0 deletions packages/nibijs/src/balancer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./balancer"
108 changes: 108 additions & 0 deletions packages/nibijs/src/test/balancer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { BigNumber } from "bignumber.js"
import { BalancerPool, Swap } from "../balancer"

describe("balancer tests", () => {
test("add x", () => {
const balancerPool = new BalancerPool(
BigNumber(100),
BigNumber(100),
BigNumber(0.5) // 50% fee
)

expect(balancerPool.swapX(BigNumber(50))).toEqual({
poolStart: {
x: BigNumber(100),
y: BigNumber(100),
swapFee: BigNumber(0.5),
},
poolEnd: {
x: BigNumber(150),
y: BigNumber(80),
swapFee: BigNumber(0.5),
},
dxAmm: BigNumber(50),
dxUser: BigNumber(-50),
dyAmm: BigNumber(-20),
dyUser: BigNumber(20),
priceImpact: BigNumber("-0.46666666666666666667"),
} as Swap)
})

test("remove x", () => {
const balancerPool = new BalancerPool(
BigNumber(100),
BigNumber(100),
BigNumber(0.5) // 50% fee
)

expect(balancerPool.swapX(BigNumber(-50))).toEqual({
poolStart: {
x: BigNumber(100),
y: BigNumber(100),
swapFee: BigNumber(0.5),
},
poolEnd: {
x: BigNumber(50),
y: BigNumber(300), // the user needs to deposit 200 y to get 50 x because of the swap fee
swapFee: BigNumber(0.5),
},
dxAmm: BigNumber(-50),
dxUser: BigNumber(50),
dyAmm: BigNumber(200),
dyUser: BigNumber(-200),
priceImpact: BigNumber(5),
} as Swap)
})

test("add y", () => {
const balancerPool = new BalancerPool(
BigNumber(100),
BigNumber(100),
BigNumber(0.5) // 50% fee
)

expect(balancerPool.swapY(BigNumber(50))).toEqual({
poolStart: {
x: BigNumber(100),
y: BigNumber(100),
swapFee: BigNumber(0.5),
},
poolEnd: {
x: BigNumber(80),
y: BigNumber(150),
swapFee: BigNumber(0.5),
},
dxAmm: BigNumber(-20),
dxUser: BigNumber(20),
dyAmm: BigNumber(50),
dyUser: BigNumber(-50),
priceImpact: BigNumber(0.875),
} as Swap)
})

test("remove y", () => {
const balancerPool = new BalancerPool(
BigNumber(100),
BigNumber(100),
BigNumber(0.5) // 50% fee
)

expect(balancerPool.swapY(BigNumber(-50))).toEqual({
poolStart: {
x: BigNumber(100),
y: BigNumber(100),
swapFee: BigNumber(0.5),
},
poolEnd: {
x: BigNumber(300), // the user needs to deposit 200 x to get 50 y because of the swap fee
y: BigNumber(50),
swapFee: BigNumber(0.5),
},
dxAmm: BigNumber(200),
dxUser: BigNumber(-200),
dyAmm: BigNumber(-50),
dyUser: BigNumber(50),
priceImpact: BigNumber("-0.83333333333333333334"),
} as Swap)
})
})

0 comments on commit c01dc68

Please sign in to comment.