diff --git a/.husky/.gitignore b/.husky/.gitignore
deleted file mode 100644
index c9cdc63..0000000
--- a/.husky/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-_
\ No newline at end of file
diff --git a/.husky/commit-msg b/.husky/commit-msg
index 5032fcb..d468455 100644
--- a/.husky/commit-msg
+++ b/.husky/commit-msg
@@ -1,5 +1 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-. "$(dirname "$0")/common.sh"
-
npx --no-install commitlint --edit $1
\ No newline at end of file
diff --git a/.husky/common.sh b/.husky/common.sh
deleted file mode 100644
index 1487014..0000000
--- a/.husky/common.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-command_exists () {
- command -v "$1" >/dev/null 2>&1
-}
-
-# Windows 10, Git Bash and Yarn workaround
-if command_exists winpty && test -t 1; then
- exec < /dev/tty
-fi
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 653a1de..66692f2 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,6 +1,2 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-. "$(dirname "$0")/common.sh"
-
npm run lint
npm run test
\ No newline at end of file
diff --git a/.release-it.json b/.release-it.json
index eaadc71..b15ef18 100644
--- a/.release-it.json
+++ b/.release-it.json
@@ -15,6 +15,9 @@
"npm": {
"publish": false
},
+ "preRelease": {
+ "suffix": "beta"
+ },
"plugins": {
"@release-it/conventional-changelog": {
"header": "# Changelog",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92219f8..4ec6249 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,71 @@
# Changelog
+## [6.1.0-beta.1](https://github.com/fasenderos/hft-limit-order-book/compare/v6.1.0-beta.0...v6.1.0-beta.1) (2024-07-28)
+
+
+### Features
+
+* add support for OCO orders ([5b19318](https://github.com/fasenderos/hft-limit-order-book/commit/5b193185b5e32ba6d0ad9d0853876708e3c3e23c))
+
+
+### Bug Fixes
+
+* use new signatures in benchmark script ([313aefe](https://github.com/fasenderos/hft-limit-order-book/commit/313aefe3cebef6dd9876e7b9828cdfc54db33f1a))
+
+
+### Chore
+
+* update husky script ([2e7dd67](https://github.com/fasenderos/hft-limit-order-book/commit/2e7dd678975974a90ebb2921193b6c24b152d67e))
+
+
+### Documentation
+
+* add stop limit and stop market documentations ([41ef939](https://github.com/fasenderos/hft-limit-order-book/commit/41ef939d172ea7f1a93e1e481f1dcffcbca55640))
+
+
+### Refactoring
+
+* improve code readability on cancelOrder ([9f062c4](https://github.com/fasenderos/hft-limit-order-book/commit/9f062c44d9d490e872c4f8c16287a7140fd1481c))
+
+## [6.1.0-beta.0](https://github.com/fasenderos/hft-limit-order-book/compare/v5.0.0...v6.1.0-beta.0) (2024-07-22)
+
+
+### Features
+
+* add getter for market price ([9f4b315](https://github.com/fasenderos/hft-limit-order-book/commit/9f4b315fac7b94325fa4f13bc6eb7d30afee2058))
+* add support for stop limit and stop market order ([92f9441](https://github.com/fasenderos/hft-limit-order-book/commit/92f9441b18593073b146cbe1456d77f8d06e1e20))
+* refactor limit and market options ([794c71a](https://github.com/fasenderos/hft-limit-order-book/commit/794c71ac4c3eda212cb11b36c32b5f9f60c99caa))
+
+
+### Chore
+
+* **deps-dev:** bump @commitlint/cli from 18.6.1 to 19.3.0 ([8695b5b](https://github.com/fasenderos/hft-limit-order-book/commit/8695b5b4ec49d60d4fb1c00aa55968402ebc7ac2))
+* **deps-dev:** bump husky from 9.0.11 to 9.1.0 ([c8da2e2](https://github.com/fasenderos/hft-limit-order-book/commit/c8da2e202bd0782a656e430c02e0925b547e7f9d))
+* **deps-dev:** bump husky from 9.1.0 to 9.1.1 ([47bb2f8](https://github.com/fasenderos/hft-limit-order-book/commit/47bb2f8225fb66d01632f635b04165de06467713))
+* **deps-dev:** bump release-it from 17.3.0 to 17.4.0 ([ec5349b](https://github.com/fasenderos/hft-limit-order-book/commit/ec5349b6521a28e65026209a3e18f7cf72ceb92a))
+* **deps-dev:** bump release-it from 17.4.0 to 17.4.1 ([beac954](https://github.com/fasenderos/hft-limit-order-book/commit/beac954283422f539c13d1ec4735e13dd028b3bd))
+* **deps-dev:** bump release-it from 17.4.1 to 17.4.2 ([ddad1df](https://github.com/fasenderos/hft-limit-order-book/commit/ddad1df79fe300b44b8c350b1a8daa12fd8e1187))
+* **deps-dev:** bump release-it from 17.4.2 to 17.5.0 ([4d8f10e](https://github.com/fasenderos/hft-limit-order-book/commit/4d8f10e0fa30e85a50b93a4a2466a1e9e4441580))
+* **deps-dev:** bump release-it from 17.5.0 to 17.6.0 ([a6f7ba3](https://github.com/fasenderos/hft-limit-order-book/commit/a6f7ba3758150131b5a1f39fb9919ef380b7fb06))
+* **deps-dev:** bump tap from 18.7.3 to 19.2.5 ([f09af63](https://github.com/fasenderos/hft-limit-order-book/commit/f09af6399c83a7a6880802bc72514ad29364ba8b))
+* **deps-dev:** bump typescript from 5.4.5 to 5.5.2 ([999dfca](https://github.com/fasenderos/hft-limit-order-book/commit/999dfcaec02be747731b16c109b310269599cd23))
+* **deps-dev:** bump typescript from 5.5.2 to 5.5.3 ([8402f83](https://github.com/fasenderos/hft-limit-order-book/commit/8402f83b4f98924c26ed69f39aab75c625426529))
+* **deps-dev:** bump webpack from 5.92.1 to 5.93.0 ([8468d06](https://github.com/fasenderos/hft-limit-order-book/commit/8468d066a61201364fb4cb18723a2af1fc9b271e))
+* **release:** hft-limit-order-book@5.1.0-beta.0 ([770721b](https://github.com/fasenderos/hft-limit-order-book/commit/770721b06d75c9255827e3bc06ce570c953986da))
+
+
+### Documentation
+
+* fix snapshot example ([073ca07](https://github.com/fasenderos/hft-limit-order-book/commit/073ca075ce673959808932f4adc26629b9641535))
+* improve snapshot and journal documentation ([3ba2d05](https://github.com/fasenderos/hft-limit-order-book/commit/3ba2d057ed14f979a287508434e3f7c83725b119))
+* new features snapshot and journaling ([442dc43](https://github.com/fasenderos/hft-limit-order-book/commit/442dc4314e637632554369e8e4af0a425b591a82))
+* update new signatures method + add stop limit and stop market ([96408a0](https://github.com/fasenderos/hft-limit-order-book/commit/96408a05b988b8597168e1b2c99b5e45fc88204d))
+
+
+### Test
+
+* add test for stop limit and stop market order ([47e6f5e](https://github.com/fasenderos/hft-limit-order-book/commit/47e6f5e9deaaf4ade4289955ef456884f53d506e))
+
## [5.0.0](https://github.com/fasenderos/hft-limit-order-book/compare/v4.0.0...v5.0.0) (2024-06-19)
diff --git a/README.md b/README.md
index 5ee639f..a601303 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,6 @@
-> Initially ported from [Go orderbook](https://github.com/i25959341/orderbook), this order book has been enhanced with new features
-
# hft-limit-order-book
:star: Star me on GitHub — it motivates me a lot!
@@ -19,6 +17,7 @@ Ultra-fast matching engine written in TypeScript
- Standard price-time priority
- Supports both market and limit orders
+- Supports conditional orders Stop Market, Stop Limit and OCO **(Experimental)**
- Supports time in force GTC, FOK and IOC
- Supports order cancelling
- Supports order price and/or size updating
@@ -50,57 +49,82 @@ To start using order book you need to import `OrderBook` and create new instance
```js
import { OrderBook } from 'hft-limit-order-book'
-const lob = new OrderBook()
+const ob = new OrderBook()
```
Then you'll be able to use next primary functions:
```js
-lob.createOrder(type: 'limit' | 'market', side: 'buy' | 'sell', size: number, price: number, orderID: string)
+ob.createOrder({ type: 'limit' | 'market', side: 'buy' | 'sell', size: number, price?: number, id?: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
+
+ob.limit({ id: string, side: 'buy' | 'sell', size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
+
+ob.market({ side: 'buy' | 'sell', size: number })
+
+ob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number })
+
+ob.cancel(orderID: string)
+```
+### Conditional Orders
+The version `v6.1.0` introduced support for Conditional Orders (`Stop Market`, `Stop Limit` and `OCO`). Even though the test coverage for these new features is at 100%, they are not yet considered stable because they have not been tested with real-world scenarios. For this reason, if you want to use conditional orders, you need to instantiate the order book with the `experimentalConditionalOrders` option set to `true`.
+```js
+import { OrderBook } from 'hft-limit-order-book'
-lob.limit(side: 'buy' | 'sell', orderID: string, size: number, price: number)
+const ob = new OrderBook({ experimentalConditionalOrders: true })
-lob.market(side: 'buy' | 'sell', size: number)
+ob.createOrder({ type: 'stop_limit' | 'stop_market' | 'oco', side: 'buy' | 'sell', size: number, price?: number, id?: string, stopPrice?: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' })
-lob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number })
+ob.stopLimit({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
-lob.cancel(orderID: string)
+ob.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number })
+
+ob.oco({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' })
```
## About primary functions
-To add an order to the order book you can call the general `createOrder()` function or calling the underlying `limit()` or `market()` functions
+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
### Create Order
```js
-// Create a limit order
-createOrder('limit', side: 'buy' | 'sell', size: number, price: number, orderID: string, timeInForce?: 'GTC' | 'FOK' | 'IOC')
+// Create limit order
+ob.createOrder({ type: 'limit', side: 'buy' | 'sell', size: number, price: number, id: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
+
+// Create market order
+ob.createOrder({ type: 'market', side: 'buy' | 'sell', size: number })
-// Create a market order
-createOrder('market', side: 'buy' | 'sell', size: number)
+// Create stop limit order
+ob.createOrder({ type: 'stop_limit', side: 'buy' | 'sell', size: number, price: number, id: string, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
+
+// Create stop market order
+ob.createOrder({ type: 'stop_market', side: 'buy' | 'sell', size: number, stopPrice: number })
+
+// Create OCO order
+ob.createOrder({ type: 'oco', side: 'buy' | 'sell', size: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' })
```
### Create Limit Order
```js
/**
- * Create a limit order
+ * Create a limit order. See {@link LimitOrderOptions} for details.
*
- * @param side - `sell` or `buy`
- * @param orderID - Unique order ID
- * @param size - How much of currency you want to trade in units of base currency
- * @param price - The price at which the order is to be fullfilled, in units of the quote currency
- * @param timeInForce - Time-in-force type supported are: GTC, FOK, IOC
- * @returns An object with the result of the processed order or an error
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.id - Unique order ID
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
+ * @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
-limit(side: 'buy' | 'sell', orderID: string, size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC')
+ob.limit({ side: 'buy' | 'sell', id: string, size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
```
For example:
```
-limit("sell", "uniqueID", 55, 100)
+ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 })
asks: 110 -> 5 110 -> 5
100 -> 1 100 -> 56
@@ -113,7 +137,7 @@ partial - null
```
```
-limit("buy", "uniqueID", 7, 120)
+ob.limit({ side: "buy", id: "uniqueID", size: 7, price: 120 })
asks: 110 -> 5
100 -> 1
@@ -127,7 +151,7 @@ partial - uniqueID order
```
```
-limit("buy", "uniqueID", 3, 120)
+ob.limit({ side: "buy", id: "uniqueID", size: 3, price: 120 })
asks: 110 -> 5
100 -> 1 110 -> 3
@@ -143,19 +167,20 @@ partial - 1 order with price 110
```js
/**
- * Create a market order
+ * Create a market order. See {@link MarketOrderOptions} for details.
*
- * @param side - `sell` or `buy`
- * @param size - How much of currency you want to trade in units of base currency
- * @returns An object with the result of the processed order or an error
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
-market(side: 'buy' | 'sell', size: number)
+ob.market({ side: 'buy' | 'sell', size: number })
```
For example:
```
-market('sell', 6)
+ob.market({ side: 'sell', size: 6 })
asks: 110 -> 5 110 -> 5
100 -> 1 100 -> 1
@@ -169,7 +194,7 @@ quantityLeft - 0
```
```
-market('buy', 10)
+ob.market({ side: 'buy', size: 10 })
asks: 110 -> 5
100 -> 1
@@ -182,6 +207,68 @@ partial - null
quantityLeft - 4
```
+### Create Stop Limit Order
+
+```js
+/**
+ * Create a stop limit order. See {@link StopLimitOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.id - Unique order ID
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
+ * @param options.stopPrice - The price at which the order will be triggered.
+ * @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ob.stopLimit({ side: 'buy' | 'sell', id: string, size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' })
+```
+
+### Create Stop Market Order
+
+```js
+/**
+ * Create a stop market order. See {@link StopMarketOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.stopPrice - The price at which the order will be triggered.
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ob.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number })
+```
+
+### Create OCO (One-Cancels-the-Other) Order
+
+```js
+/**
+ * Create an OCO (One-Cancels-the-Other) order.
+ * OCO order combines a `stop_limit` order and a `limit` order, where if stop price
+ * is triggered or limit order is fully or partially fulfilled, the other is canceled.
+ * Both orders have the same `side` and `size`. If you cancel one of the orders, the
+ * entire OCO order pair will be canceled.
+ *
+ * For BUY orders the `stopPrice` must be above the current price and the `price` below the current price
+ * For SELL orders the `stopPrice` must be below the current price and the `price` above the current price
+ *
+ * See {@link OCOOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.id - Unique order ID
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.price - The price of the `limit` order at which the order is to be fullfilled, in units of the quote currency
+ * @param options.stopPrice - The price at which the `stop_limit` order will be triggered.
+ * @param options.stopLimitPrice - The price of the `stop_limit` order at which the order is to be fullfilled, in units of the quote currency.
+ * @param options.timeInForce - Time-in-force of the `limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
+ * @param options.stopLimitTimeInForce - Time-in-force of the `stop_limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ob.oco({ side: 'buy' | 'sell', id: string, size: number, price: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' })
+```
+
### Modify an existing order
```js
@@ -195,13 +282,13 @@ quantityLeft - 4
* @param orderUpdate - An object with the modified size and/or price of an order. The shape of the object is `{size, price}`.
* @returns An object with the result of the processed order or an error
*/
-modify(orderID: string, { size: number, price: number })
+ob.modify(orderID: string, { size: number, price: number })
```
For example:
```
-limit("sell", "uniqueID", 55, 100)
+ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 })
asks: 110 -> 5 110 -> 5
100 -> 1 100 -> 56
@@ -210,7 +297,7 @@ bids: 90 -> 5 90 -> 5
80 -> 1 80 -> 1
// Modify the size from 55 to 65
-modify("uniqueID", { size: 65 })
+ob.modify("uniqueID", { size: 65 })
asks: 110 -> 5 110 -> 5
100 -> 56 100 -> 66
@@ -220,7 +307,7 @@ bids: 90 -> 5 90 -> 5
// Modify the price from 100 to 110
-modify("uniqueID", { price: 110 })
+ob.modify("uniqueID", { price: 110 })
asks: 110 -> 5 110 -> 70
100 -> 66 100 -> 1
@@ -238,13 +325,13 @@ bids: 90 -> 5 90 -> 5
* @param orderID - The ID of the order to be removed
* @returns The removed order if exists or `undefined`
*/
-cancel(orderID: string)
+ob.cancel(orderID: string)
```
For example:
```
-cancel("myUniqueID-Sell-1-with-100")
+ob.cancel("myUniqueID-Sell-1-with-100")
asks: 110 -> 5
100 -> 1 110 -> 5
@@ -269,15 +356,15 @@ Snapshots are crucial for restoring the order book to a previous state. The orde
After taking the snapshot, you can safely remove all logs preceding the `lastOp` id.
```js
-const lob = new OrderBook({ enableJournaling: true})
+const ob = new OrderBook({ enableJournaling: true})
// after every order save the log to the database
-const order = lob.limit("sell", "uniqueID", 55, 100)
+const order = ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 })
await saveLog(order.log)
// ... after some time take a snapshot of the order book and save it on the database
-const snapshot = lob.snapshot()
+const snapshot = ob.snapshot()
await saveSnapshot(snapshot)
// 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
@@ -287,7 +374,7 @@ await removePreviousLogs(snapshot.lastOp)
const logs = await getLogs()
const snapshot = await getSnapshot()
-const lob = new OrderBook({ snapshot, journal: log, enableJournaling: true })
+const ob = new OrderBook({ snapshot, journal: log, enableJournaling: true })
```
### Journal Logs
@@ -296,17 +383,17 @@ The `journal` option expects an array of journal logs that you can get by settin
// Assuming 'logs' is an array of log entries retrieved from the database
const logs = await getLogs()
-const lob = new OrderBook({ journal: logs, enableJournalLog: true })
+const ob = new OrderBook({ journal: logs, enableJournalLog: true })
```
By combining snapshots with journaling, you can effectively restore and audit the state of the order book.
### Enable Journaling
`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.
```js
-const lob = new OrderBook({ enableJournaling: true }) // false by default
+const ob = new OrderBook({ enableJournaling: true }) // false by default
// after every order save the log to the database
-const order = lob.limit("sell", "uniqueID", 55, 100)
+const order = ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 })
await saveLog(order.log)
```
diff --git a/benchmarks/benchmark_lob.js b/benchmarks/benchmark_lob.js
index 4b38096..2048ecf 100644
--- a/benchmarks/benchmark_lob.js
+++ b/benchmarks/benchmark_lob.js
@@ -5,7 +5,7 @@ const gaussian = require('gaussian')
/* New Limits */
function spamLimitOrders (book, count) {
for (let i = 0; i < count; i++) {
- book.limit('buy', i.toString(), 50, i)
+ book.limit({ side: 'buy', id: i.toString(), size: 50, price: i })
}
}
@@ -54,7 +54,7 @@ bench('Spam 300000 new Limits', function (b) {
/* New Orders */
function spamOrders (book, count, variance = 5) {
for (let i = 0; i < count; i++) {
- book.limit('buy', i.toString(), 50, i % variance)
+ book.limit({ side: 'buy', id: i.toString(), size: 50, price: i % variance })
}
}
@@ -95,9 +95,9 @@ function spamOrdersRandomCancels (
cancelEvery = 5
) {
const priceDistribution = gaussian(mean, variance)
- book.limit('buy', '0', 50, priceDistribution.ppf(Math.random()))
+ book.limit({ side: 'buy', id: '0', size: 50, price: priceDistribution.ppf(Math.random()) })
for (let i = 1; i < count; i++) {
- book.limit('buy', i.toString(), 50, priceDistribution.ppf(Math.random()))
+ book.limit({ side: 'buy', id: i.toString(), size: 50, price: priceDistribution.ppf(Math.random()) })
if (i % cancelEvery === 0) book.cancel((i - cancelEvery).toString())
}
}
@@ -145,9 +145,9 @@ function spamLimitRandomOrders (
for (let i = 1; i < count; i++) {
const price_ = price.ppf(Math.random())
const quantity_ = quantity.ppf(Math.random())
- book.limit('buy', i.toString(), 100, price_)
+ book.limit({ side: 'buy', id: i.toString(), size: 100, price: price_ })
// random submit a market order
- if (i % orderEvery === 0) book.market('sell', quantity_)
+ if (i % orderEvery === 0) book.market({ side: 'sell', size: quantity_ })
}
}
@@ -186,8 +186,8 @@ function spamLimitManyMarketOrders (
for (let i = 1; i < count; i++) {
const price_ = price.ppf(Math.random())
const quantity_ = quantity.ppf(Math.random())
- book.limit('buy', i.toString(), 100, price_)
- book.market('sell', quantity_)
+ book.limit({ side: 'buy', id: i.toString(), size: 100, price: price_ })
+ book.market({ side: 'sell', size: quantity_ })
}
}
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 77d3e56..d50e92c 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,3 +1,4 @@
+const webpack = require('webpack')
const path = require('path')
module.exports = {
@@ -25,6 +26,16 @@ module.exports = {
]
},
resolve: {
- extensions: ['.ts', '.js', '.tsx', '.jsx']
- }
+ extensions: ['.ts', '.js', '.tsx', '.jsx'],
+ fallback: {
+ crypto: require.resolve('crypto-browserify'),
+ stream: require.resolve('stream-browserify'),
+ vm: require.resolve('vm-browserify')
+ }
+ },
+ plugins: [
+ new webpack.NormalModuleReplacementPlugin(/^node:/, (resource) => {
+ resource.request = resource.request.replace(/^node:/, '')
+ })
+ ]
}
diff --git a/package-lock.json b/package-lock.json
index a8c8366..eb6fe11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,12 @@
{
"name": "hft-limit-order-book",
- "version": "5.0.0",
+ "version": "6.1.0-beta.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "hft-limit-order-book",
- "version": "5.0.0",
- "hasInstallScript": true,
+ "version": "6.1.0-beta.1",
"license": "MIT",
"dependencies": {
"denque": "2.1.0",
@@ -18,17 +17,20 @@
"@commitlint/config-conventional": "^18.5.0",
"@release-it/conventional-changelog": "^8.0.1",
"@types/functional-red-black-tree": "^1.0.6",
+ "crypto-browserify": "^3.12.0",
"gaussian": "^1.0.0",
- "husky": "^9.0.6",
+ "husky": "^9.1.1",
"nanobench": "^3.0.0",
"pinst": "^3.0.0",
"release-it": "^17.0.3",
"snazzy": "^9.0.0",
+ "stream-browserify": "^3.0.0",
"tap": "^19.2.5",
"ts-loader": "^9.0.0",
"ts-node": "^10.0.0",
"ts-standard": "^12.0.0",
"typescript": "^5.0.0",
+ "vm-browserify": "^1.1.2",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
}
@@ -4022,6 +4024,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/asn1.js/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ },
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@@ -4140,6 +4159,12 @@
"readable-stream": "^3.4.0"
}
},
+ "node_modules/bn.js": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
+ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
+ "dev": true
+ },
"node_modules/boxen": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz",
@@ -4287,6 +4312,122 @@
"node": ">=8"
}
},
+ "node_modules/brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+ "dev": true
+ },
+ "node_modules/browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "dev": true,
+ "dependencies": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "dev": true,
+ "dependencies": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "node_modules/browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "dev": true,
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/browserify-rsa": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
+ "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^5.0.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "node_modules/browserify-sign": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz",
+ "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^5.2.1",
+ "browserify-rsa": "^4.1.0",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.5",
+ "hash-base": "~3.0",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.7",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "node_modules/browserify-sign/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/browserify-sign/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
"node_modules/browserslist": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
@@ -4349,6 +4490,12 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
+ "node_modules/buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
+ "dev": true
+ },
"node_modules/builtins": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
@@ -4663,6 +4810,16 @@
"node": ">=8"
}
},
+ "node_modules/cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -5430,6 +5587,12 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
"node_modules/cosmiconfig": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
@@ -5473,6 +5636,49 @@
"typescript": ">=4"
}
},
+ "node_modules/create-ecdh": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
+ "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ }
+ },
+ "node_modules/create-ecdh/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ },
+ "node_modules/create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "dev": true,
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "dev": true,
+ "dependencies": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -5493,6 +5699,28 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "dev": true,
+ "dependencies": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/crypto-random-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
@@ -5679,11 +5907,12 @@
}
},
"node_modules/define-properties": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
- "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"dependencies": {
+ "define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
@@ -5722,6 +5951,16 @@
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
+ "node_modules/des.js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -5731,6 +5970,23 @@
"node": ">=0.3.1"
}
},
+ "node_modules/diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "node_modules/diffie-hellman/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -5779,6 +6035,27 @@
"integrity": "sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg==",
"dev": true
},
+ "node_modules/elliptic": {
+ "version": "6.5.6",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.6.tgz",
+ "integrity": "sha512-mpzdtpeCLuS3BmE3pO3Cpp5bbjlOPY2Q0PgoF+Od1XZrHLYI28Xe3ossCmYCQt11FQKEYd9+PF8jymTvtWJSHQ==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/elliptic/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -6630,6 +6907,16 @@
"node": ">=12"
}
},
+ "node_modules/evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "dev": true,
+ "dependencies": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -7418,6 +7705,29 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hash-base": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+ "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/hash.js": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
@@ -7430,6 +7740,17 @@
"node": ">= 0.4"
}
},
+ "node_modules/hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+ "dev": true,
+ "dependencies": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -9037,6 +9358,17 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "dev": true,
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
"node_modules/meow": {
"version": "12.1.1",
"resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz",
@@ -9077,6 +9409,25 @@
"node": ">=8.6"
}
},
+ "node_modules/miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "bin": {
+ "miller-rabin": "bin/miller-rabin"
+ }
+ },
+ "node_modules/miller-rabin/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -9119,6 +9470,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true
+ },
+ "node_modules/minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+ "dev": true
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -9745,10 +10108,13 @@
}
},
"node_modules/object-inspect": {
- "version": "1.12.3",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
- "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -10225,6 +10591,23 @@
"node": ">=6"
}
},
+ "node_modules/parse-asn1": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
+ "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==",
+ "dev": true,
+ "dependencies": {
+ "asn1.js": "^4.10.1",
+ "browserify-aes": "^1.2.0",
+ "evp_bytestokey": "^1.0.3",
+ "hash-base": "~3.0",
+ "pbkdf2": "^3.1.2",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -10337,6 +10720,22 @@
"node": ">=8"
}
},
+ "node_modules/pbkdf2": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
+ "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
+ "dev": true,
+ "dependencies": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -10628,6 +11027,12 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
"node_modules/process-on-spawn": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
@@ -10725,10 +11130,30 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
+ "node_modules/public-encrypt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+ "dev": true,
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/public-encrypt/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ },
"node_modules/punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"engines": {
"node": ">=6"
@@ -10784,6 +11209,16 @@
"safe-buffer": "^5.1.0"
}
},
+ "node_modules/randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -11544,6 +11979,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/ripemd160": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "dev": true,
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
"node_modules/run-applescript": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
@@ -11766,6 +12211,19 @@
"node": ">= 0.4"
}
},
+ "node_modules/sha.js": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ }
+ },
"node_modules/shallow-clone": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -11838,14 +12296,18 @@
}
},
"node_modules/side-channel": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
- "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.0",
- "get-intrinsic": "^1.0.2",
- "object-inspect": "^1.9.0"
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -12269,6 +12731,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/stream-browserify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
+ "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "~2.0.4",
+ "readable-stream": "^3.5.0"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -13495,6 +13967,12 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/vm-browserify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+ "dev": true
+ },
"node_modules/walk-up-path": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz",
@@ -16804,6 +17282,25 @@
"is-shared-array-buffer": "^1.0.2"
}
},
+ "asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ }
+ }
+ },
"ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@@ -16881,6 +17378,12 @@
"readable-stream": "^3.4.0"
}
},
+ "bn.js": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
+ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
+ "dev": true
+ },
"boxen": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz",
@@ -16979,6 +17482,125 @@
"fill-range": "^7.1.1"
}
},
+ "brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+ "dev": true
+ },
+ "browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "dev": true,
+ "requires": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "dev": true,
+ "requires": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "browserify-rsa": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
+ "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^5.0.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "browserify-sign": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz",
+ "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^5.2.1",
+ "browserify-rsa": "^4.1.0",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.5",
+ "hash-base": "~3.0",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.7",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ }
+ }
+ }
+ }
+ },
"browserslist": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
@@ -17007,6 +17629,12 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
+ "buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
+ "dev": true
+ },
"builtins": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
@@ -17224,6 +17852,16 @@
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"dev": true
},
+ "cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -17759,6 +18397,12 @@
"integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
"dev": true
},
+ "core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
"cosmiconfig": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
@@ -17780,6 +18424,51 @@
"jiti": "^1.19.1"
}
},
+ "create-ecdh": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
+ "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ }
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -17797,6 +18486,25 @@
"which": "^2.0.1"
}
},
+ "crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "dev": true,
+ "requires": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ }
+ },
"crypto-random-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
@@ -17913,11 +18621,12 @@
"dev": true
},
"define-properties": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
- "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"requires": {
+ "define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
}
@@ -17944,12 +18653,41 @@
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
+ "des.js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ }
+ }
+ },
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -17989,6 +18727,29 @@
"integrity": "sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg==",
"dev": true
},
+ "elliptic": {
+ "version": "6.5.6",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.6.tgz",
+ "integrity": "sha512-mpzdtpeCLuS3BmE3pO3Cpp5bbjlOPY2Q0PgoF+Od1XZrHLYI28Xe3ossCmYCQt11FQKEYd9+PF8jymTvtWJSHQ==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ }
+ }
+ },
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -18602,6 +19363,16 @@
"integrity": "sha512-f/qE2gImHRa4Cp2y1stEOSgw8wTFyUdVJX7G//bMwbaV9JqISFxg99NbmVQeP7YLnDUZ2un851jlaDrlpmGehQ==",
"dev": true
},
+ "evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "dev": true,
+ "requires": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
"execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -19158,6 +19929,26 @@
"has-symbols": "^1.0.2"
}
},
+ "hash-base": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+ "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "hash.js": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
"hasown": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
@@ -19167,6 +19958,17 @@
"function-bind": "^1.1.2"
}
},
+ "hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+ "dev": true,
+ "requires": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
"html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -20305,6 +21107,17 @@
"ssri": "^10.0.0"
}
},
+ "md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "dev": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
"meow": {
"version": "12.1.1",
"resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz",
@@ -20333,6 +21146,24 @@
"picomatch": "^2.3.1"
}
},
+ "miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ }
+ }
+ },
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -20360,6 +21191,18 @@
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
"dev": true
},
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true
+ },
+ "minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+ "dev": true
+ },
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -20819,9 +21662,9 @@
"dev": true
},
"object-inspect": {
- "version": "1.12.3",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
- "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"dev": true
},
"object-keys": {
@@ -21151,6 +21994,20 @@
"callsites": "^3.0.0"
}
},
+ "parse-asn1": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
+ "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==",
+ "dev": true,
+ "requires": {
+ "asn1.js": "^4.10.1",
+ "browserify-aes": "^1.2.0",
+ "evp_bytestokey": "^1.0.3",
+ "hash-base": "~3.0",
+ "pbkdf2": "^3.1.2",
+ "safe-buffer": "^5.2.1"
+ }
+ },
"parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -21235,6 +22092,19 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
+ "pbkdf2": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
+ "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
+ "dev": true,
+ "requires": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -21423,6 +22293,12 @@
"integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==",
"dev": true
},
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
"process-on-spawn": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
@@ -21509,10 +22385,32 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
+ "public-encrypt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
+ "dev": true
+ }
+ }
+ },
"punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true
},
"pupa": {
@@ -21545,6 +22443,16 @@
"safe-buffer": "^5.1.0"
}
},
+ "randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -22064,6 +22972,16 @@
"glob": "^7.1.3"
}
},
+ "ripemd160": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "dev": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
"run-applescript": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
@@ -22214,6 +23132,16 @@
"has-property-descriptors": "^1.0.1"
}
},
+ "sha.js": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"shallow-clone": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
@@ -22267,14 +23195,15 @@
}
},
"side-channel": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
- "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dev": true,
"requires": {
- "call-bind": "^1.0.0",
- "get-intrinsic": "^1.0.2",
- "object-inspect": "^1.9.0"
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
}
},
"signal-exit": {
@@ -22572,6 +23501,16 @@
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
"dev": true
},
+ "stream-browserify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
+ "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
+ "dev": true,
+ "requires": {
+ "inherits": "~2.0.4",
+ "readable-stream": "^3.5.0"
+ }
+ },
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -23426,6 +24365,12 @@
"integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
"dev": true
},
+ "vm-browserify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+ "dev": true
+ },
"walk-up-path": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz",
diff --git a/package.json b/package.json
index 2de0531..ee80528 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hft-limit-order-book",
- "version": "5.0.0",
+ "version": "6.1.0-beta.1",
"description": "Node.js Lmit Order Book for high-frequency trading (HFT).",
"author": "Andrea Fassina ",
"license": "MIT",
@@ -28,17 +28,17 @@
"lint": "ts-standard | snazzy",
"lint:fix": "ts-standard --fix | snazzy",
"package": "npm run build && npm pack",
- "postinstall": "husky install",
"postpublish": "pinst --enable",
"prepublishOnly": "pinst --disable",
"release": "release-it --ci",
+ "release:beta": "release-it --ci --preRelease=beta",
"release:major": "release-it major --ci",
"release:minor": "release-it minor --ci",
"release:patch": "release-it patch --ci",
"test": "tap",
"test:dev": "tap repl w",
"test:cov": "tap && tap report lcov",
- "prepare": "husky install"
+ "prepare": "husky"
},
"dependencies": {
"denque": "2.1.0",
@@ -49,17 +49,20 @@
"@commitlint/config-conventional": "^18.5.0",
"@release-it/conventional-changelog": "^8.0.1",
"@types/functional-red-black-tree": "^1.0.6",
+ "crypto-browserify": "^3.12.0",
"gaussian": "^1.0.0",
- "husky": "^9.0.6",
+ "husky": "^9.1.1",
"nanobench": "^3.0.0",
"pinst": "^3.0.0",
"release-it": "^17.0.3",
"snazzy": "^9.0.0",
+ "stream-browserify": "^3.0.0",
"tap": "^19.2.5",
"ts-loader": "^9.0.0",
"ts-node": "^10.0.0",
"ts-standard": "^12.0.0",
"typescript": "^5.0.0",
+ "vm-browserify": "^1.1.2",
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
},
diff --git a/src/errors.ts b/src/errors.ts
index 3ef2716..036b60b 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -1,6 +1,7 @@
export enum ERROR {
Default = 'Something wrong',
ErrInsufficientQuantity = 'orderbook: insufficient quantity to calculate price',
+ ErrInvalidConditionalOrder = 'orderbook: 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)',
ErrInvalidOrderType = "orderbook: supported order type are 'limit' and 'market'",
ErrInvalidPrice = 'orderbook: invalid order price',
ErrInvalidPriceLevel = 'orderbook: invalid order price level',
@@ -32,6 +33,8 @@ export const CustomError = (error?: ERROR | string): Error => {
return new Error(ERROR.ErrOrderNotFound)
case ERROR.ErrInvalidSide:
return new Error(ERROR.ErrInvalidSide)
+ case ERROR.ErrInvalidConditionalOrder:
+ return new Error(ERROR.ErrInvalidConditionalOrder)
case ERROR.ErrInvalidOrderType:
return new Error(ERROR.ErrInvalidOrderType)
case ERROR.ErrInvalidTimeInForce:
diff --git a/src/order.ts b/src/order.ts
index 001d13a..5a9c2bb 100644
--- a/src/order.ts
+++ b/src/order.ts
@@ -1,41 +1,31 @@
-import { Side } from './side'
-import { IOrder } from './types'
+import { CustomError, ERROR } from './errors'
+import type { Side } from './side'
+import {
+ ILimitOrder,
+ IStopLimitOrder,
+ IStopMarketOrder,
+ InternalLimitOrderOptions,
+ OrderOptions,
+ InternalStopLimitOrderOptions,
+ InternalStopMarketOrderOptions,
+ TimeInForce,
+ OrderType
+} from './types'
-export enum OrderType {
- LIMIT = 'limit',
- MARKET = 'market',
-}
-
-export enum TimeInForce {
- GTC = 'GTC',
- IOC = 'IOC',
- FOK = 'FOK',
-}
+import { randomUUID } from 'node:crypto'
-export class Order {
- private readonly _id: string
- private readonly _side: Side
- private _size: number
- private readonly _origSize: number
- private _price: number
- private _time: number
- private readonly _isMaker: boolean
- constructor (
- orderId: string,
- side: Side,
- size: number,
- price: number,
- time?: number,
- isMaker?: boolean,
- origSize?: number
- ) {
- this._id = orderId
- this._side = side
- this._price = price
- this._size = size
- this._origSize = origSize ?? size
- this._time = time ?? Date.now()
- this._isMaker = isMaker ?? false
+abstract class BaseOrder {
+ readonly _id: string
+ readonly _side: Side
+ _size: number
+ readonly _origSize: number
+ _time: number
+ constructor (options: OrderOptions) {
+ this._id = options.id ?? randomUUID()
+ this._side = options.side
+ this._size = options.size
+ this._origSize = options.origSize ?? options.size
+ this._time = options.time ?? Date.now()
}
// Getter for order ID
@@ -48,16 +38,6 @@ export class Order {
return this._side
}
- // Getter for order price
- get price (): number {
- return this._price
- }
-
- // Getter for order price
- set price (price: number) {
- this._price = price
- }
-
// Getter for order size
get size (): number {
return this._size
@@ -83,33 +63,225 @@ export class Order {
this._time = time
}
+ // This method returns a string representation of the order
+ abstract toString (): void
+ // This method returns a JSON string representation of the order
+ abstract toJSON (): void
+ // This method returns an object representation of the order
+ abstract toObject (): void
+}
+export class LimitOrder extends BaseOrder {
+ private readonly _type: OrderType.LIMIT
+ private _price: number
+ private readonly _timeInForce: TimeInForce
+ private readonly _isMaker: boolean
+ // Refers to the linked Stop Limit order stopPrice
+ private readonly _ocoStopPrice?: number
+ constructor (options: InternalLimitOrderOptions) {
+ super(options)
+ this._type = options.type
+ this._price = options.price
+ this._timeInForce = options.timeInForce
+ this._isMaker = options.isMaker
+ this._ocoStopPrice = options.ocoStopPrice
+ }
+
+ // Getter for order type
+ get type (): OrderType.LIMIT {
+ return this._type
+ }
+
+ // Getter for order price
+ get price (): number {
+ return this._price
+ }
+
+ // Getter for order price
+ set price (price: number) {
+ this._price = price
+ }
+
+ // Getter for timeInForce price
+ get timeInForce (): TimeInForce {
+ return this._timeInForce
+ }
+
// Getter for order isMaker
get isMaker (): boolean {
return this._isMaker
}
- // This method returns a string representation of the order
+ get ocoStopPrice (): number | undefined {
+ return this._ocoStopPrice
+ }
+
toString = (): string =>
`${this._id}:
+ type: ${this.type}
side: ${this._side}
- origSize: ${this._origSize.toString()}
- size: ${this._size.toString()}
+ size: ${this._size}
+ origSize: ${this._origSize}
price: ${this._price}
time: ${this._time}
+ timeInForce: ${this._timeInForce}
isMaker: ${this._isMaker as unknown as string}`
- // This method returns a JSON string representation of the order
- toJSON = (): string =>
- JSON.stringify(this.toObject())
+ toJSON = (): string => JSON.stringify(this.toObject())
- // This method returns an object representation of the order
- toObject = (): IOrder => ({
+ toObject = (): ILimitOrder => ({
+ id: this._id,
+ type: this.type,
+ side: this._side,
+ size: this._size,
+ origSize: this._origSize,
+ price: this._price,
+ time: this._time,
+ timeInForce: this._timeInForce,
+ isMaker: this._isMaker
+ })
+}
+
+export class StopMarketOrder extends BaseOrder {
+ private readonly _type: OrderType.STOP_MARKET
+ private readonly _stopPrice: number
+ constructor (options: InternalStopMarketOrderOptions) {
+ super(options)
+ this._type = options.type
+ this._stopPrice = options.stopPrice
+ }
+
+ // Getter for order type
+ get type (): OrderType.STOP_MARKET {
+ return this._type
+ }
+
+ // Getter for order stopPrice
+ get stopPrice (): number {
+ return this._stopPrice
+ }
+
+ toString = (): string =>
+ `${this._id}:
+ type: ${this.type}
+ side: ${this._side}
+ size: ${this._size}
+ origSize: ${this._origSize}
+ stopPrice: ${this._stopPrice}
+ time: ${this._time}`
+
+ toJSON = (): string => JSON.stringify(this.toObject())
+
+ toObject = (): IStopMarketOrder => ({
id: this._id,
+ type: this.type,
side: this._side,
+ size: this._size,
origSize: this._origSize,
+ stopPrice: this._stopPrice,
+ time: this._time
+ })
+}
+
+export class StopLimitOrder extends BaseOrder {
+ private readonly _type: OrderType.STOP_LIMIT
+ private _price: number
+ private readonly _stopPrice: number
+ private readonly _timeInForce: TimeInForce
+ private readonly _isMaker: boolean
+ // It's true when there is a linked Limit Order
+ private readonly _isOCO: boolean
+ constructor (options: InternalStopLimitOrderOptions) {
+ super(options)
+ this._type = options.type
+ this._price = options.price
+ this._stopPrice = options.stopPrice
+ this._timeInForce = options.timeInForce
+ this._isMaker = options.isMaker
+ this._isOCO = options.isOCO ?? false
+ }
+
+ // Getter for order type
+ get type (): OrderType.STOP_LIMIT {
+ return this._type
+ }
+
+ // Getter for order price
+ get price (): number {
+ return this._price
+ }
+
+ // Getter for order price
+ set price (price: number) {
+ this._price = price
+ }
+
+ // Getter for order stopPrice
+ get stopPrice (): number {
+ return this._stopPrice
+ }
+
+ // Getter for timeInForce price
+ get timeInForce (): TimeInForce {
+ return this._timeInForce
+ }
+
+ // Getter for order isMaker
+ get isMaker (): boolean {
+ return this._isMaker
+ }
+
+ // Getter for order isOCO
+ get isOCO (): boolean {
+ return this._isOCO
+ }
+
+ toString = (): string =>
+ `${this._id}:
+ type: ${this.type}
+ side: ${this._side}
+ size: ${this._size}
+ origSize: ${this._origSize}
+ price: ${this._price}
+ stopPrice: ${this._stopPrice}
+ timeInForce: ${this._timeInForce}
+ time: ${this._time}
+ isMaker: ${this._isMaker as unknown as string}`
+
+ toJSON = (): string => JSON.stringify(this.toObject())
+
+ toObject = (): IStopLimitOrder => ({
+ id: this._id,
+ type: this.type,
+ side: this._side,
size: this._size,
+ origSize: this._origSize,
price: this._price,
+ stopPrice: this._stopPrice,
+ timeInForce: this._timeInForce,
time: this._time,
isMaker: this._isMaker
})
}
+
+export const OrderFactory = {
+ createOrder(
+ options: T
+ ): T extends InternalLimitOrderOptions
+ ? LimitOrder
+ : T extends InternalStopLimitOrderOptions
+ ? StopLimitOrder
+ : T extends InternalStopMarketOrderOptions
+ ? StopMarketOrder
+ : never {
+ switch (options.type) {
+ case OrderType.LIMIT:
+ return new LimitOrder(options) as any
+ case OrderType.STOP_LIMIT:
+ return new StopLimitOrder(options) as any
+ case OrderType.STOP_MARKET:
+ return new StopMarketOrder(options) as any
+ default:
+ throw CustomError(ERROR.ErrInvalidOrderType)
+ }
+ }
+}
diff --git a/src/orderbook.ts b/src/orderbook.ts
index d7a0b44..8ee94d5 100644
--- a/src/orderbook.ts
+++ b/src/orderbook.ts
@@ -1,41 +1,64 @@
import { ERROR, CustomError } from './errors'
-import { Order, OrderType, TimeInForce } from './order'
+import {
+ LimitOrder,
+ OrderFactory,
+ StopLimitOrder,
+ StopMarketOrder
+} from './order'
import { OrderQueue } from './orderqueue'
import { OrderSide } from './orderside'
import { Side } from './side'
-import type {
- ICancelOrder,
- IProcessOrder,
- JournalLog,
- OrderBookOptions,
- OrderUpdatePrice,
- OrderUpdateSize,
- Snapshot
+import { StopBook } from './stopbook'
+import {
+ Order,
+ OrderType,
+ TimeInForce,
+ type CreateOrderOptions,
+ type ICancelOrder,
+ type IProcessOrder,
+ type JournalLog,
+ type OrderBookOptions,
+ type OrderUpdatePrice,
+ type OrderUpdateSize,
+ type Snapshot,
+ MarketOrderOptions,
+ StopMarketOrderOptions,
+ LimitOrderOptions,
+ StopLimitOrderOptions,
+ OCOOrderOptions,
+ StopOrder
} from './types'
const validTimeInForce = Object.values(TimeInForce)
export class OrderBook {
- private orders: { [key: string]: Order } = {}
+ private orders: { [key: string]: LimitOrder } = {}
private _lastOp: number = 0
+ private _marketPrice: number = 0
private readonly bids: OrderSide
private readonly asks: OrderSide
private readonly enableJournaling: boolean
+ private readonly stopBook: StopBook
+ private readonly experimentalConditionalOrders: boolean
/**
* Creates an instance of OrderBook.
* @param {OrderBookOptions} [options={}] - Options for configuring the order book.
* @param {JournalLog} [options.snapshot] - The orderbook snapshot will be restored before processing any journal logs, if any.
* @param {JournalLog} [options.journal] - Array of journal logs (optional).
* @param {boolean} [options.enableJournaling=false] - Flag to enable journaling. Default to false
+ * @param {boolean} [options.experimentalConditionalOrders=false] - Flag to enable experimental Conditional Order (Stop Market, Stop Limit and OCO orders). Default to false
*/
constructor ({
snapshot,
journal,
- enableJournaling = false
+ enableJournaling = false,
+ experimentalConditionalOrders = false
}: OrderBookOptions = {}) {
this.bids = new OrderSide(Side.BUY)
this.asks = new OrderSide(Side.SELL)
this.enableJournaling = enableJournaling
+ this.stopBook = new StopBook()
+ this.experimentalConditionalOrders = experimentalConditionalOrders
// First restore from orderbook snapshot
if (snapshot != null) {
this.restoreSnapshot(snapshot)
@@ -51,14 +74,36 @@ export class OrderBook {
}
}
+ // Getter for the market price
+ get marketPrice (): number {
+ return this._marketPrice
+ }
+
// Getter for the lastOp
get lastOp (): number {
return this._lastOp
}
-
/**
+ * Create new order. See {@link CreateOrderOptions} for details.
+ *
+ * @param options
+ * @param options.type - `limit` or `market`
+ * @param options.side - `sell` or `buy`
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency. Param only for limit order
+ * @param options.orderID - Unique order ID. Param only for limit order
+ * @param options.stopPrice - The price at which the order will be triggered. Used with `stop_limit` and `stop_market` order.
+ * @param options.stopLimitPrice - The price at which the order will be triggered. Used with `stop_limit` and `stop_market` order.
+ * @param options.timeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. Param only for limit order
+ * @param options.stopLimitTimeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. Param only for limit order
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ public createOrder (options: CreateOrderOptions): IProcessOrder
+ /**
+ * @deprecated This implementation has been deprecated and will be removed on v7.0.0.
+ * Use createOrder({ type, side, size, price, id, timeInForce }) instead.
+ *
* Create a trade order
- * @see {@link IProcessOrder} for the returned data structure
*
* @param type - `limit` or `market`
* @param side - `sell` or `buy`
@@ -66,9 +111,12 @@ export class OrderBook {
* @param price - The price at which the order is to be fullfilled, in units of the quote currency. Param only for limit order
* @param orderID - Unique order ID. Param only for limit order
* @param timeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. Param only for limit order
- * @returns An object with the result of the processed order or an error.
+ * @param stopPrice - The price at which the order will be triggered. Used with `stop_limit` and `stop_market` order.
+ * @param stopLimitPrice - The price at which the order will be triggered. Used with `stop_limit` and `stop_market` order.
+ * @param stopLimitTimeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. Param only for limit order
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
- public createOrder = (
+ public createOrder (
// Common for all order types
type: OrderType,
side: Side,
@@ -76,140 +124,241 @@ export class OrderBook {
// Specific for limit order type
price?: number,
orderID?: string,
- timeInForce: TimeInForce = TimeInForce.GTC
- ): IProcessOrder => {
- switch (type) {
+ timeInForce?: TimeInForce,
+ stopPrice?: number,
+ stopLimitPrice?: number,
+ stopLimitTimeInForce?: TimeInForce
+ ): IProcessOrder
+
+ public createOrder (
+ typeOrOptions: CreateOrderOptions | OrderType,
+ side?: Side,
+ size?: number,
+ price?: number,
+ orderID?: string,
+ timeInForce = TimeInForce.GTC,
+ stopPrice?: number,
+ stopLimitPrice?: number,
+ stopLimitTimeInForce = TimeInForce.GTC
+ ): IProcessOrder {
+ let options: CreateOrderOptions
+ // We don't want to test the deprecated signature.
+ /* c8 ignore start */
+ if (
+ typeof typeOrOptions === 'string' &&
+ side !== undefined &&
+ size !== undefined
+ ) {
+ options = {
+ type: typeOrOptions,
+ side,
+ size,
+ // @ts-expect-error
+ price,
+ id: orderID,
+ timeInForce,
+ // @ts-expect-error
+ stopPrice,
+ // @ts-expect-error
+ stopLimitPrice,
+ stopLimitTimeInForce
+ }
+ /* c8 ignore stop */
+ } else if (typeof typeOrOptions === 'object') {
+ options = typeOrOptions
+ /* c8 ignore start */
+ } else {
+ throw new Error('Invalid arguments.')
+ }
+ /* c8 ignore stop */
+
+ switch (options.type) {
case OrderType.MARKET:
- return this.market(side, size)
+ return this.market(options)
case OrderType.LIMIT:
- return this.limit(
- side,
- orderID as string,
- size,
- price as number,
- timeInForce
- )
+ return this.limit(options)
+ case OrderType.STOP_MARKET:
+ return this.stopMarket(options)
+ case OrderType.STOP_LIMIT:
+ return this.stopLimit(options)
+ case OrderType.OCO:
+ return this.oco(options)
default:
return {
done: [],
+ activated: [],
partial: null,
partialQuantityProcessed: 0,
- quantityLeft: size,
+ quantityLeft: 0,
err: CustomError(ERROR.ErrInvalidOrderType)
}
}
}
/**
+ * Create a market order. See {@link MarketOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ public market (options: MarketOrderOptions): IProcessOrder
+ /**
+ * @deprecated This implementation has been deprecated and will be removed on v7.0.0.
+ * Use market({ side, size }) instead.
+ *
* Create a market order
- * @see {@link IProcessOrder} for the returned data structure
*
* @param side - `sell` or `buy`
* @param size - How much of currency you want to trade in units of base currency
- * @returns An object with the result of the processed order or an error
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
- public market = (side: Side, size: number): IProcessOrder => {
- let quantityToTrade = size
- const response = this.getProcessOrderResponse(quantityToTrade)
-
- if (![Side.SELL, Side.BUY].includes(side)) {
- response.err = CustomError(ERROR.ErrInvalidSide)
- return response
- }
-
- if (typeof quantityToTrade !== 'number' || quantityToTrade <= 0) {
- response.err = CustomError(ERROR.ErrInsufficientQuantity)
- return response
- }
+ public market (side: Side, size: number): IProcessOrder
- let iter
- let sideToProcess: OrderSide
- if (side === Side.BUY) {
- iter = this.asks.minPriceQueue
- sideToProcess = this.asks
+ public market (
+ sideOrOptions: MarketOrderOptions | Side,
+ size?: number
+ ): IProcessOrder {
+ // We don't want to test the deprecated signature.
+ /* c8 ignore start */
+ if (typeof sideOrOptions === 'string' && size !== undefined) {
+ return this._market({ side: sideOrOptions, size })
+ /* c8 ignore stop */
+ } else if (typeof sideOrOptions === 'object') {
+ return this._market(sideOrOptions)
+ /* c8 ignore start */
} else {
- iter = this.bids.maxPriceQueue
- sideToProcess = this.bids
+ throw new Error('Invalid arguments.')
}
+ /* c8 ignore stop */
+ }
- while (quantityToTrade > 0 && sideToProcess.len() > 0) {
- // if sideToProcess.len > 0 it is not necessary to verify that bestPrice exists
- const bestPrice = iter()
- const { done, partial, partialQuantityProcessed, quantityLeft } =
- this.processQueue(bestPrice as OrderQueue, quantityToTrade)
- response.done = response.done.concat(done)
- response.partial = partial
- response.partialQuantityProcessed = partialQuantityProcessed
- quantityToTrade = quantityLeft
- }
- response.quantityLeft = quantityToTrade
- if (this.enableJournaling) {
- response.log = {
- opId: ++this._lastOp,
- ts: Date.now(),
- op: 'm',
- o: { side, size }
- }
- }
- return response
+ /**
+ * Create a stop market order. See {@link StopMarketOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.stopPrice - The price at which the order will be triggered.
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ public stopMarket = (options: StopMarketOrderOptions): IProcessOrder => {
+ /* c8 ignore next we don't need test for this */
+ if (!this.experimentalConditionalOrders) throw new Error('In order to use conditional orders you need to instantiate the order book with the `experimentalConditionalOrders` option set to true')
+ return this._stopMarket(options)
}
/**
+ * Create a limit order. See {@link LimitOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.id - Unique order ID
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
+ * @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ public limit (options: LimitOrderOptions): IProcessOrder
+ /**
+ * @deprecated This implementation has been deprecated and will be removed on v7.0.0.
+ * Use limit({ id, side, size, price, timeInForce }) instead.
+ *
* Create a limit order
- * @see {@link IProcessOrder} for the returned data structure
*
* @param side - `sell` or `buy`
* @param orderID - Unique order ID
* @param size - How much of currency you want to trade in units of base currency
* @param price - The price at which the order is to be fullfilled, in units of the quote currency
- * @param timeInForce - Time-in-force type supported are: GTC, FOK, IOC
- * @returns An object with the result of the processed order or an error
+ * @param timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
*/
- public limit = (
+ public limit (
side: Side,
orderID: string,
size: number,
price: number,
- timeInForce: TimeInForce = TimeInForce.GTC
- ): IProcessOrder => {
- const response = this.getProcessOrderResponse(size)
-
- if (![Side.SELL, Side.BUY].includes(side)) {
- response.err = CustomError(ERROR.ErrInvalidSide)
- return response
- }
-
- if (this.orders[orderID] !== undefined) {
- response.err = CustomError(ERROR.ErrOrderExists)
- return response
- }
-
- if (typeof size !== 'number' || size <= 0) {
- response.err = CustomError(ERROR.ErrInvalidQuantity)
- return response
- }
-
- if (typeof price !== 'number' || price <= 0) {
- response.err = CustomError(ERROR.ErrInvalidPrice)
- return response
- }
+ timeInForce?: TimeInForce
+ ): IProcessOrder
- if (!validTimeInForce.includes(timeInForce)) {
- response.err = CustomError(ERROR.ErrInvalidTimeInForce)
- return response
+ public limit (
+ sideOrOptions: LimitOrderOptions | Side,
+ orderID?: string,
+ size?: number,
+ price?: number,
+ timeInForce: TimeInForce = TimeInForce.GTC
+ ): IProcessOrder {
+ // We don't want to test the deprecated signature.
+ /* c8 ignore start */
+ if (
+ typeof sideOrOptions === 'string' &&
+ orderID !== undefined &&
+ size !== undefined &&
+ price !== undefined
+ ) {
+ return this._limit({
+ id: orderID,
+ side: sideOrOptions,
+ size,
+ price,
+ timeInForce
+ })
+ /* c8 ignore stop */
+ } else if (typeof sideOrOptions === 'object') {
+ return this._limit(sideOrOptions)
+ /* c8 ignore start */
+ } else {
+ throw new Error('Invalid arguments.')
}
+ /* c8 ignore stop */
+ }
- this.createLimitOrder(response, side, orderID, size, price, timeInForce)
- if (this.enableJournaling) {
- response.log = {
- opId: ++this._lastOp,
- ts: Date.now(),
- op: 'l',
- o: { side, orderID, size, price, timeInForce }
- }
- }
+ /**
+ * Create a stop limit order. See {@link StopLimitOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.id - Unique order ID
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.price - The price at which the order is to be fullfilled, in units of the quote currency
+ * @param options.stopPrice - The price at which the order will be triggered.
+ * @param options.timeInForce - Time-in-force type supported are: GTC, FOK, IOC. Default is GTC
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ public stopLimit = (options: StopLimitOrderOptions): IProcessOrder => {
+ /* c8 ignore next we don't need test for this */
+ if (!this.experimentalConditionalOrders) throw new Error('In order to use conditional orders you need to instantiate the order book with the `experimentalConditionalOrders` option set to true')
+ return this._stopLimit(options)
+ }
- return response
+ /**
+ * Create an OCO (One-Cancels-the-Other) order.
+ * OCO order combines a `stop_limit` order and a `limit` order, where if stop price
+ * is triggered or limit order is fully or partially fulfilled, the other is canceled.
+ * Both orders have the same `side` and `size`. If you cancel one of the orders, the
+ * entire OCO order pair will be canceled.
+ *
+ * For BUY orders the `stopPrice` must be above the current price and the `price` below the current price
+ * For SELL orders the `stopPrice` must be below the current price and the `price` above the current price
+ *
+ * See {@link OCOOrderOptions} for details.
+ *
+ * @param options
+ * @param options.side - `sell` or `buy`
+ * @param options.id - Unique order ID
+ * @param options.size - How much of currency you want to trade in units of base currency
+ * @param options.price - The price of the `limit` order at which the order is to be fullfilled, in units of the quote currency
+ * @param options.stopPrice - The price at which the `stop_limit` order will be triggered.
+ * @param options.stopLimitPrice - The price of the `stop_limit` order at which the order is to be fullfilled, in units of the quote currency.
+ * @param options.timeInForce - Time-in-force of the `limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
+ * @param options.stopLimitTimeInForce - Time-in-force of the `stop_limit` order. Type supported are: GTC, FOK, IOC. Default is GTC
+ * @returns An object with the result of the processed order or an error. See {@link IProcessOrder} for the returned data structure
+ */
+ public oco = (options: OCOOrderOptions): IProcessOrder => {
+ /* c8 ignore next we don't need test for this */
+ if (!this.experimentalConditionalOrders) throw new Error('In order to use conditional orders you need to instantiate the order book with the `experimentalConditionalOrders` option set to true')
+ return this._oco(options)
}
/**
@@ -230,6 +379,7 @@ export class OrderBook {
if (order === undefined) {
return {
done: [],
+ activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: 0,
@@ -264,6 +414,7 @@ export class OrderBook {
// Missing one of price and/or size, or the provided ones are not greater than zero
return {
done: [],
+ activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: orderUpdate?.size ?? 0,
@@ -287,7 +438,7 @@ export class OrderBook {
* @param orderID - The ID of the order to be returned
* @returns The order if exists or `undefined`
*/
- public order = (orderID: string): Order | undefined => {
+ public order = (orderID: string): LimitOrder | undefined => {
return this.orders[orderID]
}
@@ -355,8 +506,8 @@ export class OrderBook {
}
public snapshot = (): Snapshot => {
- const bids: Array<{ price: number, orders: Order[] }> = []
- const asks: Array<{ price: number, orders: Order[] }> = []
+ const bids: Array<{ price: number, orders: LimitOrder[] }> = []
+ const asks: Array<{ price: number, orders: LimitOrder[] }> = []
this.bids.priceTree().forEach((price: number, orders: OrderQueue) => {
bids.push({ price, orders: orders.toArray() })
})
@@ -366,6 +517,160 @@ export class OrderBook {
return { bids, asks, ts: Date.now(), lastOp: this._lastOp }
}
+ private readonly _market = (
+ options: MarketOrderOptions,
+ incomingResponse?: IProcessOrder
+ ): IProcessOrder => {
+ const response = incomingResponse ?? this.validateMarketOrder(options)
+ if (response.err != null) return response
+
+ let quantityToTrade = options.size
+ let iter
+ let sideToProcess: OrderSide
+ if (options.side === Side.BUY) {
+ iter = this.asks.minPriceQueue
+ sideToProcess = this.asks
+ } else {
+ iter = this.bids.maxPriceQueue
+ sideToProcess = this.bids
+ }
+ const priceBefore = this._marketPrice
+ while (quantityToTrade > 0 && sideToProcess.len() > 0) {
+ // if sideToProcess.len > 0 it is not necessary to verify that bestPrice exists
+ const bestPrice = iter() as OrderQueue
+ const { done, partial, partialQuantityProcessed, quantityLeft } =
+ this.processQueue(bestPrice, quantityToTrade)
+ response.done = response.done.concat(done)
+ response.partial = partial
+ response.partialQuantityProcessed = partialQuantityProcessed
+ quantityToTrade = quantityLeft
+ }
+ response.quantityLeft = quantityToTrade
+
+ this.executeConditionalOrder(options.side, priceBefore, response)
+
+ if (this.enableJournaling) {
+ response.log = {
+ opId: ++this._lastOp,
+ ts: Date.now(),
+ op: 'm',
+ o: { side: options.side, size: options.size }
+ }
+ }
+ return response
+ }
+
+ private readonly _limit = (
+ options: LimitOrderOptions & { ocoStopPrice?: number },
+ incomingResponse?: IProcessOrder
+ ): IProcessOrder => {
+ const response = incomingResponse ?? this.validateLimitOrder(options)
+ if (response.err != null) return response
+ const order = this.createLimitOrder(
+ response,
+ options.side,
+ options.id,
+ options.size,
+ options.price,
+ options.timeInForce ?? TimeInForce.GTC,
+ options.ocoStopPrice
+ )
+ if (this.enableJournaling && order != null) {
+ response.log = {
+ opId: ++this._lastOp,
+ ts: Date.now(),
+ op: 'l',
+ o: {
+ side: order.side,
+ id: order.id,
+ size: order.size,
+ price: order.price,
+ timeInForce: order.timeInForce
+ }
+ }
+ }
+ return response
+ }
+
+ private readonly _stopMarket = (
+ options: StopMarketOrderOptions
+ ): IProcessOrder => {
+ const response = this.validateMarketOrder(options)
+ if (response.err != null) return response
+ const stopMarket = OrderFactory.createOrder({
+ ...options,
+ type: OrderType.STOP_MARKET
+ })
+ return this._stopOrder(stopMarket, response)
+ }
+
+ private readonly _stopLimit = (
+ options: StopLimitOrderOptions
+ ): IProcessOrder => {
+ const response = this.validateLimitOrder(options)
+ if (response.err != null) return response
+ const stopLimit = OrderFactory.createOrder({
+ ...options,
+ type: OrderType.STOP_LIMIT,
+ isMaker: true,
+ timeInForce: options.timeInForce ?? TimeInForce.GTC
+ })
+ return this._stopOrder(stopLimit, response)
+ }
+
+ private readonly _oco = (options: OCOOrderOptions): IProcessOrder => {
+ const response = this.validateLimitOrder(options)
+ /* c8 ignore next already validated with limit test */
+ if (response.err != null) return response
+ if (this.validateOCOOrder(options)) {
+ // We use the same ID for Stop Limit and Limit Order, since
+ // we check only on limit order for duplicated ids
+ this._limit(
+ {
+ id: options.id,
+ side: options.side,
+ size: options.size,
+ price: options.price,
+ timeInForce: options.timeInForce,
+ ocoStopPrice: options.stopPrice
+ },
+ response
+ )
+ /* c8 ignore next already validated with limit test */
+ if (response.err != null) return response
+
+ const stopLimit = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: options.id,
+ side: options.side,
+ size: options.size,
+ price: options.stopLimitPrice,
+ stopPrice: options.stopPrice,
+ isMaker: true,
+ timeInForce: options.stopLimitTimeInForce ?? TimeInForce.GTC,
+ isOCO: true
+ })
+ this.stopBook.add(stopLimit)
+ response.done.push(stopLimit)
+ } else {
+ response.err = CustomError(ERROR.ErrInvalidConditionalOrder)
+ }
+ return response
+ }
+
+ private readonly _stopOrder = (
+ stopOrder: StopMarketOrder | StopLimitOrder,
+ response: IProcessOrder
+ ): IProcessOrder => {
+ if (this.stopBook.validConditionalOrder(this._marketPrice, stopOrder)) {
+ this.stopBook.add(stopOrder)
+ response.done.push(stopOrder)
+ } else {
+ response.err = CustomError(ERROR.ErrInvalidConditionalOrder)
+ }
+ return response
+ }
+
private readonly restoreSnapshot = (snapshot: Snapshot): void => {
this._lastOp = snapshot.lastOp
for (const level of snapshot.bids) {
@@ -383,36 +688,33 @@ export class OrderBook {
}
}
+ /**
+ * Remove an existing order with given ID from the order book
+ * @param orderID The id of the order to be deleted
+ * @param internalDeletion Set to true when the delete comes from internal operations
+ * @returns The removed order if exists or `undefined`
+ */
private readonly _cancelOrder = (
orderID: string,
- skipOpInc: boolean = false
+ internalDeletion: boolean = false
): ICancelOrder | undefined => {
const order = this.orders[orderID]
if (order === undefined) return
/* eslint-disable @typescript-eslint/no-dynamic-delete */
delete this.orders[orderID]
- if (order.side === Side.BUY) {
- const response: ICancelOrder = {
- order: this.bids.remove(order)
- }
- if (this.enableJournaling) {
- response.log = {
- opId: skipOpInc ? this._lastOp : ++this._lastOp,
- ts: Date.now(),
- op: 'd',
- o: { orderID }
- }
- }
- return response
+ const side = order.side === Side.BUY ? this.bids : this.asks
+ const response: ICancelOrder = {
+ order: side.remove(order)
}
- // Side SELL
- const response: ICancelOrder = {
- order: this.asks.remove(order)
+ // Delete OCO Order only when the delete request comes from user
+ if (!internalDeletion && order.ocoStopPrice !== undefined) {
+ response.stopOrder = this.stopBook.remove(order.side, orderID, order.ocoStopPrice)
}
+
if (this.enableJournaling) {
response.log = {
- opId: skipOpInc ? this._lastOp : ++this._lastOp,
+ opId: internalDeletion ? this._lastOp : ++this._lastOp,
ts: Date.now(),
op: 'd',
o: { orderID }
@@ -424,6 +726,7 @@ export class OrderBook {
private readonly getProcessOrderResponse = (size: number): IProcessOrder => {
return {
done: [],
+ activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: size,
@@ -437,14 +740,14 @@ export class OrderBook {
orderID: string,
size: number,
price: number,
- timeInForce: TimeInForce
- ): void => {
+ timeInForce: TimeInForce,
+ ocoStopPrice?: number
+ ): LimitOrder | undefined => {
let quantityToTrade = size
let sideToProcess: OrderSide
let sideToAdd: OrderSide
let comparator
let iter
-
if (side === Side.BUY) {
sideToAdd = this.bids
sideToProcess = this.asks
@@ -464,8 +767,8 @@ export class OrderBook {
return
}
}
-
let bestPrice = iter()
+ const priceBefore = this._marketPrice
while (
quantityToTrade > 0 &&
sideToProcess.len() > 0 &&
@@ -482,15 +785,21 @@ export class OrderBook {
bestPrice = iter()
}
+ this.executeConditionalOrder(side, priceBefore, response)
+
+ let order: LimitOrder
if (quantityToTrade > 0) {
- const order = new Order(
- orderID,
+ order = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: orderID,
side,
- quantityToTrade,
+ size: quantityToTrade,
price,
- Date.now(),
- true
- )
+ time: Date.now(),
+ timeInForce,
+ isMaker: true,
+ ...(ocoStopPrice !== undefined ? { ocoStopPrice } : {})
+ })
if (response.done.length > 0) {
response.partialQuantityProcessed = size - quantityToTrade
response.partial = order
@@ -502,7 +811,7 @@ export class OrderBook {
response.done.forEach((order: Order) => {
totalQuantity += order.size
- totalPrice += order.price * order.size
+ totalPrice += (order as LimitOrder).price * order.size
})
if (response.partialQuantityProcessed > 0 && response.partial !== null) {
@@ -510,44 +819,105 @@ export class OrderBook {
totalPrice +=
response.partial.price * response.partialQuantityProcessed
}
-
- response.done.push(
- new Order(orderID, side, size, totalPrice / totalQuantity, Date.now())
- )
+ order = OrderFactory.createOrder({
+ id: orderID,
+ type: OrderType.LIMIT,
+ side,
+ size,
+ price: totalPrice / totalQuantity,
+ time: Date.now(),
+ timeInForce,
+ isMaker: false
+ })
+ response.done.push(order)
}
// If IOC order was not matched completely remove from the order book
if (timeInForce === TimeInForce.IOC && response.quantityLeft > 0) {
this._cancelOrder(orderID, true)
}
+ return order
+ }
+
+ private readonly executeConditionalOrder = (
+ side: Side,
+ priceBefore: number,
+ response: IProcessOrder
+ ): void => {
+ if (!this.experimentalConditionalOrders) return
+ const pendingOrders = this.stopBook.getConditionalOrders(
+ side,
+ priceBefore,
+ this._marketPrice
+ )
+ if (pendingOrders.length > 0) {
+ const toBeExecuted: StopOrder[] = []
+ // Before get all orders to be executed and clean up the stop queue
+ // in order to avoid that an executed limit/market order run against
+ // the same stop order queue
+ pendingOrders.forEach((queue) => {
+ while (queue.len() > 0) {
+ const headOrder = queue.removeFromHead()
+ if (headOrder !== undefined) toBeExecuted.push(headOrder)
+ }
+ // Queue is empty now so remove the priceLevel
+ this.stopBook.removePriceLevel(side, queue.price)
+ })
+ toBeExecuted.forEach((stopOrder) => {
+ if (stopOrder.type === OrderType.STOP_MARKET) {
+ this._market(
+ {
+ id: stopOrder.id,
+ side: stopOrder.side,
+ size: stopOrder.size
+ },
+ response
+ )
+ } else {
+ if (stopOrder.isOCO) {
+ this._cancelOrder(stopOrder.id, true)
+ }
+ this._limit(
+ {
+ id: stopOrder.id,
+ side: stopOrder.side,
+ size: stopOrder.size,
+ price: stopOrder.price,
+ timeInForce: stopOrder.timeInForce
+ },
+ response
+ )
+ }
+ response.activated.push(stopOrder)
+ })
+ }
}
private readonly replayJournal = (journal: JournalLog[]): void => {
for (const log of journal) {
switch (log.op) {
- case 'm':
- if (log.o.side == null || log.o.size == null) {
+ case 'm': {
+ const { side, size } = log.o
+ if (side == null || size == null) {
throw CustomError(ERROR.ErrJournalLog)
}
- this.market(log.o.side, log.o.size)
+ this.market({ side, size })
break
- case 'l':
- if (
- log.o.side == null ||
- log.o.orderID == null ||
- log.o.size == null ||
- log.o.price == null
- ) {
+ }
+ case 'l': {
+ const { side, id, size, price, timeInForce } = log.o
+ if (side == null || id == null || size == null || price == null) {
throw CustomError(ERROR.ErrJournalLog)
}
- this.limit(
- log.o.side,
- log.o.orderID,
- log.o.size,
- log.o.price,
- log.o.timeInForce
- )
+ this.limit({
+ side,
+ id,
+ size,
+ price,
+ timeInForce
+ })
break
+ }
case 'd':
if (log.o.orderID == null) throw CustomError(ERROR.ErrJournalLog)
this.cancel(log.o.orderID)
@@ -564,6 +934,30 @@ export class OrderBook {
}
}
+ /**
+ * OCO Order:
+ * Buy: price < marketPrice < stopPrice
+ * Sell: price > marketPrice > stopPrice
+ */
+ private readonly validateOCOOrder = (options: OCOOrderOptions): boolean => {
+ let response = false
+ if (
+ options.side === Side.BUY &&
+ options.price < this._marketPrice &&
+ this._marketPrice < options.stopPrice
+ ) {
+ response = true
+ }
+ if (
+ options.side === Side.SELL &&
+ options.price > this._marketPrice &&
+ this._marketPrice > options.stopPrice
+ ) {
+ response = true
+ }
+ return response
+ }
+
private readonly greaterThanOrEqual = (a: number, b: number): boolean => {
return a >= b
}
@@ -578,6 +972,7 @@ export class OrderBook {
): IProcessOrder => {
const response: IProcessOrder = {
done: [],
+ activated: [],
partial: null,
partialQuantityProcessed: 0,
quantityLeft: quantityToTrade,
@@ -588,15 +983,17 @@ export class OrderBook {
const headOrder = orderQueue.head()
if (headOrder !== undefined) {
if (response.quantityLeft < headOrder.size) {
- response.partial = new Order(
- headOrder.id,
- headOrder.side,
- headOrder.size - response.quantityLeft,
- headOrder.price,
- headOrder.time,
- true,
- headOrder.origSize
- )
+ response.partial = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: headOrder.id,
+ side: headOrder.side,
+ size: headOrder.size - response.quantityLeft,
+ origSize: headOrder.origSize,
+ price: headOrder.price,
+ time: headOrder.time,
+ timeInForce: headOrder.timeInForce,
+ isMaker: true
+ })
this.orders[headOrder.id] = response.partial
response.partialQuantityProcessed = response.quantityLeft
orderQueue.update(headOrder, response.partial)
@@ -609,6 +1006,15 @@ export class OrderBook {
response.done.push(canceledOrder.order)
}
}
+ // Remove linked OCO Stop Order if any
+ if (this.experimentalConditionalOrders && headOrder.ocoStopPrice !== undefined) {
+ this.stopBook.remove(
+ headOrder.side,
+ headOrder.id,
+ headOrder.ocoStopPrice
+ )
+ }
+ this._marketPrice = headOrder.price
}
}
}
@@ -665,4 +1071,56 @@ export class OrderBook {
})
return cumulativeSize >= size
}
+
+ private readonly validateMarketOrder = (
+ order: MarketOrderOptions | StopMarketOrderOptions
+ ): IProcessOrder => {
+ const response = this.getProcessOrderResponse(order.size)
+
+ if (![Side.SELL, Side.BUY].includes(order.side)) {
+ response.err = CustomError(ERROR.ErrInvalidSide)
+ return response
+ }
+
+ if (typeof order.size !== 'number' || order.size <= 0) {
+ response.err = CustomError(ERROR.ErrInsufficientQuantity)
+ return response
+ }
+ return response
+ }
+
+ private readonly validateLimitOrder = (
+ options: LimitOrderOptions | StopLimitOrderOptions
+ ): IProcessOrder => {
+ const response = this.getProcessOrderResponse(options.size)
+
+ if (![Side.SELL, Side.BUY].includes(options.side)) {
+ response.err = CustomError(ERROR.ErrInvalidSide)
+ return response
+ }
+
+ if (this.orders[options.id] !== undefined) {
+ response.err = CustomError(ERROR.ErrOrderExists)
+ return response
+ }
+
+ if (typeof options.size !== 'number' || options.size <= 0) {
+ response.err = CustomError(ERROR.ErrInvalidQuantity)
+ return response
+ }
+
+ if (typeof options.price !== 'number' || options.price <= 0) {
+ response.err = CustomError(ERROR.ErrInvalidPrice)
+ return response
+ }
+
+ if (
+ options.timeInForce &&
+ !validTimeInForce.includes(options.timeInForce)
+ ) {
+ response.err = CustomError(ERROR.ErrInvalidTimeInForce)
+ return response
+ }
+ return response
+ }
}
diff --git a/src/orderqueue.ts b/src/orderqueue.ts
index 15e906c..e168e97 100644
--- a/src/orderqueue.ts
+++ b/src/orderqueue.ts
@@ -1,17 +1,17 @@
import Denque from 'denque'
-import { Order } from './order'
+import { LimitOrder } from './order'
export class OrderQueue {
private readonly _price: number
private _volume: number
- private readonly _orders: Denque
// { orderID: index } index in denque
+ private readonly _orders: Denque
private _ordersMap: { [key: string]: number } = {}
constructor (price: number) {
this._price = price
this._volume = 0
- this._orders = new Denque()
+ this._orders = new Denque()
}
// returns the number of orders in queue
@@ -19,7 +19,7 @@ export class OrderQueue {
return this._orders.length
}
- toArray = (): Order[] => {
+ toArray = (): LimitOrder[] => {
return this._orders.toArray()
}
@@ -34,17 +34,17 @@ export class OrderQueue {
}
// returns top order in queue
- head = (): Order | undefined => {
+ head = (): LimitOrder | undefined => {
return this._orders.peekFront()
}
// returns bottom order in queue
- tail = (): Order | undefined => {
+ tail = (): LimitOrder | undefined => {
return this._orders.peekBack()
}
// adds order to tail of the queue
- append = (order: Order): Order => {
+ append = (order: LimitOrder): LimitOrder => {
this._volume += order.size
this._orders.push(order)
this._ordersMap[order.id] = this._orders.length - 1
@@ -52,7 +52,7 @@ export class OrderQueue {
}
// sets up new order to list value
- update = (oldOrder: Order, newOrder: Order): void => {
+ update = (oldOrder: LimitOrder, newOrder: LimitOrder): void => {
this._volume -= oldOrder.size
this._volume += newOrder.size
// Remove old order from head
@@ -65,7 +65,7 @@ export class OrderQueue {
}
// removes order from the queue
- remove = (order: Order): void => {
+ remove = (order: LimitOrder): void => {
this._volume -= order.size
const deletedOrderIndex = this._ordersMap[order.id]
this._orders.removeOne(deletedOrderIndex)
@@ -78,7 +78,7 @@ export class OrderQueue {
}
}
- updateOrderSize = (order: Order, size: number): void => {
+ updateOrderSize = (order: LimitOrder, size: number): void => {
this._volume += size - order.size // update volume
order.size = size
order.time = Date.now()
diff --git a/src/orderside.ts b/src/orderside.ts
index 0e3bc04..6bb6c8e 100644
--- a/src/orderside.ts
+++ b/src/orderside.ts
@@ -1,6 +1,6 @@
import createRBTree from 'functional-red-black-tree'
import { CustomError, ERROR } from './errors'
-import { Order } from './order'
+import { LimitOrder, OrderFactory } from './order'
import { OrderQueue } from './orderqueue'
import { Side } from './side'
import type { OrderUpdatePrice, OrderUpdateSize } from './types'
@@ -49,7 +49,7 @@ export class OrderSide {
}
// appends order to definite price level
- append = (order: Order): Order => {
+ append = (order: LimitOrder): LimitOrder => {
const price = order.price
const strPrice = price.toString()
if (this._prices[strPrice] === undefined) {
@@ -65,7 +65,7 @@ export class OrderSide {
}
// removes order from definite price level
- remove = (order: Order): Order => {
+ remove = (order: LimitOrder): LimitOrder => {
const price = order.price
const strPrice = price.toString()
if (this._prices[strPrice] === undefined) {
@@ -87,25 +87,30 @@ export class OrderSide {
// Update the price of an order and return the order with the updated price
updateOrderPrice = (
- oldOrder: Order,
+ oldOrder: LimitOrder,
orderUpdate: OrderUpdatePrice
- ): Order => {
+ ): LimitOrder => {
this.remove(oldOrder)
- const newOrder = new Order(
- oldOrder.id,
- oldOrder.side,
- orderUpdate.size !== undefined ? orderUpdate.size : oldOrder.size,
- orderUpdate.price,
- Date.now(),
- oldOrder.isMaker,
- oldOrder.origSize
- )
+ const newOrder = OrderFactory.createOrder({
+ id: oldOrder.id,
+ type: oldOrder.type,
+ side: oldOrder.side,
+ size: orderUpdate.size !== undefined ? orderUpdate.size : oldOrder.size,
+ origSize: oldOrder.origSize,
+ price: orderUpdate.price,
+ time: Date.now(),
+ timeInForce: oldOrder.timeInForce,
+ isMaker: oldOrder.isMaker
+ })
this.append(newOrder)
return newOrder
}
// Update the price of an order and return the order with the updated price
- updateOrderSize = (oldOrder: Order, orderUpdate: OrderUpdateSize): Order => {
+ updateOrderSize = (
+ oldOrder: LimitOrder,
+ orderUpdate: OrderUpdateSize
+ ): LimitOrder => {
const newOrderPrice = orderUpdate.price ?? oldOrder.price
this._volume += orderUpdate.size - oldOrder.size
this._total +=
@@ -154,8 +159,8 @@ export class OrderSide {
}
// returns all orders
- orders = (): Order[] => {
- let orders: Order[] = []
+ orders = (): LimitOrder[] => {
+ let orders: LimitOrder[] = []
for (const price in this._prices) {
const allOrders = this._prices[price].toArray()
orders = orders.concat(allOrders)
diff --git a/src/stopbook.ts b/src/stopbook.ts
new file mode 100644
index 0000000..893cdc0
--- /dev/null
+++ b/src/stopbook.ts
@@ -0,0 +1,79 @@
+import { Side } from './side'
+import { StopQueue } from './stopqueue'
+import { StopSide } from './stopside'
+import { OrderType, StopOrder } from './types'
+
+export class StopBook {
+ private readonly bids: StopSide
+ private readonly asks: StopSide
+
+ constructor () {
+ this.bids = new StopSide(Side.BUY)
+ this.asks = new StopSide(Side.SELL)
+ }
+
+ add = (order: StopOrder): void => {
+ const stopSide = order.side === Side.BUY ? this.bids : this.asks
+ stopSide.append(order)
+ }
+
+ remove = (
+ side: Side,
+ id: string,
+ stopPrice: number
+ ): StopOrder | undefined => {
+ const stopSide = side === Side.BUY ? this.bids : this.asks
+ return stopSide.remove(id, stopPrice)
+ }
+
+ removePriceLevel = (side: Side, priceLevel: number): void => {
+ const stopSide = side === Side.BUY ? this.bids : this.asks
+ return stopSide.removePriceLevel(priceLevel)
+ }
+
+ getConditionalOrders = (
+ side: Side,
+ priceBefore: number,
+ marketPrice: number
+ ): StopQueue[] => {
+ const stopSide = side === Side.BUY ? this.bids : this.asks
+ return stopSide.between(priceBefore, marketPrice)
+ }
+
+ /**
+ * Stop-Limit Order:
+ * Buy: marketPrice < stopPrice <= price
+ * Sell: marketPrice > stopPrice >= price
+ * Stop-Market Order:
+ * Buy: marketPrice < stopPrice
+ * Sell: marketPrice > stopPrice
+ */
+ validConditionalOrder = (marketPrice: number, order: StopOrder): boolean => {
+ let response = false
+ const { type, side, stopPrice } = order
+ if (type === OrderType.STOP_LIMIT) {
+ // Buy: marketPrice < stopPrice <= price
+ if (
+ side === Side.BUY &&
+ marketPrice < stopPrice &&
+ stopPrice <= order.price
+ ) {
+ response = true
+ }
+ // Sell: marketPrice > stopPrice >= price
+ if (
+ side === Side.SELL &&
+ marketPrice > stopPrice &&
+ stopPrice >= order.price
+ ) {
+ response = true
+ }
+ } else {
+ // Buy: marketPrice < stopPrice
+ if (side === Side.BUY && marketPrice < stopPrice) response = true
+ // Sell: marketPrice > stopPrice
+ if (side === Side.SELL && marketPrice > stopPrice) response = true
+ }
+ return response
+ }
+}
diff --git a/src/stopqueue.ts b/src/stopqueue.ts
new file mode 100644
index 0000000..23c64d7
--- /dev/null
+++ b/src/stopqueue.ts
@@ -0,0 +1,55 @@
+import Denque from 'denque'
+import { StopOrder } from './types'
+
+export class StopQueue {
+ private readonly _price: number
+ private readonly _orders: Denque
+ private _ordersMap: { [key: string]: number } = {}
+
+ constructor (price: number) {
+ this._price = price
+ this._orders = new Denque()
+ }
+
+ get price (): number {
+ return this._price
+ }
+
+ // returns the number of orders in queue
+ len = (): number => {
+ return this._orders.length
+ }
+
+ // remove order from head of queue
+ removeFromHead = (): StopOrder | undefined => {
+ // We can't use the shift method here because we need
+ // to update index in the map, so we use the remove(id) function
+ const order = this._orders.peekFront()
+ if (order === undefined) return
+ return this.remove(order.id)
+ }
+
+ // adds order to tail of the queue
+ append = (order: StopOrder): StopOrder => {
+ this._orders.push(order)
+ this._ordersMap[order.id] = this._orders.length - 1
+ return order
+ }
+
+ // removes order from the queue
+ remove = (id: string): StopOrder | undefined => {
+ const deletedOrderIndex = this._ordersMap[id]
+ if (deletedOrderIndex === undefined) return
+
+ const deletedOrder = this._orders.removeOne(deletedOrderIndex)
+ // eslint-disable-next-line
+ delete this._ordersMap[id]
+ // Update all orders indexes where index is greater than the deleted one
+ for (const orderId in this._ordersMap) {
+ if (this._ordersMap[orderId] > deletedOrderIndex) {
+ this._ordersMap[orderId] -= 1
+ }
+ }
+ return deletedOrder
+ }
+}
diff --git a/src/stopside.ts b/src/stopside.ts
new file mode 100644
index 0000000..8676fcb
--- /dev/null
+++ b/src/stopside.ts
@@ -0,0 +1,82 @@
+import createRBTree from 'functional-red-black-tree'
+import { Side } from './side'
+import { StopQueue } from './stopqueue'
+import { StopOrder } from './types'
+import { CustomError, ERROR } from './errors'
+
+export class StopSide {
+ private _priceTree: createRBTree.Tree
+ private _prices: { [key: string]: StopQueue } = {}
+ private readonly _side: Side
+
+ constructor (side: Side) {
+ const compare =
+ side === Side.SELL
+ ? (a: number, b: number) => a - b
+ : (a: number, b: number) => b - a
+ this._priceTree = createRBTree(compare)
+ this._side = side
+ }
+
+ // appends order to definite price level
+ append = (order: StopOrder): StopOrder => {
+ const price = order.stopPrice
+ const strPrice = price.toString()
+ if (this._prices[strPrice] === undefined) {
+ const priceQueue = new StopQueue(price)
+ this._prices[strPrice] = priceQueue
+ this._priceTree = this._priceTree.insert(price, priceQueue)
+ }
+ return this._prices[strPrice].append(order)
+ }
+
+ // removes order from definite price level
+ remove = (id: string, stopPrice: number): StopOrder | undefined => {
+ const strPrice = stopPrice.toString()
+ if (this._prices[strPrice] === undefined) {
+ throw CustomError(ERROR.ErrInvalidPriceLevel)
+ }
+ const deletedOrder = this._prices[strPrice].remove(id)
+ if (this._prices[strPrice].len() === 0) {
+ /* eslint-disable @typescript-eslint/no-dynamic-delete */
+ delete this._prices[strPrice]
+ this._priceTree = this._priceTree.remove(stopPrice)
+ }
+ return deletedOrder
+ }
+
+ removePriceLevel = (priceLevel: number): void => {
+ /* eslint-disable @typescript-eslint/no-dynamic-delete */
+ delete this._prices[priceLevel.toString()]
+ this._priceTree = this._priceTree.remove(priceLevel)
+ }
+
+ // Get orders queue between two price levels
+ between = (priceBefore: number, marketPrice: number): StopQueue[] => {
+ const queues: StopQueue[] = []
+ let lowerBound = priceBefore
+ let upperBound = marketPrice
+ const highest = Math.max(priceBefore, marketPrice)
+ const lowest = Math.min(priceBefore, marketPrice)
+ if (this._side === Side.BUY) {
+ lowerBound = highest
+ upperBound = lowest - 1
+ } else {
+ lowerBound = lowest
+ upperBound = highest + 1
+ }
+ this._priceTree.forEach(
+ (price, queue) => {
+ if (
+ (this._side === Side.BUY && price >= lowest) ||
+ (this._side === Side.SELL && price <= highest)
+ ) {
+ queues.push(queue)
+ }
+ },
+ lowerBound, // Inclusive
+ upperBound // Exclusive (so we add +-1 depending on the side)
+ )
+ return queues
+ }
+}
diff --git a/src/types.ts b/src/types.ts
index 53fc4fa..7c6e057 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,14 +1,172 @@
-import type { Order, TimeInForce } from './order'
+import { LimitOrder, StopLimitOrder, StopMarketOrder } from './order'
import type { Side } from './side'
+export enum OrderType {
+ LIMIT = 'limit',
+ MARKET = 'market',
+ OCO = 'oco',
+ STOP_LIMIT = 'stop_limit',
+ STOP_MARKET = 'stop_market',
+}
+
+export enum TimeInForce {
+ GTC = 'GTC',
+ IOC = 'IOC',
+ FOK = 'FOK',
+}
+
+export type StopOrder = StopLimitOrder | StopMarketOrder
+export type Order = LimitOrder | StopOrder
+
+interface BaseOrderOptions {
+ id?: string
+ side: Side
+ size: number
+}
+interface InternalBaseOrderOptions extends BaseOrderOptions {
+ type: OrderType
+ time?: number
+ origSize?: number
+}
+/**
+ * Specific options for a market order.
+ */
+export interface MarketOrderOptions extends BaseOrderOptions {}
+interface IMarketOrderOptions extends InternalBaseOrderOptions {}
+export interface InternalMarketOrderOptions extends IMarketOrderOptions {
+ type: OrderType.MARKET
+}
+
+/**
+ * Specific options for a limit order.
+ */
+export interface LimitOrderOptions extends MarketOrderOptions {
+ id: string
+ price: number
+ timeInForce?: TimeInForce
+}
+interface ILimitOrderOptions extends InternalBaseOrderOptions {
+ id: string
+ price: number
+ timeInForce: TimeInForce
+ isMaker: boolean
+}
+export interface InternalLimitOrderOptions extends ILimitOrderOptions {
+ type: OrderType.LIMIT
+ ocoStopPrice?: number
+}
+
+/**
+ * Specific options for a stop market order.
+ */
+export interface StopMarketOrderOptions extends MarketOrderOptions {
+ stopPrice: number
+}
+export interface InternalStopMarketOrderOptions extends IMarketOrderOptions {
+ type: OrderType.STOP_MARKET
+ stopPrice: number
+}
+
+/**
+ * Specific options for a stop limit order.
+ */
+export interface StopLimitOrderOptions extends LimitOrderOptions {
+ stopPrice: number
+}
+export interface InternalStopLimitOrderOptions extends ILimitOrderOptions {
+ type: OrderType.STOP_LIMIT
+ stopPrice: number
+ isOCO?: boolean
+}
+
+/**
+ * Specific options for oco order.
+ */
+export interface OCOOrderOptions extends StopLimitOrderOptions {
+ stopPrice: number
+ stopLimitPrice: number
+ stopLimitTimeInForce?: TimeInForce
+}
+
+/**
+ * Object object representation of a market order returned by the toObject() method.
+ */
+export interface IMarketOrder {
+ id: string
+ type: OrderType
+ side: Side
+ size: number
+ origSize: number
+ time: number
+}
+
+/**
+ * Object object representation of a limit order returned by the toObject() method.
+ */
+export interface ILimitOrder {
+ id: string
+ type: OrderType
+ side: Side
+ size: number
+ origSize: number
+ price: number
+ time: number
+ timeInForce: TimeInForce
+ isMaker: boolean
+}
+
+/**
+ * Object object representation of a stop market order returned by the toObject() method.
+ */
+export interface IStopMarketOrder {
+ id: string
+ type: OrderType
+ side: Side
+ size: number
+ origSize: number
+ stopPrice: number
+ time: number
+}
+
+/**
+ * Object object representation of a stop limit order returned by the toObject() method.
+ */
+export interface IStopLimitOrder {
+ id: string
+ type: OrderType
+ side: Side
+ size: number
+ origSize: number
+ price: number
+ stopPrice: number
+ timeInForce: TimeInForce
+ time: number
+ isMaker: boolean
+}
+
+/**
+ * Represents an order in the order book.
+ */
+export type OrderOptions =
+ | InternalMarketOrderOptions
+ | InternalLimitOrderOptions
+ | InternalStopLimitOrderOptions
+ | InternalStopMarketOrderOptions
+
+export type StopOrderOptions =
+ | StopMarketOrderOptions
+ | StopLimitOrderOptions
+ | OCOOrderOptions
/**
* Represents the result of processing an order.
*/
export interface IProcessOrder {
/** Array of fully processed orders. */
done: Order[]
+ /** Array of activated (stop limit or stop market) orders */
+ activated: StopOrder[]
/** The partially processed order, if any. */
- partial: Order | null
+ partial: LimitOrder | null
/** The quantity that has been processed in the partial order. */
partialQuantityProcessed: number
/** The remaining quantity that needs to be processed. */
@@ -19,11 +177,34 @@ export interface IProcessOrder {
log?: JournalLog
}
+export interface ConditionOrderOptions {
+ stopPrice: number
+}
+
+/**
+ * Specific options for modifying an order.
+ */
+export interface ModifyOrderOptions {
+ /** Unique identifier of the order. */
+ orderID: string
+ /** Details of the order update (price or size). */
+ orderUpdate: OrderUpdatePrice | OrderUpdateSize
+}
+
+/**
+ * Specific options for canceling an order.
+ */
+export interface CancelOrderOptions {
+ /** Unique identifier of the order. */
+ orderID: string
+}
+
/**
* Represents a cancel order operation.
*/
export interface ICancelOrder {
- order: Order
+ order: LimitOrder
+ stopOrder?: StopOrder
/** Optional log related to the order cancellation. */
log?: CancelOrderJournalLog
}
@@ -93,49 +274,12 @@ export type JournalLog =
| ModifyOrderJournalLog
| CancelOrderJournalLog
-/**
- * Specific options for a market order.
- */
-export interface MarketOrderOptions {
- /** Side of the order (buy/sell). */
- side: Side
- /** Size of the order. */
- size: number
-}
-
-/**
- * Specific options for a limit order.
- */
-export interface LimitOrderOptions {
- /** Side of the order (buy/sell). */
- side: Side
- /** Unique identifier of the order. */
- orderID: string
- /** Size of the order. */
- size: number
- /** Price of the order. */
- price: number
- /** Time in force policy for the order. */
- timeInForce: TimeInForce
-}
-
-/**
- * Specific options for modifying an order.
- */
-export interface ModifyOrderOptions {
- /** Unique identifier of the order. */
- orderID: string
- /** Details of the order update (price or size). */
- orderUpdate: OrderUpdatePrice | OrderUpdateSize
-}
-
-/**
- * Specific options for canceling an order.
- */
-export interface CancelOrderOptions {
- /** Unique identifier of the order. */
- orderID: string
-}
+export type CreateOrderOptions =
+ | ({ type: OrderType.MARKET } & MarketOrderOptions)
+ | ({ type: OrderType.LIMIT } & LimitOrderOptions)
+ | ({ type: OrderType.STOP_MARKET } & StopMarketOrderOptions)
+ | ({ type: OrderType.STOP_LIMIT } & StopLimitOrderOptions)
+ | ({ type: OrderType.OCO } & OCOOrderOptions)
/**
* Options for configuring the order book.
@@ -150,26 +294,11 @@ export interface OrderBookOptions {
enableJournaling?: boolean
/** Array of journal logs. */
journal?: JournalLog[]
-}
-
-/**
- * Represents an order in the order book.
- */
-export interface IOrder {
- /** Unique identifier of the order. */
- id: string
- /** Side of the order (buy/sell). */
- side: Side
- /** Original size of the order. */
- origSize: number
- /** Size of the order. */
- size: number
- /** Price of the order. */
- price: number
- /** Timestamp of the order. */
- time: number
- /** Flag to indicate if the order is a maker order. */
- isMaker: boolean
+ /**
+ * Flag to enable experimental Conditional Order (Stop Market, Stop Limit and OCO orders).
+ * Default to false
+ */
+ experimentalConditionalOrders?: boolean
}
/**
@@ -201,14 +330,14 @@ export interface Snapshot {
/** Price of the ask order */
price: number
/** List of orders associated with this price */
- orders: Order[]
+ orders: LimitOrder[]
}>
/** List of bid orders, each with a price and a list of associated orders */
bids: Array<{
/** Price of the bid order */
price: number
/** List of orders associated with this price */
- orders: Order[]
+ orders: LimitOrder[]
}>
/** Unix timestamp representing when the snapshot was taken */
ts: number
diff --git a/test/order.test.ts b/test/order.test.ts
index e6aa3c7..04f7a05 100644
--- a/test/order.test.ts
+++ b/test/order.test.ts
@@ -1,123 +1,451 @@
import { test } from 'tap'
-import { Order } from '../src/order'
+import { randomUUID } from 'node:crypto'
+import {
+ LimitOrder,
+ OrderFactory,
+ StopLimitOrder,
+ StopMarketOrder
+} from '../src/order'
import { Side } from '../src/side'
+import { OrderType, TimeInForce } from '../src/types'
+import { ERROR } from '../src/errors'
-void test('it should create order object', ({ equal, same, end }) => {
+void test('it should create LimitOrder', ({ equal, same, end }) => {
const id = 'fakeId'
const side = Side.BUY
+ const type = OrderType.LIMIT
const size = 5
const price = 100
const time = Date.now()
- const order = new Order(id, side, size, price, time)
+ const timeInForce = TimeInForce.IOC
- equal(order instanceof Order, true)
+ {
+ const order = OrderFactory.createOrder({
+ id,
+ type,
+ side,
+ size,
+ price,
+ time,
+ timeInForce,
+ isMaker: false
+ })
+
+ equal(order instanceof LimitOrder, true)
+ equal(order.id, id)
+ equal(order.type, type)
+ equal(order.side, side)
+ equal(order.size, size)
+ equal(order.origSize, size)
+ equal(order.price, price)
+ equal(order.time, time)
+ equal(order.timeInForce, timeInForce)
+ equal(order.isMaker, false)
+ equal(order.ocoStopPrice, undefined)
+ same(order.toObject(), {
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ time,
+ timeInForce,
+ isMaker: order.isMaker
+ })
+ equal(
+ order.toString(),
+ `${id}:
+ type: ${type}
+ side: ${side}
+ size: ${size}
+ origSize: ${size}
+ price: ${price}
+ time: ${time}
+ timeInForce: ${timeInForce}
+ isMaker: ${false as unknown as string}`
+ )
+ equal(
+ order.toJSON(),
+ JSON.stringify({
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ time,
+ timeInForce,
+ isMaker: false
+ })
+ )
+ }
+
+ {
+ // Limit Order with ocoStopPrice
+ const ocoStopPrice = 10
+ const order = OrderFactory.createOrder({
+ id,
+ type,
+ side,
+ size,
+ price,
+ time,
+ timeInForce,
+ isMaker: false,
+ ocoStopPrice
+ })
+ equal(order instanceof LimitOrder, true)
+ equal(order.id, id)
+ equal(order.type, type)
+ equal(order.side, side)
+ equal(order.size, size)
+ equal(order.origSize, size)
+ equal(order.price, price)
+ equal(order.time, time)
+ equal(order.timeInForce, timeInForce)
+ equal(order.isMaker, false)
+ equal(order.ocoStopPrice, ocoStopPrice)
+ same(order.toObject(), {
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ time,
+ timeInForce,
+ isMaker: order.isMaker
+ })
+ equal(
+ order.toString(),
+ `${id}:
+ type: ${type}
+ side: ${side}
+ size: ${size}
+ origSize: ${size}
+ price: ${price}
+ time: ${time}
+ timeInForce: ${timeInForce}
+ isMaker: ${false as unknown as string}`
+ )
+ equal(
+ order.toJSON(),
+ JSON.stringify({
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ time,
+ timeInForce,
+ isMaker: false
+ })
+ )
+ }
+ end()
+})
+
+void test('it should create StopMarketOrder', ({ equal, same, end }) => {
+ const id = 'fakeId'
+ const side = Side.BUY
+ const type = OrderType.STOP_MARKET
+ const size = 5
+ const stopPrice = 4
+ const time = Date.now()
+ const order = OrderFactory.createOrder({
+ id,
+ type,
+ side,
+ size,
+ time,
+ stopPrice
+ })
+
+ equal(order instanceof StopMarketOrder, true)
equal(order.id, id)
+ equal(order.type, type)
equal(order.side, side)
equal(order.size, size)
equal(order.origSize, size)
- equal(order.price, price)
+ equal(order.stopPrice, stopPrice)
equal(order.time, time)
- equal(order.isMaker, false)
same(order.toObject(), {
id,
+ type,
side,
- origSize: size,
size,
- price,
- time,
- isMaker: order.isMaker
+ origSize: size,
+ stopPrice,
+ time
})
equal(
order.toString(),
`${id}:
+ type: ${type}
side: ${side}
- origSize: ${size}
size: ${size}
- price: ${price}
- time: ${time}
- isMaker: ${false as unknown as string}`
+ origSize: ${size}
+ stopPrice: ${stopPrice}
+ time: ${time}`
)
equal(
order.toJSON(),
JSON.stringify({
id,
+ type,
side,
+ size,
origSize: size,
+ stopPrice,
+ time
+ })
+ )
+ end()
+})
+
+void test('it should create StopLimitOrder', ({ equal, same, end }) => {
+ const id = 'fakeId'
+ const side = Side.BUY
+ const type = OrderType.STOP_LIMIT
+ const size = 5
+ const price = 100
+ const stopPrice = 4
+ const time = Date.now()
+ const timeInForce = TimeInForce.IOC
+ {
+ const order = OrderFactory.createOrder({
+ id,
+ type,
+ side,
size,
price,
time,
- isMaker: false
+ stopPrice,
+ timeInForce,
+ isMaker: true
})
- )
+
+ equal(order instanceof StopLimitOrder, true)
+ equal(order.id, id)
+ equal(order.type, type)
+ equal(order.side, side)
+ equal(order.size, size)
+ equal(order.price, price)
+ equal(order.origSize, size)
+ equal(order.stopPrice, stopPrice)
+ equal(order.timeInForce, timeInForce)
+ equal(order.isMaker, true)
+ equal(order.time, time)
+ equal(order.isOCO, false)
+ same(order.toObject(), {
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ stopPrice,
+ timeInForce,
+ time,
+ isMaker: true
+ })
+ equal(
+ order.toString(),
+ `${id}:
+ type: ${type}
+ side: ${side}
+ size: ${size}
+ origSize: ${size}
+ price: ${price}
+ stopPrice: ${stopPrice}
+ timeInForce: ${timeInForce}
+ time: ${time}
+ isMaker: ${true as unknown as string}`
+ )
+ equal(
+ order.toJSON(),
+ JSON.stringify({
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ stopPrice,
+ timeInForce,
+ time,
+ isMaker: true
+ })
+ )
+ // Price setter
+ const newPrice = 120
+ order.price = newPrice
+ equal(order.price, newPrice)
+ }
+
+ {
+ // Stop Limit Order created by OCO order
+ const order = OrderFactory.createOrder({
+ id,
+ type,
+ side,
+ size,
+ price,
+ time,
+ stopPrice,
+ timeInForce,
+ isMaker: true,
+ isOCO: true
+ })
+
+ equal(order instanceof StopLimitOrder, true)
+ equal(order.id, id)
+ equal(order.type, type)
+ equal(order.side, side)
+ equal(order.size, size)
+ equal(order.price, price)
+ equal(order.origSize, size)
+ equal(order.stopPrice, stopPrice)
+ equal(order.timeInForce, timeInForce)
+ equal(order.isMaker, true)
+ equal(order.time, time)
+ equal(order.isOCO, true)
+ same(order.toObject(), {
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ stopPrice,
+ timeInForce,
+ time,
+ isMaker: true
+ })
+ equal(
+ order.toString(),
+ `${id}:
+ type: ${type}
+ side: ${side}
+ size: ${size}
+ origSize: ${size}
+ price: ${price}
+ stopPrice: ${stopPrice}
+ timeInForce: ${timeInForce}
+ time: ${time}
+ isMaker: ${true as unknown as string}`
+ )
+ equal(
+ order.toJSON(),
+ JSON.stringify({
+ id,
+ type,
+ side,
+ size,
+ origSize: size,
+ price,
+ stopPrice,
+ timeInForce,
+ time,
+ isMaker: true
+ })
+ )
+ // Price setter
+ const newPrice = 120
+ order.price = newPrice
+ equal(order.price, newPrice)
+ }
end()
})
-void test('it should create order without passing a date', ({
+void test('it should create order without passing a date or id', ({
equal,
end,
teardown,
same
}) => {
const fakeTimestamp = 1487076708000
+ const fakeId = 'some-uuid'
const { now } = Date
+ const originalRandomUUID = randomUUID
+
teardown(() => (Date.now = now))
+ // @ts-expect-error cannot assign because is readonly
+ // eslint-disable-next-line
+ teardown(() => (randomUUID = originalRandomUUID));
+
Date.now = (...m) => fakeTimestamp
+ // @ts-expect-error cannot assign because is readonly
+ // eslint-disable-next-line
+ randomUUID = () => fakeId;
- const id = 'fakeId'
+ const type = OrderType.STOP_MARKET
const side = Side.BUY
const size = 5
- const price = 100
- const order = new Order(id, side, size, price)
- equal(order instanceof Order, true)
- equal(order.id, id)
- equal(order.side, side)
- equal(order.size, size)
- equal(order.origSize, size)
- equal(order.price, price)
+ const stopPrice = 4
+ const order = OrderFactory.createOrder({
+ type,
+ side,
+ size,
+ stopPrice
+ })
+ equal(order.id, fakeId)
equal(order.time, fakeTimestamp)
- equal(order.isMaker, false)
same(order.toObject(), {
- id,
+ id: fakeId,
+ type,
side,
- origSize: size,
size,
- price,
- time: fakeTimestamp,
- isMaker: false
+ origSize: size,
+ stopPrice,
+ time: fakeTimestamp
})
equal(
order.toString(),
- `${id}:
+ `${fakeId}:
+ type: ${type}
side: ${side}
- origSize: ${size}
size: ${size}
- price: ${price}
- time: ${fakeTimestamp}
- isMaker: ${false as unknown as string}`
+ origSize: ${size}
+ stopPrice: ${stopPrice}
+ time: ${fakeTimestamp}`
)
equal(
order.toJSON(),
JSON.stringify({
- id,
+ id: fakeId,
+ type,
side,
- origSize: size,
size,
- price,
- time: fakeTimestamp,
- isMaker: false
+ origSize: size,
+ stopPrice,
+ time: fakeTimestamp
})
)
end()
})
void test('test orders setters', (t) => {
+ const type = OrderType.LIMIT
const id = 'fakeId'
const side = Side.BUY
const size = 5
const price = 100
const time = Date.now()
- const order = new Order(id, side, size, price, time)
+ const timeInForce = TimeInForce.GTC
+ const order = OrderFactory.createOrder({
+ type,
+ id,
+ side,
+ size,
+ price,
+ time,
+ timeInForce,
+ isMaker: false
+ })
// Price setter
const newPrice = 300
@@ -139,3 +467,31 @@ void test('test orders setters', (t) => {
t.end()
})
+
+void test('test invalid order type', (t) => {
+ try {
+ const id = 'fakeId'
+ const side = Side.BUY
+ const type = 'invalidOrderType'
+ const size = 5
+ const price = 100
+ const time = Date.now()
+ const timeInForce = TimeInForce.IOC
+ OrderFactory.createOrder({
+ id,
+ // @ts-expect-error order type invalid
+ type,
+ side,
+ size,
+ price,
+ time,
+ timeInForce,
+ isMaker: false
+ })
+ } catch (error) {
+ if (error instanceof Error) {
+ t.equal(error?.message, ERROR.ErrInvalidOrderType)
+ }
+ }
+ t.end()
+})
diff --git a/test/orderbook.test.ts b/test/orderbook.test.ts
index 9419fe9..ff89801 100644
--- a/test/orderbook.test.ts
+++ b/test/orderbook.test.ts
@@ -1,10 +1,15 @@
-import { Order, OrderType, TimeInForce } from '../src/order'
import { test } from 'tap'
import { Side } from '../src/side'
import { OrderBook } from '../src/orderbook'
import { ERROR } from '../src/errors'
-import { JournalLog } from '../src/types'
+import {
+ IProcessOrder,
+ JournalLog,
+ OrderType,
+ TimeInForce
+} from '../src/types'
import { OrderQueue } from '../src/orderqueue'
+import { LimitOrder, StopLimitOrder, StopMarketOrder } from '../src/order'
const addDepth = (
ob: OrderBook,
@@ -13,21 +18,21 @@ const addDepth = (
journal?: JournalLog[]
): void => {
for (let index = 50; index < 100; index += 10) {
- const response = ob.limit(
- Side.BUY,
- `${prefix}buy-${index}`,
- quantity,
- index
- )
+ const response = ob.limit({
+ side: Side.BUY,
+ id: `${prefix}buy-${index}`,
+ size: quantity,
+ price: index
+ })
if (journal != null && response.log != null) journal.push(response.log)
}
for (let index = 100; index < 150; index += 10) {
- const response = ob.limit(
- Side.SELL,
- `${prefix}sell-${index}`,
- quantity,
- index
- )
+ const response = ob.limit({
+ side: Side.SELL,
+ id: `${prefix}sell-${index}`,
+ size: quantity,
+ price: index
+ })
if (journal != null && response.log != null) journal.push(response.log)
}
}
@@ -46,12 +51,12 @@ void test('test limit place', ({ equal, end }) => {
const ob = new OrderBook()
const size = 2
for (let index = 50; index < 100; index += 10) {
- const { done, partial, partialQuantityProcessed, err } = ob.limit(
- Side.BUY,
- `buy-${index}`,
+ const { done, partial, partialQuantityProcessed, err } = ob.limit({
+ side: Side.BUY,
+ id: `buy-${index}`,
size,
- index
- )
+ price: index
+ })
equal(done.length, 0)
equal(partial === null, true)
equal(partialQuantityProcessed, 0)
@@ -59,12 +64,12 @@ void test('test limit place', ({ equal, end }) => {
}
for (let index = 100; index < 150; index += 10) {
- const { done, partial, partialQuantityProcessed, err } = ob.limit(
- Side.SELL,
- `sell-${index}`,
+ const { done, partial, partialQuantityProcessed, err } = ob.limit({
+ side: Side.SELL,
+ id: `sell-${index}`,
size,
- index
- )
+ price: index
+ })
equal(done.length, 0)
equal(partial === null, true)
equal(partialQuantityProcessed, 0)
@@ -72,7 +77,7 @@ void test('test limit place', ({ equal, end }) => {
}
equal(ob.order('fake') === undefined, true)
- equal(ob.order('sell-100') instanceof Order, true)
+ equal(ob.order('sell-100') instanceof LimitOrder, true)
const depth = ob.depth()
@@ -90,81 +95,111 @@ void test('test limit', ({ equal, end }) => {
const ob = new OrderBook()
addDepth(ob, '', 2)
-
+ equal(ob.marketPrice, 0)
const process1 =
// { done, partial, partialQuantityProcessed, quantityLeft, err }
- ob.limit(Side.BUY, 'order-b100', 1, 100)
+ ob.limit({ side: Side.BUY, id: 'order-b100', size: 1, price: 100 })
+
+ equal(ob.marketPrice, 100)
equal(process1.err === null, true)
equal(process1.done[0].id, 'order-b100')
equal(process1.partial?.id, 'sell-100')
- equal(process1.partial?.isMaker, true)
+ equal((process1.partial as LimitOrder)?.isMaker, true)
equal(process1.partialQuantityProcessed, 1)
const process2 =
// { done, partial, partialQuantityProcessed, quantityLeft, err } =
- ob.limit(Side.BUY, 'order-b150', 10, 150)
-
+ ob.limit({ side: Side.BUY, id: 'order-b150', size: 10, price: 150 })
equal(process2.err === null, true)
equal(process2.done.length, 5)
equal(process2.partial?.id, 'order-b150')
- equal(process2.partial?.isMaker, true)
+ equal((process2.partial as LimitOrder)?.isMaker, true)
equal(process2.partialQuantityProcessed, 9)
- const process3 = ob.limit(Side.SELL, 'buy-70', 11, 40)
+ const process3 = ob.limit({
+ side: Side.SELL,
+ id: 'buy-70',
+ size: 11,
+ price: 40
+ })
equal(process3.err?.message, ERROR.ErrOrderExists)
- const process4 = ob.limit(Side.SELL, 'fake-70', 0, 40)
+ const process4 = ob.limit({
+ side: Side.SELL,
+ id: 'fake-70',
+ size: 0,
+ price: 40
+ })
equal(process4.err?.message, ERROR.ErrInvalidQuantity)
- // @ts-expect-error
- const process5 = ob.limit('unsupported-side', 'order-70', 70, 100)
+ const process5 = ob.limit({
+ // @ts-expect-error invalid side
+ side: 'unsupported-side',
+ id: 'order-70',
+ size: 70,
+ price: 100
+ })
equal(process5.err?.message, ERROR.ErrInvalidSide)
-
const removed = ob.cancel('order-b100')
equal(removed === undefined, true)
-
// Test also the createOrder method
- const process6 = ob.createOrder(
- OrderType.LIMIT,
- Side.SELL,
- 11,
- 40,
- 'order-s40',
- TimeInForce.GTC
- )
+ const process6 = ob.createOrder({
+ type: OrderType.LIMIT,
+ side: Side.SELL,
+ size: 11,
+ price: 40,
+ id: 'order-s40',
+ timeInForce: TimeInForce.GTC
+ })
+ equal(ob.marketPrice, 50)
equal(process6.err === null, true)
equal(process6.done.length, 7)
equal(process6.partial === null, true)
equal(process6.partialQuantityProcessed, 0)
- // @ts-expect-error
- const process7 = ob.limit(Side.SELL, 'fake-wrong-size', '0', 40)
+ const process7 = ob.limit({
+ side: Side.SELL,
+ id: 'fake-wrong-size',
+ // @ts-expect-error size must be a number
+ size: '0',
+ price: 40
+ })
equal(process7.err?.message, ERROR.ErrInvalidQuantity)
- const process8 = ob.limit(
- Side.SELL,
- 'fake-wrong-size',
- // @ts-expect-error
- null,
- 40
- )
+ const process8 = ob.limit({
+ side: Side.SELL,
+ id: 'fake-wrong-size',
+ // @ts-expect-error size must be a number
+ size: null,
+ price: 40
+ })
equal(process8.err?.message, ERROR.ErrInvalidQuantity)
- const process9 = ob.limit(
- Side.SELL,
- 'fake-wrong-price',
- 10,
- // @ts-expect-error
- '40'
- )
+ const process9 = ob.limit({
+ side: Side.SELL,
+ id: 'fake-wrong-price',
+ size: 10,
+ // @ts-expect-error price must be a number
+ price: '40'
+ })
equal(process9.err?.message, ERROR.ErrInvalidPrice)
- // @ts-expect-error
- const process10 = ob.limit(Side.SELL, 'fake-without-price', 10)
+ // @ts-expect-error missing price
+ const process10 = ob.limit({
+ side: Side.SELL,
+ id: 'fake-without-price',
+ size: 10
+ })
equal(process10.err?.message, ERROR.ErrInvalidPrice)
- // @ts-expect-error
- const process11 = ob.limit(Side.SELL, 'unsupported-tif', 10, 10, 'FAKE')
+ const process11 = ob.limit({
+ side: Side.SELL,
+ id: 'unsupported-tif',
+ size: 10,
+ price: 10,
+ // @ts-expect-error invalid time in force
+ timeInForce: 'FAKE'
+ })
equal(process11.err?.message, ERROR.ErrInvalidTimeInForce)
end()
})
@@ -172,67 +207,79 @@ void test('test limit', ({ equal, end }) => {
void test('test limit FOK and IOC', ({ equal, end }) => {
const ob = new OrderBook()
addDepth(ob, '', 2)
- const process1 = ob.limit(
- Side.BUY,
- 'order-fok-b100',
- 3,
- 100,
- TimeInForce.FOK
- )
+ const process1 = ob.limit({
+ side: Side.BUY,
+ id: 'order-fok-b100',
+ size: 3,
+ price: 100,
+ timeInForce: TimeInForce.FOK
+ })
equal(process1.err?.message, ERROR.ErrLimitFOKNotFillable)
- const process2 = ob.limit(Side.SELL, 'order-fok-s90', 3, 90, TimeInForce.FOK)
+ const process2 = ob.limit({
+ side: Side.SELL,
+ id: 'order-fok-s90',
+ size: 3,
+ price: 90,
+ timeInForce: TimeInForce.FOK
+ })
equal(process2.err?.message, ERROR.ErrLimitFOKNotFillable)
- const process3 = ob.limit(
- Side.BUY,
- 'buy-order-size-greather-than-order-side-volume',
- 30,
- 100,
- TimeInForce.FOK
- )
+ const process3 = ob.limit({
+ side: Side.BUY,
+ id: 'buy-order-size-greather-than-order-side-volume',
+ size: 30,
+ price: 100,
+ timeInForce: TimeInForce.FOK
+ })
equal(process3.err?.message, ERROR.ErrLimitFOKNotFillable)
- const process4 = ob.limit(
- Side.SELL,
- 'sell-order-size-greather-than-order-side-volume',
- 30,
- 90,
- TimeInForce.FOK
- )
+ const process4 = ob.limit({
+ side: Side.SELL,
+ id: 'sell-order-size-greather-than-order-side-volume',
+ size: 30,
+ price: 90,
+ timeInForce: TimeInForce.FOK
+ })
equal(process4.err?.message, ERROR.ErrLimitFOKNotFillable)
- ob.limit(Side.BUY, 'order-ioc-b100', 3, 100, TimeInForce.IOC)
+ ob.limit({
+ side: Side.BUY,
+ id: 'order-ioc-b100',
+ size: 3,
+ price: 100,
+ timeInForce: TimeInForce.IOC
+ })
equal(ob.order('order-ioc-b100') === undefined, true)
- const processIOC = ob.limit(
- Side.SELL,
- 'order-ioc-s90',
- 3,
- 90,
- TimeInForce.IOC
- )
+ const processIOC = ob.limit({
+ side: Side.SELL,
+ id: 'order-ioc-s90',
+ size: 3,
+ price: 90,
+ timeInForce: TimeInForce.IOC
+ })
equal(ob.order('order-ioc-s90') === undefined, true)
equal(processIOC.partial?.id, 'order-ioc-s90')
- const processFOKBuy = ob.limit(
- Side.BUY,
- 'order-fok-b110',
- 2,
- 120,
- TimeInForce.FOK
- )
+ const processFOKBuy = ob.limit({
+ side: Side.BUY,
+ id: 'order-fok-b110',
+ size: 2,
+ price: 120,
+ timeInForce: TimeInForce.FOK
+ })
equal(processFOKBuy.err === null, true)
equal(processFOKBuy.quantityLeft, 0)
- const processFOKSell = ob.limit(
- Side.SELL,
- 'order-fok-sell-4-70',
- 4,
- 70,
- TimeInForce.FOK
- )
+ const processFOKSell = ob.limit({
+ side: Side.SELL,
+ id: 'order-fok-sell-4-70',
+ size: 4,
+ price: 70,
+ timeInForce: TimeInForce.FOK
+ })
equal(processFOKSell.err === null, true)
equal(processFOKSell.quantityLeft, 0)
end()
@@ -245,7 +292,7 @@ void test('test market', ({ equal, end }) => {
const process1 =
// { done, partial, partialQuantityProcessed, quantityLeft, err }
- ob.market(Side.BUY, 3)
+ ob.market({ side: Side.BUY, size: 3 })
equal(process1.err === null, true)
equal(process1.quantityLeft, 0)
@@ -254,7 +301,7 @@ void test('test market', ({ equal, end }) => {
// Test also the createOrder method
const process3 =
// { done, partial, partialQuantityProcessed, quantityLeft, err } =
- ob.createOrder(OrderType.MARKET, Side.SELL, 12)
+ ob.createOrder({ type: OrderType.MARKET, side: Side.SELL, size: 12 })
equal(process3.done.length, 5)
equal(process3.err === null, true)
@@ -262,16 +309,16 @@ void test('test market', ({ equal, end }) => {
equal(process3.partialQuantityProcessed, 0)
equal(process3.quantityLeft, 2)
- // @ts-expect-error
- const process4 = ob.market(Side.SELL, '0')
+ // @ts-expect-error size must be a number
+ const process4 = ob.market({ side: Side.SELL, size: '0' })
equal(process4.err?.message, ERROR.ErrInsufficientQuantity)
- // @ts-expect-error
- const process5 = ob.market(Side.SELL)
+ // @ts-expect-error missing size
+ const process5 = ob.market({ side: Side.SELL })
equal(process5.err?.message, ERROR.ErrInsufficientQuantity)
- // @ts-expect-error
- const process6 = ob.market('unsupported-side', 100)
+ // @ts-expect-error invalid side
+ const process6 = ob.market({ side: 'unsupported-side', size: 100 })
equal(process6.err?.message, ERROR.ErrInvalidSide)
end()
})
@@ -279,9 +326,500 @@ void test('test market', ({ equal, end }) => {
void test('createOrder error', ({ equal, end }) => {
const ob = new OrderBook()
addDepth(ob, '', 2)
- // @ts-expect-error
- const result = ob.createOrder('wrong-market-type', Side.SELL, 10)
+ const result = ob.createOrder({
+ // @ts-expect-error invalid order type
+ type: 'wrong-market-type',
+ side: Side.SELL,
+ size: 10
+ })
equal(result.err?.message, ERROR.ErrInvalidOrderType)
+
+ // Added for testing with default timeOnForce when not provided
+ const process1 = ob.createOrder({
+ type: OrderType.LIMIT,
+ id: 'buy-1-at-90',
+ side: Side.BUY,
+ size: 1,
+ price: 90
+ })
+ equal(process1.done.length, 0)
+ equal(process1.partial, null)
+ equal(process1.partialQuantityProcessed, 0)
+ equal(process1.quantityLeft, 1)
+ equal(process1.err, null)
+ end()
+})
+
+/**
+ * Stop-Market Order:
+ * Buy: marketPrice < stopPrice
+ * Sell: marketPrice > stopPrice
+ */
+void test('test stop_market order', ({ equal, end }) => {
+ const ob = new OrderBook({ experimentalConditionalOrders: true })
+
+ addDepth(ob, '', 2)
+ // We need to create at least on maket order in order to set
+ // the market price
+ ob.market({ side: Side.BUY, size: 3 })
+ equal(ob.marketPrice, 110)
+
+ {
+ // Test stop market BUY wrong stopPrice
+ const wrongStopPrice = ob.stopMarket({
+ side: Side.BUY,
+ size: 1,
+ stopPrice: ob.marketPrice - 10
+ }) // Below market price
+ equal(wrongStopPrice.err?.message, ERROR.ErrInvalidConditionalOrder)
+ const wrongStopPrice2 = ob.stopMarket({
+ side: Side.BUY,
+ size: 1,
+ stopPrice: ob.marketPrice
+ }) // Same as market price
+ equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder)
+ const wrongOtherOrderOption1 = ob.stopMarket({
+ // @ts-expect-error invalid side
+ side: 'wrong-side',
+ size: 1
+ })
+ equal(wrongOtherOrderOption1.err != null, true)
+
+ // @ts-expect-error size must be greather than 0
+ const wrongOtherOrderOption2 = ob.stopMarket({ side: Side.BUY, size: 0 })
+ equal(wrongOtherOrderOption2.err != null, true)
+
+ // Add a stop market BUY order
+ const beforeMarketPrice = ob.marketPrice
+ const stopPrice = 120
+ const size = 1
+ const stopMarketBuy = ob.stopMarket({ side: Side.BUY, size, stopPrice })
+
+ // Market price should be the same as before
+ equal(ob.marketPrice, beforeMarketPrice)
+ equal(stopMarketBuy.done[0] instanceof StopMarketOrder, true)
+ equal(stopMarketBuy.quantityLeft, size)
+ equal(stopMarketBuy.err, null)
+ const stopOrder = stopMarketBuy.done[0].toObject() as StopMarketOrder
+ equal(stopOrder.stopPrice, stopPrice)
+
+ // Create a market order that activate the stop order
+ const resp = ob.market({ side: Side.BUY, size: 2 })
+ equal(resp.activated[0] instanceof StopMarketOrder, true)
+ equal(resp.activated[0].id, stopOrder.id)
+ equal(resp.done.length, 2)
+ equal(resp.partial, null)
+ equal(resp.err, null)
+ }
+
+ {
+ // Add a stop market SELL order
+ // Test stop market BUY wrong stopPrice
+ const wrongStopPrice = ob.stopMarket({
+ side: Side.SELL,
+ size: 1,
+ stopPrice: ob.marketPrice + 10
+ }) // Above market price
+ equal(wrongStopPrice.err?.message, ERROR.ErrInvalidConditionalOrder)
+ const wrongStopPrice2 = ob.stopMarket({
+ side: Side.SELL,
+ size: 1,
+ stopPrice: ob.marketPrice
+ }) // Same as market price
+ equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder)
+
+ // Add a stop market SELL order
+ const beforeMarketPrice = ob.marketPrice
+ const stopPrice = 100
+ const size = 2
+ const stopMarketSell = ob.stopMarket({
+ side: Side.SELL,
+ size,
+ stopPrice
+ })
+
+ // Market price should be the same as before
+ equal(ob.marketPrice, beforeMarketPrice)
+ equal(stopMarketSell.done[0] instanceof StopMarketOrder, true)
+ equal(stopMarketSell.quantityLeft, size)
+ equal(stopMarketSell.err, null)
+ const stopOrder = stopMarketSell.done[0].toObject() as StopMarketOrder
+ equal(stopOrder.stopPrice, stopPrice)
+
+ // Create a market order that activate the stop order
+ const resp = ob.market({ side: Side.SELL, size: 2 })
+ equal(resp.activated[0] instanceof StopMarketOrder, true)
+ equal(resp.activated[0].id, stopOrder.id)
+ equal(resp.done.length, 2)
+ equal(resp.partial, null)
+ equal(resp.err, null)
+ }
+
+ {
+ // Use the createOrder method to create a stop order
+ const stopOrder = ob.createOrder({
+ type: OrderType.STOP_MARKET,
+ side: Side.SELL,
+ size: 2,
+ stopPrice: ob.marketPrice - 10
+ })
+ equal(stopOrder.done[0] instanceof StopMarketOrder, true)
+ equal(stopOrder.err, null)
+ equal(stopOrder.quantityLeft, 2)
+ }
+
+ end()
+})
+
+/**
+ * Stop-Limit Order:
+ * Buy: marketPrice < stopPrice <= price
+ * Sell: marketPrice > stopPrice >= price
+ */
+void test('test stop_limit order', ({ equal, end }) => {
+ const ob = new OrderBook({ experimentalConditionalOrders: true })
+
+ addDepth(ob, '', 2)
+ // We need to create at least on maket order in order to set
+ // the market price
+ ob.market({ side: Side.BUY, size: 3 })
+ equal(ob.marketPrice, 110)
+
+ {
+ // Test stop limit BUY wrong stopPrice
+ const wrongStopPrice = ob.stopLimit({
+ id: 'fake-id',
+ side: Side.BUY,
+ size: 1,
+ stopPrice: ob.marketPrice - 10, // Below market price
+ price: ob.marketPrice
+ })
+ equal(wrongStopPrice.err?.message, ERROR.ErrInvalidConditionalOrder)
+ const wrongStopPrice2 = ob.stopLimit({
+ id: 'fake-id',
+ side: Side.BUY,
+ size: 1,
+ stopPrice: ob.marketPrice,
+ price: ob.marketPrice
+ }) // Same as market price
+ equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder)
+ const wrongOtherOrderOption1 = ob.stopLimit({
+ // @ts-expect-error invalid side
+ side: 'wrong-side',
+ size: 1,
+ price: 10
+ })
+ equal(wrongOtherOrderOption1.err != null, true)
+
+ // @ts-expect-error size must be greather than 0
+ const wrongOtherOrderOption2 = ob.stopLimit({
+ side: Side.BUY,
+ size: 0,
+ price: 10
+ })
+ equal(wrongOtherOrderOption2.err != null, true)
+
+ // @ts-expect-error price must be greather than 0
+ const wrongOtherOrderOption3 = ob.stopLimit({
+ side: Side.BUY,
+ size: 1,
+ price: 0
+ })
+ equal(wrongOtherOrderOption3.err != null, true)
+
+ // Add a stop limit BUY order
+ const beforeMarketPrice = ob.marketPrice
+ const stopPrice = 120
+ const price = 130
+ const size = 1
+ const stopLimitBuy = ob.stopLimit({
+ id: 'stop-limit-buy-1',
+ side: Side.BUY,
+ size,
+ stopPrice,
+ price,
+ timeInForce: TimeInForce.IOC
+ })
+
+ // Market price should be the same as before
+ equal(ob.marketPrice, beforeMarketPrice)
+ equal(stopLimitBuy.done[0] instanceof StopLimitOrder, true)
+ equal(stopLimitBuy.quantityLeft, size)
+ equal(stopLimitBuy.err, null)
+ const stopOrder = stopLimitBuy.done[0].toObject() as StopLimitOrder
+ equal(stopOrder.stopPrice, stopPrice)
+ equal(stopOrder.price, price)
+ equal(stopOrder.timeInForce, TimeInForce.IOC)
+
+ // Create a market order that activate the stop order
+ const resp = ob.market({ side: Side.BUY, size: 6 })
+ equal(resp.activated[0] instanceof StopLimitOrder, true)
+ equal(resp.activated[0].id, stopOrder.id)
+ equal(resp.done.length, 3)
+ // The stop order becomes a LimitOrder
+ equal(resp.partial instanceof LimitOrder, true)
+ equal(resp.partial?.id, stopOrder.id)
+ equal(resp.err, null)
+ }
+
+ // addDepth(ob, 'second-run-', 2)
+ ob.market({ side: Side.SELL, size: 1 })
+
+ {
+ // Test stop limit SELL wrong stopPrice
+ const wrongStopPrice = ob.stopLimit({
+ id: 'fake-id',
+ side: Side.SELL,
+ size: 1,
+ stopPrice: ob.marketPrice + 10, // Above market price
+ price: ob.marketPrice
+ })
+ equal(wrongStopPrice.err?.message, ERROR.ErrInvalidConditionalOrder)
+ const wrongStopPrice2 = ob.stopLimit({
+ id: 'fake-id',
+ side: Side.SELL,
+ size: 1,
+ stopPrice: ob.marketPrice,
+ price: ob.marketPrice
+ }) // Same as market price
+ equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder)
+
+ // Add a stop limit BUY order
+ const beforeMarketPrice = ob.marketPrice
+ const stopPrice = 80
+ const price = 70
+ const size = 1
+ const stopLimitSell = ob.stopLimit({
+ id: 'stop-limit-sell-1',
+ side: Side.SELL,
+ size,
+ stopPrice,
+ price
+ })
+
+ // Market price should be the same as before
+ equal(ob.marketPrice, beforeMarketPrice)
+ equal(stopLimitSell.done[0] instanceof StopLimitOrder, true)
+ equal(stopLimitSell.quantityLeft, size)
+ equal(stopLimitSell.err, null)
+ const stopOrder = stopLimitSell.done[0].toObject() as StopLimitOrder
+ equal(stopOrder.stopPrice, stopPrice)
+ equal(stopOrder.price, price)
+ equal(stopOrder.timeInForce, TimeInForce.GTC)
+
+ // Create a market order that activate the stop order
+ const resp = ob.market({ side: Side.SELL, size: 6 })
+ equal(resp.activated[0] instanceof StopLimitOrder, true)
+ equal(resp.activated[0].id, stopOrder.id)
+ equal(resp.done.length, 3)
+ // The stop order becomes a LimitOrder
+ equal(resp.partial instanceof LimitOrder, true)
+ equal(resp.partial?.id, stopOrder.id)
+ equal(resp.err, null)
+ }
+
+ {
+ // Use the createOrder method to create a stop order
+ const stopOrder = ob.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'some-order-id',
+ side: Side.SELL,
+ size: 2,
+ stopPrice: ob.marketPrice - 10,
+ price: ob.marketPrice - 10
+ })
+ equal(stopOrder.done[0] instanceof StopLimitOrder, true)
+ equal(stopOrder.err, null)
+ equal(stopOrder.quantityLeft, 2)
+ }
+
+ end()
+})
+
+/**
+ * OCO Order:
+ * Buy: price < marketPrice < stopPrice
+ * Sell: price > marketPrice > stopPrice
+ */
+void test('test oco order', ({ equal, end }) => {
+ const ob = new OrderBook({ experimentalConditionalOrders: true })
+
+ addDepth(ob, '', 2)
+ // We need to create at least on maket order in order to set
+ // the market price
+ ob.market({ side: Side.BUY, size: 3, id: 'che-cazz' })
+ equal(ob.marketPrice, 110)
+
+ const validate = (
+ orderId: string,
+ side: Side,
+ price: number,
+ stopPrice: number,
+ stopLimitPrice: number,
+ expect: boolean | ERROR | ((response: IProcessOrder) => void)
+ ): void => {
+ const order = ob.oco({
+ id: orderId,
+ side,
+ size: 1,
+ price,
+ stopPrice,
+ stopLimitPrice,
+ stopLimitTimeInForce: TimeInForce.GTC
+ })
+ if (typeof expect === 'function') {
+ expect(order)
+ } else {
+ const toValidate =
+ typeof expect === 'boolean' ? order : order.err?.message
+ equal(toValidate, expect)
+ }
+ }
+
+ // Test OCO Buy
+ // wrong stopPrice
+ validate(
+ 'fake-id',
+ Side.BUY,
+ ob.marketPrice - 10,
+ ob.marketPrice - 10,
+ ob.marketPrice,
+ ERROR.ErrInvalidConditionalOrder
+ )
+ // wrong price
+ validate(
+ 'fake-id',
+ Side.BUY,
+ ob.marketPrice + 10,
+ ob.marketPrice + 10,
+ ob.marketPrice,
+ ERROR.ErrInvalidConditionalOrder
+ )
+
+ // Here marketPrice is 110, lowest sell is 110 and highest buy is 90
+ // valid OCO with limit to 100 and stopLimit to 120
+ validate('oco-buy-1', Side.BUY, 100, 120, 121, (response) => {
+ const order = response.done[0] as StopLimitOrder
+ equal(order instanceof StopLimitOrder, true)
+ equal(order.stopPrice === 120, true)
+ equal(order.price === 121, true)
+ equal(order.isOCO, true)
+ // The limit oco must be the only one inserted in the price level 100
+ // @ts-expect-error bids is private
+ equal(ob.bids.maxPriceQueue()?.price(), 100)
+ // @ts-expect-error bids is private
+ equal(ob.bids.maxPriceQueue()?.tail()?.id, 'oco-buy-1')
+ })
+
+ // Here marketPrice is 110, lowest sell is 110 and highest buy is 90
+ // valid OCO with limit to 100 and stopLimit to 120
+ validate('oco-buy-2', Side.BUY, 100, 120, 121, (response) => {
+ const order = response.done[0] as StopLimitOrder
+ equal(order instanceof StopLimitOrder, true)
+ equal(order.stopPrice === 120, true)
+ equal(order.price === 121, true)
+ equal(order.isOCO, true)
+ // The limit oco must be the only one inserted in the price level 100
+ // @ts-expect-error bids is private
+ equal(ob.bids.maxPriceQueue()?.price(), 100)
+ // @ts-expect-error bids is private
+ equal(ob.bids.maxPriceQueue()?.tail()?.id, 'oco-buy-2')
+ })
+
+ // Test OCO Sell
+ // wrong stopPrice
+ validate(
+ 'fake-id',
+ Side.SELL,
+ ob.marketPrice + 10,
+ ob.marketPrice + 10,
+ ob.marketPrice,
+ ERROR.ErrInvalidConditionalOrder
+ )
+ // wrong price
+ validate(
+ 'fake-id',
+ Side.SELL,
+ ob.marketPrice - 10,
+ ob.marketPrice - 10,
+ ob.marketPrice,
+ ERROR.ErrInvalidConditionalOrder
+ )
+
+ // Here marketPrice is 110, lowest sell is 110 and highest buy is 100
+ // valid OCO with limit to 120 and stopLimit to 100
+ validate('oco-sell-1', Side.SELL, 120, 100, 99, (response) => {
+ const order = response.done[0] as StopLimitOrder
+ equal(order instanceof StopLimitOrder, true)
+ equal(order.stopPrice === 100, true)
+ equal(order.price === 99, true)
+ equal(order.isOCO, true)
+ // The limit oco must be in the tail of the price level 120
+ // @ts-expect-error bids is private
+ equal(ob.asks._prices[120].tail()?.id === 'oco-sell-1', true)
+ })
+
+ // Here marketPrice is 110, lowest sell is 110 and highest buy is 90
+ // valid OCO with limit to 120 and stopLimit to 100
+ validate('oco-sell-2', Side.SELL, 120, 100, 99, (response) => {
+ const order = response.done[0] as StopLimitOrder
+ equal(order instanceof StopLimitOrder, true)
+ equal(order.stopPrice === 100, true)
+ equal(order.price === 99, true)
+ equal(order.isOCO, true)
+ // The limit oco must be in the tail of the price level 120
+ // @ts-expect-error bids is private
+ equal(ob.asks._prices[120].tail()?.id === 'oco-sell-2', true)
+ })
+
+ // Removing the limit order should remove also the stop limit
+ const response = ob.cancel('oco-sell-2')
+ equal(response?.order.id, 'oco-sell-2')
+ equal(response?.stopOrder?.id, 'oco-sell-2')
+
+ // Recreate the same OCO with the createOrder method
+ {
+ const response = ob.createOrder({
+ id: 'oco-sell-2',
+ type: OrderType.OCO,
+ size: 1,
+ side: Side.SELL,
+ price: 120,
+ stopPrice: 100,
+ stopLimitPrice: 99
+ })
+ const order = response.done[0] as StopLimitOrder
+ equal(order instanceof StopLimitOrder, true)
+ equal(order.stopPrice === 100, true)
+ equal(order.price === 99, true)
+ equal(order.isOCO, true)
+ // The limit oco must be in the tail of the price level 120
+ // @ts-expect-error bids is private
+ equal(ob.asks._prices[120].tail()?.id === 'oco-sell-2', true)
+ }
+
+ {
+ const response = ob.market({ side: Side.SELL, size: 1 })
+ // market order match against the limit order oco-buy-1 and activate the two stop limit
+ // orders of the oco sell.
+ equal(response.done[0]?.id === 'oco-buy-1', true)
+ equal(response.activated[0]?.id === 'oco-sell-1', true)
+ equal(response.activated[1]?.id === 'oco-sell-2', true)
+
+ // The first stop limit oco-sell-1 match against the limit oco-buy-2
+ equal(response.done[1]?.id === 'oco-buy-2', true)
+ equal(response.done[2]?.id === 'oco-sell-1', true)
+
+ // While the second stop limit oco-sell-2 go to the order book
+ equal(response.partial?._id === 'oco-sell-2', true)
+ equal(response.partialQuantityProcessed, 0)
+
+ // Both the side of the stop book must be empty
+ // @ts-expect-error stopBook is private
+ equal(ob.stopBook.asks._priceTree.length, 0)
+ // @ts-expect-error stopBook is private
+ equal(ob.stopBook.bids._priceTree.length, 0)
+ }
end()
})
@@ -294,8 +832,18 @@ void test('test modify', ({ equal, end }) => {
const initialSize1 = 1000
const initialPrice2 = 200
const initialSize2 = 1000
- ob.limit(Side.BUY, 'first-order', initialSize1, initialPrice1)
- ob.limit(Side.SELL, 'second-order', initialSize2, initialPrice2)
+ ob.limit({
+ side: Side.BUY,
+ id: 'first-order',
+ size: initialSize1,
+ price: initialPrice1
+ })
+ ob.limit({
+ side: Side.SELL,
+ id: 'second-order',
+ size: initialSize2,
+ price: initialPrice2
+ })
{
// SIDE BUY
@@ -327,7 +875,7 @@ void test('test modify', ({ equal, end }) => {
equal(response?.err?.message, ERROR.ErrInvalidPriceOrQuantity)
// Test modify without passing size and price
- // @ts-expect-error
+ // @ts-expect-error missing size and/or price
response = ob.modify('first-order')
equal(response?.err?.message, ERROR.ErrInvalidPriceOrQuantity)
@@ -343,7 +891,9 @@ void test('test modify', ({ equal, end }) => {
const bookOrdersSize = ob.asks._priceTree.values
.filter((queue) => queue.price() <= 130)
.map((queue) =>
- queue.toArray().reduce((acc: number, curr: Order) => acc + curr.size, 0)
+ queue
+ .toArray()
+ .reduce((acc: number, curr: LimitOrder) => acc + curr.size, 0)
)
.reduce((acc: number, curr: number) => acc + curr, 0)
@@ -390,7 +940,7 @@ void test('test modify', ({ equal, end }) => {
equal(response?.err?.message, ERROR.ErrInvalidPriceOrQuantity)
// Test modify without passing size and price
- // @ts-expect-error
+ // @ts-expect-error missing size and/or price
response = ob.modify('second-order')
equal(response?.err?.message, ERROR.ErrInvalidPriceOrQuantity)
@@ -406,7 +956,9 @@ void test('test modify', ({ equal, end }) => {
const bookOrdersSize = ob.bids._priceTree.values
.filter((queue) => queue.price() >= 80)
.map((queue) =>
- queue.toArray().reduce((acc: number, curr: Order) => acc + curr.size, 0)
+ queue
+ .toArray()
+ .reduce((acc: number, curr: LimitOrder) => acc + curr.size, 0)
)
.reduce((acc: number, curr: number) => acc + curr, 0)
@@ -460,13 +1012,18 @@ void test('orderbook enableJournaling option', ({ equal, end, same }) => {
const ob = new OrderBook({ enableJournaling: true })
{
- const response = ob.limit(Side.BUY, 'first-order', 50, 100)
+ const response = ob.limit({
+ side: Side.BUY,
+ id: 'first-order',
+ size: 50,
+ price: 100
+ })
equal(response.log?.opId, 1)
equal(typeof response.log?.ts, 'number')
equal(response.log?.op, 'l')
same(response.log?.o, {
side: Side.BUY,
- orderID: 'first-order',
+ id: 'first-order',
size: 50,
price: 100,
timeInForce: TimeInForce.GTC
@@ -474,7 +1031,7 @@ void test('orderbook enableJournaling option', ({ equal, end, same }) => {
}
{
- const response = ob.market(Side.BUY, 50)
+ const response = ob.market({ side: Side.BUY, size: 50 })
equal(response.log?.opId, 2)
equal(typeof response.log?.ts, 'number')
equal(response.log?.op, 'm')
@@ -517,13 +1074,18 @@ void test('orderbook replayJournal', ({ equal, end }) => {
{
// Add Market Order
- const response = ob.market(Side.BUY, 3)
+ const response = ob.market({ side: Side.BUY, size: 3 })
if (response.log != null) journal.push(response.log)
}
{
// Add Limit Order, modify and delete the order
- const response = ob.limit(Side.BUY, 'limit-order-b100', 1, 100)
+ const response = ob.limit({
+ side: Side.BUY,
+ id: 'limit-order-b100',
+ size: 1,
+ price: 100
+ })
if (response.log != null) journal.push(response.log)
const modifyOrder = ob.modify('limit-order-b100', { size: 2 })
if (modifyOrder.log != null) journal.push(modifyOrder.log)
@@ -566,7 +1128,7 @@ void test('orderbook replayJournal test wrong journal', ({ equal, end }) => {
o: { foo: 'bar' }
}
]
- // @ts-expect-error wrong "op" provided
+ // @ts-expect-error invalid "op" provided
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ob = new OrderBook({ journal: wrongOp })
} catch (error) {
@@ -585,7 +1147,7 @@ void test('orderbook replayJournal test wrong journal', ({ equal, end }) => {
o: { foo: 'bar' }
}
]
- // @ts-expect-error wrong market order "o" prop in journal log
+ // @ts-expect-error invalid market order "o" prop in journal log
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ob = new OrderBook({ journal: wrongOp })
} catch (error) {
@@ -604,7 +1166,7 @@ void test('orderbook replayJournal test wrong journal', ({ equal, end }) => {
o: { foo: 'bar' }
}
]
- // @ts-expect-error wrong limit order "o" prop in journal log
+ // @ts-expect-error invalid limit order "o" prop in journal log
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ob = new OrderBook({ journal: wrongOp })
} catch (error) {
@@ -623,7 +1185,7 @@ void test('orderbook replayJournal test wrong journal', ({ equal, end }) => {
o: { foo: 'bar' }
}
]
- // @ts-expect-error wrong update order "o" prop in journal log
+ // @ts-expect-error invalid update order "o" prop in journal log
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ob = new OrderBook({ journal: wrongOp })
} catch (error) {
@@ -642,7 +1204,7 @@ void test('orderbook replayJournal test wrong journal', ({ equal, end }) => {
o: { foo: 'bar' }
}
]
- // @ts-expect-error wrong delete order "o" prop in journal log
+ // @ts-expect-error invalid delete order "o" prop in journal log
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ob = new OrderBook({ journal: wrongOp })
} catch (error) {
@@ -666,7 +1228,7 @@ void test('orderbook test snapshot', ({ equal, end }) => {
equal(typeof level.price, 'number')
equal(Array.isArray(level.orders), true)
level.orders.forEach((order) => {
- equal(order instanceof Order, true)
+ equal(order instanceof LimitOrder, true)
})
})
@@ -674,7 +1236,7 @@ void test('orderbook test snapshot', ({ equal, end }) => {
equal(typeof level.price, 'number')
equal(Array.isArray(level.orders), true)
level.orders.forEach((order) => {
- equal(order instanceof Order, true)
+ equal(order instanceof LimitOrder, true)
})
})
@@ -761,7 +1323,7 @@ void test('orderbook restore from snapshot', ({ equal, same, end }) => {
end()
})
-void test('orderbook test unreachable lines', ({ equal, same, end }) => {
+void test('orderbook test unreachable lines', ({ equal, end }) => {
const ob = new OrderBook({ enableJournaling: true })
addDepth(ob, '', 10)
diff --git a/test/orderqueue.test.ts b/test/orderqueue.test.ts
index 2f2f75f..1913931 100644
--- a/test/orderqueue.test.ts
+++ b/test/orderqueue.test.ts
@@ -1,7 +1,8 @@
import { test } from 'tap'
-import { Order } from '../src/order'
+import { LimitOrder, OrderFactory } from '../src/order'
import { Side } from '../src/side'
import { OrderQueue } from '../src/orderqueue'
+import { OrderType, TimeInForce } from '../src/types'
void test('it should append/update/remove orders from queue', ({
equal,
@@ -10,14 +11,30 @@ void test('it should append/update/remove orders from queue', ({
}) => {
const price = 100
const oq = new OrderQueue(price)
- const order1 = new Order('order1', Side.SELL, 5, price)
- const order2 = new Order('order2', Side.SELL, 5, price)
+ const order1 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order1',
+ side: Side.SELL,
+ size: 5,
+ price,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+ const order2 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order2',
+ side: Side.SELL,
+ size: 5,
+ price,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
const head = oq.append(order1)
const tail = oq.append(order2)
- equal(head instanceof Order, true)
- equal(tail instanceof Order, true)
+ equal(head instanceof LimitOrder, true)
+ equal(tail instanceof LimitOrder, true)
same(head, order1)
same(tail, order2)
equal(oq.volume(), 10)
@@ -27,7 +44,15 @@ void test('it should append/update/remove orders from queue', ({
same(orders[0].toObject(), order1.toObject())
same(orders[1].toObject(), order2.toObject())
- const order3 = new Order('order3', Side.SELL, 10, price)
+ const order3 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order3',
+ side: Side.SELL,
+ size: 10,
+ price,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
// Test update. Number of orders is always 2
oq.update(head, order3)
@@ -55,8 +80,24 @@ void test('it should append/update/remove orders from queue', ({
void test('it should update order size and the volume', ({ equal, end }) => {
const price = 100
const oq = new OrderQueue(price)
- const order1 = new Order('order1', Side.SELL, 5, price)
- const order2 = new Order('order2', Side.SELL, 5, price)
+ const order1 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order1',
+ side: Side.SELL,
+ size: 5,
+ price,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+ const order2 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order2',
+ side: Side.SELL,
+ size: 5,
+ price,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
oq.append(order1)
oq.append(order2)
diff --git a/test/orderside.test.ts b/test/orderside.test.ts
index c256f71..5217095 100644
--- a/test/orderside.test.ts
+++ b/test/orderside.test.ts
@@ -1,8 +1,9 @@
import { test } from 'tap'
-import { Order } from '../src/order'
+import { OrderFactory } from '../src/order'
import { Side } from '../src/side'
import { OrderSide } from '../src/orderside'
import { ERROR } from '../src/errors'
+import { OrderType, TimeInForce } from '../src/types'
void test('it should append/update/remove orders from queue on BUY side', ({
equal,
@@ -10,8 +11,24 @@ void test('it should append/update/remove orders from queue on BUY side', ({
end
}) => {
const os = new OrderSide(Side.BUY)
- const order1 = new Order('order1', Side.BUY, 5, 10)
- const order2 = new Order('order2', Side.BUY, 5, 20)
+ const order1 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order1',
+ side: Side.BUY,
+ size: 5,
+ price: 10,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+ const order2 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order2',
+ side: Side.BUY,
+ size: 5,
+ price: 20,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
equal(os.minPriceQueue() === undefined, true)
equal(os.maxPriceQueue() === undefined, true)
@@ -25,11 +42,7 @@ void test('it should append/update/remove orders from queue on BUY side', ({
os.append(order2)
equal(os.depth(), 2)
equal(os.volume(), 10)
- equal(
- os.total(),
- order1.price * order1.size +
- order2.price * order2.size
- )
+ equal(os.total(), order1.price * order1.size + order2.price * order2.size)
equal(os.len(), 2)
equal(os.priceTree().length, 2)
same(os.orders()[0], order1)
@@ -126,6 +139,15 @@ void test('it should append/update/remove orders from queue on BUY side', ({
equal(updateOrder1.price, 25)
equal(os.toString(), '\n25 -> 10\n20 -> 5')
+ // @ts-expect-error _priceTree is private property
+ os._priceTree.values.reduce((previousPrice, curr) => {
+ // BUY side are in descending order bigger to lower
+ // @ts-expect-error _price is private property
+ const currPrice = curr._price
+ equal(currPrice < previousPrice, true)
+ return currPrice
+ }, Infinity)
+
// Remove the updated order
os.remove(updatedOrder)
@@ -154,8 +176,24 @@ void test('it should append/update/remove orders from queue on SELL side', ({
end
}) => {
const os = new OrderSide(Side.SELL)
- const order1 = new Order('order1', Side.SELL, 5, 10)
- const order2 = new Order('order2', Side.SELL, 5, 20)
+ const order1 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order1',
+ side: Side.SELL,
+ size: 5,
+ price: 10,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+ const order2 = OrderFactory.createOrder({
+ type: OrderType.LIMIT,
+ id: 'order2',
+ side: Side.SELL,
+ size: 5,
+ price: 20,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
equal(os.minPriceQueue() === undefined, true)
equal(os.maxPriceQueue() === undefined, true)
@@ -170,11 +208,7 @@ void test('it should append/update/remove orders from queue on SELL side', ({
os.append(order2)
equal(os.depth(), 2)
equal(os.volume(), 10)
- equal(
- os.total(),
- order1.price * order1.size +
- order2.price * order2.size
- )
+ equal(os.total(), order1.price * order1.size + order2.price * order2.size)
equal(os.len(), 2)
equal(os.priceTree().length, 2)
same(os.orders()[0], order1)
@@ -262,6 +296,15 @@ void test('it should append/update/remove orders from queue on SELL side', ({
equal(updateOrder1.price, 25)
equal(os.toString(), '\n25 -> 10\n20 -> 5')
+ // @ts-expect-error _priceTree is private property
+ os._priceTree.values.reduce((previousPrice, curr) => {
+ // SELL side are in ascending order lower to bigger
+ // @ts-expect-error _price is private property
+ const currPrice = curr._price
+ equal(currPrice > previousPrice, true)
+ return currPrice
+ }, 0)
+
// Remove the updated order
os.remove(updatedOrder)
diff --git a/test/stopbook.test.ts b/test/stopbook.test.ts
new file mode 100644
index 0000000..1880826
--- /dev/null
+++ b/test/stopbook.test.ts
@@ -0,0 +1,149 @@
+import { test } from 'tap'
+import { OrderFactory } from '../src/order'
+import { OrderType, StopOrder, TimeInForce } from '../src/types'
+import { Side } from '../src/side'
+import { StopBook } from '../src/stopbook'
+
+void test('it should add/remove/get order to stop book', ({ equal, same, end }) => {
+ const ob = new StopBook()
+ // @ts-expect-error asks is private
+ equal(ob.asks._priceTree.length, 0)
+ // @ts-expect-error bids is private
+ equal(ob.bids._priceTree.length, 0)
+
+ const addOrder = (side: Side, orderId: string, stopPrice: number): void => {
+ const order = OrderFactory.createOrder({
+ id: orderId,
+ type: OrderType.STOP_LIMIT,
+ side,
+ size: 5,
+ price: stopPrice,
+ stopPrice,
+ isMaker: true,
+ timeInForce: TimeInForce.GTC
+ })
+ ob.add(order)
+ }
+
+ // Start with SELL side
+ addOrder(Side.SELL, 'sell-1', 110)
+ // @ts-expect-error asks is private
+ equal(ob.asks._priceTree.length, 1)
+ // @ts-expect-error bids is private
+ equal(ob.bids._priceTree.length, 0)
+
+ addOrder(Side.SELL, 'sell-2', 110) // Same price as before
+ addOrder(Side.SELL, 'sell-3', 120)
+ addOrder(Side.SELL, 'sell-4', 130)
+ addOrder(Side.SELL, 'sell-5', 140)
+
+ // @ts-expect-error asks is private
+ equal(ob.asks._priceTree.length, 4)
+ // @ts-expect-error bids is private
+ equal(ob.bids._priceTree.length, 0)
+
+ // Test BUY side
+ addOrder(Side.BUY, 'buy-1', 100)
+ // @ts-expect-error asks is private
+ equal(ob.asks._priceTree.length, 4)
+ // @ts-expect-error bids is private
+ equal(ob.bids._priceTree.length, 1)
+
+ addOrder(Side.BUY, 'buy-2', 100) // Same price as before
+ addOrder(Side.BUY, 'buy-3', 90)
+ addOrder(Side.BUY, 'buy-4', 80)
+ addOrder(Side.BUY, 'buy-5', 70)
+
+ // @ts-expect-error asks is private
+ equal(ob.asks._priceTree.length, 4)
+ // @ts-expect-error bids is private
+ equal(ob.bids._priceTree.length, 4)
+
+ { // Before removing orders, test getConditionalOrders
+ const response = ob.getConditionalOrders(Side.SELL, 110, 130)
+ let totalOrder = 0
+ response.forEach((stopQueue) => {
+ totalOrder += stopQueue.len()
+ // @ts-expect-error _price is private
+ equal(stopQueue._price >= 110 && stopQueue._price <= 130, true)
+ })
+ equal(totalOrder, 4)
+ }
+
+ { // Before removing orders, test getConditionalOrders
+ const response = ob.getConditionalOrders(Side.BUY, 70, 130)
+ let totalOrder = 0
+ response.forEach((stopQueue) => {
+ totalOrder += stopQueue.len()
+ // @ts-expect-error _price is private
+ equal(stopQueue._price >= 70 && stopQueue._price <= 100, true)
+ })
+ equal(totalOrder, 5)
+ }
+
+ same(ob.remove(Side.SELL, 'sell-3', 120)?.id, 'sell-3')
+ // @ts-expect-error asks is private
+ equal(ob.asks._priceTree.length, 3)
+
+ // Lenght non changed because there were two orders at price level 100
+ same(ob.remove(Side.BUY, 'buy-2', 100)?.id, 'buy-2')
+ // @ts-expect-error asks is private
+ equal(ob.bids._priceTree.length, 4)
+
+ // Try to remove non existing order
+ equal(ob.remove(Side.SELL, 'fake-id', 130), undefined)
+
+ end()
+})
+
+void test('it should validate conditional order', ({ equal, end }) => {
+ const ob = new StopBook()
+
+ const validate = (
+ orderType: OrderType.STOP_LIMIT | OrderType.STOP_MARKET,
+ side: Side,
+ price: number | null = null,
+ stopPrice: number,
+ expect: boolean,
+ marketPrice: number
+ ): void => {
+ // @ts-expect-error price is available only for STOP_LIMIT
+ const order = OrderFactory.createOrder({
+ id: 'foo',
+ type: orderType,
+ side,
+ size: 5,
+ ...(price !== null ? { price } : {}),
+ stopPrice,
+ isMaker: true,
+ timeInForce: TimeInForce.GTC
+ }) as StopOrder
+ equal(ob.validConditionalOrder(marketPrice, order), expect)
+ }
+
+ // Stop LIMIT BUY
+ validate(OrderType.STOP_LIMIT, Side.BUY, 100, 90, true, 80)
+ validate(OrderType.STOP_LIMIT, Side.BUY, 100, 90, false, 90)
+ validate(OrderType.STOP_LIMIT, Side.BUY, 100, 90, false, 110)
+ validate(OrderType.STOP_LIMIT, Side.BUY, 90, 90, true, 80)
+ validate(OrderType.STOP_LIMIT, Side.BUY, 90, 90, true, 80)
+ validate(OrderType.STOP_LIMIT, Side.BUY, 90, 100, false, 80)
+
+ // Stop LIMIT SELL
+ validate(OrderType.STOP_LIMIT, Side.SELL, 90, 100, true, 110)
+ validate(OrderType.STOP_LIMIT, Side.SELL, 90, 100, false, 100)
+ validate(OrderType.STOP_LIMIT, Side.SELL, 90, 90, true, 110)
+ validate(OrderType.STOP_LIMIT, Side.SELL, 90, 80, false, 110)
+
+ // Stop MARKET BUY
+ validate(OrderType.STOP_MARKET, Side.BUY, null, 90, true, 80)
+ validate(OrderType.STOP_MARKET, Side.BUY, null, 90, false, 90)
+ validate(OrderType.STOP_MARKET, Side.BUY, null, 90, false, 110)
+
+ // Stop MARKET SELL
+ validate(OrderType.STOP_MARKET, Side.SELL, null, 90, true, 100)
+ validate(OrderType.STOP_MARKET, Side.SELL, null, 90, false, 90)
+ validate(OrderType.STOP_MARKET, Side.SELL, null, 90, false, 80)
+
+ end()
+})
diff --git a/test/stopqueue.test.ts b/test/stopqueue.test.ts
new file mode 100644
index 0000000..e60666b
--- /dev/null
+++ b/test/stopqueue.test.ts
@@ -0,0 +1,88 @@
+import { test } from 'tap'
+import { OrderFactory, StopLimitOrder } from '../src/order'
+import { Side } from '../src/side'
+import { StopQueue } from '../src/stopqueue'
+import { OrderType, TimeInForce } from '../src/types'
+
+void test('it should append/remove orders from queue', ({
+ equal,
+ same,
+ end
+}) => {
+ const price = 100
+ const stopPrice = 90
+ const oq = new StopQueue(price)
+ // Test edge case where head is undefined (queue is empty)
+ equal(oq.removeFromHead(), undefined)
+
+ const order1 = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order1',
+ side: Side.SELL,
+ size: 5,
+ price,
+ stopPrice,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+ const order2 = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order2',
+ side: Side.SELL,
+ size: 5,
+ price,
+ stopPrice,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+
+ const head = oq.append(order1)
+ const tail = oq.append(order2)
+
+ equal(head instanceof StopLimitOrder, true)
+ equal(tail instanceof StopLimitOrder, true)
+ same(head, order1)
+ same(tail, order2)
+ equal(oq.len(), 2)
+
+ const order3 = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order3',
+ side: Side.SELL,
+ size: 10,
+ price,
+ stopPrice,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+ oq.append(order3)
+ equal(oq.len(), 3)
+
+ const order4 = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order4',
+ side: Side.SELL,
+ size: 10,
+ price,
+ stopPrice,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+ oq.append(order4)
+ equal(oq.len(), 4)
+
+ same(oq.removeFromHead(), order1)
+ same(oq.remove(order4.id), order4)
+ equal(oq.len(), 2)
+
+ same(oq.removeFromHead(), order2)
+ equal(oq.len(), 1)
+
+ equal(oq.remove('fake-id'), undefined)
+ equal(oq.len(), 1)
+
+ same(oq.remove(order3.id), order3)
+ equal(oq.len(), 0)
+
+ end()
+})
diff --git a/test/stopside.test.ts b/test/stopside.test.ts
new file mode 100644
index 0000000..f8a5fa2
--- /dev/null
+++ b/test/stopside.test.ts
@@ -0,0 +1,333 @@
+import { test } from 'tap'
+import { OrderFactory } from '../src/order'
+import { Side } from '../src/side'
+import { StopSide } from '../src/stopside'
+import { OrderType, TimeInForce } from '../src/types'
+import { ERROR } from '../src/errors'
+
+void test('it should append/remove orders from queue on BUY side', ({
+ equal,
+ end
+}) => {
+ const os = new StopSide(Side.BUY)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 0)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 0)
+ {
+ const order = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order1',
+ side: Side.BUY,
+ size: 5,
+ price: 10,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true,
+ stopPrice: 10
+ })
+ os.append(order)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 1)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 1)
+ }
+
+ {
+ const order = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order2',
+ side: Side.BUY,
+ size: 5,
+ price: 10,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true,
+ stopPrice: 10 // same stopPrice as before, so same price level
+ })
+ os.append(order)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 1)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 1)
+ }
+
+ {
+ const order = OrderFactory.createOrder({
+ type: OrderType.STOP_MARKET,
+ side: Side.BUY,
+ size: 5,
+ stopPrice: 20,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+
+ os.append(order)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 2)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 2)
+ }
+
+ // @ts-expect-error _priceTree is private property
+ os._priceTree.values.reduce((previousPrice, curr) => {
+ // BUY side are in descending order bigger to lower
+ // @ts-expect-error _price is private property
+ const currPrice = curr._price
+ equal(currPrice < previousPrice, true)
+ return currPrice
+ }, Infinity)
+
+ {
+ // Remove the first order
+ const response = os.remove('order1', 10)
+
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 2)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 2)
+ equal(response?.id, 'order1')
+ }
+
+ {
+ // Try to remove the same order already deleted
+ const response = os.remove('order1', 10)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 2)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 2)
+ equal(response, undefined)
+ }
+
+ {
+ // Remove the second order order, so the price level is empty
+ const response = os.remove('order2', 10)
+
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 1)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 1)
+ equal(response?.id, 'order2')
+ }
+
+ // Test for error when price level not exists
+ try {
+ // order1 has been replaced whit updateOrder, so trying to update order1 will throw an error of type ErrInvalidPriceLevel
+ os.remove('some-id', 100)
+ } catch (error) {
+ if (error instanceof Error) {
+ // TypeScript knows err is Error
+ equal(error?.message, ERROR.ErrInvalidPriceLevel)
+ }
+ }
+
+ end()
+})
+
+void test('it should append/remove orders from queue on SELL side', ({
+ equal,
+ end
+}) => {
+ const os = new StopSide(Side.SELL)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 0)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 0)
+ {
+ const order = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order1',
+ side: Side.SELL,
+ size: 5,
+ price: 10,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true,
+ stopPrice: 10
+ })
+ os.append(order)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 1)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 1)
+ }
+
+ {
+ const order = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: 'order2',
+ side: Side.SELL,
+ size: 5,
+ price: 10,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true,
+ stopPrice: 10 // same stopPrice as before, so same price level
+ })
+ os.append(order)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 1)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 1)
+ }
+
+ {
+ const order = OrderFactory.createOrder({
+ type: OrderType.STOP_MARKET,
+ side: Side.SELL,
+ size: 5,
+ stopPrice: 20,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true
+ })
+
+ os.append(order)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 2)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 2)
+ }
+
+ // @ts-expect-error _priceTree is private property
+ os._priceTree.values.reduce((previousPrice, curr) => {
+ // SELL side are in ascending order lower to bigger
+ // @ts-expect-error _price is private property
+ const currPrice = curr._price
+ equal(currPrice > previousPrice, true)
+ return currPrice
+ }, 0)
+
+ {
+ // Remove the first order
+ const response = os.remove('order1', 10)
+
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 2)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 2)
+ equal(response?.id, 'order1')
+ }
+
+ {
+ // Try to remove the same order already deleted
+ const response = os.remove('order1', 10)
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 2)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 2)
+ equal(response, undefined)
+ }
+
+ {
+ // Remove the second order order, so the price level is empty
+ const response = os.remove('order2', 10)
+
+ // @ts-expect-error _prices is private
+ equal(Object.keys(os._prices).length, 1)
+ // @ts-expect-error _priceTree is private
+ equal(os._priceTree.length, 1)
+ equal(response?.id, 'order2')
+ }
+
+ // Test for error when price level not exists
+ try {
+ // order1 has been replaced whit updateOrder, so trying to update order1 will throw an error of type ErrInvalidPriceLevel
+ os.remove('some-id', 100)
+ } catch (error) {
+ if (error instanceof Error) {
+ // TypeScript knows err is Error
+ equal(error?.message, ERROR.ErrInvalidPriceLevel)
+ }
+ }
+
+ end()
+})
+
+void test('it should find all queue between upper and lower bound', ({
+ equal,
+ end
+}) => {
+ const appenOrder = (orderId: string, stopPrice: number, side, os: StopSide): void => {
+ const order = OrderFactory.createOrder({
+ type: OrderType.STOP_LIMIT,
+ id: orderId,
+ side,
+ size: 5,
+ price: 10,
+ timeInForce: TimeInForce.GTC,
+ isMaker: true,
+ stopPrice
+ })
+ os.append(order)
+ }
+
+ {
+ const side = Side.BUY
+ const os = new StopSide(side)
+ appenOrder('order1', 10, side, os)
+ appenOrder('order1-1', 19.5, side, os)
+ appenOrder('order2', 20, side, os)
+ appenOrder('order2-1', 20, side, os)
+ appenOrder('order2-3', 20, side, os)
+ appenOrder('order3', 30, side, os)
+ appenOrder('order4', 40, side, os)
+ appenOrder('order4-1', 40, side, os)
+ appenOrder('order4-2', 40.5, side, os)
+ appenOrder('order5', 50, side, os)
+
+ {
+ const response = os.between(40, 20)
+
+ response.forEach((queue) => {
+ // @ts-expect-error _price is private
+ equal(queue._price <= 40, true)
+ // @ts-expect-error _price is private
+ equal(queue._price >= 20, true)
+ })
+ }
+
+ {
+ const response = os.between(20, 40)
+ response.forEach((queue) => {
+ // @ts-expect-error _price is private
+ equal(queue._price <= 40, true)
+ // @ts-expect-error _price is private
+ equal(queue._price >= 20, true)
+ })
+ }
+ }
+
+ {
+ const side = Side.SELL
+ const os = new StopSide(side)
+ appenOrder('order1', 10, side, os)
+ appenOrder('order1-1', 19.5, side, os)
+ appenOrder('order2', 20, side, os)
+ appenOrder('order2-1', 20, side, os)
+ appenOrder('order2-3', 20, side, os)
+ appenOrder('order3', 30, side, os)
+ appenOrder('order4', 40, side, os)
+ appenOrder('order4-1', 40, side, os)
+ appenOrder('order4-2', 40.5, side, os)
+ appenOrder('order5', 50, side, os)
+
+ {
+ const response = os.between(40, 20)
+
+ response.forEach((queue) => {
+ // @ts-expect-error _price is private
+ equal(queue._price <= 40, true)
+ // @ts-expect-error _price is private
+ equal(queue._price >= 20, true)
+ })
+ }
+
+ {
+ const response = os.between(20, 40)
+ response.forEach((queue) => {
+ // @ts-expect-error _price is private
+ equal(queue._price <= 40, true)
+ // @ts-expect-error _price is private
+ equal(queue._price >= 20, true)
+ })
+ }
+ }
+
+ end()
+})