UNPKG

nodejs-order-book

Version:

Node.js Lmit Order Book for high-frequency trading (HFT).

889 lines (888 loc) 42.8 kB
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 };