nodejs-order-book 10.0.1 → 10.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,7 @@ Ultra-fast Node.js Order Book written in TypeScript </br> for high-frequency tra
29
29
  - [Create OCO (One-Cancels-the-Other) order `oco()`](#create-oco-one-cancels-the-other-order)
30
30
  - [Modify an existing order `modifiy()`](#modify-an-existing-order)
31
31
  - [Cancel order `cancel()`](#cancel-order)
32
+ - [Self-Trade Prevention (STP)](#self-trade-prevention-stp)
32
33
  - [Order Book Options](#order-book-options)
33
34
  - [Snapshot](#snapshot)
34
35
  - [Journal Logs](#journal-logs)
@@ -50,6 +51,7 @@ Ultra-fast Node.js Order Book written in TypeScript </br> for high-frequency tra
50
51
  - Supports `post-only` limit order <img src="https://img.shields.io/badge/New-green" alt="New">
51
52
  - Supports conditional orders [**Stop Limit, Stop Market and OCO**](#conditional-orders) <img src="https://img.shields.io/badge/New-green" alt="New">
52
53
  - Supports time in force GTC, FOK and IOC <img src="https://img.shields.io/badge/New-green" alt="New">
54
+ - Support [Self-Trade Prevention (STP)](#self-trade-prevention-stp)
53
55
  - Supports order cancelling
54
56
  - Supports order price and/or size updating <img src="https://img.shields.io/badge/New-green" alt="New">
55
57
  - Snapshot and journaling functionalities for restoring the order book during server startup <img src="https://img.shields.io/badge/New-green" alt="New">
@@ -486,6 +488,202 @@ bids: 90 -> 5 90 -> 5
486
488
  80 -> 1 80 -> 1
487
489
  ```
488
490
 
491
+ ## Self-Trade Prevention (STP)
492
+
493
+ > 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.
494
+
495
+ ### How it works
496
+
497
+ 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:
498
+
499
+ | Mode | Effect |
500
+ |------|--------|
501
+ | `NONE` | No prevention — orders match normally |
502
+ | `EXPIRE_MAKER` | The resting maker order(s) expire; the taker order continues |
503
+ | `EXPIRE_TAKER` | The taker order is rejected; the resting maker order(s) stay on the book |
504
+ | `EXPIRE_BOTH` | Both the taker and the matching maker order(s) expire |
505
+
506
+ The STP mode of the **taker** order always takes precedence — the mode stored on a resting maker order is ignored for STP purposes.
507
+
508
+ ### API reference
509
+
510
+ Add `accountId` and `stpMode` to any order:
511
+
512
+ ```ts
513
+ import { OrderBook, SelfTradePreventionMode, Side } from 'nodejs-order-book'
514
+
515
+ const ob = new OrderBook()
516
+
517
+ // Place a resting limit order from account "alice"
518
+ ob.limit({
519
+ side: Side.BUY,
520
+ id: 'maker-order',
521
+ size: 5,
522
+ price: 100,
523
+ accountId: 'alice',
524
+ })
525
+
526
+ // Taker from the same account with STP enabled
527
+ const result = ob.limit({
528
+ side: Side.SELL,
529
+ id: 'taker-order',
530
+ size: 3,
531
+ price: 90,
532
+ accountId: 'alice',
533
+ stpMode: SelfTradePreventionMode.EXPIRE_MAKER,
534
+ })
535
+
536
+ // Check which orders expired due to STP
537
+ console.log(result.stpExpired) // [{ id: 'maker-order', ... }]
538
+ ```
539
+
540
+ ### Response fields
541
+
542
+ When STP is triggered, the response (`IProcessOrder`) includes:
543
+
544
+ | Field | Type | Description |
545
+ |-------|------|-------------|
546
+ | `stpExpired` | `IOrder[] \| undefined` | Orders that were removed from the book due to STP |
547
+ | `err` | `OrderBookError \| null` | Error with `code: 1202` and `message: "Self-trade prevention triggered"` for `EXPIRE_TAKER` / `EXPIRE_BOTH` |
548
+
549
+ ### Error code
550
+
551
+ STP rejections return error code `1202`:
552
+
553
+ ```ts
554
+ import { ErrorCodes } from 'nodejs-order-book'
555
+
556
+ assert.equal(result.err?.code, ErrorCodes.STP_TRIGGERED)
557
+ // → 1202
558
+ assert.equal(result.err?.message, 'Self-trade prevention triggered')
559
+ ```
560
+
561
+ ### Scenarios
562
+
563
+ #### A) EXPIRE_MAKER — maker expires, taker continues
564
+
565
+ ```
566
+ Maker BUY @ 100 qty: 5 account: "alice"
567
+ Maker BUY @ 90 qty: 5 account: "alice"
568
+ Taker SELL @ 90 qty: 3 account: "alice" mode: EXPIRE_MAKER
569
+ ```
570
+
571
+ 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.
572
+
573
+ ```
574
+ stpExpired → [maker-buy-100, maker-buy-90]
575
+ err → null
576
+ ```
577
+
578
+ #### B) EXPIRE_TAKER — taker expires, maker stays
579
+
580
+ ```
581
+ Maker BUY @ 100 qty: 5 account: "alice"
582
+ Taker SELL @ 90 qty: 3 account: "alice" mode: EXPIRE_TAKER
583
+ ```
584
+
585
+ The taker order is rejected immediately. The resting maker order remains untouched on the book.
586
+
587
+ ```
588
+ stpExpired → undefined
589
+ err → { code: 1202, message: "Self-trade prevention triggered" }
590
+ ```
591
+
592
+ #### C) EXPIRE_BOTH — both orders expire
593
+
594
+ ```
595
+ Maker BUY @ 100 qty: 5 account: "alice"
596
+ Taker SELL @ 90 qty: 3 account: "alice" mode: EXPIRE_BOTH
597
+ ```
598
+
599
+ The maker is removed from the book and the taker is rejected. Both sides expire.
600
+
601
+ ```
602
+ stpExpired → [maker-buy-100]
603
+ err → { code: 1202, message: "Self-trade prevention triggered" }
604
+ ```
605
+
606
+ #### D) Different accounts — normal matching (no STP)
607
+
608
+ ```
609
+ Maker BUY @ 100 qty: 5 account: "alice"
610
+ Taker SELL @ 90 qty: 3 account: "bob" mode: EXPIRE_MAKER
611
+ ```
612
+
613
+ The accounts differ, so STP does **not** trigger. The orders match normally.
614
+
615
+ ```
616
+ done → [filled trade summary]
617
+ stpExpired → undefined
618
+ ```
619
+
620
+ #### E) Mode NONE — no prevention
621
+
622
+ ```
623
+ Maker BUY @ 100 qty: 5 account: "alice"
624
+ Taker SELL @ 90 qty: 3 account: "alice" mode: NONE
625
+ ```
626
+
627
+ Even though both orders are from the same account, `NONE` mode allows the match.
628
+
629
+ ```
630
+ done → [filled trade summary]
631
+ stpExpired → undefined
632
+ ```
633
+
634
+ #### F) Market order with EXPIRE_MAKER
635
+
636
+ ```
637
+ Maker BUY @ 100 qty: 5 account: "alice"
638
+ Taker SELL (market) qty: 3 account: "alice" mode: EXPIRE_MAKER
639
+ ```
640
+
641
+ The resting maker is expired via STP. The market order has no remaining liquidity, so it also expires.
642
+
643
+ ```
644
+ stpExpired → [maker-buy-100]
645
+ err → null
646
+ ```
647
+
648
+ #### G) Mixed accounts at the same price level
649
+
650
+ ```
651
+ Maker "alice" BUY @ 100 qty: 5
652
+ Maker "bob" BUY @ 100 qty: 5
653
+ Taker "alice" SELL @ 90 qty: 8 mode: EXPIRE_MAKER
654
+ ```
655
+
656
+ 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.
657
+
658
+ ```
659
+ stpExpired → [maker-alice-100]
660
+ done → [maker-bob-100]
661
+ ```
662
+
663
+ #### H) STP carries through triggered stop orders
664
+
665
+ 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.
666
+
667
+ ```ts
668
+ ob.createOrder({
669
+ type: OrderType.STOP_LIMIT,
670
+ side: Side.BUY,
671
+ size: 3,
672
+ price: 110,
673
+ stopPrice: 108,
674
+ accountId: 'alice',
675
+ stpMode: SelfTradePreventionMode.EXPIRE_MAKER,
676
+ })
677
+ ```
678
+
679
+ ### Important notes
680
+
681
+ - STP is evaluated using the **taker order's** mode, regardless of what mode the resting maker orders carry.
682
+ - If no `accountId` is specified on either side, STP is **not** triggered (backward compatible).
683
+ - If no `stpMode` is specified, it defaults to `NONE` (no prevention).
684
+ - Stop market and stop limit orders preserve the `stpMode` and apply it when triggered.
685
+ - Modify operations reset `stpMode` to `NONE`.
686
+
489
687
  ## Order Book Options
490
688
 
491
689
  The orderbook can be initialized with the following options by passing them to the constructor:
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- var _a, _b;
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.CustomError = exports.OrderBookError = exports.ErrorMessages = exports.ErrorCodes = exports.ERROR = void 0;
5
4
  var ERROR;
@@ -19,63 +18,66 @@ var ERROR;
19
18
  ERROR["LIMIT_ORDER_POST_ONLY"] = "LIMIT_ORDER_POST_ONLY";
20
19
  ERROR["ORDER_ALREDY_EXISTS"] = "ORDER_ALREDY_EXISTS";
21
20
  ERROR["ORDER_NOT_FOUND"] = "ORDER_NOT_FOUND";
21
+ ERROR["STP_TRIGGERED"] = "STP_TRIGGERED";
22
22
  })(ERROR || (exports.ERROR = ERROR = {}));
23
- exports.ErrorCodes = (_a = {},
23
+ exports.ErrorCodes = {
24
24
  // 10xx General issues
25
- _a[ERROR.DEFAULT] = 1000,
25
+ [ERROR.DEFAULT]: 1000,
26
26
  // 11xx Request issues
27
- _a[ERROR.INVALID_ORDER_TYPE] = 1100,
28
- _a[ERROR.INVALID_SIDE] = 1101,
29
- _a[ERROR.INVALID_QUANTITY] = 1102,
30
- _a[ERROR.INVALID_PRICE] = 1103,
31
- _a[ERROR.INVALID_PRICE_OR_QUANTITY] = 1104,
32
- _a[ERROR.INVALID_TIF] = 1105,
33
- _a[ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE] = 1106,
34
- _a[ERROR.LIMIT_ORDER_POST_ONLY] = 1107,
35
- _a[ERROR.INVALID_CONDITIONAL_ORDER] = 1108,
36
- _a[ERROR.ORDER_ALREDY_EXISTS] = 1109,
37
- _a[ERROR.ORDER_NOT_FOUND] = 1110,
27
+ [ERROR.INVALID_ORDER_TYPE]: 1100,
28
+ [ERROR.INVALID_SIDE]: 1101,
29
+ [ERROR.INVALID_QUANTITY]: 1102,
30
+ [ERROR.INVALID_PRICE]: 1103,
31
+ [ERROR.INVALID_PRICE_OR_QUANTITY]: 1104,
32
+ [ERROR.INVALID_TIF]: 1105,
33
+ [ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE]: 1106,
34
+ [ERROR.LIMIT_ORDER_POST_ONLY]: 1107,
35
+ [ERROR.INVALID_CONDITIONAL_ORDER]: 1108,
36
+ [ERROR.ORDER_ALREDY_EXISTS]: 1109,
37
+ [ERROR.ORDER_NOT_FOUND]: 1110,
38
38
  // 12xx Internal error
39
- _a[ERROR.INSUFFICIENT_QUANTITY] = 1200,
40
- _a[ERROR.INVALID_PRICE_LEVEL] = 1201,
41
- _a[ERROR.INVALID_JOURNAL_LOG] = 1201,
42
- _a);
43
- exports.ErrorMessages = (_b = {},
44
- _b[ERROR.DEFAULT] = "Something wrong",
45
- _b[ERROR.INSUFFICIENT_QUANTITY] = "Insufficient quantity to calculate price",
46
- _b[ERROR.INVALID_CONDITIONAL_ORDER] = "Stop-Limit Order (BUY: marketPrice < stopPrice <= price, SELL: marketPrice > stopPrice >= price). Stop-Market Order (BUY: marketPrice < stopPrice, SELL: marketPrice > stopPrice). OCO order (BUY: price < marketPrice < stopPrice, SELL: price > marketPrice > stopPrice)",
47
- _b[ERROR.INVALID_ORDER_TYPE] = "Supported order type are 'limit' and 'market'",
48
- _b[ERROR.INVALID_PRICE] = "Invalid order price",
49
- _b[ERROR.INVALID_PRICE_LEVEL] = "Invalid order price level",
50
- _b[ERROR.INVALID_PRICE_OR_QUANTITY] = "Invalid order price or quantity",
51
- _b[ERROR.INVALID_QUANTITY] = "Invalid order quantity",
52
- _b[ERROR.INVALID_SIDE] = "Invalid side: must be either 'sell' or 'buy'",
53
- _b[ERROR.INVALID_TIF] = "Invalid TimeInForce: must be one of 'GTC', 'IOC' or 'FOK'",
54
- _b[ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE] = "Limit FOK order not fillable",
55
- _b[ERROR.LIMIT_ORDER_POST_ONLY] = "Post-only limit order rejected because would execute immediately",
56
- _b[ERROR.ORDER_ALREDY_EXISTS] = "Order already exists",
57
- _b[ERROR.ORDER_NOT_FOUND] = "Order not found",
58
- _b[ERROR.INVALID_JOURNAL_LOG] = "Invalid journal log format",
59
- _b);
60
- var OrderBookError = /** @class */ (function () {
61
- function OrderBookError(error) {
62
- var _a;
63
- var errorMessage;
39
+ [ERROR.INSUFFICIENT_QUANTITY]: 1200,
40
+ [ERROR.INVALID_PRICE_LEVEL]: 1201,
41
+ [ERROR.INVALID_JOURNAL_LOG]: 1201,
42
+ [ERROR.STP_TRIGGERED]: 1202,
43
+ };
44
+ exports.ErrorMessages = {
45
+ [ERROR.DEFAULT]: "Something wrong",
46
+ [ERROR.INSUFFICIENT_QUANTITY]: "Insufficient quantity to calculate price",
47
+ [ERROR.INVALID_CONDITIONAL_ORDER]: "Stop-Limit Order (BUY: marketPrice < stopPrice <= price, SELL: marketPrice > stopPrice >= price). Stop-Market Order (BUY: marketPrice < stopPrice, SELL: marketPrice > stopPrice). OCO order (BUY: price < marketPrice < stopPrice, SELL: price > marketPrice > stopPrice)",
48
+ [ERROR.INVALID_ORDER_TYPE]: "Supported order type are 'limit' and 'market'",
49
+ [ERROR.INVALID_PRICE]: "Invalid order price",
50
+ [ERROR.INVALID_PRICE_LEVEL]: "Invalid order price level",
51
+ [ERROR.INVALID_PRICE_OR_QUANTITY]: "Invalid order price or quantity",
52
+ [ERROR.INVALID_QUANTITY]: "Invalid order quantity",
53
+ [ERROR.INVALID_SIDE]: "Invalid side: must be either 'sell' or 'buy'",
54
+ [ERROR.INVALID_TIF]: "Invalid TimeInForce: must be one of 'GTC', 'IOC' or 'FOK'",
55
+ [ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE]: "Limit FOK order not fillable",
56
+ [ERROR.LIMIT_ORDER_POST_ONLY]: "Post-only limit order rejected because would execute immediately",
57
+ [ERROR.ORDER_ALREDY_EXISTS]: "Order already exists",
58
+ [ERROR.ORDER_NOT_FOUND]: "Order not found",
59
+ [ERROR.INVALID_JOURNAL_LOG]: "Invalid journal log format",
60
+ [ERROR.STP_TRIGGERED]: "Self-trade prevention triggered",
61
+ };
62
+ class OrderBookError {
63
+ message;
64
+ code;
65
+ constructor(error) {
66
+ let errorMessage;
64
67
  if (error != null && exports.ErrorMessages[error] != null) {
65
68
  errorMessage = exports.ErrorMessages[error];
66
69
  }
67
70
  else {
68
- var customMessage = error === undefined || error === "" ? "" : ": ".concat(error);
69
- errorMessage = "".concat(exports.ErrorMessages.DEFAULT).concat(customMessage);
71
+ const customMessage = error === undefined || error === "" ? "" : `: ${error}`;
72
+ errorMessage = `${exports.ErrorMessages.DEFAULT}${customMessage}`;
70
73
  }
71
74
  this.message = errorMessage;
72
- this.code = (_a = exports.ErrorCodes[error]) !== null && _a !== void 0 ? _a : exports.ErrorCodes[ERROR.DEFAULT];
75
+ this.code = exports.ErrorCodes[error] ?? exports.ErrorCodes[ERROR.DEFAULT];
73
76
  }
74
- return OrderBookError;
75
- }());
77
+ }
76
78
  exports.OrderBookError = OrderBookError;
77
79
  /* node:coverage ignore next - Don't know why this line is uncoverd */
78
- var CustomError = function (error) {
80
+ const CustomError = (error) => {
79
81
  return new OrderBookError(error);
80
82
  };
81
83
  exports.CustomError = CustomError;
package/dist/cjs/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Side = exports.OrderType = exports.OrderBook = void 0;
3
+ exports.Side = exports.SelfTradePreventionMode = exports.OrderType = exports.OrderBook = void 0;
4
4
  /* node:coverage disable */
5
- var orderbook_1 = require("./orderbook");
5
+ const orderbook_1 = require("./orderbook");
6
6
  Object.defineProperty(exports, "OrderBook", { enumerable: true, get: function () { return orderbook_1.OrderBook; } });
7
- var types_1 = require("./types");
7
+ const types_1 = require("./types");
8
8
  Object.defineProperty(exports, "OrderType", { enumerable: true, get: function () { return types_1.OrderType; } });
9
+ Object.defineProperty(exports, "SelfTradePreventionMode", { enumerable: true, get: function () { return types_1.SelfTradePreventionMode; } });
9
10
  Object.defineProperty(exports, "Side", { enumerable: true, get: function () { return types_1.Side; } });
10
11
  /* node:coverage enable */