Skip to content

Commit

Permalink
fix: market order simulation (#130)
Browse files Browse the repository at this point in the history
* fix: market order simulation

* chore: format

---------

Co-authored-by: maxencerb <maxencerb@users.noreply.github.com>
  • Loading branch information
maxencerb and maxencerb committed Sep 6, 2024
1 parent d163298 commit 61623e3
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-fireants-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mangrovedao/mgv": patch
---

Fixed market order simulation and added tests
191 changes: 191 additions & 0 deletions src/lib/market-order-simulation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { parseEther, parseUnits } from 'viem'
import { beforeAll, describe, expect, inject, it } from 'vitest'
import { getBook } from '~mgv/actions/book.js'
import { simulatePopulate } from '~mgv/actions/kandel/populate.js'
import { simulateSow } from '~mgv/actions/kandel/sow.js'
import { validateKandelParams } from '~mgv/index.js'
import { getClient } from '~test/src/client.js'
import { mintAndApprove } from '~test/src/contracts/index.js'
import type { Book } from '../types/index.js'
import { BS } from './enums.js'
import { marketOrderSimulation } from './market-order-simulation.js'
import { inboundFromOutbound, outboundFromInbound } from './tick.js'

const client = getClient()
const actionParams = inject('mangrove')
const kandelSeeder = inject('kandel')
const { wethUSDC } = inject('markets')

const KANDEL_GASREQ = 128_000n

describe('marketOrderSimulation', () => {
let book: Book

beforeAll(async () => {
// Get the book
book = await getBook(client, actionParams, wethUSDC)

const { params, minProvision } = validateKandelParams({
minPrice: 2990,
midPrice: 3000,
maxPrice: 3010,
pricePoints: 5n,
market: wethUSDC,
baseAmount: parseEther('10'),
quoteAmount: parseUnits('30000', 6),
stepSize: 1n,
gasreq: KANDEL_GASREQ,
factor: 3,
asksLocalConfig: book.asksConfig,
bidsLocalConfig: book.bidsConfig,
marketConfig: book.marketConfig,
deposit: true,
})

const { request: sowReq, result: kandel } = await simulateSow(
client,
wethUSDC,
kandelSeeder.kandelSeeder,
{
account: client.account.address,
},
)
const sowTx = await client.writeContract(sowReq)
await client.waitForTransactionReceipt({ hash: sowTx })

await mintAndApprove(
client,
wethUSDC.base.address,
client.account.address,
params.baseAmount || 0n,
kandel,
)
await mintAndApprove(
client,
wethUSDC.quote.address,
client.account.address,
params.quoteAmount || 0n,
kandel,
)

const { request: populateReq } = await simulatePopulate(client, kandel, {
...params,
account: client.account.address,
value: minProvision,
})
const populateTx = await client.writeContract(populateReq)
await client.waitForTransactionReceipt({ hash: populateTx })

book = await getBook(client, actionParams, wethUSDC)
})

it('should simulate a buy market order', () => {
const baseAmount = parseEther('4')
const quoteAmount = inboundFromOutbound(
book.asks[0]!.offer.tick,
baseAmount,
)
const fee = (baseAmount * book.asksConfig.fee) / 10_000n

const result = marketOrderSimulation({
book,
bs: BS.buy,
base: baseAmount, // 5 tokens
})

expect(result.baseAmount).toBe(baseAmount - fee)
expect(result.quoteAmount).toBe(quoteAmount)
expect(result.gas).toBe(KANDEL_GASREQ + book.asksConfig.offer_gasbase)
expect(result.feePaid).toBe(fee)
expect(result.maxTickEncountered).toBe(book.asks[0]?.offer.tick)
expect(result.minSlippage).toBe(0)
expect(result.fillWants).toBe(true)
expect(result.rawPrice).approximately(3000 / 1e12, 10e-12)
expect(result.fillVolume).toBe(baseAmount)
})

it('should simulate a sell market order', () => {
const baseAmount = parseEther('4')
const quoteAmount = outboundFromInbound(
book.bids[0]!.offer.tick,
baseAmount,
)
const fee = (quoteAmount * book.bidsConfig.fee) / 10_000n

const result = marketOrderSimulation({
book,
bs: BS.sell,
base: baseAmount,
})

expect(result.baseAmount).toBe(baseAmount)
expect(result.quoteAmount).toBe(quoteAmount - fee)
expect(result.gas).toBe(KANDEL_GASREQ + book.bidsConfig.offer_gasbase)
expect(result.feePaid).toBe(fee)
expect(result.maxTickEncountered).toBe(book.bids[0]?.offer.tick)
expect(result.minSlippage).toBe(0)
expect(result.fillWants).toBe(false)
expect(result.rawPrice).approximately(3000 / 1e12, 10e-12)
expect(result.fillVolume).toBe(baseAmount)
})

it('should simulate a buy market order with quote amount', () => {
const quoteAmount = parseUnits('12000', 6) // 12000 USDC
const baseAmount = outboundFromInbound(
book.asks[0]!.offer.tick,
quoteAmount,
)
const fee = (baseAmount * book.asksConfig.fee) / 10_000n

const result = marketOrderSimulation({
book,
bs: BS.buy,
quote: quoteAmount, // 12000 USDC
})

expect(result.baseAmount).toBe(baseAmount - fee)
expect(result.quoteAmount).toBe(quoteAmount)
expect(result.gas).toBe(KANDEL_GASREQ + book.asksConfig.offer_gasbase)
expect(result.feePaid).toBe(fee)
expect(result.maxTickEncountered).toBe(book.asks[0]!.offer.tick)
expect(result.minSlippage).toBe(0)
expect(result.fillWants).toBe(false)
expect(result.rawPrice).approximately(3000 / 1e12, 10e-12)
expect(result.fillVolume).toBe(quoteAmount)
})

it('should simulate a sell order with quote amount', () => {
const quoteAmount = parseUnits('12000', 6) // 12000 USDC
const baseAmount = inboundFromOutbound(
book.bids[0]!.offer.tick,
quoteAmount,
)
const fee = (quoteAmount * book.bidsConfig.fee) / 10_000n

const result = marketOrderSimulation({
book,
bs: BS.sell,
quote: quoteAmount, // 12000 USDC
})

expect(result.baseAmount).toBe(baseAmount)
expect(result.quoteAmount).toBe(quoteAmount - fee)
expect(result.gas).toBe(KANDEL_GASREQ + book.bidsConfig.offer_gasbase)
expect(result.feePaid).toBe(fee)
expect(result.maxTickEncountered).toBe(book.bids[0]!.offer.tick)
expect(result.minSlippage).toBe(0)
expect(result.fillWants).toBe(true)
expect(result.rawPrice).approximately(3000 / 1e12, 10e-12)
expect(result.fillVolume).toBe(quoteAmount)
})

it('should throw an error if neither base nor quote is specified', () => {
expect(() =>
// @ts-expect-error
marketOrderSimulation({
book,
bs: BS.buy,
}),
).toThrow('either base or quote must be specified')
})
})
51 changes: 32 additions & 19 deletions src/lib/market-order-simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,15 @@ export type RawMarketOrderSimulationResult = {
export function rawMarketOrderSimulation(
params: RawMarketOrderSimulationParams,
): RawMarketOrderSimulationResult {
const {
let {
orderBook,
fillVolume: _fillVolume,
fillVolume,
localConfig,
globalConfig,
fillWants = true,
maxTick = MAX_TICK,
} = params

// if fillWants is true, then we need to multiply the fillVolume by 10_000n / (10_000n - fee) in order to account for the fee
let fillVolume = fillWants
? (_fillVolume * 10_000n) / (10_000n - localConfig.fee)
: _fillVolume

const result: RawMarketOrderSimulationResult = {
totalGot: 0n,
totalGave: 0n,
Expand All @@ -84,18 +79,36 @@ export function rawMarketOrderSimulation(
i < globalConfig.maxRecursionDepth;
i++
) {
const offer = orderBook[i]!
if (offer.offer.tick > maxTick) break
const maxGot = fillWants
? fillVolume
: outboundFromInbound(offer.offer.tick, fillVolume)
const got = maxGot < offer.offer.gives ? maxGot : offer.offer.gives
const gave = inboundFromOutbound(offer.offer.tick, got)
result.totalGot += got
result.totalGave += gave
result.gas += localConfig.offer_gasbase + offer.detail.gasreq
result.maxTickEncountered = offer.offer.tick
fillVolume -= fillWants ? got : gave
if (orderBook[i]!.offer.tick > maxTick) break

const offerGives = orderBook[i]!.offer.gives
const offerWants = inboundFromOutbound(orderBook[i]!.offer.tick, offerGives)

result.gas += localConfig.offer_gasbase + orderBook[i]!.detail.gasreq
result.maxTickEncountered = orderBook[i]!.offer.tick

let takerWants = 0n
let takerGives = 0n

if (
(fillWants && offerGives <= fillVolume) ||
(!fillWants && offerWants <= fillVolume)
) {
// We can take the entire offer
takerWants = offerGives
takerGives = offerWants
} else if (fillWants) {
takerWants = fillVolume
takerGives = inboundFromOutbound(orderBook[i]!.offer.tick, fillVolume)
} else {
takerWants = outboundFromInbound(orderBook[i]!.offer.tick, fillVolume)
takerGives = fillVolume
}

result.totalGot += takerWants
result.totalGave += takerGives

fillVolume -= fillWants ? takerWants : takerGives
}

result.feePaid = (result.totalGot * localConfig.fee) / 10_000n
Expand Down

0 comments on commit 61623e3

Please sign in to comment.