UNPKG

@drift-labs/sdk

Version:
1,035 lines • 55.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DLOB = void 0; const NodeList_1 = require("./NodeList"); const anchor_1 = require("@coral-xyz/anchor"); const numericConstants_1 = require("../constants/numericConstants"); const userName_1 = require("../userName"); const orders_1 = require("../math/orders"); const types_1 = require("../types"); const userStatus_1 = require("../math/userStatus"); const web3_js_1 = require("@solana/web3.js"); const exchangeStatus_1 = require("../math/exchangeStatus"); const orderBookLevels_1 = require("./orderBookLevels"); const auction_1 = require("../math/auction"); const conversion_1 = require("../math/conversion"); const SUPPORTED_ORDER_TYPES = [ 'market', 'limit', 'triggerMarket', 'triggerLimit', 'oracle', ]; class DLOB { constructor(protectedMakerParamsMap) { this.openOrders = new Map(); this.orderLists = new Map(); this.maxSlotForRestingLimitOrders = 0; this.initialized = false; this.protectedMakerParamsMap = protectedMakerParamsMap || { perp: new Map(), spot: new Map(), }; this.init(); } init() { this.openOrders.set('perp', new Set()); this.openOrders.set('spot', new Set()); this.orderLists.set('perp', new Map()); this.orderLists.set('spot', new Map()); } clear() { for (const marketType of this.openOrders.keys()) { this.openOrders.get(marketType).clear(); } this.openOrders.clear(); for (const marketType of this.orderLists.keys()) { for (const marketIndex of this.orderLists.get(marketType).keys()) { const marketNodeLists = this.orderLists .get(marketType) .get(marketIndex); for (const side of Object.keys(marketNodeLists)) { for (const orderType of Object.keys(marketNodeLists[side])) { marketNodeLists[side][orderType].clear(); } } } } this.orderLists.clear(); this.maxSlotForRestingLimitOrders = 0; this.init(); } /** * initializes a new DLOB instance * * @returns a promise that resolves when the DLOB is initialized */ async initFromUserMap(userMap, slot) { var _a; if (this.initialized) { return false; } // initialize the dlob with the user map for (const user of userMap.values()) { const userAccount = user.getUserAccount(); const userAccountPubkey = user.getUserAccountPublicKey(); const userAccountPubkeyString = userAccountPubkey.toString(); const protectedMaker = (0, userStatus_1.isUserProtectedMaker)(userAccount); for (const order of userAccount.orders) { let baseAssetAmount = order.baseAssetAmount; if (order.reduceOnly) { const existingBaseAmount = ((_a = userAccount.perpPositions.find((pos) => pos.marketIndex === order.marketIndex && pos.openOrders > 0)) === null || _a === void 0 ? void 0 : _a.baseAssetAmount) || numericConstants_1.ZERO; baseAssetAmount = (0, orders_1.calculateOrderBaseAssetAmount)(order, existingBaseAmount); } this.insertOrder(order, userAccountPubkeyString, slot, protectedMaker, baseAssetAmount); } } this.initialized = true; return true; } insertOrder(order, userAccount, slot, isUserProtectedMaker, baseAssetAmount, onInsert) { var _a; if (!(0, types_1.isVariant)(order.status, 'open')) { return; } if (!(0, types_1.isOneOfVariant)(order.orderType, SUPPORTED_ORDER_TYPES)) { return; } const marketType = (0, types_1.getVariant)(order.marketType); if (!this.orderLists.get(marketType).has(order.marketIndex)) { this.addOrderList(marketType, order.marketIndex); } if ((0, types_1.isVariant)(order.status, 'open')) { this.openOrders .get(marketType) .add((0, NodeList_1.getOrderSignature)(order.orderId, userAccount)); } (_a = this.getListForOnChainOrder(order, slot, isUserProtectedMaker)) === null || _a === void 0 ? void 0 : _a.insert(order, marketType, userAccount, isUserProtectedMaker, this.protectedMakerParamsMap[marketType].get(order.marketIndex), baseAssetAmount); if (onInsert) { onInsert(); } } insertSignedMsgOrder(order, userAccount, isUserProtectedMaker, baseAssetAmount, onInsert) { const marketType = (0, types_1.getVariant)(order.marketType); const marketIndex = order.marketIndex; const bidOrAsk = (0, types_1.isVariant)(order.direction, 'long') ? 'bid' : 'ask'; if (!this.orderLists.get(marketType).has(order.marketIndex)) { this.addOrderList(marketType, order.marketIndex); } this.openOrders .get(marketType) .add((0, NodeList_1.getOrderSignature)(order.orderId, userAccount)); this.orderLists .get(marketType) .get(marketIndex) .signedMsg[bidOrAsk].insert(order, marketType, userAccount, isUserProtectedMaker, this.protectedMakerParamsMap[marketType].get(order.marketIndex), baseAssetAmount); if (onInsert) { onInsert(); } } addOrderList(marketType, marketIndex) { this.orderLists.get(marketType).set(marketIndex, { restingLimit: { ask: new NodeList_1.NodeList('restingLimit', 'asc'), bid: new NodeList_1.NodeList('restingLimit', 'desc'), }, floatingLimit: { ask: new NodeList_1.NodeList('floatingLimit', 'asc'), bid: new NodeList_1.NodeList('floatingLimit', 'desc'), }, protectedFloatingLimit: { ask: new NodeList_1.NodeList('protectedFloatingLimit', 'asc'), bid: new NodeList_1.NodeList('protectedFloatingLimit', 'desc'), }, takingLimit: { ask: new NodeList_1.NodeList('takingLimit', 'asc'), bid: new NodeList_1.NodeList('takingLimit', 'asc'), // always sort ascending for market orders }, market: { ask: new NodeList_1.NodeList('market', 'asc'), bid: new NodeList_1.NodeList('market', 'asc'), // always sort ascending for market orders }, trigger: { above: new NodeList_1.NodeList('trigger', 'asc'), below: new NodeList_1.NodeList('trigger', 'desc'), }, signedMsg: { ask: new NodeList_1.NodeList('signedMsg', 'asc'), bid: new NodeList_1.NodeList('signedMsg', 'asc'), }, }); } delete(order, userAccount, slot, isUserProtectedMaker, onDelete) { var _a; if (!(0, types_1.isVariant)(order.status, 'open')) { return; } this.updateRestingLimitOrders(slot); (_a = this.getListForOnChainOrder(order, slot, isUserProtectedMaker)) === null || _a === void 0 ? void 0 : _a.remove(order, userAccount.toString()); if (onDelete) { onDelete(); } } getListForOnChainOrder(order, slot, isProtectedMaker) { const isInactiveTriggerOrder = (0, orders_1.mustBeTriggered)(order) && !(0, orders_1.isTriggered)(order); let type; if (isInactiveTriggerOrder) { type = 'trigger'; } else if ((0, types_1.isOneOfVariant)(order.orderType, ['market', 'triggerMarket', 'oracle'])) { type = 'market'; } else if (order.oraclePriceOffset !== 0) { type = isProtectedMaker ? 'protectedFloatingLimit' : 'floatingLimit'; } else { const isResting = (0, orders_1.isRestingLimitOrder)(order, slot); type = isResting ? 'restingLimit' : 'takingLimit'; } let subType; if (isInactiveTriggerOrder) { subType = (0, types_1.isVariant)(order.triggerCondition, 'above') ? 'above' : 'below'; } else { subType = (0, types_1.isVariant)(order.direction, 'long') ? 'bid' : 'ask'; } const marketType = (0, types_1.getVariant)(order.marketType); if (!this.orderLists.has(marketType)) { return undefined; } return this.orderLists.get(marketType).get(order.marketIndex)[type][subType]; } updateRestingLimitOrders(slot) { if (slot <= this.maxSlotForRestingLimitOrders) { return; } this.maxSlotForRestingLimitOrders = slot; this.updateRestingLimitOrdersForMarketType(slot, 'perp'); this.updateRestingLimitOrdersForMarketType(slot, 'spot'); } updateRestingLimitOrdersForMarketType(slot, marketTypeStr) { for (const [_, nodeLists] of this.orderLists.get(marketTypeStr)) { const nodesToUpdate = []; for (const node of nodeLists.takingLimit.ask.getGenerator()) { if (!(0, orders_1.isRestingLimitOrder)(node.order, slot)) { continue; } nodesToUpdate.push({ side: 'ask', node, }); } for (const node of nodeLists.takingLimit.bid.getGenerator()) { if (!(0, orders_1.isRestingLimitOrder)(node.order, slot)) { continue; } nodesToUpdate.push({ side: 'bid', node, }); } for (const nodeToUpdate of nodesToUpdate) { const { side, node } = nodeToUpdate; nodeLists.takingLimit[side].remove(node.order, node.userAccount); nodeLists.restingLimit[side].insert(node.order, marketTypeStr, node.userAccount, node.isProtectedMaker, this.protectedMakerParamsMap[marketTypeStr].get(node.order.marketIndex)); } } } getOrder(orderId, userAccount) { const orderSignature = (0, NodeList_1.getOrderSignature)(orderId, userAccount.toString()); for (const nodeList of this.getNodeLists()) { const node = nodeList.get(orderSignature); if (node) { return node.order; } } return undefined; } findNodesToFill(marketIndex, fallbackBid, fallbackAsk, slot, ts, marketType, oraclePriceData, stateAccount, marketAccount) { if ((0, exchangeStatus_1.fillPaused)(stateAccount, marketAccount)) { return []; } const isAmmPaused = (0, exchangeStatus_1.ammPaused)(stateAccount, marketAccount); const minAuctionDuration = (0, types_1.isVariant)(marketType, 'perp') ? stateAccount.minPerpAuctionDuration : 0; const { makerRebateNumerator, makerRebateDenominator } = this.getMakerRebate(marketType, stateAccount, marketAccount); const takingOrderNodesToFill = this.findTakingNodesToFill(marketIndex, slot, marketType, oraclePriceData, isAmmPaused, minAuctionDuration, fallbackAsk, fallbackBid); const restingLimitOrderNodesToFill = this.findRestingLimitOrderNodesToFill(marketIndex, slot, marketType, oraclePriceData, isAmmPaused, minAuctionDuration, makerRebateNumerator, makerRebateDenominator, fallbackAsk, fallbackBid); // get expired market nodes const expiredNodesToFill = this.findExpiredNodesToFill(marketIndex, ts, marketType, new anchor_1.BN(slot)); const stepSize = (0, types_1.isVariant)(marketType, 'perp') ? marketAccount.amm.orderStepSize : marketAccount.orderStepSize; const cancelReduceOnlyNodesToFill = this.findUnfillableReduceOnlyOrdersToCancel(marketIndex, marketType, stepSize); return this.mergeNodesToFill(restingLimitOrderNodesToFill, takingOrderNodesToFill) .concat(expiredNodesToFill) .concat(cancelReduceOnlyNodesToFill); } getMakerRebate(marketType, stateAccount, marketAccount) { let makerRebateNumerator; let makerRebateDenominator; if ((0, types_1.isVariant)(marketType, 'perp')) { makerRebateNumerator = stateAccount.perpFeeStructure.feeTiers[0].makerRebateNumerator; makerRebateDenominator = stateAccount.perpFeeStructure.feeTiers[0].makerRebateDenominator; } else { makerRebateNumerator = stateAccount.spotFeeStructure.feeTiers[0].makerRebateNumerator; makerRebateDenominator = stateAccount.spotFeeStructure.feeTiers[0].makerRebateDenominator; } // @ts-ignore const feeAdjustment = marketAccount.feeAdjustment || 0; if (feeAdjustment !== 0) { makerRebateNumerator += (makerRebateNumerator * feeAdjustment) / 100; } return { makerRebateNumerator, makerRebateDenominator }; } mergeNodesToFill(restingLimitOrderNodesToFill, takingOrderNodesToFill) { const mergedNodesToFill = new Map(); const mergeNodesToFillHelper = (nodesToFillArray) => { nodesToFillArray.forEach((nodeToFill) => { const nodeSignature = (0, NodeList_1.getOrderSignature)(nodeToFill.node.order.orderId, nodeToFill.node.userAccount); if (!mergedNodesToFill.has(nodeSignature)) { mergedNodesToFill.set(nodeSignature, { node: nodeToFill.node, makerNodes: [], }); } if (nodeToFill.makerNodes) { mergedNodesToFill .get(nodeSignature) .makerNodes.push(...nodeToFill.makerNodes); } }); }; mergeNodesToFillHelper(restingLimitOrderNodesToFill); mergeNodesToFillHelper(takingOrderNodesToFill); return Array.from(mergedNodesToFill.values()); } findRestingLimitOrderNodesToFill(marketIndex, slot, marketType, oraclePriceData, isAmmPaused, minAuctionDuration, makerRebateNumerator, makerRebateDenominator, fallbackAsk, fallbackBid) { const nodesToFill = new Array(); const crossingNodes = this.findCrossingRestingLimitOrders(marketIndex, slot, marketType, oraclePriceData); for (const crossingNode of crossingNodes) { nodesToFill.push(crossingNode); } if (fallbackBid && !isAmmPaused) { const askGenerator = this.getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData); const fallbackBidWithBuffer = fallbackBid.sub(fallbackBid.muln(makerRebateNumerator).divn(makerRebateDenominator)); const asksCrossingFallback = this.findNodesCrossingFallbackLiquidity(marketType, slot, oraclePriceData, askGenerator, (askPrice) => { return askPrice.lte(fallbackBidWithBuffer); }, minAuctionDuration); for (const askCrossingFallback of asksCrossingFallback) { nodesToFill.push(askCrossingFallback); } } if (fallbackAsk && !isAmmPaused) { const bidGenerator = this.getRestingLimitBids(marketIndex, slot, marketType, oraclePriceData); const fallbackAskWithBuffer = fallbackAsk.add(fallbackAsk.muln(makerRebateNumerator).divn(makerRebateDenominator)); const bidsCrossingFallback = this.findNodesCrossingFallbackLiquidity(marketType, slot, oraclePriceData, bidGenerator, (bidPrice) => { return bidPrice.gte(fallbackAskWithBuffer); }, minAuctionDuration); for (const bidCrossingFallback of bidsCrossingFallback) { nodesToFill.push(bidCrossingFallback); } } return nodesToFill; } findTakingNodesToFill(marketIndex, slot, marketType, oraclePriceData, isAmmPaused, minAuctionDuration, fallbackAsk, fallbackBid) { const nodesToFill = new Array(); let takingOrderGenerator = this.getTakingAsks(marketIndex, marketType, slot, oraclePriceData); const takingAsksCrossingBids = this.findTakingNodesCrossingMakerNodes(marketIndex, slot, marketType, oraclePriceData, takingOrderGenerator, this.getRestingLimitBids.bind(this), (takerPrice, makerPrice) => { if ((0, types_1.isVariant)(marketType, 'spot')) { if (takerPrice === undefined) { return false; } if (fallbackBid && makerPrice.lt(fallbackBid)) { return false; } } return takerPrice === undefined || takerPrice.lte(makerPrice); }); for (const takingAskCrossingBid of takingAsksCrossingBids) { nodesToFill.push(takingAskCrossingBid); } if (fallbackBid && !isAmmPaused) { takingOrderGenerator = this.getTakingAsks(marketIndex, marketType, slot, oraclePriceData); const takingAsksCrossingFallback = this.findNodesCrossingFallbackLiquidity(marketType, slot, oraclePriceData, takingOrderGenerator, (takerPrice) => { return takerPrice === undefined || takerPrice.lte(fallbackBid); }, minAuctionDuration); for (const takingAskCrossingFallback of takingAsksCrossingFallback) { nodesToFill.push(takingAskCrossingFallback); } } takingOrderGenerator = this.getTakingBids(marketIndex, marketType, slot, oraclePriceData); const takingBidsToFill = this.findTakingNodesCrossingMakerNodes(marketIndex, slot, marketType, oraclePriceData, takingOrderGenerator, this.getRestingLimitAsks.bind(this), (takerPrice, makerPrice) => { if ((0, types_1.isVariant)(marketType, 'spot')) { if (takerPrice === undefined) { return false; } if (fallbackAsk && makerPrice.gt(fallbackAsk)) { return false; } } return takerPrice === undefined || takerPrice.gte(makerPrice); }); for (const takingBidToFill of takingBidsToFill) { nodesToFill.push(takingBidToFill); } if (fallbackAsk && !isAmmPaused) { takingOrderGenerator = this.getTakingBids(marketIndex, marketType, slot, oraclePriceData); const takingBidsCrossingFallback = this.findNodesCrossingFallbackLiquidity(marketType, slot, oraclePriceData, takingOrderGenerator, (takerPrice) => { return takerPrice === undefined || takerPrice.gte(fallbackAsk); }, minAuctionDuration); for (const marketBidCrossingFallback of takingBidsCrossingFallback) { nodesToFill.push(marketBidCrossingFallback); } } return nodesToFill; } findTakingNodesCrossingMakerNodes(marketIndex, slot, marketType, oraclePriceData, takerNodeGenerator, makerNodeGeneratorFn, doesCross) { const nodesToFill = new Array(); for (const takerNode of takerNodeGenerator) { const makerNodeGenerator = makerNodeGeneratorFn(marketIndex, slot, marketType, oraclePriceData); for (const makerNode of makerNodeGenerator) { // Can't match orders from the same user const sameUser = takerNode.userAccount === makerNode.userAccount; if (sameUser) { continue; } const makerPrice = makerNode.getPrice(oraclePriceData, slot); const takerPrice = takerNode.getPrice(oraclePriceData, slot); const ordersCross = doesCross(takerPrice, makerPrice); if (!ordersCross) { // market orders aren't sorted by price, they are sorted by time, so we need to traverse // through all of em break; } nodesToFill.push({ node: takerNode, makerNodes: [makerNode], }); const makerOrder = makerNode.order; const takerOrder = takerNode.order; const makerBaseRemaining = makerOrder.baseAssetAmount.sub(makerOrder.baseAssetAmountFilled); const takerBaseRemaining = takerOrder.baseAssetAmount.sub(takerOrder.baseAssetAmountFilled); const baseFilled = anchor_1.BN.min(makerBaseRemaining, takerBaseRemaining); const newMakerOrder = { ...makerOrder }; newMakerOrder.baseAssetAmountFilled = makerOrder.baseAssetAmountFilled.add(baseFilled); this.getListForOnChainOrder(newMakerOrder, slot, makerNode.isProtectedMaker).update(newMakerOrder, makerNode.userAccount); const newTakerOrder = { ...takerOrder }; newTakerOrder.baseAssetAmountFilled = takerOrder.baseAssetAmountFilled.add(baseFilled); if (takerNode.isSignedMsg) { const marketTypeStr = (0, types_1.getVariant)(marketType); const orderList = (0, types_1.isVariant)(takerOrder.direction, 'long') ? this.orderLists.get(marketTypeStr).get(marketIndex).signedMsg.bid : this.orderLists.get(marketTypeStr).get(marketIndex).signedMsg.ask; orderList.update(newTakerOrder, takerNode.userAccount); } else { this.getListForOnChainOrder(newTakerOrder, slot, takerNode.isProtectedMaker).update(newTakerOrder, takerNode.userAccount); } if (newTakerOrder.baseAssetAmountFilled.eq(takerOrder.baseAssetAmount)) { break; } } } return nodesToFill; } findNodesCrossingFallbackLiquidity(marketType, slot, oraclePriceData, nodeGenerator, doesCross, minAuctionDuration) { var _a; const nodesToFill = new Array(); let nextNode = nodeGenerator.next(); while (!nextNode.done) { const node = nextNode.value; if ((0, types_1.isVariant)(marketType, 'spot') && ((_a = node.order) === null || _a === void 0 ? void 0 : _a.postOnly)) { nextNode = nodeGenerator.next(); continue; } const nodePrice = (0, orders_1.getLimitPrice)(node.order, oraclePriceData, slot); // order crosses if there is no limit price or it crosses fallback price const crosses = doesCross(nodePrice); // fallback is available if auction is complete or it's a spot order const fallbackAvailable = (0, types_1.isVariant)(marketType, 'spot') || (0, auction_1.isFallbackAvailableLiquiditySource)(node.order, minAuctionDuration, slot); if (crosses && fallbackAvailable) { nodesToFill.push({ node: node, makerNodes: [], // filled by fallback }); } nextNode = nodeGenerator.next(); } return nodesToFill; } findExpiredNodesToFill(marketIndex, ts, marketType, slot) { const nodesToFill = new Array(); const marketTypeStr = (0, types_1.getVariant)(marketType); const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); if (!nodeLists) { return nodesToFill; } // All bids/asks that can expire // dont try to expire limit orders with tif as its inefficient use of blockspace const bidGenerators = [ nodeLists.takingLimit.bid.getGenerator(), nodeLists.restingLimit.bid.getGenerator(), nodeLists.floatingLimit.bid.getGenerator(), nodeLists.market.bid.getGenerator(), nodeLists.signedMsg.bid.getGenerator(), ]; const askGenerators = [ nodeLists.takingLimit.ask.getGenerator(), nodeLists.restingLimit.ask.getGenerator(), nodeLists.floatingLimit.ask.getGenerator(), nodeLists.market.ask.getGenerator(), nodeLists.signedMsg.ask.getGenerator(), ]; for (const bidGenerator of bidGenerators) { for (const bid of bidGenerator) { if (bid.isSignedMsg && slot.gt(bid.order.slot.addn(bid.order.auctionDuration))) { this.orderLists .get(marketTypeStr) .get(marketIndex) .signedMsg.bid.remove(bid.order, bid.userAccount); } else if ((0, orders_1.isOrderExpired)(bid.order, ts, true, 25)) { nodesToFill.push({ node: bid, makerNodes: [], }); } } } for (const askGenerator of askGenerators) { for (const ask of askGenerator) { if (ask.isSignedMsg && slot.gt(ask.order.slot.addn(ask.order.auctionDuration))) { this.orderLists .get(marketTypeStr) .get(marketIndex) .signedMsg.ask.remove(ask.order, ask.userAccount); } else if ((0, orders_1.isOrderExpired)(ask.order, ts, true, 25)) { nodesToFill.push({ node: ask, makerNodes: [], }); } } } return nodesToFill; } findUnfillableReduceOnlyOrdersToCancel(marketIndex, marketType, stepSize) { const nodesToFill = new Array(); const marketTypeStr = (0, types_1.getVariant)(marketType); const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); if (!nodeLists) { return nodesToFill; } const generators = [ nodeLists.takingLimit.bid.getGenerator(), nodeLists.restingLimit.bid.getGenerator(), nodeLists.floatingLimit.bid.getGenerator(), nodeLists.market.bid.getGenerator(), nodeLists.signedMsg.bid.getGenerator(), nodeLists.takingLimit.ask.getGenerator(), nodeLists.restingLimit.ask.getGenerator(), nodeLists.floatingLimit.ask.getGenerator(), nodeLists.market.ask.getGenerator(), nodeLists.signedMsg.ask.getGenerator(), nodeLists.trigger.above.getGenerator(), nodeLists.trigger.below.getGenerator(), ]; for (const generator of generators) { for (const node of generator) { if (!node.order.reduceOnly) { continue; } if (node.baseAssetAmount.lt(stepSize)) { nodesToFill.push({ node, makerNodes: [], }); } } } return nodesToFill; } *getTakingBids(marketIndex, marketType, slot, oraclePriceData, filterFcn) { const marketTypeStr = (0, types_1.getVariant)(marketType); const orderLists = this.orderLists.get(marketTypeStr).get(marketIndex); if (!orderLists) { return; } this.updateRestingLimitOrders(slot); const generatorList = [ orderLists.market.bid.getGenerator(), orderLists.takingLimit.bid.getGenerator(), this.signedMsgGenerator(orderLists.signedMsg.bid, (x) => !(0, orders_1.isRestingLimitOrder)(x.order, slot)), ]; yield* this.getBestNode(generatorList, oraclePriceData, slot, (bestNode, currentNode) => { return bestNode.order.slot.lt(currentNode.order.slot); }, filterFcn); } *getTakingAsks(marketIndex, marketType, slot, oraclePriceData, filterFcn) { const marketTypeStr = (0, types_1.getVariant)(marketType); const orderLists = this.orderLists.get(marketTypeStr).get(marketIndex); if (!orderLists) { return; } this.updateRestingLimitOrders(slot); const generatorList = [ orderLists.market.ask.getGenerator(), orderLists.takingLimit.ask.getGenerator(), this.signedMsgGenerator(orderLists.signedMsg.ask, (x) => !(0, orders_1.isRestingLimitOrder)(x.order, slot)), ]; yield* this.getBestNode(generatorList, oraclePriceData, slot, (bestNode, currentNode) => { return bestNode.order.slot.lt(currentNode.order.slot); }, filterFcn); } *signedMsgGenerator(signedMsgOrderList, filter) { for (const signedMsgOrder of signedMsgOrderList.getGenerator()) { if (filter(signedMsgOrder)) { yield signedMsgOrder; } } } *getBestNode(generatorList, oraclePriceData, slot, compareFcn, filterFcn) { const generators = generatorList.map((generator) => { return { next: generator.next(), generator, }; }); let sideExhausted = false; while (!sideExhausted) { const bestGenerator = generators.reduce((bestGenerator, currentGenerator) => { if (currentGenerator.next.done) { return bestGenerator; } if (bestGenerator.next.done) { return currentGenerator; } const bestValue = bestGenerator.next.value; const currentValue = currentGenerator.next.value; return compareFcn(bestValue, currentValue, slot, oraclePriceData) ? bestGenerator : currentGenerator; }); if (!bestGenerator.next.done) { // skip this node if it's already completely filled if (bestGenerator.next.value.isBaseFilled()) { bestGenerator.next = bestGenerator.generator.next(); continue; } if (filterFcn && !filterFcn(bestGenerator.next.value)) { bestGenerator.next = bestGenerator.generator.next(); continue; } yield bestGenerator.next.value; bestGenerator.next = bestGenerator.generator.next(); } else { sideExhausted = true; } } } *getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData, filterFcn) { if ((0, types_1.isVariant)(marketType, 'spot') && !oraclePriceData) { throw new Error('Must provide OraclePriceData to get spot asks'); } this.updateRestingLimitOrders(slot); const marketTypeStr = (0, types_1.getVariant)(marketType); const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); if (!nodeLists) { return; } const generatorList = [ nodeLists.restingLimit.ask.getGenerator(), nodeLists.floatingLimit.ask.getGenerator(), nodeLists.protectedFloatingLimit.ask.getGenerator(), this.signedMsgGenerator(nodeLists.signedMsg.ask, (x) => (0, orders_1.isRestingLimitOrder)(x.order, slot)), ]; yield* this.getBestNode(generatorList, oraclePriceData, slot, (bestNode, currentNode, slot, oraclePriceData) => { return bestNode .getPrice(oraclePriceData, slot) .lt(currentNode.getPrice(oraclePriceData, slot)); }, filterFcn); } *getRestingLimitBids(marketIndex, slot, marketType, oraclePriceData, filterFcn) { if ((0, types_1.isVariant)(marketType, 'spot') && !oraclePriceData) { throw new Error('Must provide OraclePriceData to get spot bids'); } this.updateRestingLimitOrders(slot); const marketTypeStr = (0, types_1.getVariant)(marketType); const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); if (!nodeLists) { return; } const generatorList = [ nodeLists.restingLimit.bid.getGenerator(), nodeLists.floatingLimit.bid.getGenerator(), nodeLists.protectedFloatingLimit.bid.getGenerator(), this.signedMsgGenerator(nodeLists.signedMsg.bid, (x) => (0, orders_1.isRestingLimitOrder)(x.order, slot)), ]; yield* this.getBestNode(generatorList, oraclePriceData, slot, (bestNode, currentNode, slot, oraclePriceData) => { return bestNode .getPrice(oraclePriceData, slot) .gt(currentNode.getPrice(oraclePriceData, slot)); }, filterFcn); } /** * This will look at both the taking and resting limit asks * @param marketIndex * @param fallbackAsk * @param slot * @param marketType * @param oraclePriceData * @param filterFcn */ *getAsks(marketIndex, _fallbackAsk, slot, marketType, oraclePriceData, filterFcn) { if ((0, types_1.isVariant)(marketType, 'spot') && !oraclePriceData) { throw new Error('Must provide OraclePriceData to get spot asks'); } const generatorList = [ this.getTakingAsks(marketIndex, marketType, slot, oraclePriceData), this.getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData), ]; yield* this.getBestNode(generatorList, oraclePriceData, slot, (bestNode, currentNode, slot, oraclePriceData) => { var _a, _b; const bestNodePrice = (_a = bestNode.getPrice(oraclePriceData, slot)) !== null && _a !== void 0 ? _a : numericConstants_1.ZERO; const currentNodePrice = (_b = currentNode.getPrice(oraclePriceData, slot)) !== null && _b !== void 0 ? _b : numericConstants_1.ZERO; if (bestNodePrice.eq(currentNodePrice)) { return bestNode.order.slot.lt(currentNode.order.slot); } return bestNodePrice.lt(currentNodePrice); }, filterFcn); } /** * This will look at both the taking and resting limit bids * @param marketIndex * @param fallbackBid * @param slot * @param marketType * @param oraclePriceData * @param filterFcn */ *getBids(marketIndex, _fallbackBid, slot, marketType, oraclePriceData, filterFcn) { if ((0, types_1.isVariant)(marketType, 'spot') && !oraclePriceData) { throw new Error('Must provide OraclePriceData to get spot bids'); } const generatorList = [ this.getTakingBids(marketIndex, marketType, slot, oraclePriceData), this.getRestingLimitBids(marketIndex, slot, marketType, oraclePriceData), ]; yield* this.getBestNode(generatorList, oraclePriceData, slot, (bestNode, currentNode, slot, oraclePriceData) => { var _a, _b; const bestNodePrice = (_a = bestNode.getPrice(oraclePriceData, slot)) !== null && _a !== void 0 ? _a : numericConstants_1.BN_MAX; const currentNodePrice = (_b = currentNode.getPrice(oraclePriceData, slot)) !== null && _b !== void 0 ? _b : numericConstants_1.BN_MAX; if (bestNodePrice.eq(currentNodePrice)) { return bestNode.order.slot.lt(currentNode.order.slot); } return bestNodePrice.gt(currentNodePrice); }, filterFcn); } findCrossingRestingLimitOrders(marketIndex, slot, marketType, oraclePriceData) { const nodesToFill = new Array(); for (const askNode of this.getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData)) { const bidGenerator = this.getRestingLimitBids(marketIndex, slot, marketType, oraclePriceData); for (const bidNode of bidGenerator) { const bidPrice = bidNode.getPrice(oraclePriceData, slot); const askPrice = askNode.getPrice(oraclePriceData, slot); // orders don't cross if (bidPrice.lt(askPrice)) { break; } const bidOrder = bidNode.order; const askOrder = askNode.order; // Can't match orders from the same user const sameUser = bidNode.userAccount === askNode.userAccount; if (sameUser) { continue; } const makerAndTaker = this.determineMakerAndTaker(askNode, bidNode); // unable to match maker and taker due to post only or slot if (!makerAndTaker) { continue; } const { takerNode, makerNode } = makerAndTaker; const bidBaseRemaining = bidOrder.baseAssetAmount.sub(bidOrder.baseAssetAmountFilled); const askBaseRemaining = askOrder.baseAssetAmount.sub(askOrder.baseAssetAmountFilled); const baseFilled = anchor_1.BN.min(bidBaseRemaining, askBaseRemaining); const newBidOrder = { ...bidOrder }; newBidOrder.baseAssetAmountFilled = bidOrder.baseAssetAmountFilled.add(baseFilled); this.getListForOnChainOrder(newBidOrder, slot, bidNode.isProtectedMaker).update(newBidOrder, bidNode.userAccount); // ask completely filled const newAskOrder = { ...askOrder }; newAskOrder.baseAssetAmountFilled = askOrder.baseAssetAmountFilled.add(baseFilled); this.getListForOnChainOrder(newAskOrder, slot, askNode.isProtectedMaker).update(newAskOrder, askNode.userAccount); nodesToFill.push({ node: takerNode, makerNodes: [makerNode], }); if (newAskOrder.baseAssetAmount.eq(newAskOrder.baseAssetAmountFilled)) { break; } } } return nodesToFill; } determineMakerAndTaker(askNode, bidNode) { const askSlot = askNode.order.slot.add(new anchor_1.BN(askNode.order.auctionDuration)); const bidSlot = bidNode.order.slot.add(new anchor_1.BN(bidNode.order.auctionDuration)); if (bidNode.order.postOnly && askNode.order.postOnly) { return undefined; } else if (bidNode.order.postOnly) { return { takerNode: askNode, makerNode: bidNode, }; } else if (askNode.order.postOnly) { return { takerNode: bidNode, makerNode: askNode, }; } else if (askSlot.lte(bidSlot)) { return { takerNode: bidNode, makerNode: askNode, }; } else { return { takerNode: askNode, makerNode: bidNode, }; } } getBestAsk(marketIndex, slot, marketType, oraclePriceData) { const bestAsk = this.getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData).next().value; if (bestAsk) { return bestAsk.getPrice(oraclePriceData, slot); } return undefined; } getBestBid(marketIndex, slot, marketType, oraclePriceData) { const bestBid = this.getRestingLimitBids(marketIndex, slot, marketType, oraclePriceData).next().value; if (bestBid) { return bestBid.getPrice(oraclePriceData, slot); } return undefined; } *getStopLosses(marketIndex, marketType, direction) { const marketTypeStr = (0, types_1.getVariant)(marketType); const marketNodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); if ((0, types_1.isVariant)(direction, 'long') && marketNodeLists.trigger.below) { for (const node of marketNodeLists.trigger.below.getGenerator()) { if ((0, types_1.isVariant)(node.order.direction, 'short')) { yield node; } } } else if ((0, types_1.isVariant)(direction, 'short') && marketNodeLists.trigger.above) { for (const node of marketNodeLists.trigger.above.getGenerator()) { if ((0, types_1.isVariant)(node.order.direction, 'long')) { yield node; } } } } *getStopLossMarkets(marketIndex, marketType, direction) { for (const node of this.getStopLosses(marketIndex, marketType, direction)) { if ((0, types_1.isVariant)(node.order.orderType, 'triggerMarket')) { yield node; } } } *getStopLossLimits(marketIndex, marketType, direction) { for (const node of this.getStopLosses(marketIndex, marketType, direction)) { if ((0, types_1.isVariant)(node.order.orderType, 'triggerLimit')) { yield node; } } } *getTakeProfits(marketIndex, marketType, direction) { const marketTypeStr = (0, types_1.getVariant)(marketType); const marketNodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); if ((0, types_1.isVariant)(direction, 'long') && marketNodeLists.trigger.above) { for (const node of marketNodeLists.trigger.above.getGenerator()) { if ((0, types_1.isVariant)(node.order.direction, 'short')) { yield node; } } } else if ((0, types_1.isVariant)(direction, 'short') && marketNodeLists.trigger.below) { for (const node of marketNodeLists.trigger.below.getGenerator()) { if ((0, types_1.isVariant)(node.order.direction, 'long')) { yield node; } } } } *getTakeProfitMarkets(marketIndex, marketType, direction) { for (const node of this.getTakeProfits(marketIndex, marketType, direction)) { if ((0, types_1.isVariant)(node.order.orderType, 'triggerMarket')) { yield node; } } } *getTakeProfitLimits(marketIndex, marketType, direction) { for (const node of this.getTakeProfits(marketIndex, marketType, direction)) { if ((0, types_1.isVariant)(node.order.orderType, 'triggerLimit')) { yield node; } } } findNodesToTrigger(marketIndex, slot, triggerPrice, marketType, stateAccount) { if ((0, exchangeStatus_1.exchangePaused)(stateAccount)) { return []; } const nodesToTrigger = []; const marketTypeStr = (0, types_1.getVariant)(marketType); const marketNodeLists = this.orderLists.get(marketTypeStr).get(marketIndex); const triggerAboveList = marketNodeLists ? marketNodeLists.trigger.above : undefined; if (triggerAboveList) { for (const node of triggerAboveList.getGenerator()) { if (triggerPrice.gt(node.order.triggerPrice)) { nodesToTrigger.push({ node: node, }); } else { break; } } } const triggerBelowList = marketNodeLists ? marketNodeLists.trigger.below : undefined; if (triggerBelowList) { for (const node of triggerBelowList.getGenerator()) { if (triggerPrice.lt(node.order.triggerPrice)) { nodesToTrigger.push({ node: node, }); } else { break; } } } return nodesToTrigger; } printTop(driftClient, slotSubscriber, marketIndex, marketType) { if ((0, types_1.isVariant)(marketType, 'perp')) { const slot = slotSubscriber.getSlot(); const oraclePriceData = driftClient.getMMOracleDataForPerpMarket(marketIndex); const bestAsk = this.getBestAsk(marketIndex, slot, marketType, oraclePriceData); const bestBid = this.getBestBid(marketIndex, slot, marketType, oraclePriceData); const mid = bestAsk.add(bestBid).div(new anchor_1.BN(2)); const bidSpread = ((0, conversion_1.convertToNumber)(bestBid, numericConstants_1.PRICE_PRECISION) / (0, conversion_1.convertToNumber)(oraclePriceData.price, numericConstants_1.PRICE_PRECISION) - 1) * 100.0; const askSpread = ((0, conversion_1.convertToNumber)(bestAsk, numericConstants_1.PRICE_PRECISION) / (0, conversion_1.convertToNumber)(oraclePriceData.price, numericConstants_1.PRICE_PRECISION) - 1) * 100.0; const name = (0, userName_1.decodeName)(driftClient.getPerpMarketAccount(marketIndex).name); console.log(`Market ${name} Orders`); console.log(` Ask`, (0, conversion_1.convertToNumber)(bestAsk, numericConstants_1.PRICE_PRECISION).toFixed(3), `(${askSpread.toFixed(4)}%)`); console.log(` Mid`, (0, conversion_1.convertToNumber)(mid, numericConstants_1.PRICE_PRECISION).toFixed(3)); console.log(` Bid`, (0, conversion_1.convertToNumber)(bestBid, numericConstants_1.PRICE_PRECISION).toFixed(3), `(${bidSpread.toFixed(4)}%)`); } else if ((0, types_1.isVariant)(marketType, 'spot')) { const slot = slotSubscriber.getSlot(); const oraclePriceData = driftClient.getOracleDataForSpotMarket(marketIndex); const bestAsk = this.getBestAsk(marketIndex, slot, types_1.MarketType.SPOT, oraclePriceData); const bestBid = this.getBestBid(marketIndex, slot, types_1.MarketType.SPOT, oraclePriceData); const mid = bestAsk.add(bestBid).div(new anchor_1.BN(2)); const bidSpread = ((0, conversion_1.convertToNumber)(bestBid, numericConstants_1.PRICE_PRECISION) / (0, conversion_1.convertToNumber)(oraclePriceData.price, numericConstants_1.PRICE_PRECISION) - 1) * 100.0; const askSpread = ((0, conversion_1.convertToNumber)(bestAsk, numericConstants_1.PRICE_PRECISION) / (0, conversion_1.convertToNumber)(oraclePriceData.price, numericConstants_1.PRICE_PRECISION) - 1) * 100.0; const name = (0, userName_1.decodeName)(driftClient.getSpotMarketAccount(marketIndex).name); console.log(`Market ${name} Orders`); console.log(` Ask`, (0, conversion_1.convertToNumber)(bestAsk, numericConstants_1.PRICE_PRECISION).toFixed(3), `(${askSpread.toFixed(4)}%)`); console.log(` Mid`, (0, conversion_1.convertToNumber)(mid, numericConstants_1.PRICE_PRECISION).toFixed(3)); console.log(` Bid`, (0, conversion_1.convertToNumber)(bestBid, numericConstants_1.PRICE_PRECISION).toFixed(3), `(${bidSpread.toFixed(4)}%)`); } } getDLOBOrders() { const dlobOrders = []; for (const nodeList of this.getNodeLists()) { for (const node of nodeList.getGenerator()) { dlobOrders.push({ user: new web3_js_1.PublicKey(node.userAccount), order: node.order, }); } } return dlobOrders; } *getNodeLists() { for (const [_, nodeLists] of this.orderLists.get('perp')) { yield nodeLists.restingLimit.bid; yield nodeLists.restingLimit.ask; yield nodeLists.takingLimit.bid; yield nodeLists.takingLimit.ask; yield nodeLists.market.bid; yield nodeLists.market.ask; yield nodeLists.floatingLimit.bid; yield nodeLists.floatingLimit.ask; yield nodeLists.protectedFloatingLimit.bid; yield nodeLists.protectedFloatingLimit.ask; yield nodeLists.trigger.above; yield nodeLists.trigger.below; } for (const [_, nodeLists] of this.orderLists.get('spot')) { yield nodeLists.restingLimit.bid; yield nodeLists.restingLimit.ask; yield nodeLists.takingLimit.bid; yield nodeLists.takingLimit.ask; yield nodeLists.market.bid; yield nodeLists.market.ask; yield nodeLists.floatingLimit.bid; yield nodeLists.floatingLimit.ask; yield nodeLists.protectedFloatingLimit.bid; yield nodeLists.protectedFloatingLimit.ask; yield nodeLists.trigger.above; yield nodeLists.trigger.below; } } /** * Get an L2 view of the order book for a given market. * * @param marketIndex * @param marketType * @param slot * @param oraclePriceData * @param depth how many levels of the order book to return * @param fallbackL2Generators L2 generators for fallback liquidity e.g. vAMM {@link getVammL2Generator}, openbook {@link SerumSubscriber} */ getL2({ marketIndex, marketType, slot, oraclePriceData, depth, fallbackL2Generators = [], }) { const makerAskL2LevelGenerator = (0, orderBookLevels_1.getL2GeneratorFromDLOBNodes)(this.getRestingLimitAsks(marketIndex, slot, marketType, oraclePriceData), oraclePriceData, slot); const fallbackAskGenerators = fallbackL2Generators.map((fallbackL2Generator) => { return fallbackL2Generator.getL2Asks(); })