UNPKG

@drift-labs/sdk

Version:
405 lines (404 loc) • 16.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.uncrossL2 = exports.groupL2 = exports.getVammL2Generator = exports.createL2Levels = exports.mergeL2LevelGenerators = exports.getL2GeneratorFromDLOBNodes = exports.MAJORS_TOP_OF_BOOK_QUOTE_AMOUNTS = exports.DEFAULT_TOP_OF_BOOK_QUOTE_AMOUNTS = void 0; const anchor_1 = require("@coral-xyz/anchor"); const numericConstants_1 = require("../constants/numericConstants"); const amm_1 = require("../math/amm"); const exchangeStatus_1 = require("../math/exchangeStatus"); const types_1 = require("../types"); const orders_1 = require("../math/orders"); exports.DEFAULT_TOP_OF_BOOK_QUOTE_AMOUNTS = [ new anchor_1.BN(500).mul(numericConstants_1.QUOTE_PRECISION), new anchor_1.BN(1000).mul(numericConstants_1.QUOTE_PRECISION), new anchor_1.BN(2000).mul(numericConstants_1.QUOTE_PRECISION), new anchor_1.BN(5000).mul(numericConstants_1.QUOTE_PRECISION), ]; exports.MAJORS_TOP_OF_BOOK_QUOTE_AMOUNTS = [ new anchor_1.BN(5000).mul(numericConstants_1.QUOTE_PRECISION), new anchor_1.BN(10000).mul(numericConstants_1.QUOTE_PRECISION), new anchor_1.BN(20000).mul(numericConstants_1.QUOTE_PRECISION), new anchor_1.BN(50000).mul(numericConstants_1.QUOTE_PRECISION), ]; const INDICATIVE_QUOTES_PUBKEY = 'inDNdu3ML4vG5LNExqcwuCQtLcCU8KfK5YM2qYV3JJz'; /** * Get an {@link Generator<L2Level>} generator from a {@link Generator<DLOBNode>} * @param dlobNodes e.g. {@link DLOB#getRestingLimitAsks} or {@link DLOB#getRestingLimitBids} * @param oraclePriceData * @param slot */ function* getL2GeneratorFromDLOBNodes(dlobNodes, oraclePriceData, slot) { for (const dlobNode of dlobNodes) { const size = dlobNode.baseAssetAmount.sub(dlobNode.order.baseAssetAmountFilled); if (size.lte(numericConstants_1.ZERO)) { continue; } yield { size, price: dlobNode.getPrice(oraclePriceData, slot), sources: dlobNode.userAccount == INDICATIVE_QUOTES_PUBKEY ? { indicative: size } : { dlob: size, }, }; } } exports.getL2GeneratorFromDLOBNodes = getL2GeneratorFromDLOBNodes; function* mergeL2LevelGenerators(l2LevelGenerators, compare) { const generators = l2LevelGenerators.map((generator) => { return { generator, next: generator.next(), }; }); let next; do { next = generators.reduce((best, next) => { if (next.next.done) { return best; } if (!best) { return next; } if (compare(next.next.value, best.next.value)) { return next; } else { return best; } }, undefined); if (next) { yield next.next.value; next.next = next.generator.next(); } } while (next !== undefined); } exports.mergeL2LevelGenerators = mergeL2LevelGenerators; function createL2Levels(generator, depth) { const levels = []; for (const level of generator) { const price = level.price; const size = level.size; if (levels.length > 0 && levels[levels.length - 1].price.eq(price)) { const currentLevel = levels[levels.length - 1]; currentLevel.size = currentLevel.size.add(size); for (const [source, size] of Object.entries(level.sources)) { if (currentLevel.sources[source]) { currentLevel.sources[source] = currentLevel.sources[source].add(size); } else { currentLevel.sources[source] = size; } } } else if (levels.length === depth) { break; } else { levels.push(level); } } return levels; } exports.createL2Levels = createL2Levels; function getVammL2Generator({ marketAccount, mmOraclePriceData, numOrders, now = new anchor_1.BN(Math.floor(Date.now() / 1000)), topOfBookQuoteAmounts = [], latestSlot, }) { const updatedAmm = (0, amm_1.calculateUpdatedAMM)(marketAccount.amm, mmOraclePriceData); const paused = (0, exchangeStatus_1.isOperationPaused)(marketAccount.pausedOperations, types_1.PerpOperation.AMM_FILL); let [openBids, openAsks] = paused ? [numericConstants_1.ZERO, numericConstants_1.ZERO] : (0, amm_1.calculateMarketOpenBidAsk)(updatedAmm.baseAssetReserve, updatedAmm.minBaseAssetReserve, updatedAmm.maxBaseAssetReserve, updatedAmm.orderStepSize); if (openBids.lt(marketAccount.amm.minOrderSize.muln(2))) openBids = numericConstants_1.ZERO; if (openAsks.abs().lt(marketAccount.amm.minOrderSize.muln(2))) openAsks = numericConstants_1.ZERO; const [bidReserves, askReserves] = (0, amm_1.calculateSpreadReserves)(updatedAmm, mmOraclePriceData, now, (0, types_1.isVariant)(marketAccount.contractType, 'prediction'), latestSlot); const numBaseOrders = Math.max(1, numOrders - topOfBookQuoteAmounts.length); const commonOpts = { numOrders, numBaseOrders, mmOraclePriceData, orderTickSize: marketAccount.amm.orderTickSize, orderStepSize: marketAccount.amm.orderStepSize, pegMultiplier: updatedAmm.pegMultiplier, sqrtK: updatedAmm.sqrtK, topOfBookQuoteAmounts, }; const makeL2Gen = ({ openLiquidity, startReserves, swapDir, positionDir, }) => { return function* () { let count = 0; let topSize = numericConstants_1.ZERO; let size = openLiquidity.abs().divn(commonOpts.numBaseOrders); const amm = { ...startReserves, sqrtK: commonOpts.sqrtK, pegMultiplier: commonOpts.pegMultiplier, }; while (count < commonOpts.numOrders && size.gt(numericConstants_1.ZERO)) { let baseSwap = size; if (count < commonOpts.topOfBookQuoteAmounts.length) { const raw = commonOpts.topOfBookQuoteAmounts[count] .mul(numericConstants_1.AMM_TO_QUOTE_PRECISION_RATIO) .mul(numericConstants_1.PRICE_PRECISION) .div(commonOpts.mmOraclePriceData.price); baseSwap = (0, orders_1.standardizeBaseAssetAmount)(raw, commonOpts.orderStepSize); const remaining = openLiquidity.abs().sub(topSize); if (remaining.lt(baseSwap)) baseSwap = remaining; } if (baseSwap.isZero()) return; const [newQuoteRes, newBaseRes] = (0, amm_1.calculateAmmReservesAfterSwap)(amm, 'base', baseSwap, swapDir); const quoteSwapped = (0, amm_1.calculateQuoteAssetAmountSwapped)(amm.quoteAssetReserve.sub(newQuoteRes).abs(), amm.pegMultiplier, swapDir); const price = (0, orders_1.standardizePrice)(quoteSwapped.mul(numericConstants_1.BASE_PRECISION).div(baseSwap), commonOpts.orderTickSize, positionDir); amm.baseAssetReserve = newBaseRes; amm.quoteAssetReserve = newQuoteRes; if (count < commonOpts.topOfBookQuoteAmounts.length) { topSize = topSize.add(baseSwap); size = openLiquidity .abs() .sub(topSize) .divn(commonOpts.numBaseOrders); } yield { price, size: baseSwap, sources: { vamm: baseSwap } }; count++; } }; }; return { getL2Bids: makeL2Gen({ openLiquidity: openBids, startReserves: bidReserves, swapDir: types_1.SwapDirection.ADD, positionDir: types_1.PositionDirection.LONG, }), getL2Asks: makeL2Gen({ openLiquidity: openAsks, startReserves: askReserves, swapDir: types_1.SwapDirection.REMOVE, positionDir: types_1.PositionDirection.SHORT, }), }; } exports.getVammL2Generator = getVammL2Generator; function groupL2(l2, grouping, depth) { return { bids: groupL2Levels(l2.bids, grouping, types_1.PositionDirection.LONG, depth), asks: groupL2Levels(l2.asks, grouping, types_1.PositionDirection.SHORT, depth), slot: l2.slot, }; } exports.groupL2 = groupL2; function cloneL2Level(level) { if (!level) return level; return { price: level.price, size: level.size, sources: { ...level.sources }, }; } function groupL2Levels(levels, grouping, direction, depth) { const groupedLevels = []; for (const level of levels) { const price = (0, orders_1.standardizePrice)(level.price, grouping, direction); const size = level.size; if (groupedLevels.length > 0 && groupedLevels[groupedLevels.length - 1].price.eq(price)) { // Clones things so we don't mutate the original const currentLevel = cloneL2Level(groupedLevels[groupedLevels.length - 1]); currentLevel.size = currentLevel.size.add(size); for (const [source, size] of Object.entries(level.sources)) { if (currentLevel.sources[source]) { currentLevel.sources[source] = currentLevel.sources[source].add(size); } else { currentLevel.sources[source] = size; } } groupedLevels[groupedLevels.length - 1] = currentLevel; } else { const groupedLevel = { price: price, size, sources: level.sources, }; groupedLevels.push(groupedLevel); } if (groupedLevels.length === depth) { break; } } return groupedLevels; } /** * Method to merge bids or asks by price */ const mergeByPrice = (bidsOrAsks) => { const merged = new Map(); for (const level of bidsOrAsks) { const key = level.price.toString(); if (merged.has(key)) { const existing = merged.get(key); existing.size = existing.size.add(level.size); for (const [source, size] of Object.entries(level.sources)) { if (existing.sources[source]) { existing.sources[source] = existing.sources[source].add(size); } else { existing.sources[source] = size; } } } else { merged.set(key, cloneL2Level(level)); } } return Array.from(merged.values()); }; /** * The purpose of this function is uncross the L2 orderbook by modifying the bid/ask price at the top of the book * This will make the liquidity look worse but more intuitive (users familiar with clob get confused w temporarily * crossing book) * * Things to note about how it works: * - it will not uncross the user's liquidity * - it does the uncrossing by "shifting" the crossing liquidity to the nearest uncrossed levels. Thus the output liquidity maintains the same total size. * * @param bids * @param asks * @param oraclePrice * @param oracleTwap5Min * @param markTwap5Min * @param grouping * @param userBids * @param userAsks */ function uncrossL2(bids, asks, oraclePrice, oracleTwap5Min, markTwap5Min, grouping, userBids, userAsks) { // If there are no bids or asks, there is nothing to center if (bids.length === 0 || asks.length === 0) { return { bids, asks }; } // If the top of the book is already centered, there is nothing to do if (bids[0].price.lt(asks[0].price)) { return { bids, asks }; } const newBids = []; const newAsks = []; const updateLevels = (newPrice, oldLevel, levels) => { if (levels.length > 0 && levels[levels.length - 1].price.eq(newPrice)) { levels[levels.length - 1].size = levels[levels.length - 1].size.add(oldLevel.size); for (const [source, size] of Object.entries(oldLevel.sources)) { if (levels[levels.length - 1].sources[source]) { levels[levels.length - 1].sources = { ...levels[levels.length - 1].sources, [source]: levels[levels.length - 1].sources[source].add(size), }; } else { levels[levels.length - 1].sources[source] = size; } } } else { levels.push({ price: newPrice, size: oldLevel.size, sources: oldLevel.sources, }); } }; // This is the best estimate of the premium in the market vs oracle to filter crossing around const referencePrice = oraclePrice.add(markTwap5Min.sub(oracleTwap5Min)); let bidIndex = 0; let askIndex = 0; let maxBid; let minAsk; const getPriceAndSetBound = (newPrice, direction) => { if ((0, types_1.isVariant)(direction, 'long')) { maxBid = maxBid ? anchor_1.BN.min(maxBid, newPrice) : newPrice; return maxBid; } else { minAsk = minAsk ? anchor_1.BN.max(minAsk, newPrice) : newPrice; return minAsk; } }; while (bidIndex < bids.length || askIndex < asks.length) { const nextBid = cloneL2Level(bids[bidIndex]); const nextAsk = cloneL2Level(asks[askIndex]); if (!nextBid) { newAsks.push(nextAsk); askIndex++; continue; } if (!nextAsk) { newBids.push(nextBid); bidIndex++; continue; } if (userBids.has(nextBid.price.toString())) { newBids.push(nextBid); bidIndex++; continue; } if (userAsks.has(nextAsk.price.toString())) { newAsks.push(nextAsk); askIndex++; continue; } if (nextBid.price.gte(nextAsk.price)) { if (nextBid.price.gt(referencePrice) && nextAsk.price.gt(referencePrice)) { let newBidPrice = nextAsk.price.sub(grouping); newBidPrice = getPriceAndSetBound(newBidPrice, types_1.PositionDirection.LONG); updateLevels(newBidPrice, nextBid, newBids); bidIndex++; } else if (nextAsk.price.lt(referencePrice) && nextBid.price.lt(referencePrice)) { let newAskPrice = nextBid.price.add(grouping); newAskPrice = getPriceAndSetBound(newAskPrice, types_1.PositionDirection.SHORT); updateLevels(newAskPrice, nextAsk, newAsks); askIndex++; } else { let newBidPrice = referencePrice.sub(grouping); let newAskPrice = referencePrice.add(grouping); newBidPrice = getPriceAndSetBound(newBidPrice, types_1.PositionDirection.LONG); newAskPrice = getPriceAndSetBound(newAskPrice, types_1.PositionDirection.SHORT); updateLevels(newBidPrice, nextBid, newBids); updateLevels(newAskPrice, nextAsk, newAsks); bidIndex++; askIndex++; } } else { if (minAsk && nextAsk.price.lte(minAsk)) { const newAskPrice = getPriceAndSetBound(nextAsk.price, types_1.PositionDirection.SHORT); updateLevels(newAskPrice, nextAsk, newAsks); } else { newAsks.push(nextAsk); } askIndex++; if (maxBid && nextBid.price.gte(maxBid)) { const newBidPrice = getPriceAndSetBound(nextBid.price, types_1.PositionDirection.LONG); updateLevels(newBidPrice, nextBid, newBids); } else { newBids.push(nextBid); } bidIndex++; } } newBids.sort((a, b) => b.price.cmp(a.price)); newAsks.sort((a, b) => a.price.cmp(b.price)); const finalNewBids = mergeByPrice(newBids); const finalNewAsks = mergeByPrice(newAsks); return { bids: finalNewBids, asks: finalNewAsks, }; } exports.uncrossL2 = uncrossL2;