UNPKG

@sangaman/xud

Version:
164 lines 8.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = __importDefault(require("assert")); const fastpriorityqueue_1 = __importDefault(require("fastpriorityqueue")); const enums_1 = require("../types/enums"); /** A class to represent a matching engine responsible for matching orders for a given trading pair according to their price and quantity. */ class MatchingEngine { constructor(logger, pairId) { this.logger = logger; this.pairId = pairId; /** * Match an order against its opposite queue, and optionally add the unmatched portion of the order to the queue. * @param discardRemaining whether to discard any unmatched portion of the order rather than add it to the queue * @returns a [[MatchingResult]] with the matches as well as the remaining, unmatched portion of the order */ this.matchOrAddOwnOrder = (order, discardRemaining) => { const isBuyOrder = order.quantity > 0; const addTo = isBuyOrder ? this.buyOrders : this.sellOrders; const matchingResult = this.match(order); if (matchingResult.remainingOrder && !discardRemaining) { addTo.add(matchingResult.remainingOrder); } return matchingResult; }; /** * Match an order against its opposite queue. * @returns a [[MatchingResult]] with the matches as well as the remaining, unmatched portion of the order */ this.match = (takerOrder) => { const isBuyOrder = takerOrder.quantity > 0; const matches = []; /** The unmatched remaining taker order, if there is still leftover quantity after matching is complete it will enter the queue. */ let remainingOrder = Object.assign({}, takerOrder); const matchAgainst = isBuyOrder ? this.sellOrders : this.buyOrders; const getMatchingQuantity = (remainingOrder, oppositeOrder) => isBuyOrder ? MatchingEngine.getMatchingQuantity(remainingOrder, oppositeOrder) : MatchingEngine.getMatchingQuantity(oppositeOrder, remainingOrder); // as long as we have remaining quantity to match and orders to match against, keep checking for matches while (remainingOrder && !matchAgainst.isEmpty()) { const oppositeOrder = matchAgainst.peek(); const matchingQuantity = getMatchingQuantity(remainingOrder, oppositeOrder); if (matchingQuantity <= 0) { // there's no match with the best available maker order, so end the matching routine break; } else { const makerOrder = matchAgainst.poll(); const makerOrderAbsQuantity = Math.abs(makerOrder.quantity); const remainingOrderAbsQuantity = Math.abs(remainingOrder.quantity); if (makerOrderAbsQuantity === matchingQuantity && remainingOrderAbsQuantity === matchingQuantity) { // order quantities are fully matching matches.push({ maker: makerOrder, taker: remainingOrder }); remainingOrder = undefined; } else if (remainingOrderAbsQuantity === matchingQuantity) { // maker order quantity is not sufficient. taker order will split const splitOrder = MatchingEngine.splitOrderByQuantity(makerOrder, matchingQuantity); matches.push({ maker: splitOrder.matched, taker: remainingOrder }); matchAgainst.add(splitOrder.remaining); remainingOrder = undefined; } else if (makerOrderAbsQuantity === matchingQuantity) { // taker order quantity is not sufficient. maker order will split const splitOrder = MatchingEngine.splitOrderByQuantity(remainingOrder, matchingQuantity); matches.push({ maker: makerOrder, taker: splitOrder.matched }); remainingOrder = splitOrder.remaining; } else { assert_1.default(false, 'matchingQuantity should not be lower than both orders quantity values'); } } } return { matches, remainingOrder }; }; this.addPeerOrder = (order) => { (order.quantity > 0 ? this.buyOrders : this.sellOrders).add(order); }; this.removeOwnOrder = (orderId) => { return this.removeOrder(orderId); }; this.removePeerOrder = (orderId, quantityToDecrease) => { const order = this.removeOrder(orderId); if (order && quantityToDecrease) { order.quantity = order.quantity - quantityToDecrease; this.addPeerOrder(order); // Return how much was removed return Object.assign({}, order, { quantity: quantityToDecrease }); } return order; }; /** * Remove all orders from the queue matching the given peer id. */ this.removePeerOrders = (peerPubKey) => { const callback = (order) => { return order.peerPubKey === peerPubKey; }; return [ ...this.buyOrders.removeMany(callback), ...this.sellOrders.removeMany(callback), ]; }; /** * Check whether the matching queue is empty. * @returns true if both the buy orders and sell orders queues are empty, otherwise false */ this.isEmpty = () => { return this.buyOrders.isEmpty() && this.sellOrders.isEmpty(); }; this.removeOrder = (orderId) => { return this.buyOrders.removeOne(order => order.id === orderId) || this.sellOrders.removeOne(order => order.id === orderId); }; this.buyOrders = MatchingEngine.createPriorityQueue(enums_1.OrderingDirection.DESC); this.sellOrders = MatchingEngine.createPriorityQueue(enums_1.OrderingDirection.ASC); } } MatchingEngine.createPriorityQueue = (orderingDirection) => { const comparator = MatchingEngine.getOrdersPriorityQueueComparator(orderingDirection); return new fastpriorityqueue_1.default(comparator); }; MatchingEngine.getOrdersPriorityQueueComparator = (orderingDirection) => { const directionComparator = orderingDirection === enums_1.OrderingDirection.ASC ? (a, b) => a < b : (a, b) => a > b; return (a, b) => { if (a.price === b.price) { return a.createdAt < b.createdAt; } else { return directionComparator(a.price, b.price); } }; }; /** * Get the matching quantity between two orders. * @returns the smaller of the quantity between the two orders if their price matches, 0 otherwise */ MatchingEngine.getMatchingQuantity = (buyOrder, sellOrder) => { if (buyOrder.price >= sellOrder.price) { return Math.min(buyOrder.quantity, sellOrder.quantity * -1); } else { return 0; } }; /** * Split an order by quantity into a matched portion and a remaining portion. */ MatchingEngine.splitOrderByQuantity = (order, matchingQuantity) => { const { quantity } = order; const absQuantity = Math.abs(quantity); assert_1.default(absQuantity > matchingQuantity, 'order abs quantity must be greater than matchingQuantity'); const direction = quantity / absQuantity; return { matched: Object.assign({}, order, { quantity: matchingQuantity * direction }), remaining: Object.assign({}, order, { quantity: quantity - (matchingQuantity * direction) }), }; }; exports.default = MatchingEngine; //# sourceMappingURL=MatchingEngine.js.map