nodejs-order-book
Version:
Node.js Lmit Order Book for high-frequency trading (HFT).
889 lines (888 loc) • 42.8 kB
JavaScript
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
/* node:coverage ignore next - Don't know why first and last line of each file count as uncovered */
import { CustomError, ERROR } from "./errors";
import { OrderFactory, } from "./order";
import { OrderSide } from "./orderside";
import { StopBook } from "./stopbook";
import { OrderType, Side, TimeInForce, } from "./types";
var validTimeInForce = Object.values(TimeInForce);
var OrderBook = /** @class */ (function () {
/**
* Creates an instance of OrderBook.
* @param {OrderBookOptions} [options={}] - Options for configuring the order book.
* @param {JournalLog} [options.snapshot] - The orderbook snapshot will be restored before processing any journal logs, if any.
* @param {JournalLog} [options.journal] - Array of journal logs (optional).
* @param {boolean} [options.enableJournaling=false] - Flag to enable journaling. Default to false
*/
function OrderBook(_a) {
var _b = _a === void 0 ? {} : _a, snapshot = _b.snapshot, journal = _b.journal, _c = _b.enableJournaling, enableJournaling = _c === void 0 ? false : _c;
var _this = this;
this.orders = {};
this._lastOp = 0;
this._marketPrice = 0;
/**
* Create a stop market order. See {@link StopMarketOrderOptions} for details.
*
* @param options
* @param options.side - `sell` or `buy`
* @param options.size - How much of currency you want to trade in units of base currency
* @param options.stopPrice - The price at which the order will be triggered.
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
this.stopMarket = function (options) {
var response = _this._stopMarket(options);
if (_this.enableJournaling && response.err === null) {
response.log = {
opId: ++_this._lastOp,
ts: Date.now(),
op: "sm",
o: options,
};
}
return response;
};
/**
* Create a stop limit order. See {@link StopLimitOrderOptions} for details.
*
* @param options
* @param options.side - `sell` or `buy`
* @param options.id - Unique order ID
* @param options.size - How much of currency you want to trade in units of base currency
* @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
* @param options.stopPrice - The price at which the order will be triggered.
* @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
this.stopLimit = function (options) {
var response = _this._stopLimit(options);
if (_this.enableJournaling && response.err === null) {
response.log = {
opId: ++_this._lastOp,
ts: Date.now(),
op: "sl",
o: options,
};
}
return response;
};
/**
* Create an OCO (One-Cancels-the-Other) order.
* OCO order combines a `stop_limit` order and a `limit` order, where if stop price
* is triggered or limit order is fully or partially fulfilled, the other is canceled.
* Both orders have the same `side` and `size`. If you cancel one of the orders, the
* entire OCO order pair will be canceled.
*
* For BUY orders the `stopPrice` must be above the current price and the `price` below the current price
* For SELL orders the `stopPrice` must be below the current price and the `price` above the current price
*
* See {@link OCOOrderOptions} for details.
*
* @param options
* @param options.side - `sell` or `buy`
* @param options.id - Unique order ID
* @param options.size - How much of currency you want to trade in units of base currency
* @param options.price - The price of the `limit` order at which the order is to be fullfilled, in units of the quote currency
* @param options.stopPrice - The price at which the `stop_limit` order will be triggered.
* @param options.stopLimitPrice - The price of the `stop_limit` order at which the order is to be fullfilled, in units of the quote currency.
* @param options.timeInForce - Time-in-force of the `limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
* @param options.stopLimitTimeInForce - Time-in-force of the `stop_limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
this.oco = function (options) {
var response = _this._oco(options);
if (_this.enableJournaling && response.err === null) {
response.log = {
opId: ++_this._lastOp,
ts: Date.now(),
op: "oco",
o: options,
};
}
return response;
};
/**
* Modify an existing order with given ID. When an order is modified by price or quantity,
* it will be deemed as a new entry. Under the price-time-priority algorithm, orders are
* prioritized according to their order price and order time. Hence, the latest orders
* will be placed at the back of the matching order queue.
*
* @param orderID - The ID of the order to be modified
* @param orderUpdate - An object with the modified size and/or price of an order. The shape of the object is `{size, price}`.
* @returns An object with the result of the processed order or an error
*/
this.modify = function (orderID, orderUpdate) {
var _a, _b, _c;
var order = _this.orders[orderID];
if (order === undefined) {
return {
done: [],
activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: 0,
err: CustomError(ERROR.ORDER_NOT_FOUND),
};
}
if ((orderUpdate === null || orderUpdate === void 0 ? void 0 : orderUpdate.price) !== undefined || (orderUpdate === null || orderUpdate === void 0 ? void 0 : orderUpdate.size) !== undefined) {
var newPrice = (_a = orderUpdate.price) !== null && _a !== void 0 ? _a : order.price;
var newSize = (_b = orderUpdate.size) !== null && _b !== void 0 ? _b : order.size;
if (newPrice > 0 && newSize > 0) {
var response = _this.getProcessOrderResponse(newSize);
_this._cancelOrder(order.id, true);
_this.createLimitOrder(response, order.side, order.id, newSize, newPrice, order.postOnly, TimeInForce.GTC);
if (_this.enableJournaling) {
response.log = {
opId: ++_this._lastOp,
ts: Date.now(),
op: "u",
o: { orderID: orderID, orderUpdate: orderUpdate },
};
}
return response;
}
}
// Missing one of price and/or size, or the provided ones are not greater than zero
return {
done: [],
activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: (_c = orderUpdate === null || orderUpdate === void 0 ? void 0 : orderUpdate.size) !== null && _c !== void 0 ? _c : 0,
err: CustomError(ERROR.INVALID_PRICE_OR_QUANTITY),
};
};
/**
* Remove an existing order with given ID from the order book
*
* @param orderID - The ID of the order to be removed
* @returns The removed order if exists or `undefined`
*/
this.cancel = function (orderID) {
return _this._cancelOrder(orderID);
};
/**
* Get an existing order with the given ID
*
* @param orderID - The ID of the order to be returned
* @returns The order if exists or `undefined`
*/
this.order = function (orderID) {
var _a;
return (_a = _this.orders[orderID]) === null || _a === void 0 ? void 0 : _a.toObject();
};
// Returns price levels and volume at price level
this.depth = function () {
var asks = [];
var bids = [];
_this.asks.priceTree().forEach(function (levelPrice, level) {
asks.push([levelPrice, level.volume()]);
});
_this.bids.priceTree().forEach(function (levelPrice, level) {
bids.push([levelPrice, level.volume()]);
});
return [asks, bids];
};
this.toString = function () {
/* node:coverage ignore next - Don't know what is the uncovered branch here */
return "".concat(_this.asks.toString(), "\r\n------------------------------------").concat(_this.bids.toString());
};
// Returns total market price for requested quantity
// if err is not null price returns total price of all levels in side
this.calculateMarketPrice = function (side, size) {
var price = 0;
var err = null;
var level;
var iter;
var quantity = size;
if (side === Side.BUY) {
level = _this.asks.minPriceQueue();
iter = _this.asks.greaterThan;
}
else {
level = _this.bids.maxPriceQueue();
iter = _this.bids.lowerThan;
}
while (quantity > 0 && level !== undefined) {
var levelVolume = level.volume();
var levelPrice = level.price();
if (_this.greaterThanOrEqual(quantity, levelVolume)) {
price += levelPrice * levelVolume;
quantity -= levelVolume;
level = iter(levelPrice);
}
else {
price += levelPrice * quantity;
quantity = 0;
}
}
if (quantity > 0) {
err = CustomError(ERROR.INSUFFICIENT_QUANTITY);
}
return { price: price, err: err };
};
this.snapshot = function () {
var bids = [];
var asks = [];
_this.bids.priceTree().forEach(function (price, orders) {
bids.push({ price: price, orders: orders.toArray().map(function (o) { return o.toObject(); }) });
});
_this.asks.priceTree().forEach(function (price, orders) {
asks.push({ price: price, orders: orders.toArray().map(function (o) { return o.toObject(); }) });
});
var stopBook = _this.stopBook.snapshot();
return { bids: bids, asks: asks, stopBook: stopBook, ts: Date.now(), lastOp: _this._lastOp };
};
this._market = function (options, incomingResponse) {
var response = incomingResponse !== null && incomingResponse !== void 0 ? incomingResponse : _this.validateMarketOrder(options);
if (response.err !== null)
return response;
var quantityToTrade = options.size;
var iter;
var sideToProcess;
if (options.side === Side.BUY) {
iter = _this.asks.minPriceQueue;
sideToProcess = _this.asks;
}
else {
iter = _this.bids.maxPriceQueue;
sideToProcess = _this.bids;
}
var priceBefore = _this._marketPrice;
while (quantityToTrade > 0 && sideToProcess.len() > 0) {
// if sideToProcess.len > 0 it is not necessary to verify that bestPrice exists
var bestPrice = iter();
var _a = _this.processQueue(bestPrice, quantityToTrade), done = _a.done, partial = _a.partial, partialQuantityProcessed = _a.partialQuantityProcessed, quantityLeft = _a.quantityLeft;
response.done = response.done.concat(done);
response.partial = partial;
response.partialQuantityProcessed = partialQuantityProcessed;
quantityToTrade = quantityLeft;
}
response.quantityLeft = quantityToTrade;
_this.executeConditionalOrder(options.side, priceBefore, response);
return response;
};
this._limit = function (options, incomingResponse) {
var _a, _b;
var response = incomingResponse !== null && incomingResponse !== void 0 ? incomingResponse : _this.validateLimitOrder(options);
if (response.err !== null)
return response;
_this.createLimitOrder(response, options.side, options.id, options.size, options.price, (_a = options.postOnly) !== null && _a !== void 0 ? _a : false, (_b = options.timeInForce) !== null && _b !== void 0 ? _b : TimeInForce.GTC, options.ocoStopPrice);
return response;
};
this._stopMarket = function (options) {
var response = _this.validateMarketOrder(options);
if (response.err !== null)
return response;
var stopMarket = OrderFactory.createOrder(__assign(__assign({}, options), { type: OrderType.STOP_MARKET }));
return _this._stopOrder(stopMarket, response);
};
this._stopLimit = function (options) {
var _a;
var response = _this.validateLimitOrder(options);
if (response.err !== null)
return response;
var stopLimit = OrderFactory.createOrder(__assign(__assign({}, options), { type: OrderType.STOP_LIMIT, timeInForce: (_a = options.timeInForce) !== null && _a !== void 0 ? _a : TimeInForce.GTC }));
return _this._stopOrder(stopLimit, response);
};
this._oco = function (options) {
var _a;
var response = _this.validateLimitOrder(options);
/* node:coverage ignore next - Already validated with limit test */
if (response.err !== null)
return response;
if (_this.validateOCOOrder(options)) {
// We use the same ID for Stop Limit and Limit Order, since
// we check only on limit order for duplicated ids
_this._limit({
id: options.id,
side: options.side,
size: options.size,
price: options.price,
timeInForce: options.timeInForce,
ocoStopPrice: options.stopPrice,
}, response);
/* node:coverage ignore next - Already validated with limit test */
if (response.err !== null)
return response;
var stopLimit = OrderFactory.createOrder({
type: OrderType.STOP_LIMIT,
id: options.id,
side: options.side,
size: options.size,
price: options.stopLimitPrice,
stopPrice: options.stopPrice,
timeInForce: (_a = options.stopLimitTimeInForce) !== null && _a !== void 0 ? _a : TimeInForce.GTC,
isOCO: true,
});
_this.stopBook.add(stopLimit);
response.done.push(stopLimit.toObject());
}
else {
response.err = CustomError(ERROR.INVALID_CONDITIONAL_ORDER);
}
return response;
};
this._stopOrder = function (stopOrder, response) {
if (_this.stopBook.validConditionalOrder(_this._marketPrice, stopOrder)) {
_this.stopBook.add(stopOrder);
response.done.push(stopOrder.toObject());
}
else {
response.err = CustomError(ERROR.INVALID_CONDITIONAL_ORDER);
}
return response;
};
this.restoreSnapshot = function (snapshot) {
var _a, _b, _c, _d;
_this._lastOp = snapshot.lastOp;
for (var _i = 0, _e = snapshot.bids; _i < _e.length; _i++) {
var level = _e[_i];
for (var _f = 0, _g = level.orders; _f < _g.length; _f++) {
var order = _g[_f];
var newOrder = OrderFactory.createOrder(order);
_this.orders[newOrder.id] = newOrder;
_this.bids.append(newOrder);
}
}
for (var _h = 0, _j = snapshot.asks; _h < _j.length; _h++) {
var level = _j[_h];
for (var _k = 0, _l = level.orders; _k < _l.length; _k++) {
var order = _l[_k];
var newOrder = OrderFactory.createOrder(order);
_this.orders[newOrder.id] = newOrder;
_this.asks.append(newOrder);
}
}
if (((_b = (_a = snapshot.stopBook) === null || _a === void 0 ? void 0 : _a.bids) === null || _b === void 0 ? void 0 : _b.length) > 0) {
for (var _m = 0, _o = snapshot.stopBook.bids; _m < _o.length; _m++) {
var level = _o[_m];
for (var _p = 0, _q = level.orders; _p < _q.length; _p++) {
var order = _q[_p];
// @ts-expect-error // TODO fix types
var newOrder = OrderFactory.createOrder(order);
// @ts-expect-error // TODO fix types
_this.stopBook.add(newOrder);
}
}
}
if (((_d = (_c = snapshot.stopBook) === null || _c === void 0 ? void 0 : _c.asks) === null || _d === void 0 ? void 0 : _d.length) > 0) {
for (var _r = 0, _s = snapshot.stopBook.asks; _r < _s.length; _r++) {
var level = _s[_r];
for (var _t = 0, _u = level.orders; _t < _u.length; _t++) {
var order = _u[_t];
// @ts-expect-error // TODO fix types
var newOrder = OrderFactory.createOrder(order);
// @ts-expect-error // TODO fix types
_this.stopBook.add(newOrder);
}
}
}
};
/**
* Remove an existing order with given ID from the order book
* @param orderID The id of the order to be deleted
* @param internalDeletion Set to true when the delete comes from internal operations
* @returns The removed order if exists or `undefined`
*/
this._cancelOrder = function (orderID, internalDeletion) {
var _a, _b;
if (internalDeletion === void 0) { internalDeletion = false; }
var order = _this.orders[orderID];
if (order === undefined)
return;
delete _this.orders[orderID];
var side = order.side === Side.BUY ? _this.bids : _this.asks;
var response = {
order: (_a = side.remove(order)) === null || _a === void 0 ? void 0 : _a.toObject(),
};
// Delete OCO Order only when the delete request comes from user
if (!internalDeletion && order.ocoStopPrice !== undefined) {
response.stopOrder = (_b = _this.stopBook
.remove(order.side, orderID, order.ocoStopPrice)) === null || _b === void 0 ? void 0 : _b.toObject();
}
if (_this.enableJournaling) {
response.log = {
opId: internalDeletion ? _this._lastOp : ++_this._lastOp,
ts: Date.now(),
op: "d",
o: { orderID: orderID },
};
}
return response;
};
this.getProcessOrderResponse = function (size) {
return {
done: [],
activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: size,
err: null,
};
};
this.createLimitOrder = function (response, side, orderID, size, price, postOnly, timeInForce, ocoStopPrice) {
var quantityToTrade = size;
var sideToProcess;
var sideToAdd;
var comparator;
var iter;
if (side === Side.BUY) {
sideToAdd = _this.bids;
sideToProcess = _this.asks;
comparator = _this.greaterThanOrEqual;
iter = _this.asks.minPriceQueue;
}
else {
sideToAdd = _this.asks;
sideToProcess = _this.bids;
comparator = _this.lowerThanOrEqual;
iter = _this.bids.maxPriceQueue;
}
if (timeInForce === TimeInForce.FOK) {
var fillable = _this.canFillOrder(sideToProcess, side, size, price);
if (!fillable) {
response.err = CustomError(ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE);
return;
}
}
var bestPrice = iter();
var priceBefore = _this._marketPrice;
while (quantityToTrade > 0 &&
sideToProcess.len() > 0 &&
bestPrice !== undefined &&
comparator(price, bestPrice.price())) {
if (postOnly) {
response.err = CustomError(ERROR.LIMIT_ORDER_POST_ONLY);
return;
}
var _a = _this.processQueue(bestPrice, quantityToTrade), done = _a.done, partial = _a.partial, partialQuantityProcessed = _a.partialQuantityProcessed, quantityLeft = _a.quantityLeft;
response.done = response.done.concat(done);
response.partial = partial;
response.partialQuantityProcessed = partialQuantityProcessed;
quantityToTrade = quantityLeft;
response.quantityLeft = quantityToTrade;
bestPrice = iter();
}
_this.executeConditionalOrder(side, priceBefore, response);
var order;
var takerQty = size - quantityToTrade;
var makerQty = quantityToTrade;
if (quantityToTrade > 0) {
order = OrderFactory.createOrder(__assign({ type: OrderType.LIMIT, id: orderID, side: side, size: quantityToTrade, origSize: size, price: price, time: Date.now(), timeInForce: timeInForce, postOnly: postOnly, takerQty: takerQty, makerQty: makerQty }, (ocoStopPrice !== undefined ? { ocoStopPrice: ocoStopPrice } : {})));
if (response.done.length > 0) {
response.partialQuantityProcessed = size - quantityToTrade;
response.partial = order.toObject();
}
_this.orders[orderID] = sideToAdd.append(order);
}
else {
var totalQuantity_1 = 0;
var totalPrice_1 = 0;
response.done.forEach(function (order) {
totalQuantity_1 += order.size;
totalPrice_1 += order.price * order.size;
});
if (response.partialQuantityProcessed > 0 && response.partial !== null) {
totalQuantity_1 += response.partialQuantityProcessed;
totalPrice_1 +=
response.partial.price * response.partialQuantityProcessed;
}
order = OrderFactory.createOrder({
id: orderID,
type: OrderType.LIMIT,
side: side,
size: size,
origSize: size,
price: totalPrice_1 / totalQuantity_1,
time: Date.now(),
timeInForce: timeInForce,
postOnly: postOnly,
takerQty: takerQty,
makerQty: makerQty,
});
response.done.push(order.toObject());
}
// If IOC order was not matched completely remove from the order book
if (timeInForce === TimeInForce.IOC && response.quantityLeft > 0) {
_this._cancelOrder(orderID, true);
}
return order;
};
this.executeConditionalOrder = function (side, priceBefore, response) {
var pendingOrders = _this.stopBook.getConditionalOrders(side, priceBefore, _this._marketPrice);
if (pendingOrders.length > 0) {
var toBeExecuted_1 = [];
// Before get all orders to be executed and clean up the stop queue
// in order to avoid that an executed limit/market order run against
// the same stop order queue
pendingOrders.forEach(function (queue) {
while (queue.len() > 0) {
var headOrder = queue.removeFromHead();
if (headOrder !== undefined)
toBeExecuted_1.push(headOrder);
}
// Queue is empty now so remove the priceLevel
_this.stopBook.removePriceLevel(side, queue.price);
});
toBeExecuted_1.forEach(function (stopOrder) {
if (stopOrder.type === OrderType.STOP_MARKET) {
_this._market({
id: stopOrder.id,
side: stopOrder.side,
size: stopOrder.size,
}, response);
}
else {
if (stopOrder.isOCO) {
_this._cancelOrder(stopOrder.id, true);
}
_this._limit({
id: stopOrder.id,
side: stopOrder.side,
size: stopOrder.size,
price: stopOrder.price,
timeInForce: stopOrder.timeInForce,
}, response);
}
response.activated.push(stopOrder.toObject());
});
}
};
this.replayJournal = function (journal) {
for (var _i = 0, journal_1 = journal; _i < journal_1.length; _i++) {
var log = journal_1[_i];
switch (log.op) {
case "m": {
var _a = log.o, side = _a.side, size = _a.size;
if (side == null || size == null) {
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
}
_this.market(log.o);
break;
}
case "l": {
var _b = log.o, side = _b.side, id = _b.id, size = _b.size, price = _b.price;
if (side == null || id == null || size == null || price == null) {
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
}
_this.limit(log.o);
break;
}
case "sm": {
var _c = log.o, side = _c.side, size = _c.size, stopPrice = _c.stopPrice;
if (side == null || size == null || stopPrice == null) {
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
}
_this.stopMarket(log.o);
break;
}
case "sl": {
var _d = log.o, side = _d.side, id = _d.id, size = _d.size, price = _d.price, stopPrice = _d.stopPrice;
if (side == null ||
id == null ||
size == null ||
price == null ||
stopPrice == null) {
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
}
_this.stopLimit(log.o);
break;
}
case "oco": {
var _e = log.o, side = _e.side, id = _e.id, size = _e.size, price = _e.price, stopPrice = _e.stopPrice, stopLimitPrice = _e.stopLimitPrice;
if (side == null ||
id == null ||
size == null ||
price == null ||
stopPrice == null ||
stopLimitPrice == null) {
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
}
_this.oco(log.o);
break;
}
case "d":
if (log.o.orderID == null)
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
_this.cancel(log.o.orderID);
break;
case "u":
if (log.o.orderID == null || log.o.orderUpdate == null) {
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
}
_this.modify(log.o.orderID, log.o.orderUpdate);
break;
default:
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
}
}
};
/**
* OCO Order:
* Buy: price < marketPrice < stopPrice
* Sell: price > marketPrice > stopPrice
*/
this.validateOCOOrder = function (options) {
var response = false;
if (options.side === Side.BUY &&
options.price < _this._marketPrice &&
_this._marketPrice < options.stopPrice) {
response = true;
}
if (options.side === Side.SELL &&
options.price > _this._marketPrice &&
_this._marketPrice > options.stopPrice) {
response = true;
}
return response;
};
this.greaterThanOrEqual = function (a, b) {
return a >= b;
};
this.lowerThanOrEqual = function (a, b) {
return a <= b;
};
this.processQueue = function (orderQueue, quantityToTrade) {
var response = {
done: [],
activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: quantityToTrade,
err: null,
};
if (response.quantityLeft > 0) {
while (orderQueue.len() > 0 && response.quantityLeft > 0) {
var headOrder = orderQueue.head();
if (headOrder !== undefined) {
if (response.quantityLeft < headOrder.size) {
var partial = OrderFactory.createOrder(__assign(__assign({}, headOrder.toObject()), { size: headOrder.size - response.quantityLeft }));
response.partial = partial.toObject();
_this.orders[headOrder.id] = partial;
response.partialQuantityProcessed = response.quantityLeft;
orderQueue.update(headOrder, partial);
response.quantityLeft = 0;
}
else {
response.quantityLeft = response.quantityLeft - headOrder.size;
var canceledOrder = _this._cancelOrder(headOrder.id, true);
/* node:coverage ignore next - Unable to test when order is undefined */
if ((canceledOrder === null || canceledOrder === void 0 ? void 0 : canceledOrder.order) !== undefined) {
response.done.push(canceledOrder.order);
}
}
// Remove linked OCO Stop Order if any
if (headOrder.ocoStopPrice !== undefined) {
_this.stopBook.remove(headOrder.side, headOrder.id, headOrder.ocoStopPrice);
}
_this._marketPrice = headOrder.price;
}
}
}
return response;
};
this.canFillOrder = function (orderSide, side, size, price) {
return side === Side.BUY
? _this.buyOrderCanBeFilled(orderSide, size, price)
: _this.sellOrderCanBeFilled(orderSide, size, price);
};
this.buyOrderCanBeFilled = function (orderSide, size, price) {
if (orderSide.volume() < size) {
return false;
}
var cumulativeSize = 0;
// biome-ignore lint/suspicious/useIterableCallbackReturn: the forEach of the priceTree must return true to break the loop
orderSide.priceTree().forEach(function (_, level) {
if (price >= level.price() && cumulativeSize < size) {
cumulativeSize += level.volume();
}
else {
return true; // break the loop
}
});
return cumulativeSize >= size;
};
this.sellOrderCanBeFilled = function (orderSide, size, price) {
if (orderSide.volume() < size) {
return false;
}
var cumulativeSize = 0;
// biome-ignore lint/suspicious/useIterableCallbackReturn: the forEach of the priceTree must return true to break the loop
orderSide.priceTree().forEach(function (_, level) {
if (price <= level.price() && cumulativeSize < size) {
cumulativeSize += level.volume();
}
else {
return true; // break the loop
}
});
return cumulativeSize >= size;
};
this.validateMarketOrder = function (order) {
var response = _this.getProcessOrderResponse(order.size);
if (![Side.SELL, Side.BUY].includes(order.side)) {
response.err = CustomError(ERROR.INVALID_SIDE);
return response;
}
if (typeof order.size !== "number" || order.size <= 0) {
response.err = CustomError(ERROR.INSUFFICIENT_QUANTITY);
return response;
}
return response;
};
this.validateLimitOrder = function (options) {
var response = _this.getProcessOrderResponse(options.size);
if (![Side.SELL, Side.BUY].includes(options.side)) {
response.err = CustomError(ERROR.INVALID_SIDE);
return response;
}
if (_this.orders[options.id] !== undefined) {
response.err = CustomError(ERROR.ORDER_ALREDY_EXISTS);
return response;
}
if (typeof options.size !== "number" || options.size <= 0) {
response.err = CustomError(ERROR.INVALID_QUANTITY);
return response;
}
if (typeof options.price !== "number" || options.price <= 0) {
response.err = CustomError(ERROR.INVALID_PRICE);
return response;
}
if (options.timeInForce &&
!validTimeInForce.includes(options.timeInForce)) {
response.err = CustomError(ERROR.INVALID_TIF);
return response;
}
return response;
};
this.bids = new OrderSide(Side.BUY);
this.asks = new OrderSide(Side.SELL);
this.enableJournaling = enableJournaling;
this.stopBook = new StopBook();
// First restore from orderbook snapshot
if (snapshot != null) {
this.restoreSnapshot(snapshot);
}
// Than replay from journal log
if (journal != null) {
if (!Array.isArray(journal))
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
// If a snapshot is available be sure to remove logs before the last restored operation
if (snapshot != null && snapshot.lastOp > 0) {
journal = journal.filter(function (log) { return log.opId > snapshot.lastOp; });
}
this.replayJournal(journal);
}
}
Object.defineProperty(OrderBook.prototype, "marketPrice", {
// Getter for the market price
get: function () {
return this._marketPrice;
},
enumerable: false,
configurable: true
});
Object.defineProperty(OrderBook.prototype, "lastOp", {
// Getter for the lastOp
get: function () {
return this._lastOp;
},
enumerable: false,
configurable: true
});
/**
* Create new order. See {@link CreateOrderOptions} for details.
*
* @param options
* @param options.type - `limit` | `market` | 'stop_limit' | 'stop_market' | 'oco'
* @param options.side - `sell` or `buy`
* @param options.size - How much of currency you want to trade in units of base currency
* @param options.price - The price at which the order is to be fullfilled, in units of the quote currency. Param only for limit order
* @param options.orderID - Unique order ID. Param only for limit order
* @param options.postOnly - Can be used with 'limit' order and when it's `true` the order will be rejected if immediately matches and trades as a taker. Default is `false`
* @param options.stopPrice - The price at which the order will be triggered. Used with `stop_limit` and `stop_market` order.
* @param options.stopLimitPrice - The price at which the order will be triggered. Used with `stop_limit` and `stop_market` order.
* @param options.timeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. Param only for limit order
* @param options.stopLimitTimeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. Param only for limit order
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
OrderBook.prototype.createOrder = function (options) {
switch (options.type) {
case OrderType.MARKET:
return this.market(options);
case OrderType.LIMIT:
return this.limit(options);
case OrderType.STOP_MARKET:
return this.stopMarket(options);
case OrderType.STOP_LIMIT:
return this.stopLimit(options);
case OrderType.OCO:
return this.oco(options);
default:
return {
done: [],
activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: 0,
err: CustomError(ERROR.INVALID_ORDER_TYPE),
};
}
};
/**
* Create a market order. See {@link MarketOrderOptions} for details.
*
* @param options
* @param options.side - `sell` or `buy`
* @param options.size - How much of currency you want to trade in units of base currency
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
OrderBook.prototype.market = function (options) {
var response = this._market(options);
if (this.enableJournaling && response.err === null) {
response.log = {
opId: ++this._lastOp,
ts: Date.now(),
op: "m",
o: options,
};
}
return response;
};
/**
* Create a limit order. See {@link LimitOrderOptions} for details.
*
* @param options
* @param options.side - `sell` or `buy`
* @param options.id - Unique order ID
* @param options.size - How much of currency you want to trade in units of base currency
* @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
* @param options.postOnly - When `true` the order will be rejected if immediately matches and trades as a taker. Default is `false`
* @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
OrderBook.prototype.limit = function (options) {
var response = this._limit(options);
if (this.enableJournaling && response.err === null) {
response.log = {
opId: ++this._lastOp,
ts: Date.now(),
op: "l",
o: options,
};
}
return response;
};
return OrderBook;
}());
export { OrderBook };