Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: market order simulation #130

Merged
merged 2 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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