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