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 @@ Built with TypeScript

-> 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() +})