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 +198 -0
- 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 +848 -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 +844 -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/types.d.ts +24 -0
- package/package.json +13 -12
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:
|
package/dist/cjs/errors.js
CHANGED
|
@@ -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 =
|
|
23
|
+
exports.ErrorCodes = {
|
|
24
24
|
// 10xx General issues
|
|
25
|
-
|
|
25
|
+
[ERROR.DEFAULT]: 1000,
|
|
26
26
|
// 11xx Request issues
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
errorMessage =
|
|
71
|
+
const customMessage = error === undefined || error === "" ? "" : `: ${error}`;
|
|
72
|
+
errorMessage = `${exports.ErrorMessages.DEFAULT}${customMessage}`;
|
|
70
73
|
}
|
|
71
74
|
this.message = errorMessage;
|
|
72
|
-
this.code =
|
|
75
|
+
this.code = exports.ErrorCodes[error] ?? exports.ErrorCodes[ERROR.DEFAULT];
|
|
73
76
|
}
|
|
74
|
-
|
|
75
|
-
}());
|
|
77
|
+
}
|
|
76
78
|
exports.OrderBookError = OrderBookError;
|
|
77
79
|
/* node:coverage ignore next - Don't know why this line is uncoverd */
|
|
78
|
-
|
|
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
|
-
|
|
5
|
+
const orderbook_1 = require("./orderbook");
|
|
6
6
|
Object.defineProperty(exports, "OrderBook", { enumerable: true, get: function () { return orderbook_1.OrderBook; } });
|
|
7
|
-
|
|
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 */
|