nodejs-order-book 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,850 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.OrderBook = void 0;
15
+ /* node:coverage ignore next - Don't know why first and last line of each file count as uncovered */
16
+ var errors_1 = require("./errors");
17
+ var order_1 = require("./order");
18
+ var orderside_1 = require("./orderside");
19
+ var stopbook_1 = require("./stopbook");
20
+ var types_1 = require("./types");
21
+ var validTimeInForce = Object.values(types_1.TimeInForce);
22
+ var OrderBook = /** @class */ (function () {
23
+ /**
24
+ * Creates an instance of OrderBook.
25
+ * @param {OrderBookOptions} [options={}] - Options for configuring the order book.
26
+ * @param {JournalLog} [options.snapshot] - The orderbook snapshot will be restored before processing any journal logs, if any.
27
+ * @param {JournalLog} [options.journal] - Array of journal logs (optional).
28
+ * @param {boolean} [options.enableJournaling=false] - Flag to enable journaling. Default to false
29
+ * @param {boolean} [options.experimentalConditionalOrders=false] - Flag to enable experimental Conditional Order (Stop Market, Stop Limit and OCO orders). Default to false
30
+ */
31
+ function OrderBook(_a) {
32
+ var _b = _a === void 0 ? {} : _a, snapshot = _b.snapshot, journal = _b.journal, _c = _b.enableJournaling, enableJournaling = _c === void 0 ? false : _c, _d = _b.experimentalConditionalOrders, experimentalConditionalOrders = _d === void 0 ? false : _d;
33
+ var _this = this;
34
+ this.orders = {};
35
+ this._lastOp = 0;
36
+ this._marketPrice = 0;
37
+ /**
38
+ * Create a stop market order. See {@link StopMarketOrderOptions} for details.
39
+ *
40
+ * @param options
41
+ * @param options.side - `sell` or `buy`
42
+ * @param options.size - How much of currency you want to trade in units of base currency
43
+ * @param options.stopPrice - The price at which the order will be triggered.
44
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
45
+ */
46
+ this.stopMarket = function (options) {
47
+ /* node:coverage ignore next 4 - We don't need test for this */
48
+ if (!_this.experimentalConditionalOrders)
49
+ throw new Error("In order to use conditional orders you need to instantiate the order book with the `experimentalConditionalOrders` option set to true");
50
+ return _this._stopMarket(options);
51
+ };
52
+ /**
53
+ * Create a stop limit order. See {@link StopLimitOrderOptions} for details.
54
+ *
55
+ * @param options
56
+ * @param options.side - `sell` or `buy`
57
+ * @param options.id - Unique order ID
58
+ * @param options.size - How much of currency you want to trade in units of base currency
59
+ * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
60
+ * @param options.stopPrice - The price at which the order will be triggered.
61
+ * @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
62
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
63
+ */
64
+ this.stopLimit = function (options) {
65
+ /* node:coverage ignore next 4 - We don't need test for this */
66
+ if (!_this.experimentalConditionalOrders)
67
+ throw new Error("In order to use conditional orders you need to instantiate the order book with the `experimentalConditionalOrders` option set to true");
68
+ return _this._stopLimit(options);
69
+ };
70
+ /**
71
+ * Create an OCO (One-Cancels-the-Other) order.
72
+ * OCO order combines a `stop_limit` order and a `limit` order, where if stop price
73
+ * is triggered or limit order is fully or partially fulfilled, the other is canceled.
74
+ * Both orders have the same `side` and `size`. If you cancel one of the orders, the
75
+ * entire OCO order pair will be canceled.
76
+ *
77
+ * For BUY orders the `stopPrice` must be above the current price and the `price` below the current price
78
+ * For SELL orders the `stopPrice` must be below the current price and the `price` above the current price
79
+ *
80
+ * See {@link OCOOrderOptions} for details.
81
+ *
82
+ * @param options
83
+ * @param options.side - `sell` or `buy`
84
+ * @param options.id - Unique order ID
85
+ * @param options.size - How much of currency you want to trade in units of base currency
86
+ * @param options.price - The price of the `limit` order at which the order is to be fullfilled, in units of the quote currency
87
+ * @param options.stopPrice - The price at which the `stop_limit` order will be triggered.
88
+ * @param options.stopLimitPrice - The price of the `stop_limit` order at which the order is to be fullfilled, in units of the quote currency.
89
+ * @param options.timeInForce - Time-in-force of the `limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
90
+ * @param options.stopLimitTimeInForce - Time-in-force of the `stop_limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
91
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
92
+ */
93
+ this.oco = function (options) {
94
+ /* node:coverage ignore next 4 - We don't need test for this */
95
+ if (!_this.experimentalConditionalOrders)
96
+ throw new Error("In order to use conditional orders you need to instantiate the order book with the `experimentalConditionalOrders` option set to true");
97
+ return _this._oco(options);
98
+ };
99
+ /**
100
+ * Modify an existing order with given ID. When an order is modified by price or quantity,
101
+ * it will be deemed as a new entry. Under the price-time-priority algorithm, orders are
102
+ * prioritized according to their order price and order time. Hence, the latest orders
103
+ * will be placed at the back of the matching order queue.
104
+ *
105
+ * @param orderID - The ID of the order to be modified
106
+ * @param orderUpdate - An object with the modified size and/or price of an order. The shape of the object is `{size, price}`.
107
+ * @returns An object with the result of the processed order or an error
108
+ */
109
+ this.modify = function (orderID, orderUpdate) {
110
+ var _a, _b, _c;
111
+ var order = _this.orders[orderID];
112
+ if (order === undefined) {
113
+ return {
114
+ done: [],
115
+ activated: [],
116
+ partial: null,
117
+ partialQuantityProcessed: 0,
118
+ quantityLeft: 0,
119
+ err: (0, errors_1.CustomError)(errors_1.ERROR.ORDER_NOT_FOUND),
120
+ };
121
+ }
122
+ if ((orderUpdate === null || orderUpdate === void 0 ? void 0 : orderUpdate.price) !== undefined || (orderUpdate === null || orderUpdate === void 0 ? void 0 : orderUpdate.size) !== undefined) {
123
+ var newPrice = (_a = orderUpdate.price) !== null && _a !== void 0 ? _a : order.price;
124
+ var newSize = (_b = orderUpdate.size) !== null && _b !== void 0 ? _b : order.size;
125
+ if (newPrice > 0 && newSize > 0) {
126
+ var response = _this.getProcessOrderResponse(newSize);
127
+ _this._cancelOrder(order.id, true);
128
+ _this.createLimitOrder(response, order.side, order.id, newSize, newPrice, order.postOnly, types_1.TimeInForce.GTC);
129
+ if (_this.enableJournaling) {
130
+ response.log = {
131
+ opId: ++_this._lastOp,
132
+ ts: Date.now(),
133
+ op: "u",
134
+ o: { orderID: orderID, orderUpdate: orderUpdate },
135
+ };
136
+ }
137
+ return response;
138
+ }
139
+ }
140
+ // Missing one of price and/or size, or the provided ones are not greater than zero
141
+ return {
142
+ done: [],
143
+ activated: [],
144
+ partial: null,
145
+ partialQuantityProcessed: 0,
146
+ quantityLeft: (_c = orderUpdate === null || orderUpdate === void 0 ? void 0 : orderUpdate.size) !== null && _c !== void 0 ? _c : 0,
147
+ err: (0, errors_1.CustomError)(errors_1.ERROR.INVALID_PRICE_OR_QUANTITY),
148
+ };
149
+ };
150
+ /**
151
+ * Remove an existing order with given ID from the order book
152
+ *
153
+ * @param orderID - The ID of the order to be removed
154
+ * @returns The removed order if exists or `undefined`
155
+ */
156
+ this.cancel = function (orderID) {
157
+ return _this._cancelOrder(orderID);
158
+ };
159
+ /**
160
+ * Get an existing order with the given ID
161
+ *
162
+ * @param orderID - The ID of the order to be returned
163
+ * @returns The order if exists or `undefined`
164
+ */
165
+ this.order = function (orderID) {
166
+ return _this.orders[orderID];
167
+ };
168
+ // Returns price levels and volume at price level
169
+ this.depth = function () {
170
+ var asks = [];
171
+ var bids = [];
172
+ _this.asks.priceTree().forEach(function (levelPrice, level) {
173
+ asks.push([levelPrice, level.volume()]);
174
+ });
175
+ _this.bids.priceTree().forEach(function (levelPrice, level) {
176
+ bids.push([levelPrice, level.volume()]);
177
+ });
178
+ return [asks, bids];
179
+ };
180
+ this.toString = function () {
181
+ /* node:coverage ignore next - Don't know what is the uncovered branch here */
182
+ return "".concat(_this.asks.toString(), "\r\n------------------------------------").concat(_this.bids.toString());
183
+ };
184
+ // Returns total market price for requested quantity
185
+ // if err is not null price returns total price of all levels in side
186
+ this.calculateMarketPrice = function (side, size) {
187
+ var price = 0;
188
+ var err = null;
189
+ var level;
190
+ var iter;
191
+ var quantity = size;
192
+ if (side === types_1.Side.BUY) {
193
+ level = _this.asks.minPriceQueue();
194
+ iter = _this.asks.greaterThan;
195
+ }
196
+ else {
197
+ level = _this.bids.maxPriceQueue();
198
+ iter = _this.bids.lowerThan;
199
+ }
200
+ while (quantity > 0 && level !== undefined) {
201
+ var levelVolume = level.volume();
202
+ var levelPrice = level.price();
203
+ if (_this.greaterThanOrEqual(quantity, levelVolume)) {
204
+ price += levelPrice * levelVolume;
205
+ quantity -= levelVolume;
206
+ level = iter(levelPrice);
207
+ }
208
+ else {
209
+ price += levelPrice * quantity;
210
+ quantity = 0;
211
+ }
212
+ }
213
+ if (quantity > 0) {
214
+ err = (0, errors_1.CustomError)(errors_1.ERROR.INSUFFICIENT_QUANTITY);
215
+ }
216
+ return { price: price, err: err };
217
+ };
218
+ this.snapshot = function () {
219
+ var bids = [];
220
+ var asks = [];
221
+ _this.bids.priceTree().forEach(function (price, orders) {
222
+ bids.push({ price: price, orders: orders.toArray() });
223
+ });
224
+ _this.asks.priceTree().forEach(function (price, orders) {
225
+ asks.push({ price: price, orders: orders.toArray() });
226
+ });
227
+ return { bids: bids, asks: asks, ts: Date.now(), lastOp: _this._lastOp };
228
+ };
229
+ this._market = function (options, incomingResponse) {
230
+ var response = incomingResponse !== null && incomingResponse !== void 0 ? incomingResponse : _this.validateMarketOrder(options);
231
+ if (response.err != null)
232
+ return response;
233
+ var quantityToTrade = options.size;
234
+ var iter;
235
+ var sideToProcess;
236
+ if (options.side === types_1.Side.BUY) {
237
+ iter = _this.asks.minPriceQueue;
238
+ sideToProcess = _this.asks;
239
+ }
240
+ else {
241
+ iter = _this.bids.maxPriceQueue;
242
+ sideToProcess = _this.bids;
243
+ }
244
+ var priceBefore = _this._marketPrice;
245
+ while (quantityToTrade > 0 && sideToProcess.len() > 0) {
246
+ // if sideToProcess.len > 0 it is not necessary to verify that bestPrice exists
247
+ var bestPrice = iter();
248
+ var _a = _this.processQueue(bestPrice, quantityToTrade), done = _a.done, partial = _a.partial, partialQuantityProcessed = _a.partialQuantityProcessed, quantityLeft = _a.quantityLeft;
249
+ response.done = response.done.concat(done);
250
+ response.partial = partial;
251
+ response.partialQuantityProcessed = partialQuantityProcessed;
252
+ quantityToTrade = quantityLeft;
253
+ }
254
+ response.quantityLeft = quantityToTrade;
255
+ _this.executeConditionalOrder(options.side, priceBefore, response);
256
+ if (_this.enableJournaling) {
257
+ response.log = {
258
+ opId: ++_this._lastOp,
259
+ ts: Date.now(),
260
+ op: "m",
261
+ o: { side: options.side, size: options.size },
262
+ };
263
+ }
264
+ return response;
265
+ };
266
+ this._limit = function (options, incomingResponse) {
267
+ var _a, _b;
268
+ var response = incomingResponse !== null && incomingResponse !== void 0 ? incomingResponse : _this.validateLimitOrder(options);
269
+ if (response.err != null)
270
+ return response;
271
+ var order = _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 : types_1.TimeInForce.GTC, options.ocoStopPrice);
272
+ if (_this.enableJournaling && order != null) {
273
+ response.log = {
274
+ opId: ++_this._lastOp,
275
+ ts: Date.now(),
276
+ op: "l",
277
+ o: {
278
+ side: order.side,
279
+ id: order.id,
280
+ size: order.size,
281
+ price: order.price,
282
+ timeInForce: order.timeInForce,
283
+ },
284
+ };
285
+ }
286
+ return response;
287
+ };
288
+ this._stopMarket = function (options) {
289
+ var response = _this.validateMarketOrder(options);
290
+ if (response.err != null)
291
+ return response;
292
+ var stopMarket = order_1.OrderFactory.createOrder(__assign(__assign({}, options), { type: types_1.OrderType.STOP_MARKET }));
293
+ return _this._stopOrder(stopMarket, response);
294
+ };
295
+ this._stopLimit = function (options) {
296
+ var _a;
297
+ var response = _this.validateLimitOrder(options);
298
+ if (response.err != null)
299
+ return response;
300
+ var stopLimit = order_1.OrderFactory.createOrder(__assign(__assign({}, options), { type: types_1.OrderType.STOP_LIMIT, timeInForce: (_a = options.timeInForce) !== null && _a !== void 0 ? _a : types_1.TimeInForce.GTC }));
301
+ return _this._stopOrder(stopLimit, response);
302
+ };
303
+ this._oco = function (options) {
304
+ var _a;
305
+ var response = _this.validateLimitOrder(options);
306
+ /* node:coverage ignore next - Already validated with limit test */
307
+ if (response.err != null)
308
+ return response;
309
+ if (_this.validateOCOOrder(options)) {
310
+ // We use the same ID for Stop Limit and Limit Order, since
311
+ // we check only on limit order for duplicated ids
312
+ _this._limit({
313
+ id: options.id,
314
+ side: options.side,
315
+ size: options.size,
316
+ price: options.price,
317
+ timeInForce: options.timeInForce,
318
+ ocoStopPrice: options.stopPrice,
319
+ }, response);
320
+ /* node:coverage ignore next - Already validated with limit test */
321
+ if (response.err != null)
322
+ return response;
323
+ var stopLimit = order_1.OrderFactory.createOrder({
324
+ type: types_1.OrderType.STOP_LIMIT,
325
+ id: options.id,
326
+ side: options.side,
327
+ size: options.size,
328
+ price: options.stopLimitPrice,
329
+ stopPrice: options.stopPrice,
330
+ timeInForce: (_a = options.stopLimitTimeInForce) !== null && _a !== void 0 ? _a : types_1.TimeInForce.GTC,
331
+ isOCO: true,
332
+ });
333
+ _this.stopBook.add(stopLimit);
334
+ response.done.push(stopLimit);
335
+ }
336
+ else {
337
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INVALID_CONDITIONAL_ORDER);
338
+ }
339
+ return response;
340
+ };
341
+ this._stopOrder = function (stopOrder, response) {
342
+ if (_this.stopBook.validConditionalOrder(_this._marketPrice, stopOrder)) {
343
+ _this.stopBook.add(stopOrder);
344
+ response.done.push(stopOrder);
345
+ }
346
+ else {
347
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INVALID_CONDITIONAL_ORDER);
348
+ }
349
+ return response;
350
+ };
351
+ this.restoreSnapshot = function (snapshot) {
352
+ _this._lastOp = snapshot.lastOp;
353
+ for (var _i = 0, _a = snapshot.bids; _i < _a.length; _i++) {
354
+ var level = _a[_i];
355
+ for (var _b = 0, _c = level.orders; _b < _c.length; _b++) {
356
+ var order = _c[_b];
357
+ _this.orders[order.id] = order;
358
+ _this.bids.append(order);
359
+ }
360
+ }
361
+ for (var _d = 0, _e = snapshot.asks; _d < _e.length; _d++) {
362
+ var level = _e[_d];
363
+ for (var _f = 0, _g = level.orders; _f < _g.length; _f++) {
364
+ var order = _g[_f];
365
+ _this.orders[order.id] = order;
366
+ _this.asks.append(order);
367
+ }
368
+ }
369
+ };
370
+ /**
371
+ * Remove an existing order with given ID from the order book
372
+ * @param orderID The id of the order to be deleted
373
+ * @param internalDeletion Set to true when the delete comes from internal operations
374
+ * @returns The removed order if exists or `undefined`
375
+ */
376
+ this._cancelOrder = function (orderID, internalDeletion) {
377
+ if (internalDeletion === void 0) { internalDeletion = false; }
378
+ var order = _this.orders[orderID];
379
+ if (order === undefined)
380
+ return;
381
+ delete _this.orders[orderID];
382
+ var side = order.side === types_1.Side.BUY ? _this.bids : _this.asks;
383
+ var response = {
384
+ order: side.remove(order),
385
+ };
386
+ // Delete OCO Order only when the delete request comes from user
387
+ if (!internalDeletion && order.ocoStopPrice !== undefined) {
388
+ response.stopOrder = _this.stopBook.remove(order.side, orderID, order.ocoStopPrice);
389
+ }
390
+ if (_this.enableJournaling) {
391
+ response.log = {
392
+ opId: internalDeletion ? _this._lastOp : ++_this._lastOp,
393
+ ts: Date.now(),
394
+ op: "d",
395
+ o: { orderID: orderID },
396
+ };
397
+ }
398
+ return response;
399
+ };
400
+ this.getProcessOrderResponse = function (size) {
401
+ return {
402
+ done: [],
403
+ activated: [],
404
+ partial: null,
405
+ partialQuantityProcessed: 0,
406
+ quantityLeft: size,
407
+ err: null,
408
+ };
409
+ };
410
+ this.createLimitOrder = function (response, side, orderID, size, price, postOnly, timeInForce, ocoStopPrice) {
411
+ var quantityToTrade = size;
412
+ var sideToProcess;
413
+ var sideToAdd;
414
+ var comparator;
415
+ var iter;
416
+ if (side === types_1.Side.BUY) {
417
+ sideToAdd = _this.bids;
418
+ sideToProcess = _this.asks;
419
+ comparator = _this.greaterThanOrEqual;
420
+ iter = _this.asks.minPriceQueue;
421
+ }
422
+ else {
423
+ sideToAdd = _this.asks;
424
+ sideToProcess = _this.bids;
425
+ comparator = _this.lowerThanOrEqual;
426
+ iter = _this.bids.maxPriceQueue;
427
+ }
428
+ if (timeInForce === types_1.TimeInForce.FOK) {
429
+ var fillable = _this.canFillOrder(sideToProcess, side, size, price);
430
+ if (!fillable) {
431
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE);
432
+ return;
433
+ }
434
+ }
435
+ var bestPrice = iter();
436
+ var priceBefore = _this._marketPrice;
437
+ while (quantityToTrade > 0 &&
438
+ sideToProcess.len() > 0 &&
439
+ bestPrice !== undefined &&
440
+ comparator(price, bestPrice.price())) {
441
+ if (postOnly) {
442
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.LIMIT_ORDER_POST_ONLY);
443
+ return;
444
+ }
445
+ var _a = _this.processQueue(bestPrice, quantityToTrade), done = _a.done, partial = _a.partial, partialQuantityProcessed = _a.partialQuantityProcessed, quantityLeft = _a.quantityLeft;
446
+ response.done = response.done.concat(done);
447
+ response.partial = partial;
448
+ response.partialQuantityProcessed = partialQuantityProcessed;
449
+ quantityToTrade = quantityLeft;
450
+ response.quantityLeft = quantityToTrade;
451
+ bestPrice = iter();
452
+ }
453
+ _this.executeConditionalOrder(side, priceBefore, response);
454
+ var order;
455
+ var takerQty = size - quantityToTrade;
456
+ var makerQty = quantityToTrade;
457
+ if (quantityToTrade > 0) {
458
+ order = order_1.OrderFactory.createOrder(__assign({ type: types_1.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 } : {})));
459
+ if (response.done.length > 0) {
460
+ response.partialQuantityProcessed = size - quantityToTrade;
461
+ response.partial = order;
462
+ }
463
+ _this.orders[orderID] = sideToAdd.append(order);
464
+ }
465
+ else {
466
+ var totalQuantity_1 = 0;
467
+ var totalPrice_1 = 0;
468
+ response.done.forEach(function (order) {
469
+ totalQuantity_1 += order.size;
470
+ totalPrice_1 += order.price * order.size;
471
+ });
472
+ if (response.partialQuantityProcessed > 0 && response.partial !== null) {
473
+ totalQuantity_1 += response.partialQuantityProcessed;
474
+ totalPrice_1 +=
475
+ response.partial.price * response.partialQuantityProcessed;
476
+ }
477
+ order = order_1.OrderFactory.createOrder({
478
+ id: orderID,
479
+ type: types_1.OrderType.LIMIT,
480
+ side: side,
481
+ size: size,
482
+ origSize: size,
483
+ price: totalPrice_1 / totalQuantity_1,
484
+ time: Date.now(),
485
+ timeInForce: timeInForce,
486
+ postOnly: postOnly,
487
+ takerQty: takerQty,
488
+ makerQty: makerQty,
489
+ });
490
+ response.done.push(order);
491
+ }
492
+ // If IOC order was not matched completely remove from the order book
493
+ if (timeInForce === types_1.TimeInForce.IOC && response.quantityLeft > 0) {
494
+ _this._cancelOrder(orderID, true);
495
+ }
496
+ return order;
497
+ };
498
+ this.executeConditionalOrder = function (side, priceBefore, response) {
499
+ if (!_this.experimentalConditionalOrders)
500
+ return;
501
+ var pendingOrders = _this.stopBook.getConditionalOrders(side, priceBefore, _this._marketPrice);
502
+ if (pendingOrders.length > 0) {
503
+ var toBeExecuted_1 = [];
504
+ // Before get all orders to be executed and clean up the stop queue
505
+ // in order to avoid that an executed limit/market order run against
506
+ // the same stop order queue
507
+ pendingOrders.forEach(function (queue) {
508
+ while (queue.len() > 0) {
509
+ var headOrder = queue.removeFromHead();
510
+ if (headOrder !== undefined)
511
+ toBeExecuted_1.push(headOrder);
512
+ }
513
+ // Queue is empty now so remove the priceLevel
514
+ _this.stopBook.removePriceLevel(side, queue.price);
515
+ });
516
+ toBeExecuted_1.forEach(function (stopOrder) {
517
+ if (stopOrder.type === types_1.OrderType.STOP_MARKET) {
518
+ _this._market({
519
+ id: stopOrder.id,
520
+ side: stopOrder.side,
521
+ size: stopOrder.size,
522
+ }, response);
523
+ }
524
+ else {
525
+ if (stopOrder.isOCO) {
526
+ _this._cancelOrder(stopOrder.id, true);
527
+ }
528
+ _this._limit({
529
+ id: stopOrder.id,
530
+ side: stopOrder.side,
531
+ size: stopOrder.size,
532
+ price: stopOrder.price,
533
+ timeInForce: stopOrder.timeInForce,
534
+ }, response);
535
+ }
536
+ response.activated.push(stopOrder);
537
+ });
538
+ }
539
+ };
540
+ this.replayJournal = function (journal) {
541
+ for (var _i = 0, journal_1 = journal; _i < journal_1.length; _i++) {
542
+ var log = journal_1[_i];
543
+ switch (log.op) {
544
+ case "m": {
545
+ var _a = log.o, side = _a.side, size = _a.size;
546
+ if (side == null || size == null) {
547
+ throw (0, errors_1.CustomError)(errors_1.ERROR.INVALID_JOURNAL_LOG);
548
+ }
549
+ _this.market({ side: side, size: size });
550
+ break;
551
+ }
552
+ case "l": {
553
+ var _b = log.o, side = _b.side, id = _b.id, size = _b.size, price = _b.price, timeInForce = _b.timeInForce;
554
+ if (side == null || id == null || size == null || price == null) {
555
+ throw (0, errors_1.CustomError)(errors_1.ERROR.INVALID_JOURNAL_LOG);
556
+ }
557
+ _this.limit({
558
+ side: side,
559
+ id: id,
560
+ size: size,
561
+ price: price,
562
+ timeInForce: timeInForce,
563
+ });
564
+ break;
565
+ }
566
+ case "d":
567
+ if (log.o.orderID == null)
568
+ throw (0, errors_1.CustomError)(errors_1.ERROR.INVALID_JOURNAL_LOG);
569
+ _this.cancel(log.o.orderID);
570
+ break;
571
+ case "u":
572
+ if (log.o.orderID == null || log.o.orderUpdate == null) {
573
+ throw (0, errors_1.CustomError)(errors_1.ERROR.INVALID_JOURNAL_LOG);
574
+ }
575
+ _this.modify(log.o.orderID, log.o.orderUpdate);
576
+ break;
577
+ default:
578
+ throw (0, errors_1.CustomError)(errors_1.ERROR.INVALID_JOURNAL_LOG);
579
+ }
580
+ }
581
+ };
582
+ /**
583
+ * OCO Order:
584
+ * Buy: price < marketPrice < stopPrice
585
+ * Sell: price > marketPrice > stopPrice
586
+ */
587
+ this.validateOCOOrder = function (options) {
588
+ var response = false;
589
+ if (options.side === types_1.Side.BUY &&
590
+ options.price < _this._marketPrice &&
591
+ _this._marketPrice < options.stopPrice) {
592
+ response = true;
593
+ }
594
+ if (options.side === types_1.Side.SELL &&
595
+ options.price > _this._marketPrice &&
596
+ _this._marketPrice > options.stopPrice) {
597
+ response = true;
598
+ }
599
+ return response;
600
+ };
601
+ this.greaterThanOrEqual = function (a, b) {
602
+ return a >= b;
603
+ };
604
+ this.lowerThanOrEqual = function (a, b) {
605
+ return a <= b;
606
+ };
607
+ this.processQueue = function (orderQueue, quantityToTrade) {
608
+ var response = {
609
+ done: [],
610
+ activated: [],
611
+ partial: null,
612
+ partialQuantityProcessed: 0,
613
+ quantityLeft: quantityToTrade,
614
+ err: null,
615
+ };
616
+ if (response.quantityLeft > 0) {
617
+ while (orderQueue.len() > 0 && response.quantityLeft > 0) {
618
+ var headOrder = orderQueue.head();
619
+ if (headOrder !== undefined) {
620
+ if (response.quantityLeft < headOrder.size) {
621
+ response.partial = order_1.OrderFactory.createOrder(__assign(__assign({}, headOrder.toObject()), { size: headOrder.size - response.quantityLeft }));
622
+ _this.orders[headOrder.id] = response.partial;
623
+ response.partialQuantityProcessed = response.quantityLeft;
624
+ orderQueue.update(headOrder, response.partial);
625
+ response.quantityLeft = 0;
626
+ }
627
+ else {
628
+ response.quantityLeft = response.quantityLeft - headOrder.size;
629
+ var canceledOrder = _this._cancelOrder(headOrder.id, true);
630
+ /* node:coverage ignore next - Unable to test when order is undefined */
631
+ if ((canceledOrder === null || canceledOrder === void 0 ? void 0 : canceledOrder.order) !== undefined) {
632
+ response.done.push(canceledOrder.order);
633
+ }
634
+ }
635
+ // Remove linked OCO Stop Order if any
636
+ if (_this.experimentalConditionalOrders &&
637
+ headOrder.ocoStopPrice !== undefined) {
638
+ _this.stopBook.remove(headOrder.side, headOrder.id, headOrder.ocoStopPrice);
639
+ }
640
+ _this._marketPrice = headOrder.price;
641
+ }
642
+ }
643
+ }
644
+ return response;
645
+ };
646
+ this.canFillOrder = function (orderSide, side, size, price) {
647
+ return side === types_1.Side.BUY
648
+ ? _this.buyOrderCanBeFilled(orderSide, size, price)
649
+ : _this.sellOrderCanBeFilled(orderSide, size, price);
650
+ };
651
+ this.buyOrderCanBeFilled = function (orderSide, size, price) {
652
+ if (orderSide.volume() < size) {
653
+ return false;
654
+ }
655
+ var cumulativeSize = 0;
656
+ orderSide.priceTree().forEach(function (_, level) {
657
+ if (price >= level.price() && cumulativeSize < size) {
658
+ cumulativeSize += level.volume();
659
+ }
660
+ else {
661
+ return true; // break the loop
662
+ }
663
+ });
664
+ return cumulativeSize >= size;
665
+ };
666
+ this.sellOrderCanBeFilled = function (orderSide, size, price) {
667
+ if (orderSide.volume() < size) {
668
+ return false;
669
+ }
670
+ var cumulativeSize = 0;
671
+ orderSide.priceTree().forEach(function (_, level) {
672
+ if (price <= level.price() && cumulativeSize < size) {
673
+ cumulativeSize += level.volume();
674
+ }
675
+ else {
676
+ return true; // break the loop
677
+ }
678
+ });
679
+ return cumulativeSize >= size;
680
+ };
681
+ this.validateMarketOrder = function (order) {
682
+ var response = _this.getProcessOrderResponse(order.size);
683
+ if (![types_1.Side.SELL, types_1.Side.BUY].includes(order.side)) {
684
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INVALID_SIDE);
685
+ return response;
686
+ }
687
+ if (typeof order.size !== "number" || order.size <= 0) {
688
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INSUFFICIENT_QUANTITY);
689
+ return response;
690
+ }
691
+ return response;
692
+ };
693
+ this.validateLimitOrder = function (options) {
694
+ var response = _this.getProcessOrderResponse(options.size);
695
+ if (![types_1.Side.SELL, types_1.Side.BUY].includes(options.side)) {
696
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INVALID_SIDE);
697
+ return response;
698
+ }
699
+ if (_this.orders[options.id] !== undefined) {
700
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.ORDER_ALREDY_EXISTS);
701
+ return response;
702
+ }
703
+ if (typeof options.size !== "number" || options.size <= 0) {
704
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INVALID_QUANTITY);
705
+ return response;
706
+ }
707
+ if (typeof options.price !== "number" || options.price <= 0) {
708
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INVALID_PRICE);
709
+ return response;
710
+ }
711
+ if (options.timeInForce &&
712
+ !validTimeInForce.includes(options.timeInForce)) {
713
+ response.err = (0, errors_1.CustomError)(errors_1.ERROR.INVALID_TIF);
714
+ return response;
715
+ }
716
+ return response;
717
+ };
718
+ this.bids = new orderside_1.OrderSide(types_1.Side.BUY);
719
+ this.asks = new orderside_1.OrderSide(types_1.Side.SELL);
720
+ this.enableJournaling = enableJournaling;
721
+ this.stopBook = new stopbook_1.StopBook();
722
+ this.experimentalConditionalOrders = experimentalConditionalOrders;
723
+ // First restore from orderbook snapshot
724
+ if (snapshot != null) {
725
+ this.restoreSnapshot(snapshot);
726
+ }
727
+ // Than replay from journal log
728
+ if (journal != null) {
729
+ if (!Array.isArray(journal))
730
+ throw (0, errors_1.CustomError)(errors_1.ERROR.INVALID_JOURNAL_LOG);
731
+ // If a snapshot is available be sure to remove logs before the last restored operation
732
+ if (snapshot != null && snapshot.lastOp > 0) {
733
+ journal = journal.filter(function (log) { return log.opId > snapshot.lastOp; });
734
+ }
735
+ this.replayJournal(journal);
736
+ }
737
+ }
738
+ Object.defineProperty(OrderBook.prototype, "marketPrice", {
739
+ // Getter for the market price
740
+ get: function () {
741
+ return this._marketPrice;
742
+ },
743
+ enumerable: false,
744
+ configurable: true
745
+ });
746
+ Object.defineProperty(OrderBook.prototype, "lastOp", {
747
+ // Getter for the lastOp
748
+ get: function () {
749
+ return this._lastOp;
750
+ },
751
+ enumerable: false,
752
+ configurable: true
753
+ });
754
+ OrderBook.prototype.createOrder = function (typeOrOptions, side, size, price, orderID, timeInForce, stopPrice, stopLimitPrice, stopLimitTimeInForce, postOnly) {
755
+ if (timeInForce === void 0) { timeInForce = types_1.TimeInForce.GTC; }
756
+ if (stopLimitTimeInForce === void 0) { stopLimitTimeInForce = types_1.TimeInForce.GTC; }
757
+ var options;
758
+ // We don't want to test the deprecated signature.
759
+ /* node:coverage disable */
760
+ if (typeof typeOrOptions === "string" &&
761
+ side !== undefined &&
762
+ size !== undefined) {
763
+ options = {
764
+ type: typeOrOptions,
765
+ side: side,
766
+ size: size,
767
+ // @ts-expect-error
768
+ price: price,
769
+ id: orderID,
770
+ timeInForce: timeInForce,
771
+ // @ts-expect-error
772
+ stopPrice: stopPrice,
773
+ // @ts-expect-error
774
+ stopLimitPrice: stopLimitPrice,
775
+ stopLimitTimeInForce: stopLimitTimeInForce,
776
+ postOnly: postOnly,
777
+ };
778
+ /* node:coverage enable */
779
+ }
780
+ else if (typeof typeOrOptions === "object") {
781
+ options = typeOrOptions;
782
+ /* node:coverage disable */
783
+ }
784
+ else {
785
+ throw new Error("Invalid arguments.");
786
+ }
787
+ /* node:coverage enable */
788
+ switch (options.type) {
789
+ case types_1.OrderType.MARKET:
790
+ return this.market(options);
791
+ case types_1.OrderType.LIMIT:
792
+ return this.limit(options);
793
+ case types_1.OrderType.STOP_MARKET:
794
+ return this.stopMarket(options);
795
+ case types_1.OrderType.STOP_LIMIT:
796
+ return this.stopLimit(options);
797
+ case types_1.OrderType.OCO:
798
+ return this.oco(options);
799
+ default:
800
+ return {
801
+ done: [],
802
+ activated: [],
803
+ partial: null,
804
+ partialQuantityProcessed: 0,
805
+ quantityLeft: 0,
806
+ err: (0, errors_1.CustomError)(errors_1.ERROR.INVALID_ORDER_TYPE),
807
+ };
808
+ }
809
+ };
810
+ OrderBook.prototype.market = function (sideOrOptions, size) {
811
+ // We don't want to test the deprecated signature.
812
+ /* node:coverage disable */
813
+ if (typeof sideOrOptions === "string" && size !== undefined) {
814
+ return this._market({ side: sideOrOptions, size: size });
815
+ /* node:coverage enable */
816
+ }
817
+ if (typeof sideOrOptions === "object") {
818
+ return this._market(sideOrOptions);
819
+ /* node:coverage disable */
820
+ }
821
+ throw new Error("Invalid arguments.");
822
+ /* node:coverage enable */
823
+ };
824
+ OrderBook.prototype.limit = function (sideOrOptions, orderID, size, price, timeInForce) {
825
+ if (timeInForce === void 0) { timeInForce = types_1.TimeInForce.GTC; }
826
+ // We don't want to test the deprecated signature.
827
+ /* node:coverage disable */
828
+ if (typeof sideOrOptions === "string" &&
829
+ orderID !== undefined &&
830
+ size !== undefined &&
831
+ price !== undefined) {
832
+ return this._limit({
833
+ id: orderID,
834
+ side: sideOrOptions,
835
+ size: size,
836
+ price: price,
837
+ timeInForce: timeInForce,
838
+ });
839
+ /* node:coverage enable */
840
+ }
841
+ if (typeof sideOrOptions === "object") {
842
+ return this._limit(sideOrOptions);
843
+ /* node:coverage disable */
844
+ }
845
+ throw new Error("Invalid arguments.");
846
+ /* node:coverage enable */
847
+ };
848
+ return OrderBook;
849
+ }());
850
+ exports.OrderBook = OrderBook;