nodejs-order-book 10.0.1 → 10.1.1

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