UNPKG

xud

Version:
414 lines 22.9 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 events_1 = require("events"); const fastpriorityqueue_1 = __importDefault(require("fastpriorityqueue")); const enums_1 = require("../constants/enums"); const errors_1 = __importDefault(require("./errors")); const types_1 = require("./types"); /** * Represents a single trading pair in the order book. Responsible for managing all active orders * and for matching orders according to their price and quantity. */ let TradingPair = /** @class */ (() => { class TradingPair extends events_1.EventEmitter { constructor(logger, pairId, nomatching = false) { super(); this.logger = logger; this.pairId = pairId; this.nomatching = nomatching; /** * Adds a peer order for this trading pair. * @returns `true` if the order was added, `false` if it could not be added because there * already exists an order with the same order id */ this.addPeerOrder = (order) => { let peerOrdersMaps = this.peersOrders.get(order.peerPubKey); if (!peerOrdersMaps) { peerOrdersMaps = { buyMap: new Map(), sellMap: new Map(), }; this.peersOrders.set(order.peerPubKey, peerOrdersMaps); } return this.addOrder(order, peerOrdersMaps); }; /** * Adds an own order for this trading pair. * @returns `true` if the order was added, `false` if it could not be added because there * already exists an order with the same order id */ this.addOwnOrder = (order) => { return this.addOrder(order, this.ownOrders); }; /** * Attempts to add an order for this trading pair. * @returns `true` if the order was added, `false` if it could not be added because there * already exists an order with the same order id */ this.addOrder = (order, maps) => { const map = order.isBuy ? maps.buyMap : maps.sellMap; if (map.has(order.id)) { return false; } map.set(order.id, order); this.logger.trace(`order added: ${JSON.stringify(order)}`); if (!this.nomatching) { const queue = order.isBuy ? this.queues.buyQueue : this.queues.sellQueue; queue.add(order); } return true; }; /** * Removes all of a peer's orders. * @param peerPubKey the node pub key of the peer */ this.removePeerOrders = (peerPubKey) => { // if incoming peerPubKey is undefined or empty, don't even try to find it in order queues if (!peerPubKey) return []; const peerOrders = this.peersOrders.get(peerPubKey); if (!peerOrders) return []; if (!this.nomatching) { const callback = (order) => order.peerPubKey === peerPubKey; this.queues.buyQueue.removeMany(callback); this.queues.sellQueue.removeMany(callback); } this.peersOrders.delete(peerPubKey); return [...peerOrders.buyMap.values(), ...peerOrders.sellMap.values()]; }; /** * Removes all or part of a peer order. * @param quantityToRemove the quantity to remove, if undefined or if greater than or equal to the available * quantity then the entire order is removed * @returns the portion of the order that was removed, and a flag indicating whether the entire order was removed */ this.removePeerOrder = (orderId, peerPubKey, quantityToRemove) => { let peerOrdersMaps; if (peerPubKey) { peerOrdersMaps = this.peersOrders.get(peerPubKey); } else { // if not given a peerPubKey, we must check all peer order maps for the specified orderId for (const orderSidesMaps of this.peersOrders.values()) { if (orderSidesMaps.buyMap.has(orderId) || orderSidesMaps.sellMap.has(orderId)) { peerOrdersMaps = orderSidesMaps; break; } } } if (!peerOrdersMaps) { throw errors_1.default.ORDER_NOT_FOUND(orderId); } return this.removeOrder(orderId, peerOrdersMaps, quantityToRemove); }; /** * Removes all or part of an own order. * @param quantityToRemove the quantity to remove, if undefined or if greater than or equal to the available * quantity then the entire order is removed * @returns the portion of the order that was removed, and a flag indicating whether the entire order was removed */ this.removeOwnOrder = (orderId, quantityToRemove) => { return this.removeOrder(orderId, this.ownOrders, quantityToRemove); }; /** * Removes all or part of an order. * @param quantityToRemove the quantity to remove, if undefined or if greater than or equal to the available * quantity then the entire order is removed * @returns the portion of the order that was removed, and a flag indicating whether the entire order was removed */ this.removeOrder = (orderId, maps, quantityToRemove) => { assert_1.default(quantityToRemove === undefined || quantityToRemove > 0, 'quantityToRemove cannot be 0 or negative'); const order = maps.buyMap.get(orderId) || maps.sellMap.get(orderId); if (!order) { throw errors_1.default.ORDER_NOT_FOUND(orderId); } if (quantityToRemove && quantityToRemove < order.quantity) { const remainingQuantity = order.quantity - quantityToRemove; if (remainingQuantity < TradingPair.QUANTITY_DUST_LIMIT || (remainingQuantity * order.price) < TradingPair.QUANTITY_DUST_LIMIT) { // the remaining quantity doesn't meet the dust limit, so we remove the entire order this.logger.trace(`removing entire order ${orderId} because remaining quantity does not meet dust limit`); } else { // if quantityToRemove is below the order quantity but above dust limit, reduce the order quantity if (types_1.isOwnOrder(order)) { assert_1.default(quantityToRemove <= order.quantity - order.hold, 'cannot remove more than available quantity after holds'); } order.quantity = order.quantity - quantityToRemove; this.logger.trace(`order quantity reduced by ${quantityToRemove}: ${orderId}`); return { order: Object.assign(Object.assign({}, order), { quantity: quantityToRemove }), fullyRemoved: false }; } } // otherwise, remove the order entirely if (types_1.isOwnOrder(order)) { assert_1.default(order.hold === 0, 'cannot remove an order with a hold'); } const startingQuantity = order.quantity; order.quantity = 0; const map = order.isBuy ? maps.buyMap : maps.sellMap; map.delete(order.id); if (!this.nomatching) { const queue = order.isBuy ? this.queues.buyQueue : this.queues.sellQueue; queue.remove(order); } this.logger.trace(`order removed: ${orderId}`); return { order: Object.assign(Object.assign({}, order), { quantity: startingQuantity }), fullyRemoved: true }; }; this.getOrderMap = (order) => { if (types_1.isOwnOrder(order)) { return order.isBuy ? this.ownOrders.buyMap : this.ownOrders.sellMap; } else { const peerOrdersMaps = this.peersOrders.get(order.peerPubKey); if (!peerOrdersMaps) return; return order.isBuy ? peerOrdersMaps.buyMap : peerOrdersMaps.sellMap; } }; this.getOrders = (lists) => { return { buyArray: Array.from(lists.buyMap.values()), sellArray: Array.from(lists.sellMap.values()), }; }; this.getPeersOrders = () => { const res = { buyArray: [], sellArray: [] }; this.peersOrders.forEach((peerOrders) => { const peerOrdersArrs = this.getOrders(peerOrders); res.buyArray = res.buyArray.concat(peerOrdersArrs.buyArray); res.sellArray = res.sellArray.concat(peerOrdersArrs.sellArray); }); return res; }; this.getOwnOrders = () => { return this.getOrders(this.ownOrders); }; this.getOwnOrder = (orderId) => { const order = this.getOrder(orderId, this.ownOrders); if (!order) { throw errors_1.default.ORDER_NOT_FOUND(orderId); } return order; }; this.getPeerOrder = (orderId, peerPubKey) => { const peerOrders = this.peersOrders.get(peerPubKey); if (!peerOrders) { throw errors_1.default.ORDER_NOT_FOUND(orderId, peerPubKey); } const order = this.getOrder(orderId, peerOrders); if (!order) { throw errors_1.default.ORDER_NOT_FOUND(orderId, peerPubKey); } return order; }; this.getOrder = (orderId, maps) => { return maps.buyMap.get(orderId) || maps.sellMap.get(orderId); }; this.addOrderHold = (orderId, holdAmount) => { const order = this.getOwnOrder(orderId); if (holdAmount === undefined) { if (order.hold > 0) { // we can't put an entire order on hold if part of it is already on hold throw errors_1.default.QUANTITY_ON_HOLD(order.localId, order.hold); } order.hold = order.quantity; this.logger.trace(`placed entire order ${orderId} on hold`); } else { assert_1.default(holdAmount > 0); assert_1.default(order.hold + holdAmount <= order.quantity, 'the amount of an order on hold cannot exceed the available quantity'); order.hold += holdAmount; this.logger.trace(`added hold of ${holdAmount} on order ${orderId}`); } }; this.removeOrderHold = (orderId, holdAmount) => { const order = this.getOwnOrder(orderId); if (holdAmount === undefined) { assert_1.default(order.hold > 0); order.hold = 0; this.logger.trace(`removed entire hold on order ${orderId}`); } else { assert_1.default(holdAmount > 0); assert_1.default(order.hold >= holdAmount, 'cannot remove more than is currently on hold for an order'); order.hold -= holdAmount; this.logger.trace(`removed hold of ${holdAmount} on order ${orderId}`); } }; this.quoteBid = () => { var _a, _b, _c; return (_c = (_b = (_a = this.queues) === null || _a === void 0 ? void 0 : _a.buyQueue.peek()) === null || _b === void 0 ? void 0 : _b.price) !== null && _c !== void 0 ? _c : 0; }; this.quoteAsk = () => { var _a, _b, _c; return (_c = (_b = (_a = this.queues) === null || _a === void 0 ? void 0 : _a.sellQueue.peek()) === null || _b === void 0 ? void 0 : _b.price) !== null && _c !== void 0 ? _c : Number.POSITIVE_INFINITY; }; /** * Matches an order against its opposite queue. Matched maker orders are removed immediately. * @returns a [[MatchingResult]] with the matches as well as the remaining, unmatched portion of the order */ this.match = (takerOrder) => { assert_1.default(!this.nomatching); 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 queue = takerOrder.isBuy ? this.queues.sellQueue : this.queues.buyQueue; const queueRemovedOrdersWithHold = []; const getMatchingQuantity = (remainingOrder, oppositeOrder) => takerOrder.isBuy ? TradingPair.getMatchingQuantity(remainingOrder, oppositeOrder) : TradingPair.getMatchingQuantity(oppositeOrder, remainingOrder); // as long as we have remaining quantity to match and orders to match against, keep checking for matches while (remainingOrder && !queue.isEmpty()) { // get the best available maker order from the top of the queue const makerOrder = queue.peek(); const makerAvailableQuantityOrder = types_1.isOwnOrder(makerOrder) ? Object.assign(Object.assign({}, makerOrder), { quantity: makerOrder.quantity - makerOrder.hold, hold: 0 }) : makerOrder; const matchingQuantity = getMatchingQuantity(remainingOrder, makerAvailableQuantityOrder); if (matchingQuantity * makerOrder.price < TradingPair.QUANTITY_DUST_LIMIT) { // there's no match with the best available maker order OR there's a match // but it doesn't meet the dust minimum on both sides of the trade if (types_1.isOwnOrder(makerOrder) && makerOrder.hold > 0) { // part of this order is on hold, so put it aside and try to match the next order assert_1.default(queue.poll() === makerOrder); queueRemovedOrdersWithHold.push(makerOrder); } else { // there's no hold, so end the matching routine break; } break; } else { /** Whether the maker order is fully matched and should be removed from the queue. */ const makerFullyMatched = makerOrder.quantity === matchingQuantity; const makerAvailableQuantityFullyMatched = makerAvailableQuantityOrder.quantity === matchingQuantity; const remainingFullyMatched = remainingOrder.quantity === matchingQuantity; if (makerFullyMatched && remainingFullyMatched) { // maker & taker order quantities equal and fully matching matches.push({ maker: makerOrder, taker: remainingOrder }); } else if (remainingFullyMatched) { // taker order quantity is not sufficient. maker order will split const matchedMakerOrder = TradingPair.splitOrderByQuantity(makerOrder, matchingQuantity); this.logger.debug(`reduced order ${makerOrder.id} by ${matchingQuantity} quantity while matching order ${takerOrder.id}`); matches.push({ maker: matchedMakerOrder, taker: remainingOrder }); } else if (makerAvailableQuantityFullyMatched) { // maker order quantity is not sufficient. taker order will split const matchedTakerOrder = TradingPair.splitOrderByQuantity(remainingOrder, matchingQuantity); matches.push({ maker: makerAvailableQuantityOrder, taker: matchedTakerOrder }); } else { assert_1.default(false, 'matchingQuantity should not be lower than both orders available quantity values'); } if (remainingFullyMatched) { remainingOrder = undefined; } if (makerFullyMatched) { // maker order is fully matched, so remove it from the queue and map assert_1.default(queue.poll() === makerOrder); const map = this.getOrderMap(makerOrder); map.delete(makerOrder.id); this.logger.debug(`removed order ${makerOrder.id} while matching order ${takerOrder.id}`); } else if (makerAvailableQuantityFullyMatched) { // only an own order can be fully matched for available quantity, but not fully matched in the overall assert_1.default(types_1.isOwnOrder(makerOrder)); assert_1.default(queue.poll() === makerOrder); queueRemovedOrdersWithHold.push(makerOrder); } else { // we must make sure that we don't leave an order that is too small to swap in the order book const makerLeftoverAvailableQuantity = types_1.isOwnOrder(makerOrder) ? makerOrder.quantity - makerOrder.hold : makerOrder.quantity; if (makerLeftoverAvailableQuantity < TradingPair.QUANTITY_DUST_LIMIT || (makerLeftoverAvailableQuantity * makerOrder.price < TradingPair.QUANTITY_DUST_LIMIT)) { if (types_1.isOwnOrder(makerOrder)) { this.emit('ownOrder.dust', Object.assign(Object.assign({}, makerOrder), { quantity: makerLeftoverAvailableQuantity })); } else { this.emit('peerOrder.dust', makerOrder); } } } } } // return the removed orders with hold to the queue. // their hold quantity might be released later queueRemovedOrdersWithHold.forEach(order => queue.add(order)); return { matches, remainingOrder }; }; if (!nomatching) { this.queues = { buyQueue: TradingPair.createPriorityQueue(enums_1.OrderingDirection.Desc), sellQueue: TradingPair.createPriorityQueue(enums_1.OrderingDirection.Asc), }; } this.ownOrders = { buyMap: new Map(), sellMap: new Map(), }; this.peersOrders = new Map(); } } /** The minimum quantity for both sides of a trade that is considered swappable and not dust. */ TradingPair.QUANTITY_DUST_LIMIT = 100; TradingPair.createPriorityQueue = (orderingDirection) => { const comparator = TradingPair.getOrdersPriorityQueueComparator(orderingDirection); return new fastpriorityqueue_1.default(comparator); }; TradingPair.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) { if (types_1.isOwnOrder(a) && !types_1.isOwnOrder(b)) { return true; } else if (!types_1.isOwnOrder(a) && types_1.isOwnOrder(b)) { return false; } else { return a.createdAt < b.createdAt; } } else { return directionComparator(a.price, b.price); } }; }; /** * Gets the quantity that can be matched between two orders. * @returns the smaller of the quantity between the two orders if their price matches, 0 otherwise */ TradingPair.getMatchingQuantity = (buyOrder, sellOrder) => { if (buyOrder.price >= sellOrder.price) { return Math.min(buyOrder.quantity, sellOrder.quantity); } else { return 0; } }; /** * Splits an order by quantity into a matched portion and subtracts the matched quantity from the original order. * @param order the order that is being split * @param matchingQuantity the quantity for the split order and to subtract from the original order * @returns the split portion of the order with the matching quantity */ TradingPair.splitOrderByQuantity = (order, matchingQuantity) => { assert_1.default(order.quantity > matchingQuantity, 'order quantity must be greater than matchingQuantity'); order.quantity -= matchingQuantity; const matchedOrder = Object.assign({}, order, { quantity: matchingQuantity }); return matchedOrder; }; return TradingPair; })(); exports.default = TradingPair; //# sourceMappingURL=TradingPair.js.map