nodejs-order-book 10.0.1 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +505 -157
- package/dist/cjs/errors.js +47 -45
- package/dist/cjs/index.js +4 -3
- package/dist/cjs/order.js +229 -272
- package/dist/cjs/orderbook.js +904 -784
- package/dist/cjs/orderqueue.js +68 -66
- package/dist/cjs/orderside.js +139 -147
- package/dist/cjs/stopbook.js +69 -69
- package/dist/cjs/stopqueue.js +47 -51
- package/dist/cjs/stopside.js +68 -68
- package/dist/cjs/types.js +8 -1
- package/dist/cjs/utils.js +2 -2
- package/dist/esm/errors.js +47 -46
- package/dist/esm/index.js +2 -2
- package/dist/esm/order.js +227 -273
- package/dist/esm/orderbook.js +900 -781
- package/dist/esm/orderqueue.js +67 -66
- package/dist/esm/orderside.js +134 -143
- package/dist/esm/stopbook.js +67 -68
- package/dist/esm/stopqueue.js +46 -51
- package/dist/esm/stopside.js +64 -65
- package/dist/esm/types.js +7 -0
- package/dist/esm/utils.js +2 -2
- package/dist/types/errors.d.ts +2 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/order.d.ts +5 -1
- package/dist/types/orderbook.d.ts +28 -2
- package/dist/types/types.d.ts +24 -0
- package/package.json +13 -12
package/dist/esm/orderbook.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
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,747 +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
|
-
|
|
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
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: the forEach of the priceTree must return true to break the loop
|
|
705
|
-
orderSide.priceTree().forEach(function (_, level) {
|
|
706
|
-
if (price >= level.price() && cumulativeSize < size) {
|
|
707
|
-
cumulativeSize += level.volume();
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
return true; // break the loop
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
return cumulativeSize >= size;
|
|
714
|
-
};
|
|
715
|
-
this.sellOrderCanBeFilled = function (orderSide, size, price) {
|
|
716
|
-
if (orderSide.volume() < size) {
|
|
717
|
-
return false;
|
|
718
|
-
}
|
|
719
|
-
var cumulativeSize = 0;
|
|
720
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: the forEach of the priceTree must return true to break the loop
|
|
721
|
-
orderSide.priceTree().forEach(function (_, level) {
|
|
722
|
-
if (price <= level.price() && cumulativeSize < size) {
|
|
723
|
-
cumulativeSize += level.volume();
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
return true; // break the loop
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
return cumulativeSize >= size;
|
|
730
|
-
};
|
|
731
|
-
this.validateMarketOrder = function (order) {
|
|
732
|
-
var response = _this.getProcessOrderResponse(order.size);
|
|
733
|
-
if (![Side.SELL, Side.BUY].includes(order.side)) {
|
|
734
|
-
response.err = CustomError(ERROR.INVALID_SIDE);
|
|
735
|
-
return response;
|
|
736
|
-
}
|
|
737
|
-
if (typeof order.size !== "number" || order.size <= 0) {
|
|
738
|
-
response.err = CustomError(ERROR.INSUFFICIENT_QUANTITY);
|
|
739
|
-
return response;
|
|
740
|
-
}
|
|
741
|
-
return response;
|
|
742
|
-
};
|
|
743
|
-
this.validateLimitOrder = function (options) {
|
|
744
|
-
var response = _this.getProcessOrderResponse(options.size);
|
|
745
|
-
if (![Side.SELL, Side.BUY].includes(options.side)) {
|
|
746
|
-
response.err = CustomError(ERROR.INVALID_SIDE);
|
|
747
|
-
return response;
|
|
748
|
-
}
|
|
749
|
-
if (_this.orders[options.id] !== undefined) {
|
|
750
|
-
response.err = CustomError(ERROR.ORDER_ALREDY_EXISTS);
|
|
751
|
-
return response;
|
|
752
|
-
}
|
|
753
|
-
if (typeof options.size !== "number" || options.size <= 0) {
|
|
754
|
-
response.err = CustomError(ERROR.INVALID_QUANTITY);
|
|
755
|
-
return response;
|
|
756
|
-
}
|
|
757
|
-
if (typeof options.price !== "number" || options.price <= 0) {
|
|
758
|
-
response.err = CustomError(ERROR.INVALID_PRICE);
|
|
759
|
-
return response;
|
|
760
|
-
}
|
|
761
|
-
if (options.timeInForce &&
|
|
762
|
-
!validTimeInForce.includes(options.timeInForce)) {
|
|
763
|
-
response.err = CustomError(ERROR.INVALID_TIF);
|
|
764
|
-
return response;
|
|
765
|
-
}
|
|
766
|
-
return response;
|
|
767
|
-
};
|
|
23
|
+
constructor({ snapshot, journal, enableJournaling = false, } = {}) {
|
|
768
24
|
this.bids = new OrderSide(Side.BUY);
|
|
769
25
|
this.asks = new OrderSide(Side.SELL);
|
|
770
26
|
this.enableJournaling = enableJournaling;
|
|
@@ -779,27 +35,19 @@ var OrderBook = /** @class */ (function () {
|
|
|
779
35
|
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
780
36
|
// If a snapshot is available be sure to remove logs before the last restored operation
|
|
781
37
|
if (snapshot != null && snapshot.lastOp > 0) {
|
|
782
|
-
journal = journal.filter(
|
|
38
|
+
journal = journal.filter((log) => log.opId > snapshot.lastOp);
|
|
783
39
|
}
|
|
784
40
|
this.replayJournal(journal);
|
|
785
41
|
}
|
|
786
42
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
Object.defineProperty(OrderBook.prototype, "lastOp", {
|
|
796
|
-
// Getter for the lastOp
|
|
797
|
-
get: function () {
|
|
798
|
-
return this._lastOp;
|
|
799
|
-
},
|
|
800
|
-
enumerable: false,
|
|
801
|
-
configurable: true
|
|
802
|
-
});
|
|
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
|
+
}
|
|
803
51
|
/**
|
|
804
52
|
* Create new order. See {@link CreateOrderOptions} for details.
|
|
805
53
|
*
|
|
@@ -816,7 +64,7 @@ var OrderBook = /** @class */ (function () {
|
|
|
816
64
|
* @param options.stopLimitTimeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. Param only for limit order
|
|
817
65
|
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
|
|
818
66
|
*/
|
|
819
|
-
|
|
67
|
+
createOrder(options) {
|
|
820
68
|
switch (options.type) {
|
|
821
69
|
case OrderType.MARKET:
|
|
822
70
|
return this.market(options);
|
|
@@ -838,7 +86,7 @@ var OrderBook = /** @class */ (function () {
|
|
|
838
86
|
err: CustomError(ERROR.INVALID_ORDER_TYPE),
|
|
839
87
|
};
|
|
840
88
|
}
|
|
841
|
-
}
|
|
89
|
+
}
|
|
842
90
|
/**
|
|
843
91
|
* Create a market order. See {@link MarketOrderOptions} for details.
|
|
844
92
|
*
|
|
@@ -847,8 +95,8 @@ var OrderBook = /** @class */ (function () {
|
|
|
847
95
|
* @param options.size - How much of currency you want to trade in units of base currency
|
|
848
96
|
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
|
|
849
97
|
*/
|
|
850
|
-
|
|
851
|
-
|
|
98
|
+
market(options) {
|
|
99
|
+
const response = this._market(options);
|
|
852
100
|
if (this.enableJournaling && response.err === null) {
|
|
853
101
|
response.log = {
|
|
854
102
|
opId: ++this._lastOp,
|
|
@@ -858,6 +106,27 @@ var OrderBook = /** @class */ (function () {
|
|
|
858
106
|
};
|
|
859
107
|
}
|
|
860
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;
|
|
861
130
|
};
|
|
862
131
|
/**
|
|
863
132
|
* Create a limit order. See {@link LimitOrderOptions} for details.
|
|
@@ -871,8 +140,8 @@ var OrderBook = /** @class */ (function () {
|
|
|
871
140
|
* @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
|
|
872
141
|
* @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
|
|
873
142
|
*/
|
|
874
|
-
|
|
875
|
-
|
|
143
|
+
limit(options) {
|
|
144
|
+
const response = this._limit(options);
|
|
876
145
|
if (this.enableJournaling && response.err === null) {
|
|
877
146
|
response.log = {
|
|
878
147
|
opId: ++this._lastOp,
|
|
@@ -882,7 +151,857 @@ var OrderBook = /** @class */ (function () {
|
|
|
882
151
|
};
|
|
883
152
|
}
|
|
884
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
|
+
// FOK is atomic, so this check is STP-aware.
|
|
561
|
+
const fillable = this.canFillOrder(sideToProcess, (levelPrice) => comparator(price, levelPrice), size, takerAccountId, stpMode);
|
|
562
|
+
if (!fillable) {
|
|
563
|
+
response.err = CustomError(ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
let bestPrice = iter();
|
|
568
|
+
const priceBefore = this._marketPrice;
|
|
569
|
+
while (quantityToTrade > 0 &&
|
|
570
|
+
sideToProcess.len() > 0 &&
|
|
571
|
+
bestPrice !== undefined &&
|
|
572
|
+
comparator(price, bestPrice.price()) &&
|
|
573
|
+
response.err === null) {
|
|
574
|
+
if (postOnly) {
|
|
575
|
+
response.err = CustomError(ERROR.LIMIT_ORDER_POST_ONLY);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const { done, partial, partialQuantityProcessed, quantityLeft, stpExpired, err, } = this.processQueue(bestPrice, quantityToTrade, takerAccountId, stpMode);
|
|
579
|
+
response.done = response.done.concat(done);
|
|
580
|
+
response.partial = partial;
|
|
581
|
+
response.partialQuantityProcessed = partialQuantityProcessed;
|
|
582
|
+
quantityToTrade = quantityLeft;
|
|
583
|
+
response.quantityLeft = quantityToTrade;
|
|
584
|
+
if (err !== null) {
|
|
585
|
+
response.err = err;
|
|
586
|
+
}
|
|
587
|
+
if (stpExpired !== undefined) {
|
|
588
|
+
if (response.stpExpired === undefined) {
|
|
589
|
+
response.stpExpired = [];
|
|
590
|
+
}
|
|
591
|
+
response.stpExpired = response.stpExpired.concat(stpExpired);
|
|
592
|
+
}
|
|
593
|
+
bestPrice = iter();
|
|
594
|
+
}
|
|
595
|
+
// If STP triggered (EXPIRE_TAKER or EXPIRE_BOTH), don't add order to book
|
|
596
|
+
if (response.err !== null) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
this.executeConditionalOrder(side, priceBefore, response);
|
|
600
|
+
let order;
|
|
601
|
+
const takerQty = size - quantityToTrade;
|
|
602
|
+
const makerQty = quantityToTrade;
|
|
603
|
+
if (quantityToTrade > 0) {
|
|
604
|
+
order = OrderFactory.createOrder({
|
|
605
|
+
type: OrderType.LIMIT,
|
|
606
|
+
id: orderID,
|
|
607
|
+
side,
|
|
608
|
+
size: quantityToTrade,
|
|
609
|
+
origSize: size,
|
|
610
|
+
price,
|
|
611
|
+
time: Date.now(),
|
|
612
|
+
timeInForce,
|
|
613
|
+
postOnly,
|
|
614
|
+
takerQty,
|
|
615
|
+
makerQty,
|
|
616
|
+
accountId: takerAccountId,
|
|
617
|
+
stpMode: stpMode,
|
|
618
|
+
...(ocoStopPrice !== undefined ? { ocoStopPrice } : {}),
|
|
619
|
+
});
|
|
620
|
+
if (response.done.length > 0) {
|
|
621
|
+
response.partialQuantityProcessed = size - quantityToTrade;
|
|
622
|
+
response.partial = order.toObject();
|
|
623
|
+
}
|
|
624
|
+
this.orders[orderID] = sideToAdd.append(order);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
let totalQuantity = 0;
|
|
628
|
+
let totalPrice = 0;
|
|
629
|
+
response.done.forEach((order) => {
|
|
630
|
+
totalQuantity += order.size;
|
|
631
|
+
totalPrice += order.price * order.size;
|
|
632
|
+
});
|
|
633
|
+
if (response.partialQuantityProcessed > 0 && response.partial !== null) {
|
|
634
|
+
totalQuantity += response.partialQuantityProcessed;
|
|
635
|
+
totalPrice +=
|
|
636
|
+
response.partial.price * response.partialQuantityProcessed;
|
|
637
|
+
}
|
|
638
|
+
order = OrderFactory.createOrder({
|
|
639
|
+
id: orderID,
|
|
640
|
+
type: OrderType.LIMIT,
|
|
641
|
+
side,
|
|
642
|
+
size,
|
|
643
|
+
origSize: size,
|
|
644
|
+
price: totalPrice / totalQuantity,
|
|
645
|
+
time: Date.now(),
|
|
646
|
+
timeInForce,
|
|
647
|
+
postOnly,
|
|
648
|
+
takerQty,
|
|
649
|
+
makerQty,
|
|
650
|
+
accountId: takerAccountId,
|
|
651
|
+
stpMode: stpMode,
|
|
652
|
+
});
|
|
653
|
+
response.done.push(order.toObject());
|
|
654
|
+
}
|
|
655
|
+
// If IOC order was not matched completely remove from the order book
|
|
656
|
+
if (timeInForce === TimeInForce.IOC && response.quantityLeft > 0) {
|
|
657
|
+
this._cancelOrder(orderID, true);
|
|
658
|
+
}
|
|
659
|
+
return order;
|
|
660
|
+
};
|
|
661
|
+
executeConditionalOrder = (side, priceBefore, response) => {
|
|
662
|
+
const pendingOrders = this.stopBook.getConditionalOrders(side, priceBefore, this._marketPrice);
|
|
663
|
+
if (pendingOrders.length > 0) {
|
|
664
|
+
const toBeExecuted = [];
|
|
665
|
+
// Before get all orders to be executed and clean up the stop queue
|
|
666
|
+
// in order to avoid that an executed limit/market order run against
|
|
667
|
+
// the same stop order queue
|
|
668
|
+
pendingOrders.forEach((queue) => {
|
|
669
|
+
while (queue.len() > 0) {
|
|
670
|
+
const headOrder = queue.removeFromHead();
|
|
671
|
+
if (headOrder !== undefined)
|
|
672
|
+
toBeExecuted.push(headOrder);
|
|
673
|
+
}
|
|
674
|
+
// Queue is empty now so remove the priceLevel
|
|
675
|
+
this.stopBook.removePriceLevel(side, queue.price);
|
|
676
|
+
});
|
|
677
|
+
toBeExecuted.forEach((stopOrder) => {
|
|
678
|
+
if (stopOrder.type === OrderType.STOP_MARKET) {
|
|
679
|
+
this._market({
|
|
680
|
+
id: stopOrder.id,
|
|
681
|
+
side: stopOrder.side,
|
|
682
|
+
size: stopOrder.size,
|
|
683
|
+
accountId: stopOrder.accountId,
|
|
684
|
+
stpMode: stopOrder.stpMode,
|
|
685
|
+
}, response);
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
if (stopOrder.isOCO) {
|
|
689
|
+
this._cancelOrder(stopOrder.id, true);
|
|
690
|
+
}
|
|
691
|
+
this._limit({
|
|
692
|
+
id: stopOrder.id,
|
|
693
|
+
side: stopOrder.side,
|
|
694
|
+
size: stopOrder.size,
|
|
695
|
+
price: stopOrder.price,
|
|
696
|
+
timeInForce: stopOrder.timeInForce,
|
|
697
|
+
accountId: stopOrder.accountId,
|
|
698
|
+
stpMode: stopOrder.stpMode,
|
|
699
|
+
}, response);
|
|
700
|
+
}
|
|
701
|
+
response.activated.push(stopOrder.toObject());
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
replayJournal = (journal) => {
|
|
706
|
+
for (const log of journal) {
|
|
707
|
+
switch (log.op) {
|
|
708
|
+
case "m": {
|
|
709
|
+
const { side, size } = log.o;
|
|
710
|
+
if (side == null || size == null) {
|
|
711
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
712
|
+
}
|
|
713
|
+
this.market(log.o);
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
case "l": {
|
|
717
|
+
const { side, id, size, price } = log.o;
|
|
718
|
+
if (side == null || id == null || size == null || price == null) {
|
|
719
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
720
|
+
}
|
|
721
|
+
this.limit(log.o);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "sm": {
|
|
725
|
+
const { side, size, stopPrice } = log.o;
|
|
726
|
+
if (side == null || size == null || stopPrice == null) {
|
|
727
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
728
|
+
}
|
|
729
|
+
this.stopMarket(log.o);
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case "sl": {
|
|
733
|
+
const { side, id, size, price, stopPrice } = log.o;
|
|
734
|
+
if (side == null ||
|
|
735
|
+
id == null ||
|
|
736
|
+
size == null ||
|
|
737
|
+
price == null ||
|
|
738
|
+
stopPrice == null) {
|
|
739
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
740
|
+
}
|
|
741
|
+
this.stopLimit(log.o);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case "oco": {
|
|
745
|
+
const { side, id, size, price, stopPrice, stopLimitPrice } = log.o;
|
|
746
|
+
if (side == null ||
|
|
747
|
+
id == null ||
|
|
748
|
+
size == null ||
|
|
749
|
+
price == null ||
|
|
750
|
+
stopPrice == null ||
|
|
751
|
+
stopLimitPrice == null) {
|
|
752
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
753
|
+
}
|
|
754
|
+
this.oco(log.o);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "d":
|
|
758
|
+
if (log.o.orderID == null)
|
|
759
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
760
|
+
this.cancel(log.o.orderID);
|
|
761
|
+
break;
|
|
762
|
+
case "u":
|
|
763
|
+
if (log.o.orderID == null || log.o.orderUpdate == null) {
|
|
764
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
765
|
+
}
|
|
766
|
+
this.modify(log.o.orderID, log.o.orderUpdate);
|
|
767
|
+
break;
|
|
768
|
+
default:
|
|
769
|
+
throw CustomError(ERROR.INVALID_JOURNAL_LOG);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
/**
|
|
774
|
+
* OCO Order:
|
|
775
|
+
* Buy: price < marketPrice < stopPrice
|
|
776
|
+
* Sell: price > marketPrice > stopPrice
|
|
777
|
+
*/
|
|
778
|
+
validateOCOOrder = (options) => {
|
|
779
|
+
let response = false;
|
|
780
|
+
if (options.side === Side.BUY &&
|
|
781
|
+
options.price < this._marketPrice &&
|
|
782
|
+
this._marketPrice < options.stopPrice) {
|
|
783
|
+
response = true;
|
|
784
|
+
}
|
|
785
|
+
if (options.side === Side.SELL &&
|
|
786
|
+
options.price > this._marketPrice &&
|
|
787
|
+
this._marketPrice > options.stopPrice) {
|
|
788
|
+
response = true;
|
|
789
|
+
}
|
|
790
|
+
return response;
|
|
791
|
+
};
|
|
792
|
+
greaterThanOrEqual = (a, b) => {
|
|
793
|
+
return a >= b;
|
|
794
|
+
};
|
|
795
|
+
lowerThanOrEqual = (a, b) => {
|
|
796
|
+
return a <= b;
|
|
797
|
+
};
|
|
798
|
+
processQueue = (orderQueue, quantityToTrade, takerAccountId, stpMode) => {
|
|
799
|
+
const response = {
|
|
800
|
+
done: [],
|
|
801
|
+
activated: [],
|
|
802
|
+
partial: null,
|
|
803
|
+
partialQuantityProcessed: 0,
|
|
804
|
+
quantityLeft: quantityToTrade,
|
|
805
|
+
err: null,
|
|
806
|
+
};
|
|
807
|
+
if (response.quantityLeft > 0) {
|
|
808
|
+
while (orderQueue.len() > 0 && response.quantityLeft > 0) {
|
|
809
|
+
const headOrder = orderQueue.head();
|
|
810
|
+
if (headOrder !== undefined) {
|
|
811
|
+
// Self-Trade Prevention check
|
|
812
|
+
if (takerAccountId &&
|
|
813
|
+
headOrder.accountId === takerAccountId &&
|
|
814
|
+
stpMode != null &&
|
|
815
|
+
stpMode !== SelfTradePreventionMode.NONE) {
|
|
816
|
+
switch (stpMode) {
|
|
817
|
+
case SelfTradePreventionMode.EXPIRE_MAKER: {
|
|
818
|
+
// Remove the maker order from the book, continue matching
|
|
819
|
+
const removedOrder = this._cancelOrder(headOrder.id, true);
|
|
820
|
+
if (removedOrder?.order !== undefined) {
|
|
821
|
+
if (response.stpExpired === undefined) {
|
|
822
|
+
response.stpExpired = [];
|
|
823
|
+
}
|
|
824
|
+
response.stpExpired.push(removedOrder.order);
|
|
825
|
+
}
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
case SelfTradePreventionMode.EXPIRE_TAKER: {
|
|
829
|
+
// Taker expires immediately, nothing matches.
|
|
830
|
+
// response.quantityLeft is intentionally left untouched: it already
|
|
831
|
+
// holds the unfilled quantity, which is lower than the level-entry
|
|
832
|
+
// `quantityToTrade` when STP triggers after a partial fill inside
|
|
833
|
+
// this price level. Resetting it would resurrect filled quantity.
|
|
834
|
+
response.err = CustomError(ERROR.STP_TRIGGERED);
|
|
835
|
+
return response;
|
|
836
|
+
}
|
|
837
|
+
case SelfTradePreventionMode.EXPIRE_BOTH: {
|
|
838
|
+
// Remove maker from book AND expire taker.
|
|
839
|
+
// As in EXPIRE_TAKER, response.quantityLeft must keep the running
|
|
840
|
+
// unfilled quantity rather than the level-entry quantityToTrade.
|
|
841
|
+
const removedOrder = this._cancelOrder(headOrder.id, true);
|
|
842
|
+
if (removedOrder?.order !== undefined) {
|
|
843
|
+
if (response.stpExpired === undefined) {
|
|
844
|
+
response.stpExpired = [];
|
|
845
|
+
}
|
|
846
|
+
response.stpExpired.push(removedOrder.order);
|
|
847
|
+
}
|
|
848
|
+
response.err = CustomError(ERROR.STP_TRIGGERED);
|
|
849
|
+
return response;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (response.quantityLeft < headOrder.size) {
|
|
854
|
+
const partial = OrderFactory.createOrder({
|
|
855
|
+
...headOrder.toObject(),
|
|
856
|
+
size: headOrder.size - response.quantityLeft,
|
|
857
|
+
});
|
|
858
|
+
response.partial = partial.toObject();
|
|
859
|
+
this.orders[headOrder.id] = partial;
|
|
860
|
+
response.partialQuantityProcessed = response.quantityLeft;
|
|
861
|
+
orderQueue.update(headOrder, partial);
|
|
862
|
+
response.quantityLeft = 0;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
response.quantityLeft = response.quantityLeft - headOrder.size;
|
|
866
|
+
const canceledOrder = this._cancelOrder(headOrder.id, true);
|
|
867
|
+
/* node:coverage ignore next - Unable to test when order is undefined */
|
|
868
|
+
if (canceledOrder?.order !== undefined) {
|
|
869
|
+
response.done.push(canceledOrder.order);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// Remove linked OCO Stop Order if any
|
|
873
|
+
if (headOrder.ocoStopPrice !== undefined) {
|
|
874
|
+
this.stopBook.remove(headOrder.side, headOrder.id, headOrder.ocoStopPrice);
|
|
875
|
+
}
|
|
876
|
+
this._marketPrice = headOrder.price;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return response;
|
|
881
|
+
};
|
|
882
|
+
/**
|
|
883
|
+
* Tells whether a self-trade check is required for this order, which is also the
|
|
884
|
+
* point past which level volumes stop being enough and orders must be walked
|
|
885
|
+
* individually.
|
|
886
|
+
*
|
|
887
|
+
* The conditions mirror the guard in `processQueue`, and the two must stay
|
|
888
|
+
* equivalent: if this reports a check where the matcher would not, a FOK order
|
|
889
|
+
* gets rejected as not fillable while the same order would otherwise have
|
|
890
|
+
* matched.
|
|
891
|
+
*/
|
|
892
|
+
shouldCheckSelfTrade = (takerAccountId, stpMode) => {
|
|
893
|
+
return (Boolean(takerAccountId) &&
|
|
894
|
+
stpMode != null &&
|
|
895
|
+
stpMode !== SelfTradePreventionMode.NONE);
|
|
896
|
+
};
|
|
897
|
+
/**
|
|
898
|
+
* Decides whether a FOK order can be filled in full, before the book is touched.
|
|
899
|
+
*
|
|
900
|
+
* Liquidity is walked in the same order the matcher consumes it, and a maker
|
|
901
|
+
* belonging to the taker's own account is not tradeable when STP is active.
|
|
902
|
+
* The three modes differ in how they stop the walk:
|
|
903
|
+
* EXPIRE_MAKER expires the maker and matching continues past it, while
|
|
904
|
+
* EXPIRE_TAKER and EXPIRE_BOTH stop the taker there, leaving no liquidity
|
|
905
|
+
* beyond that maker reachable.
|
|
906
|
+
*
|
|
907
|
+
* Only the STP-active path has to inspect individual orders. Without STP there
|
|
908
|
+
* is nothing to rule out, so level volumes are exact and the walk stays one
|
|
909
|
+
* step per price level.
|
|
910
|
+
*
|
|
911
|
+
* @param isLevelReachable - tells whether a level is within the taker's price.
|
|
912
|
+
* @returns true when `size` can be filled in full.
|
|
913
|
+
*/
|
|
914
|
+
canFillOrder = (orderSide, isLevelReachable, size, takerAccountId, stpMode) => {
|
|
915
|
+
// Necessary condition, so it is safe to reject on regardless of STP.
|
|
916
|
+
if (orderSide.volume() < size) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
if (!this.shouldCheckSelfTrade(takerAccountId, stpMode)) {
|
|
920
|
+
let cumulativeSize = 0;
|
|
921
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: the forEach of the priceTree must return true to break the loop
|
|
922
|
+
orderSide.priceTree().forEach((levelPrice, level) => {
|
|
923
|
+
if (isLevelReachable(levelPrice) && cumulativeSize < size) {
|
|
924
|
+
cumulativeSize += level.volume();
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
return true; // break the loop
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
return cumulativeSize >= size;
|
|
931
|
+
}
|
|
932
|
+
// A same-account maker is not tradeable, so the walk has to inspect orders
|
|
933
|
+
// rather than just level volumes. The price tree is ordered best-price-first
|
|
934
|
+
// on both sides, which is the order the matcher consumes levels in. Snapshot
|
|
935
|
+
// the reachable levels rather than polling for the next one: this walk removes
|
|
936
|
+
// nothing, so polling would keep returning the level it is already on.
|
|
937
|
+
const levels = [];
|
|
938
|
+
orderSide.priceTree().forEach((levelPrice, level) => {
|
|
939
|
+
if (isLevelReachable(levelPrice)) {
|
|
940
|
+
levels.push(level);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
let quantityLeft = size;
|
|
944
|
+
for (const level of levels) {
|
|
945
|
+
for (const order of level.toArray()) {
|
|
946
|
+
// `shouldCheckSelfTrade` above already guarantees takerAccountId is a
|
|
947
|
+
// non-empty string, so this cannot match an order that has no
|
|
948
|
+
// accountId, nor a second order with an empty one.
|
|
949
|
+
if (order.accountId === takerAccountId) {
|
|
950
|
+
if (stpMode !== SelfTradePreventionMode.EXPIRE_MAKER) {
|
|
951
|
+
// EXPIRE_TAKER / EXPIRE_BOTH stop the taker here, so no
|
|
952
|
+
// liquidity beyond this maker is reachable. STP is guaranteed
|
|
953
|
+
// active in this branch, so every other mode aborts.
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
// EXPIRE_MAKER expires the maker, so it offers no tradeable size
|
|
957
|
+
// and the walk continues past it.
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (quantityLeft <= order.size) {
|
|
961
|
+
// The taker completes here, so an STP maker further down the queue
|
|
962
|
+
// is never reached, exactly as in `processQueue`.
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
quantityLeft -= order.size;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return false;
|
|
969
|
+
};
|
|
970
|
+
validateMarketOrder = (order) => {
|
|
971
|
+
const response = this.getProcessOrderResponse(order.size);
|
|
972
|
+
if (![Side.SELL, Side.BUY].includes(order.side)) {
|
|
973
|
+
response.err = CustomError(ERROR.INVALID_SIDE);
|
|
974
|
+
return response;
|
|
975
|
+
}
|
|
976
|
+
if (typeof order.size !== "number" || order.size <= 0) {
|
|
977
|
+
response.err = CustomError(ERROR.INSUFFICIENT_QUANTITY);
|
|
978
|
+
return response;
|
|
979
|
+
}
|
|
980
|
+
return response;
|
|
981
|
+
};
|
|
982
|
+
validateLimitOrder = (options) => {
|
|
983
|
+
const response = this.getProcessOrderResponse(options.size);
|
|
984
|
+
if (![Side.SELL, Side.BUY].includes(options.side)) {
|
|
985
|
+
response.err = CustomError(ERROR.INVALID_SIDE);
|
|
986
|
+
return response;
|
|
987
|
+
}
|
|
988
|
+
if (this.orders[options.id] !== undefined) {
|
|
989
|
+
response.err = CustomError(ERROR.ORDER_ALREDY_EXISTS);
|
|
990
|
+
return response;
|
|
991
|
+
}
|
|
992
|
+
if (typeof options.size !== "number" || options.size <= 0) {
|
|
993
|
+
response.err = CustomError(ERROR.INVALID_QUANTITY);
|
|
994
|
+
return response;
|
|
995
|
+
}
|
|
996
|
+
if (typeof options.price !== "number" || options.price <= 0) {
|
|
997
|
+
response.err = CustomError(ERROR.INVALID_PRICE);
|
|
998
|
+
return response;
|
|
999
|
+
}
|
|
1000
|
+
if (options.timeInForce &&
|
|
1001
|
+
!validTimeInForce.includes(options.timeInForce)) {
|
|
1002
|
+
response.err = CustomError(ERROR.INVALID_TIF);
|
|
1003
|
+
return response;
|
|
1004
|
+
}
|
|
1005
|
+
return response;
|
|
885
1006
|
};
|
|
886
|
-
|
|
887
|
-
}());
|
|
888
|
-
export { OrderBook };
|
|
1007
|
+
}
|