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 CHANGED
@@ -10,54 +10,75 @@
10
10
  # Node.js Order Book
11
11
 
12
12
  <p align="center">
13
- Ultra-fast Node.js Order Book written in TypeScript </br> for high-frequency trading (HFT) :rocket::rocket: </br></br>
13
+ A fast, feature-complete limit order book engine for Node.js, written in TypeScript. </br>
14
+ Designed for trading systems, exchanges, and HFT simulations. </br></br>
14
15
  :star: Star me on GitHub — it motivates me a lot!
15
16
  </p>
16
17
 
18
+ **Why this library?** Originally ported from a [Go orderbook](https://github.com/i25959341/orderbook), this engine has been extended with conditional orders, Self-Trade Prevention (STP), snapshot/journaling for crash recovery, and full TypeScript support — while maintaining high throughput.
19
+
17
20
  ## Table of Contents
18
21
 
19
22
  - [Features](#features)
23
+ - [Quick Start](#quick-start)
24
+ - [Requirements](#requirements)
20
25
  - [Installation](#installation)
21
26
  - [Usage](#usage)
22
27
  - [Conditional Orders](#conditional-orders)
23
- - [About Primary Functions](#about-primary-functions)
24
- - [Create order `createOrder()`](#create-order)
25
- - [Create Limit order `limit()`](#create-limit-order)
26
- - [Create Market order `market()`](#create-market-order)
27
- - [Create Stop Limit order `stopLimit()`](#create-stop-limit-order)
28
- - [Create Stop Market order `stopMarket()`](#create-stop-market-order)
29
- - [Create OCO (One-Cancels-the-Other) order `oco()`](#create-oco-one-cancels-the-other-order)
30
- - [Modify an existing order `modifiy()`](#modify-an-existing-order)
31
- - [Cancel order `cancel()`](#cancel-order)
28
+ - [Primary Functions](#primary-functions)
29
+ - [createOrder()](#createorder)
30
+ - [limit()](#limit)
31
+ - [market()](#market)
32
+ - [stopLimit()](#stoplimit)
33
+ - [stopMarket()](#stopmarket)
34
+ - [oco()](#oco)
35
+ - [modify()](#modify)
36
+ - [cancel()](#cancel)
37
+ - [Understanding Order Results](#understanding-order-results)
38
+ - [Self-Trade Prevention (STP)](#self-trade-prevention-stp)
32
39
  - [Order Book Options](#order-book-options)
33
40
  - [Snapshot](#snapshot)
34
41
  - [Journal Logs](#journal-logs)
35
42
  - [Enable Journaling](#enable-journaling)
36
43
  - [Development](#development)
37
- - [Build](#build)
38
- - [Testing](#testing)
39
- - [Coverage](#coverage)
40
- - [Benchmarking](#benchmarking)
41
44
  - [Contributing](#contributing)
42
- - [Donation](#donation)
43
45
  - [License](#license)
46
+ - [Donation](#donation)
44
47
 
45
48
  ## Features
46
- > Initially ported from [Go orderbook](https://github.com/i25959341/orderbook), this order book has been enhanced with new features
47
49
 
48
- - Standard price-time priority
49
- - Supports both market and limit orders
50
- - Supports `post-only` limit order <img src="https://img.shields.io/badge/New-green" alt="New">
51
- - Supports conditional orders [**Stop Limit, Stop Market and OCO**](#conditional-orders) <img src="https://img.shields.io/badge/New-green" alt="New">
52
- - Supports time in force GTC, FOK and IOC <img src="https://img.shields.io/badge/New-green" alt="New">
53
- - Supports order cancelling
54
- - Supports order price and/or size updating <img src="https://img.shields.io/badge/New-green" alt="New">
55
- - Snapshot and journaling functionalities for restoring the order book during server startup <img src="https://img.shields.io/badge/New-green" alt="New">
56
- - **High performance (above 300k trades per second)**
50
+ - Standard price-time priority matching
51
+ - Market, limit, and post-only limit orders
52
+ - Conditional orders: Stop Limit, Stop Market, and OCO (One-Cancels-the-Other)
53
+ - Time-in-force: GTC (Good-Til-Cancelled), FOK (Fill-Or-Kill), IOC (Immediate-Or-Cancel)
54
+ - Self-Trade Prevention (STP) with 4 modes (NONE, EXPIRE_MAKER, EXPIRE_TAKER, EXPIRE_BOTH)
55
+ - Order cancellation
56
+ - Order price and/or size modification
57
+ - Snapshot and journaling for order book state persistence and recovery
58
+ - **High throughput** — benchmarked at 300k+ trades per second
59
+ - Full TypeScript support with dual ESM/CJS exports
60
+
61
+ ## Quick Start
62
+
63
+ ```ts
64
+ import { OrderBook, Side } from 'nodejs-order-book'
65
+
66
+ const ob = new OrderBook()
67
+
68
+ // Place a sell limit order
69
+ ob.limit({ side: Side.SELL, id: 'order-1', size: 55, price: 100 })
70
+
71
+ // Place a buy market order
72
+ const result = ob.market({ side: Side.BUY, size: 10 })
73
+
74
+ console.log(result.done) // Filled orders
75
+ console.log(result.partial) // Partial fill, if any
76
+ ```
57
77
 
58
- **Machine:** ASUS ExpertBook, 11th Gen Intel(R) Core(TM) i7-1165G7, 2.80Ghz, 16GB RAM, Node.js v18.4.0.
78
+ ## Requirements
59
79
 
60
- <img src="https://user-images.githubusercontent.com/1219087/181792292-8619ee25-bf75-4871-a06c-bd6c82157f33.png" alt="nodejs-order-book-benchmark" title="nodejs-order-book benchmark" />
80
+ - **Node.js** 18+ (ES2022 target)
81
+ - **npm**, **yarn**, or **pnpm**
61
82
 
62
83
  ## Installation
63
84
 
@@ -81,7 +102,17 @@ pnpm add nodejs-order-book
81
102
 
82
103
  ## Usage
83
104
 
84
- To start using order book you need to import `OrderBook` and create new instance:
105
+ The package supports both **ESM** and **CommonJS**:
106
+
107
+ ```ts
108
+ // ESM (recommended)
109
+ import { OrderBook, Side, OrderType, SelfTradePreventionMode } from 'nodejs-order-book'
110
+
111
+ // CommonJS
112
+ const { OrderBook, Side, OrderType, SelfTradePreventionMode } = require('nodejs-order-book')
113
+ ```
114
+
115
+ To start using the order book you need to import `OrderBook` and create a new instance:
85
116
 
86
117
  ```ts
87
118
  import { OrderBook } from 'nodejs-order-book'
@@ -89,7 +120,7 @@ import { OrderBook } from 'nodejs-order-book'
89
120
  const ob = new OrderBook()
90
121
  ```
91
122
 
92
- Then you'll be able to use next primary functions:
123
+ Then you'll be able to use the following primary functions:
93
124
 
94
125
  ```ts
95
126
  ob.createOrder({
@@ -113,7 +144,7 @@ ob.limit({
113
144
 
114
145
  ob.market({ side: 'buy' | 'sell', size: number })
115
146
 
116
- ob.modify(orderID: string, {
147
+ ob.modify(orderID: string, {
117
148
  side: 'buy' | 'sell',
118
149
  size: number,
119
150
  price: number
@@ -121,14 +152,17 @@ ob.modify(orderID: string, {
121
152
 
122
153
  ob.cancel(orderID: string)
123
154
  ```
155
+
124
156
  ### Conditional Orders
125
- Currently `Stop Market`, `Stop Limit` and `OCO` orders are supported.
157
+
158
+ `Stop Market`, `Stop Limit` and `OCO` orders are supported.
159
+
126
160
  ```ts
127
161
  import { OrderBook } from 'nodejs-order-book'
128
162
 
129
163
  const ob = new OrderBook()
130
164
 
131
- ob.createOrder({
165
+ ob.createOrder({
132
166
  type: 'stop_limit' | 'stop_market' | 'oco',
133
167
  side: 'buy' | 'sell',
134
168
  size: number,
@@ -166,14 +200,16 @@ ob.oco({
166
200
  })
167
201
  ```
168
202
 
169
- ## About primary functions
203
+ ## Primary Functions
170
204
 
171
- To add an order to the order book you can call the general `createOrder()` function or calling the underlying `limit()`, `market()`, `stopLimit()`, `stopMarket()` or `oco()` functions
205
+ To add an order to the order book you can call the general `createOrder()` function or use the underlying `limit()`, `market()`, `stopLimit()`, `stopMarket()` or `oco()` directly.
172
206
 
173
- ### Create Order
207
+ ### createOrder()
208
+
209
+ A unified entry point that accepts a `type` field to dispatch to the correct handler:
174
210
 
175
211
  ```ts
176
- // Create limit order
212
+ // Limit order
177
213
  ob.createOrder({
178
214
  type: 'limit',
179
215
  side: 'buy' | 'sell',
@@ -184,14 +220,14 @@ ob.createOrder({
184
220
  timeInForce?: 'GTC' | 'FOK' | 'IOC'
185
221
  })
186
222
 
187
- // Create market order
223
+ // Market order
188
224
  ob.createOrder({
189
225
  type: 'market',
190
226
  side: 'buy' | 'sell',
191
227
  size: number
192
228
  })
193
229
 
194
- // Create stop limit order
230
+ // Stop limit order
195
231
  ob.createOrder({
196
232
  type: 'stop_limit',
197
233
  side: 'buy' | 'sell',
@@ -202,7 +238,7 @@ ob.createOrder({
202
238
  timeInForce?: 'GTC' | 'FOK' | 'IOC'
203
239
  })
204
240
 
205
- // Create stop market order
241
+ // Stop market order
206
242
  ob.createOrder({
207
243
  type: 'stop_market',
208
244
  side: 'buy' | 'sell',
@@ -210,7 +246,7 @@ ob.createOrder({
210
246
  stopPrice: number
211
247
  })
212
248
 
213
- // Create OCO order
249
+ // OCO order
214
250
  ob.createOrder({
215
251
  type: 'oco',
216
252
  side: 'buy' | 'sell',
@@ -222,20 +258,19 @@ ob.createOrder({
222
258
  })
223
259
  ```
224
260
 
225
- ### Create Limit Order
261
+ ### limit()
262
+
263
+ Create a limit order.
226
264
 
227
265
  ```ts
228
266
  /**
229
- * Create a limit order. See {@link LimitOrderOptions} for details.
230
- *
231
- * @param options
232
267
  * @param options.side - `sell` or `buy`
233
268
  * @param options.id - Unique order ID
234
269
  * @param options.size - How much of currency you want to trade in units of base currency
235
- * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
236
- * @param options.postOnly - When `true` the order will be rejected if immediately matches and trades as a taker. Default is `false`
237
- * @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
238
- * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
270
+ * @param options.price - The price at which the order is to be fulfilled, in units of the quote currency
271
+ * @param options.postOnly - When `true` the order is rejected if it immediately matches as a taker. Default is `false`
272
+ * @param options.timeInForce - GTC, FOK, or IOC. Default is GTC
273
+ * @returns An object with the result of the processed order or an error.
239
274
  */
240
275
  ob.limit({
241
276
  side: 'buy' | 'sell',
@@ -289,16 +324,15 @@ done - 1 order with 100 price, (may be also few orders with 110 price) + uniq
289
324
  partial - 1 order with price 110
290
325
  ```
291
326
 
292
- ### Create Market Order
327
+ ### market()
328
+
329
+ Create a market order.
293
330
 
294
331
  ```ts
295
332
  /**
296
- * Create a market order. See {@link MarketOrderOptions} for details.
297
- *
298
- * @param options
299
333
  * @param options.side - `sell` or `buy`
300
334
  * @param options.size - How much of currency you want to trade in units of base currency
301
- * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
335
+ * @returns An object with the result of the processed order or an error.
302
336
  */
303
337
  ob.market({ side: 'buy' | 'sell', size: number })
304
338
  ```
@@ -333,20 +367,19 @@ partial - null
333
367
  quantityLeft - 4
334
368
  ```
335
369
 
336
- ### Create Stop Limit Order
370
+ ### stopLimit()
371
+
372
+ Create a stop limit order.
337
373
 
338
374
  ```ts
339
375
  /**
340
- * Create a stop limit order. See {@link StopLimitOrderOptions} for details.
341
- *
342
- * @param options
343
376
  * @param options.side - `sell` or `buy`
344
377
  * @param options.id - Unique order ID
345
378
  * @param options.size - How much of currency you want to trade in units of base currency
346
- * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
347
- * @param options.stopPrice - The price at which the order will be triggered.
348
- * @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
349
- * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
379
+ * @param options.price - The price at which the order is to be fulfilled, in units of the quote currency
380
+ * @param options.stopPrice - The price at which the order is triggered
381
+ * @param options.timeInForce - GTC, FOK, or IOC. Default is GTC
382
+ * @returns An object with the result of the processed order or an error.
350
383
  */
351
384
  ob.stopLimit({
352
385
  side: 'buy' | 'sell',
@@ -358,17 +391,16 @@ ob.stopLimit({
358
391
  })
359
392
  ```
360
393
 
361
- ### Create Stop Market Order
394
+ ### stopMarket()
395
+
396
+ Create a stop market order.
362
397
 
363
398
  ```ts
364
399
  /**
365
- * Create a stop market order. See {@link StopMarketOrderOptions} for details.
366
- *
367
- * @param options
368
400
  * @param options.side - `sell` or `buy`
369
401
  * @param options.size - How much of currency you want to trade in units of base currency
370
- * @param options.stopPrice - The price at which the order will be triggered.
371
- * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
402
+ * @param options.stopPrice - The price at which the order is triggered
403
+ * @returns An object with the result of the processed order or an error.
372
404
  */
373
405
  ob.stopMarket({
374
406
  side: 'buy' | 'sell',
@@ -377,31 +409,24 @@ ob.stopMarket({
377
409
  })
378
410
  ```
379
411
 
380
- ### Create OCO (One-Cancels-the-Other) Order
412
+ ### oco()
413
+
414
+ Create an OCO (One-Cancels-the-Other) order. An OCO combines a `stop_limit` and a `limit` order: when one is triggered or filled, the other is automatically canceled. Both orders share the same `side` and `size`. If you cancel one, the entire OCO pair is canceled.
415
+
416
+ For BUY orders: `stopPrice` must be above the current price, `price` below.
417
+ For SELL orders: `stopPrice` must be below the current price, `price` above.
381
418
 
382
419
  ```ts
383
420
  /**
384
- * Create an OCO (One-Cancels-the-Other) order.
385
- * OCO order combines a `stop_limit` order and a `limit` order, where if stop price
386
- * is triggered or limit order is fully or partially fulfilled, the other is canceled.
387
- * Both orders have the same `side` and `size`. If you cancel one of the orders, the
388
- * entire OCO order pair will be canceled.
389
- *
390
- * For BUY orders the `stopPrice` must be above the current price and the `price` below the current price
391
- * For SELL orders the `stopPrice` must be below the current price and the `price` above the current price
392
- *
393
- * See {@link OCOOrderOptions} for details.
394
- *
395
- * @param options
396
421
  * @param options.side - `sell` or `buy`
397
422
  * @param options.id - Unique order ID
398
423
  * @param options.size - How much of currency you want to trade in units of base currency
399
- * @param options.price - The price of the `limit` order at which the order is to be fullfilled, in units of the quote currency
400
- * @param options.stopPrice - The price at which the `stop_limit` order will be triggered.
401
- * @param options.stopLimitPrice - The price of the `stop_limit` order at which the order is to be fullfilled, in units of the quote currency.
402
- * @param options.timeInForce - Time-in-force of the `limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
403
- * @param options.stopLimitTimeInForce - Time-in-force of the `stop_limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
404
- * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
424
+ * @param options.price - The limit order price, in units of the quote currency
425
+ * @param options.stopPrice - The stop trigger price
426
+ * @param options.stopLimitPrice - The stop_limit order price, in units of the quote currency
427
+ * @param options.timeInForce - Time-in-force of the limit order. GTC, FOK, IOC. Default is GTC
428
+ * @param options.stopLimitTimeInForce - Time-in-force of the stop_limit order. GTC, FOK, IOC. Default is GTC
429
+ * @returns An object with the result of the processed order or an error.
405
430
  */
406
431
  ob.oco({
407
432
  side: 'buy' | 'sell',
@@ -415,18 +440,15 @@ ob.oco({
415
440
  })
416
441
  ```
417
442
 
418
- ### Modify an existing order
443
+ ### modify()
444
+
445
+ Modify an existing order by ID. When an order is modified (price or quantity), it is treated as a new entry: under price-time-priority, it moves to the back of the matching queue.
419
446
 
420
447
  ```ts
421
448
  /**
422
- * Modify an existing order with given ID. When an order is modified by price or quantity,
423
- * it will be deemed as a new entry. Under the price-time-priority algorithm, orders are
424
- * prioritized according to their order price and order time. Hence, the latest orders
425
- * will be placed at the back of the matching order queue.
426
- *
427
- * @param orderID - The ID of the order to be modified
428
- * @param orderUpdate - An object with the modified size and/or price of an order. The shape of the object is `{size, price}`.
429
- * @returns An object with the result of the processed order or an error
449
+ * @param orderID - The ID of the order to modify
450
+ * @param orderUpdate - An object with `{size, price}`. Only provided fields are updated
451
+ * @returns An object with the result or an error
430
452
  */
431
453
  ob.modify(orderID: string, { size: number, price: number })
432
454
  ```
@@ -462,14 +484,14 @@ bids: 90 -> 5 90 -> 5
462
484
  80 -> 1 80 -> 1
463
485
  ```
464
486
 
465
- ### Cancel Order
487
+ ### cancel()
488
+
489
+ Remove an existing order by ID from the order book.
466
490
 
467
491
  ```ts
468
492
  /**
469
- * Remove an existing order with given ID from the order book
470
- *
471
- * @param orderID - The ID of the order to be removed
472
- * @returns The removed order if exists or `undefined`
493
+ * @param orderID - The ID of the order to remove
494
+ * @returns The removed order if found, or `undefined`
473
495
  */
474
496
  ob.cancel(orderID: string)
475
497
  ```
@@ -486,120 +508,446 @@ bids: 90 -> 5 90 -> 5
486
508
  80 -> 1 80 -> 1
487
509
  ```
488
510
 
511
+ ## Understanding Order Results
512
+
513
+ When creating an order, the library returns an `IProcessOrder` object:
514
+
515
+ ```ts
516
+ interface IProcessOrder {
517
+ done: IOrder[]; // Fully consumed orders
518
+ activated: IStopOrder[]; // Triggered stop orders (stop limit, stop market, OCO)
519
+ partial: ILimitOrder | null; // Partially consumed limit order (if any)
520
+ quantityLeft: number; // Unfilled quantity of the taker order
521
+ partialQuantityProcessed: number; // Quantity consumed from the order in 'partial'
522
+ err: OrderBookError | null;
523
+ log?: JournalLog; // Journal entry (only when enableJournaling is true)
524
+ stpExpired?: IOrder[]; // Orders expired due to Self-Trade Prevention
525
+ }
526
+ ```
527
+
528
+ ### When Does the Taker Appear in Results?
529
+
530
+ **The taker order does NOT always appear in the result arrays.**
531
+
532
+ | Order Type | Fill Status | Taker in `done[]` | Taker in `partial` | `quantityLeft` |
533
+ |------------|-------------|-------------------|--------------------|----------------|
534
+ | LIMIT | Fully filled | ✅ YES | ❌ NO | `0` |
535
+ | LIMIT | Partially filled | ❌ NO | ✅ YES | `> 0` |
536
+ | MARKET | Fully or partially filled | ❌ NO | ❌ NO | `>= 0` |
537
+
538
+ **Key facts:**
539
+ - **Market orders never appear in `done[]` or `partial`** - only the matched maker orders appear
540
+ - **Limit orders fully filled**: Taker appears in `done[]` alongside matched makers
541
+ - **Limit orders partially filled**: Taker appears in `partial`, matched makers appear in `done[]`
542
+ - **`quantityLeft`**: Always represents unfilled quantity of the taker, regardless of where it appears
543
+
544
+ > **Note on `activated[]`**: When a stop limit, stop market, or OCO order is triggered, the triggered order(s) appear in the `activated` array. These are orders that were resting in the stop book and have now been activated for matching.
545
+ >
546
+ > **Note on `stpExpired[]`**: When Self-Trade Prevention is configured and triggered, expired orders are listed in `stpExpired`. See [Self-Trade Prevention (STP)](#self-trade-prevention-stp) for details.
547
+
548
+ ### What is `partialQuantityProcessed`?
549
+
550
+ This represents **how much of the order in `partial` was processed**, not how much is left.
551
+
552
+ - If `partial` contains the **taker** (partially filled limit order): represents amount of taker that was filled
553
+ - If `partial` contains a **maker** (partially consumed resting order): represents amount of maker that was consumed
554
+
555
+ **Example 1 - Taker in partial:**
556
+ ```ts
557
+ // 10-unit buy order, only 5 available
558
+ {
559
+ done: [{ id: 'maker-1', size: 5 }], // Fully consumed maker
560
+ partial: { id: 'taker', size: 5 }, // Taker (5 still unfilled)
561
+ quantityLeft: 5, // 5 units of taker unfilled
562
+ partialQuantityProcessed: 5 // 5 units of taker were filled
563
+ }
564
+ ```
565
+
566
+ **Example 2 - Maker in partial:**
567
+ ```ts
568
+ // 8-unit buy order, 20 available from one maker
569
+ {
570
+ done: [{ id: 'taker', size: 8 }], // Fully filled taker
571
+ partial: { id: 'maker-1', size: 12 }, // Maker: 12 still unfilled (20 - 8)
572
+ quantityLeft: 0, // Taker fully filled
573
+ partialQuantityProcessed: 8 // 8 units of maker were consumed
574
+ }
575
+ ```
576
+
577
+ > `partial.size` always represents the **remaining** quantity of that order, not what was consumed. In this example, the maker started with size 20, had 8 consumed, so `partial.size` is 12 (what's left on the book).
578
+
579
+ ### Example: Market Order (Fully Filled)
580
+
581
+ ```ts
582
+ // Market order for 10 units (10 available)
583
+ book.createOrder({ type: 'market', id: 'buy-1', size: 10, side: 'buy' })
584
+
585
+ // Result:
586
+ {
587
+ done: [{ id: 'sell-1', side: 'sell', size: 10 }], // Matched maker only
588
+ partial: null, // Taker NOT here
589
+ quantityLeft: 0, // Fully filled
590
+ partialQuantityProcessed: 0
591
+ }
592
+ ```
593
+
594
+ ### Example: Limit Order (Fully Filled)
595
+
596
+ ```ts
597
+ // Limit order for 10 units (10 available)
598
+ book.createOrder({ type: 'limit', id: 'buy-1', price: 100, size: 10, side: 'buy' })
599
+
600
+ // Result:
601
+ {
602
+ done: [
603
+ { id: 'sell-1', side: 'sell', size: 10 }, // Matched maker
604
+ { id: 'buy-1', side: 'buy', size: 10 } // Taker ✅
605
+ ],
606
+ partial: null,
607
+ quantityLeft: 0,
608
+ partialQuantityProcessed: 0
609
+ }
610
+ ```
611
+
612
+ ### Example: Limit Order (Partially Filled)
613
+
614
+ ```ts
615
+ // Limit order for 10 units (only 5 available)
616
+ book.createOrder({ type: 'limit', id: 'buy-1', price: 100, size: 10, side: 'buy' })
617
+
618
+ // Result:
619
+ {
620
+ done: [{ id: 'sell-1', side: 'sell', size: 5 }], // Fully consumed maker
621
+ partial: { id: 'buy-1', side: 'buy', size: 5 }, // Taker ✅ (5 unfilled)
622
+ quantityLeft: 5, // 5 units unfilled
623
+ partialQuantityProcessed: 5 // 5 units filled
624
+ }
625
+ ```
626
+
627
+ ## Self-Trade Prevention (STP)
628
+
629
+ > Inspired by [Binance's Self-Trade Prevention](https://developers.binance.com/docs/derivatives/usds-margined-futures/faq/stp-faq) — prevents orders from the same account from matching against each other.
630
+
631
+ ### How it works
632
+
633
+ Each order can carry an `accountId` and a `stpMode`. When a taker order enters the book and would match against a maker order with the same `accountId`, the STP mode of the **taker order** determines what happens:
634
+
635
+ | Mode | Effect |
636
+ |------|--------|
637
+ | `NONE` | No prevention — orders match normally |
638
+ | `EXPIRE_MAKER` | The resting maker order(s) expire; the taker order continues |
639
+ | `EXPIRE_TAKER` | The taker order is rejected; the resting maker order(s) stay on the book |
640
+ | `EXPIRE_BOTH` | Both the taker and the matching maker order(s) expire |
641
+
642
+ The STP mode of the **taker** order always takes precedence — the mode stored on a resting maker order is ignored for STP purposes.
643
+
644
+ ### API reference
645
+
646
+ Add `accountId` and `stpMode` to any order:
647
+
648
+ ```ts
649
+ import { OrderBook, SelfTradePreventionMode, Side } from 'nodejs-order-book'
650
+
651
+ const ob = new OrderBook()
652
+
653
+ // Place a resting limit order from account "alice"
654
+ ob.limit({
655
+ side: Side.BUY,
656
+ id: 'maker-order',
657
+ size: 5,
658
+ price: 100,
659
+ accountId: 'alice',
660
+ })
661
+
662
+ // Taker from the same account with STP enabled
663
+ const result = ob.limit({
664
+ side: Side.SELL,
665
+ id: 'taker-order',
666
+ size: 3,
667
+ price: 90,
668
+ accountId: 'alice',
669
+ stpMode: SelfTradePreventionMode.EXPIRE_MAKER,
670
+ })
671
+
672
+ // Check which orders expired due to STP
673
+ console.log(result.stpExpired) // [{ id: 'maker-order', ... }]
674
+ ```
675
+
676
+ ### Response fields
677
+
678
+ When STP is triggered, the response (`IProcessOrder`) includes:
679
+
680
+ | Field | Type | Description |
681
+ |-------|------|-------------|
682
+ | `stpExpired` | `IOrder[] \| undefined` | Orders removed from the book due to STP |
683
+ | `err` | `OrderBookError \| null` | Error with `code: 1202` and `message: "Self-trade prevention triggered"` for `EXPIRE_TAKER` / `EXPIRE_BOTH` |
684
+
685
+ ### Error code
686
+
687
+ STP rejections return error code `1202`:
688
+
689
+ ```ts
690
+ import { ErrorCodes } from 'nodejs-order-book'
691
+
692
+ assert.equal(result.err?.code, ErrorCodes.STP_TRIGGERED)
693
+ // → 1202
694
+ assert.equal(result.err?.message, 'Self-trade prevention triggered')
695
+ ```
696
+
697
+ ### Scenarios
698
+
699
+ #### A) EXPIRE_MAKER — maker expires, taker continues
700
+
701
+ ```
702
+ Maker BUY @ 100 qty: 5 account: "alice"
703
+ Maker BUY @ 90 qty: 5 account: "alice"
704
+ Taker SELL @ 90 qty: 3 account: "alice" mode: EXPIRE_MAKER
705
+ ```
706
+
707
+ The two resting buy orders share the same account as the taker. With `EXPIRE_MAKER`, they are removed from the book and reported in `stpExpired[]`. The taker order (size 3) is placed on the book as a new maker.
708
+
709
+ ```
710
+ stpExpired → [maker-buy-100, maker-buy-90]
711
+ err → null
712
+ ```
713
+
714
+ #### B) EXPIRE_TAKER — taker expires, maker stays
715
+
716
+ ```
717
+ Maker BUY @ 100 qty: 5 account: "alice"
718
+ Taker SELL @ 90 qty: 3 account: "alice" mode: EXPIRE_TAKER
719
+ ```
720
+
721
+ The taker order is rejected immediately. The resting maker order remains untouched on the book.
722
+
723
+ ```
724
+ stpExpired → undefined
725
+ err → { code: 1202, message: "Self-trade prevention triggered" }
726
+ ```
727
+
728
+ #### C) EXPIRE_BOTH — both orders expire
729
+
730
+ ```
731
+ Maker BUY @ 100 qty: 5 account: "alice"
732
+ Taker SELL @ 90 qty: 3 account: "alice" mode: EXPIRE_BOTH
733
+ ```
734
+
735
+ The maker is removed from the book and the taker is rejected. Both sides expire.
736
+
737
+ ```
738
+ stpExpired → [maker-buy-100]
739
+ err → { code: 1202, message: "Self-trade prevention triggered" }
740
+ ```
741
+
742
+ #### D) Different accounts — normal matching (no STP)
743
+
744
+ ```
745
+ Maker BUY @ 100 qty: 5 account: "alice"
746
+ Taker SELL @ 90 qty: 3 account: "bob" mode: EXPIRE_MAKER
747
+ ```
748
+
749
+ The accounts differ, so STP does **not** trigger. The orders match normally.
750
+
751
+ ```
752
+ done → [filled trade summary]
753
+ stpExpired → undefined
754
+ ```
755
+
756
+ #### E) Mode NONE — no prevention
757
+
758
+ ```
759
+ Maker BUY @ 100 qty: 5 account: "alice"
760
+ Taker SELL @ 90 qty: 3 account: "alice" mode: NONE
761
+ ```
762
+
763
+ Even though both orders are from the same account, `NONE` mode allows the match.
764
+
765
+ ```
766
+ done → [filled trade summary]
767
+ stpExpired → undefined
768
+ ```
769
+
770
+ #### F) Market order with EXPIRE_MAKER
771
+
772
+ ```
773
+ Maker BUY @ 100 qty: 5 account: "alice"
774
+ Taker SELL (market) qty: 3 account: "alice" mode: EXPIRE_MAKER
775
+ ```
776
+
777
+ The resting maker is expired via STP. The market order has no remaining liquidity, so it also expires.
778
+
779
+ ```
780
+ stpExpired → [maker-buy-100]
781
+ err → null
782
+ ```
783
+
784
+ #### G) Mixed accounts at the same price level
785
+
786
+ ```
787
+ Maker "alice" BUY @ 100 qty: 5
788
+ Maker "bob" BUY @ 100 qty: 5
789
+ Taker "alice" SELL @ 90 qty: 8 mode: EXPIRE_MAKER
790
+ ```
791
+
792
+ At price level 100, alice's maker is expired (`stpExpired`), while bob's maker matches normally (`done`). The remaining taker quantity (3) rests on the book.
793
+
794
+ ```
795
+ stpExpired → [maker-alice-100]
796
+ done → [maker-bob-100]
797
+ ```
798
+
799
+ #### H) STP carries through triggered stop orders
800
+
801
+ Stop orders preserve the `stpMode` they were created with. When a stop order is triggered and becomes a taker, its STP mode is applied at match time.
802
+
803
+ ```ts
804
+ ob.createOrder({
805
+ type: OrderType.STOP_LIMIT,
806
+ side: Side.BUY,
807
+ size: 3,
808
+ price: 110,
809
+ stopPrice: 108,
810
+ accountId: 'alice',
811
+ stpMode: SelfTradePreventionMode.EXPIRE_MAKER,
812
+ })
813
+ ```
814
+
815
+ ### Important notes
816
+
817
+ - STP is evaluated using the **taker order's** mode, regardless of what mode the resting maker orders carry.
818
+ - If no `accountId` is specified on either side, STP is **not** triggered (backward compatible).
819
+ - If no `stpMode` is specified, it defaults to `NONE` (no prevention).
820
+ - Stop market and stop limit orders preserve the `stpMode` and apply it when triggered.
821
+ - Modify operations reset `stpMode` to `NONE`.
822
+
489
823
  ## Order Book Options
490
824
 
491
- The orderbook can be initialized with the following options by passing them to the constructor:
825
+ The order book can be initialized with the following options by passing them to the constructor:
492
826
 
493
827
  ### Snapshot
494
- A `snapshot` represents the state of the order book at a specific point in time. It includes the following properties:
495
828
 
496
- - `asks`: List of ask orders, each with a `price` and a list of associated `orders`.
497
- - `bids`: List of bid orders, each with a `price` and a list of associated `orders`.
498
- - `stopBook`: an object with `bids` and `asks` properties related to every `StopOrder` in the orderbook.
499
- - `ts`: A timestamp indicating when the snapshot was taken, in Unix timestamp format.
500
- - `lastOp`: The id of the last operation included in the snapshot
829
+ A `snapshot` represents the state of the order book at a specific point in time. It includes:
830
+
831
+ - `asks`: List of ask orders, each with a `price` and a list of associated `orders`.
832
+ - `bids`: List of bid orders, each with a `price` and a list of associated `orders`.
833
+ - `stopBook`: An object with `bids` and `asks` properties related to every `StopOrder` in the order book.
834
+ - `ts`: A Unix timestamp of when the snapshot was taken.
835
+ - `lastOp`: The ID of the last operation included in the snapshot.
501
836
 
502
- Snapshots are crucial for restoring the order book to a previous state. The orderbook can restore from a snapshot before processing any journal logs, ensuring consistency and accuracy.
503
- After taking the snapshot, you can safely remove all logs preceding the `lastOp` id.
837
+ Snapshots are crucial for restoring the order book to a previous state. The order book can restore from a snapshot before processing any journal logs, ensuring consistency and accuracy. After taking a snapshot, you can safely remove all logs preceding the `lastOp` id.
504
838
 
505
- **Note**: The snapshot of the order book returns an object containing an `array` of `bids` and `asks`, which in turn are arrays of order objects. If the snapshot is saved to the database as a `string`, make sure to pass the snapshot in its original format when initializing the order book. For example, you can achieve this by using `JSON.parse` to convert the string back into its original object form.
839
+ **Note**: The snapshot returns an object containing arrays of `bids` and `asks`. If the snapshot is saved to the database as a string, use `JSON.parse` to restore it when initializing the order book.
506
840
 
507
841
  ```ts
508
- const ob = new OrderBook({ enableJournaling: true})
842
+ const ob = new OrderBook({ enableJournaling: true })
509
843
 
510
- // after every order save the log to the database
844
+ // After every order, save the log to the database
511
845
  const order = ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 })
512
846
  await saveLog(order.log)
513
847
 
514
- // ... after some time take a snapshot of the order book and save it on the database
515
-
848
+ // ... after some time, take a snapshot and save it
516
849
  const snapshot = ob.snapshot()
517
850
  await saveSnapshot(JSON.stringify(snapshot))
518
851
 
519
- // If you want you can safely remove all logs preceding the `lastOp` id of the snapshot, and continue to save each subsequent log to the database
852
+ // Safe to remove logs before the snapshot's lastOp
520
853
  await removePreviousLogs(snapshot.lastOp)
521
854
 
522
- // On server restart get the snapshot and logs from the database and initialize the order book
855
+ // On server restart, restore from snapshot + logs
523
856
  const logs = await getLogs()
524
857
  const snapshot = await getSnapshot()
525
858
 
526
- const ob = new OrderBook({ snapshot: JSON.parse(snapshot), journal: log, enableJournaling: true })
859
+ const ob = new OrderBook({
860
+ snapshot: JSON.parse(snapshot),
861
+ journal: logs,
862
+ enableJournaling: true,
863
+ })
527
864
  ```
528
865
 
529
866
  ### Journal Logs
530
- The `journal` option expects an array of journal logs that you can get by setting `enableJournaling` to true. When the journal is provided, the order book will replay all the operations, bringing the order book to the same state as the last log.
531
- ```ts
532
- // Assuming 'logs' is an array of log entries retrieved from the database
533
867
 
868
+ The `journal` option accepts an array of journal logs (obtained by setting `enableJournaling` to `true`). When provided, the order book replays all operations, restoring its state to match the last log.
869
+
870
+ ```ts
534
871
  const logs = await getLogs()
535
872
  const ob = new OrderBook({ journal: logs, enableJournalLog: true })
536
873
  ```
537
- By combining snapshots with journaling, you can effectively restore and audit the state of the order book.
874
+
875
+ Combining snapshots with journaling gives you full state persistence and auditability.
538
876
 
539
877
  ### Enable Journaling
540
- `enabledJournaling` is a configuration setting that determines whether journaling is enabled or disabled. When enabled, the property `log` will be added to the body of the response for each operation. The logs must be saved to the database and can then be used when a new instance of the order book is instantiated.
878
+
879
+ When `enableJournaling` is `true`, the property `log` is attached to every operation response. These logs should be persisted and can be used to restore the order book on restart.
880
+
541
881
  ```ts
542
882
  const ob = new OrderBook({ enableJournaling: true }) // false by default
543
883
 
544
- // after every order save the log to the database
884
+ // After every operation, save the log
545
885
  const order = ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 })
546
886
  await saveLog(order.log)
547
887
  ```
548
888
 
549
889
  ## Development
550
890
 
551
- ### Build
891
+ ### Prerequisites
552
892
 
553
- Build production (distribution) files in your dist folder:
893
+ - Node.js 18+
894
+ - npm (or yarn / pnpm)
554
895
 
555
- ```
556
- npm run build
557
- ```
896
+ ### Setup
558
897
 
559
- ### Testing
898
+ ```bash
899
+ # Install dependencies
900
+ npm install
560
901
 
561
- To run all the unit-test
902
+ # Build all distributions (CJS, ESM, types)
903
+ npm run build
562
904
 
563
- ```
905
+ # Run tests
564
906
  npm run test
565
- ```
566
-
567
- ### Coverage
568
-
569
- Run testing coverage
570
907
 
571
- ```
908
+ # Run tests with coverage
572
909
  npm run test:cov
573
- ```
574
-
575
- ### Benchmarking
576
910
 
577
- Before running benchmark, make sure to have built the source code with `npm run build` first
578
-
579
- ```
911
+ # Run benchmarks (build first)
580
912
  npm run bench
581
913
  ```
582
914
 
583
- ## Contributing
915
+ ### Commands
584
916
 
585
- I would greatly appreciate any contributions to make this project better. Please make sure to follow the below guidelines before getting your hands dirty.
917
+ | Command | Description |
918
+ |---------|-------------|
919
+ | `npm run build` | Build CJS, ESM, and type declarations |
920
+ | `npm run test` | Run unit tests |
921
+ | `npm run test:dev` | Run tests in watch mode |
922
+ | `npm run test:cov` | Run tests with lcov coverage report |
923
+ | `npm run bench` | Run performance benchmarks |
924
+ | `npm run lint` | Check code style with Biome |
925
+ | `npm run lint:fix` | Auto-fix lint issues |
926
+ | `npm run clean` | Clean build output |
927
+ | `npm run package` | Build and pack for local testing |
586
928
 
587
- 1. Fork the repository
588
- 2. Create your branch (git checkout -b my-branch)
589
- 3. Commit any changes to your branch
590
- 4. Push your changes to your remote branch
591
- 5. Open a pull request
929
+ ## Contributing
592
930
 
593
- ## Donation
931
+ Contributions are welcome! Please read the [Contributing Guidelines](CONTRIBUTING.md) before getting started.
594
932
 
595
- If this project help you reduce time to develop, you can give me a cup of coffee 🍵 :)
933
+ 1. Fork the repository
934
+ 2. Create your feature branch (`git checkout -b my-feature`)
935
+ 3. Commit your changes (`git commit -m 'feat: add my feature'`)
936
+ 4. Push to the branch (`git push origin my-feature`)
937
+ 5. Open a Pull Request
596
938
 
597
- - USDT (TRC20): `TXArNxsq2Ee8Jvsk45PudVio52Joiq1yEe`
598
- - BTC: `1GYDVSAQNgG7MFhV5bk15XJy3qoE4NFenp`
599
- - BTC (BEP20): `0xf673ee099be8129ec05e2f549d96ebea24ac5d97`
600
- - ETH (ERC20): `0xf673ee099be8129ec05e2f549d96ebea24ac5d97`
601
- - BNB (BEP20): `0xf673ee099be8129ec05e2f549d96ebea24ac5d97`
939
+ Please also refer to the [Code of Conduct](CODE_OF_CONDUCT.md) and [Security Policy](SECURITY.md).
602
940
 
603
941
  ## License
604
942
 
605
943
  Copyright [Andrea Fassina](https://github.com/fasenderos), Licensed under [MIT](LICENSE).
944
+
945
+ ## Donation
946
+
947
+ If this project saves you time or helps your business, consider buying me a coffee.
948
+
949
+ - **USDT (TRC20):** `TXArNxsq2Ee8Jvsk45PudVio52Joiq1yEe`
950
+ - **BTC:** `1GYDVSAQNgG7MFhV5bk15XJy3qoE4NFenp`
951
+ - **BTC (BEP20):** `0xf673ee099be8129ec05e2f549d96ebea24ac5d97`
952
+ - **ETH (ERC20):** `0xf673ee099be8129ec05e2f549d96ebea24ac5d97`
953
+ - **BNB (BEP20):** `0xf673ee099be8129ec05e2f549d96ebea24ac5d97`