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/README.md
CHANGED
|
@@ -10,54 +10,75 @@
|
|
|
10
10
|
# Node.js Order Book
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
|
|
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
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
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
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
- Snapshot and journaling
|
|
56
|
-
- **High
|
|
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
|
-
|
|
78
|
+
## Requirements
|
|
59
79
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
203
|
+
## Primary Functions
|
|
170
204
|
|
|
171
|
-
To add an order to the order book you can call the general `createOrder()` function or
|
|
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
|
-
###
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
###
|
|
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
|
|
236
|
-
* @param options.postOnly - When `true` the order
|
|
237
|
-
* @param options.timeInForce -
|
|
238
|
-
* @returns An object with the result of the processed order or an error.
|
|
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
|
-
###
|
|
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.
|
|
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
|
-
###
|
|
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
|
|
347
|
-
* @param options.stopPrice - The price at which the order
|
|
348
|
-
* @param options.timeInForce -
|
|
349
|
-
* @returns An object with the result of the processed order or an error.
|
|
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
|
-
###
|
|
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
|
|
371
|
-
* @returns An object with the result of the processed order or an error.
|
|
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
|
-
###
|
|
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
|
|
400
|
-
* @param options.stopPrice - The
|
|
401
|
-
* @param options.stopLimitPrice - The
|
|
402
|
-
* @param options.timeInForce - Time-in-force of the
|
|
403
|
-
* @param options.stopLimitTimeInForce - Time-in-force of the
|
|
404
|
-
* @returns An object with the result of the processed order or an error.
|
|
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
|
-
###
|
|
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
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
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
|
-
###
|
|
487
|
+
### cancel()
|
|
488
|
+
|
|
489
|
+
Remove an existing order by ID from the order book.
|
|
466
490
|
|
|
467
491
|
```ts
|
|
468
492
|
/**
|
|
469
|
-
*
|
|
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
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
852
|
+
// Safe to remove logs before the snapshot's lastOp
|
|
520
853
|
await removePreviousLogs(snapshot.lastOp)
|
|
521
854
|
|
|
522
|
-
// On server restart
|
|
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({
|
|
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
|
-
|
|
874
|
+
|
|
875
|
+
Combining snapshots with journaling gives you full state persistence and auditability.
|
|
538
876
|
|
|
539
877
|
### Enable Journaling
|
|
540
|
-
|
|
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
|
-
//
|
|
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
|
-
###
|
|
891
|
+
### Prerequisites
|
|
552
892
|
|
|
553
|
-
|
|
893
|
+
- Node.js 18+
|
|
894
|
+
- npm (or yarn / pnpm)
|
|
554
895
|
|
|
555
|
-
|
|
556
|
-
npm run build
|
|
557
|
-
```
|
|
896
|
+
### Setup
|
|
558
897
|
|
|
559
|
-
|
|
898
|
+
```bash
|
|
899
|
+
# Install dependencies
|
|
900
|
+
npm install
|
|
560
901
|
|
|
561
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
```
|
|
911
|
+
# Run benchmarks (build first)
|
|
580
912
|
npm run bench
|
|
581
913
|
```
|
|
582
914
|
|
|
583
|
-
|
|
915
|
+
### Commands
|
|
584
916
|
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
931
|
+
Contributions are welcome! Please read the [Contributing Guidelines](CONTRIBUTING.md) before getting started.
|
|
594
932
|
|
|
595
|
-
|
|
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
|
-
|
|
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`
|