@sangaman/xud
Version:
Exchange Union Daemon
164 lines • 8.3 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 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