xud
Version:
Exchange Union Daemon
414 lines • 22.9 kB
JavaScript
"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