From 9f4b315fac7b94325fa4f13bc6eb7d30afee2058 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 8 Jul 2024 09:24:50 +0200 Subject: [PATCH 01/17] feat: add getter for market price --- src/orderbook.ts | 50 +++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/orderbook.ts b/src/orderbook.ts index d7a0b44..d3f1444 100644 --- a/src/orderbook.ts +++ b/src/orderbook.ts @@ -18,6 +18,7 @@ const validTimeInForce = Object.values(TimeInForce) export class OrderBook { private orders: { [key: string]: Order } = {} private _lastOp: number = 0 + private _marketPrice: number = 0 private readonly bids: OrderSide private readonly asks: OrderSide private readonly enableJournaling: boolean @@ -51,6 +52,11 @@ export class OrderBook { } } + // Getter for the market price + get marketPrice (): number { + return this._marketPrice + } + // Getter for the lastOp get lastOp (): number { return this._lastOp @@ -134,9 +140,10 @@ export class OrderBook { while (quantityToTrade > 0 && sideToProcess.len() > 0) { // if sideToProcess.len > 0 it is not necessary to verify that bestPrice exists - const bestPrice = iter() + const bestPrice = iter() as OrderQueue const { done, partial, partialQuantityProcessed, quantityLeft } = - this.processQueue(bestPrice as OrderQueue, quantityToTrade) + this.processQueue(bestPrice, quantityToTrade) + this._marketPrice = bestPrice?.price() response.done = response.done.concat(done) response.partial = partial response.partialQuantityProcessed = partialQuantityProcessed @@ -474,6 +481,7 @@ export class OrderBook { ) { const { done, partial, partialQuantityProcessed, quantityLeft } = this.processQueue(bestPrice, quantityToTrade) + this._marketPrice = bestPrice.price() response.done = response.done.concat(done) response.partial = partial response.partialQuantityProcessed = partialQuantityProcessed @@ -483,14 +491,14 @@ export class OrderBook { } if (quantityToTrade > 0) { - const order = new Order( - orderID, + const order = new Order({ + id: orderID, side, - quantityToTrade, + size: quantityToTrade, price, - Date.now(), - true - ) + time: Date.now(), + isMaker: true + }) if (response.done.length > 0) { response.partialQuantityProcessed = size - quantityToTrade response.partial = order @@ -512,7 +520,13 @@ export class OrderBook { } response.done.push( - new Order(orderID, side, size, totalPrice / totalQuantity, Date.now()) + new Order({ + id: orderID, + side, + size, + price: totalPrice / totalQuantity, + time: Date.now() + }) ) } @@ -588,15 +602,15 @@ 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 = new Order({ + id: headOrder.id, + side: headOrder.side, + size: headOrder.size - response.quantityLeft, + price: headOrder.price, + time: headOrder.time, + isMaker: true, + origSize: headOrder.origSize + }) this.orders[headOrder.id] = response.partial response.partialQuantityProcessed = response.quantityLeft orderQueue.update(headOrder, response.partial) From 794c71ac4c3eda212cb11b36c32b5f9f60c99caa Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 8 Jul 2024 09:35:51 +0200 Subject: [PATCH 02/17] feat: refactor limit and market options --- src/orderbook.ts | 64 ++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/orderbook.ts b/src/orderbook.ts index d3f1444..7cd628d 100644 --- a/src/orderbook.ts +++ b/src/orderbook.ts @@ -7,6 +7,8 @@ import type { ICancelOrder, IProcessOrder, JournalLog, + LimitOrderOptions, + MarketOrderOptions, OrderBookOptions, OrderUpdatePrice, OrderUpdateSize, @@ -86,14 +88,16 @@ export class OrderBook { ): IProcessOrder => { switch (type) { case OrderType.MARKET: - return this.market(side, size) + return this.market({ side, size }) case OrderType.LIMIT: return this.limit( - side, - orderID as string, - size, - price as number, - timeInForce + { + side, + orderID, + size, + price, + timeInForce + } ) default: return { @@ -114,7 +118,7 @@ export class OrderBook { * @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 */ - public market = (side: Side, size: number): IProcessOrder => { + public market = ({ side, size }: MarketOrderOptions): IProcessOrder => { let quantityToTrade = size const response = this.getProcessOrderResponse(quantityToTrade) @@ -142,7 +146,7 @@ export class OrderBook { // 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) + this.processQueue(bestPrice, quantityToTrade) this._marketPrice = bestPrice?.price() response.done = response.done.concat(done) response.partial = partial @@ -173,11 +177,13 @@ export class OrderBook { * @returns An object with the result of the processed order or an error */ public limit = ( - side: Side, - orderID: string, - size: number, - price: number, - timeInForce: TimeInForce = TimeInForce.GTC + { + side, + orderID, + size, + price, + timeInForce = TimeInForce.GTC + }: LimitOrderOptions ): IProcessOrder => { const response = this.getProcessOrderResponse(size) @@ -539,29 +545,35 @@ export class OrderBook { 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': + } + case 'l': { + const { side, orderID, size, price, timeInForce } = log.o if ( - log.o.side == null || - log.o.orderID == null || - log.o.size == null || - log.o.price == null + side == null || + orderID == 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 + { + side, + orderID, + size, + price, + timeInForce + } ) break + } case 'd': if (log.o.orderID == null) throw CustomError(ERROR.ErrJournalLog) this.cancel(log.o.orderID) From 92f9441b18593073b146cbe1456d77f8d06e1e20 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 22 Jul 2024 10:31:13 +0200 Subject: [PATCH 03/17] feat: add support for stop limit and stop market order --- src/errors.ts | 3 + src/order.ts | 265 +++++++++++++++----- src/orderbook.ts | 597 ++++++++++++++++++++++++++++++++++------------ src/orderqueue.ts | 20 +- src/orderside.ts | 39 +-- src/stopbook.ts | 61 +++++ src/stopqueue.ts | 30 +++ src/stopside.ts | 42 ++++ src/types.ts | 242 +++++++++++++------ 9 files changed, 992 insertions(+), 307 deletions(-) create mode 100644 src/stopbook.ts create mode 100644 src/stopqueue.ts create mode 100644 src/stopside.ts diff --git a/src/errors.ts b/src/errors.ts index 3ef2716..7f75d61 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -7,6 +7,7 @@ export enum ERROR { ErrInvalidPriceOrQuantity = 'orderbook: invalid order price or quantity', ErrInvalidQuantity = 'orderbook: invalid order quantity', ErrInvalidSide = "orderbook: given neither 'bid' nor 'ask'", + ErrInvalidStopPrice = 'orderbook: Invalid Stop Price. For Stop-Limit Order (BUY: marketPrice < stopPrice <= price, SELL: marketPrice > stopPrice >= price). For Stop-Market Order (BUY: marketPrice < stopPrice, SELL: marketPrice > stopPrice)', ErrInvalidTimeInForce = "orderbook: supported time in force are 'GTC', 'IOC' and 'FOK'", ErrLimitFOKNotFillable = 'orderbook: limit FOK order not fillable', ErrOrderExists = 'orderbook: order already exists', @@ -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.ErrInvalidStopPrice: + return new Error(ERROR.ErrInvalidStopPrice) case ERROR.ErrInvalidOrderType: return new Error(ERROR.ErrInvalidOrderType) case ERROR.ErrInvalidTimeInForce: diff --git a/src/order.ts b/src/order.ts index 001d13a..4b25097 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,210 @@ 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 + constructor (options: InternalLimitOrderOptions) { + super(options) + this._type = options.type + this._price = options.price + this._timeInForce = options.timeInForce + this._isMaker = options.isMaker + } + + // 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 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 + 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 + } + + // 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 + } + + 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 7cd628d..c6ffba1 100644 --- a/src/orderbook.ts +++ b/src/orderbook.ts @@ -1,29 +1,42 @@ 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, - LimitOrderOptions, +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, - OrderBookOptions, - OrderUpdatePrice, - OrderUpdateSize, - Snapshot + StopMarketOrderOptions, + LimitOrderOptions, + StopLimitOrderOptions } 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 /** * Creates an instance of OrderBook. * @param {OrderBookOptions} [options={}] - Options for configuring the order book. @@ -39,6 +52,7 @@ export class OrderBook { this.bids = new OrderSide(Side.BUY) this.asks = new OrderSide(Side.SELL) this.enableJournaling = enableJournaling + this.stopBook = new StopBook() // First restore from orderbook snapshot if (snapshot != null) { this.restoreSnapshot(snapshot) @@ -63,10 +77,25 @@ export class OrderBook { 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.timeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. 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. + * @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` @@ -74,9 +103,10 @@ 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. + * @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, @@ -84,145 +114,178 @@ export class OrderBook { // Specific for limit order type price?: number, orderID?: string, - timeInForce: TimeInForce = TimeInForce.GTC - ): IProcessOrder => { - switch (type) { + timeInForce?: TimeInForce, + stopPrice?: number + ): IProcessOrder + + public createOrder ( + typeOrOptions: CreateOrderOptions | OrderType, + side?: Side, + size?: number, + price?: number, + orderID?: string, + timeInForce = TimeInForce.GTC, + stopPrice?: number + ): 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 + } + /* 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, - size, - price, - timeInForce - } - ) + return this.limit(options) + case OrderType.STOP_MARKET: + return this.stopMarket(options) + case OrderType.STOP_LIMIT: + return this.stopLimit(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, size }: MarketOrderOptions): 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 - } - - let iter - let sideToProcess: OrderSide - if (side === Side.BUY) { - iter = this.asks.minPriceQueue - sideToProcess = this.asks + public market (side: Side, size: number): IProcessOrder + + 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() as OrderQueue - const { done, partial, partialQuantityProcessed, quantityLeft } = - this.processQueue(bestPrice, quantityToTrade) - this._marketPrice = bestPrice?.price() - 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 + public stopMarket = (options: StopMarketOrderOptions): IProcessOrder => { + 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 = ( - { - side, - orderID, - size, - price, - timeInForce = TimeInForce.GTC - }: LimitOrderOptions - ): 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 - } - - if (!validTimeInForce.includes(timeInForce)) { - response.err = CustomError(ERROR.ErrInvalidTimeInForce) - return response - } + public limit ( + side: Side, + orderID: string, + size: number, + price: number, + timeInForce?: TimeInForce + ): IProcessOrder - 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 } - } + 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({ + side: sideOrOptions, + size, + id: orderID, + 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 */ + } - return response + public stopLimit = (options: StopLimitOrderOptions): IProcessOrder => { + return this._stopLimit(options) } /** @@ -243,6 +306,7 @@ export class OrderBook { if (order === undefined) { return { done: [], + activated: [], partial: null, partialQuantityProcessed: 0, quantityLeft: 0, @@ -277,6 +341,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, @@ -300,7 +365,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] } @@ -368,8 +433,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() }) }) @@ -379,6 +444,120 @@ 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 + let oppositeSide: Side + if (options.side === Side.BUY) { + iter = this.asks.minPriceQueue + sideToProcess = this.asks + oppositeSide = Side.SELL + } else { + iter = this.bids.maxPriceQueue + sideToProcess = this.bids + oppositeSide = Side.BUY + } + 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(oppositeSide, 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, + 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 + ) + 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 _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.ErrInvalidStopPrice) + } + return response + } + private readonly restoreSnapshot = (snapshot: Snapshot): void => { this._lastOp = snapshot.lastOp for (const level of snapshot.bids) { @@ -437,6 +616,7 @@ export class OrderBook { private readonly getProcessOrderResponse = (size: number): IProcessOrder => { return { done: [], + activated: [], partial: null, partialQuantityProcessed: 0, quantityLeft: size, @@ -451,23 +631,25 @@ export class OrderBook { size: number, price: number, timeInForce: TimeInForce - ): void => { + ): LimitOrder | undefined => { let quantityToTrade = size let sideToProcess: OrderSide let sideToAdd: OrderSide let comparator let iter - + let oppositeSide: Side if (side === Side.BUY) { sideToAdd = this.bids sideToProcess = this.asks comparator = this.greaterThanOrEqual iter = this.asks.minPriceQueue + oppositeSide = Side.SELL } else { sideToAdd = this.asks sideToProcess = this.bids comparator = this.lowerThanOrEqual iter = this.bids.maxPriceQueue + oppositeSide = Side.BUY } if (timeInForce === TimeInForce.FOK) { @@ -479,6 +661,7 @@ export class OrderBook { } let bestPrice = iter() + const priceBefore = this._marketPrice while ( quantityToTrade > 0 && sideToProcess.len() > 0 && @@ -487,7 +670,6 @@ export class OrderBook { ) { const { done, partial, partialQuantityProcessed, quantityLeft } = this.processQueue(bestPrice, quantityToTrade) - this._marketPrice = bestPrice.price() response.done = response.done.concat(done) response.partial = partial response.partialQuantityProcessed = partialQuantityProcessed @@ -496,13 +678,18 @@ export class OrderBook { bestPrice = iter() } + this.executeConditionalOrder(oppositeSide, priceBefore, response) + + let order: LimitOrder if (quantityToTrade > 0) { - const order = new Order({ + order = OrderFactory.createOrder({ + type: OrderType.LIMIT, id: orderID, side, size: quantityToTrade, price, time: Date.now(), + timeInForce, isMaker: true }) if (response.done.length > 0) { @@ -516,7 +703,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) { @@ -524,28 +711,73 @@ export class OrderBook { totalPrice += response.partial.price * response.partialQuantityProcessed } - - response.done.push( - new Order({ - id: orderID, - side, - size, - price: totalPrice / totalQuantity, - time: 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 = ( + oppositeSide: Side, + priceBefore: number, + response: IProcessOrder + ): void => { + const pendingOrders = this.stopBook.getConditionalOrders( + oppositeSide, + priceBefore, + this._marketPrice + ) + if (pendingOrders.length > 0) { + pendingOrders.forEach((queue) => { + while (queue.len() > 0) { + const headOrder = queue.removeFromHead() + if (headOrder !== undefined) { + if (headOrder.type === OrderType.STOP_MARKET) { + this._market( + { + id: headOrder.id, + side: headOrder.side, + size: headOrder.size + }, + response + ) + } else { + this._limit( + { + id: headOrder.id, + side: headOrder.side, + size: headOrder.size, + price: headOrder.price, + timeInForce: headOrder.timeInForce + }, + response + ) + } + response.activated.push(headOrder) + } + } + }) + } } private readonly replayJournal = (journal: JournalLog[]): void => { for (const log of journal) { switch (log.op) { - case 'm':{ + case 'm': { const { side, size } = log.o if (side == null || size == null) { throw CustomError(ERROR.ErrJournalLog) @@ -554,24 +786,17 @@ export class OrderBook { break } case 'l': { - const { side, orderID, size, price, timeInForce } = log.o - if ( - side == null || - orderID == null || - size == null || - price == null - ) { + const { side, id, size, price, timeInForce } = log.o + if (side == null || id == null || size == null || price == null) { throw CustomError(ERROR.ErrJournalLog) } - this.limit( - { - side, - orderID, - size, - price, - timeInForce - } - ) + this.limit({ + side, + id, + size, + price, + timeInForce + }) break } case 'd': @@ -604,6 +829,7 @@ export class OrderBook { ): IProcessOrder => { const response: IProcessOrder = { done: [], + activated: [], partial: null, partialQuantityProcessed: 0, quantityLeft: quantityToTrade, @@ -614,14 +840,16 @@ export class OrderBook { const headOrder = orderQueue.head() if (headOrder !== undefined) { if (response.quantityLeft < headOrder.size) { - response.partial = new Order({ + 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, - isMaker: true, - origSize: headOrder.origSize + timeInForce: headOrder.timeInForce, + isMaker: true }) this.orders[headOrder.id] = response.partial response.partialQuantityProcessed = response.quantityLeft @@ -635,6 +863,7 @@ export class OrderBook { response.done.push(canceledOrder.order) } } + this._marketPrice = headOrder.price } } } @@ -691,4 +920,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..ef01b16 --- /dev/null +++ b/src/stopbook.ts @@ -0,0 +1,61 @@ +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) + } + + getConditionalOrders = ( + oppositeSide: Side, + upperBound: number, + lowerBound: number + ): StopQueue[] => { + const stopSide = oppositeSide === Side.BUY ? this.asks : this.bids + return stopSide.between(upperBound, lowerBound) + } + + /** + * 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) { + if ( + side === Side.BUY && + marketPrice < stopPrice && + stopPrice <= order.price + ) { + response = true + } + if ( + side === Side.SELL && + marketPrice > stopPrice && + stopPrice >= order.price + ) { + response = true + } + } else { + if (side === Side.BUY && marketPrice < stopPrice) response = true + 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..bfc7220 --- /dev/null +++ b/src/stopqueue.ts @@ -0,0 +1,30 @@ +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() + } + + // returns the number of orders in queue + len = (): number => { + return this._orders.length + } + + // remove order from head of queue + removeFromHead = (): StopOrder | undefined => { + return this._orders.shift() + } + + // 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 + } +} diff --git a/src/stopside.ts b/src/stopside.ts new file mode 100644 index 0000000..66cc8c7 --- /dev/null +++ b/src/stopside.ts @@ -0,0 +1,42 @@ +import createRBTree from 'functional-red-black-tree' +import { Side } from './side' +import { StopQueue } from './stopqueue' +import { StopOrder } from './types' + +export class StopSide { + private _priceTree: createRBTree.Tree + private _prices: { [key: string]: StopQueue } = {} + + constructor (side: Side) { + const compare = + side === Side.SELL + ? (a: number, b: number) => a - b + : (a: number, b: number) => b - a + this._priceTree = createRBTree(compare) + } + + // 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) + } + + // Get orders queue between upper and lower price levels + between = (upperBound: number, lowerBound: number): StopQueue[] => { + const queues: StopQueue[] = [] + this._priceTree.forEach( + (_, queue) => { + queues.push(queue) + }, + lowerBound, // Inclusive + upperBound + 1 // Exclusive (so we add +1) + ) + return queues + } +} diff --git a/src/types.ts b/src/types.ts index 53fc4fa..e63c6dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,156 @@ -import type { Order, TimeInForce } from './order' +import { LimitOrder, StopLimitOrder, StopMarketOrder } from './order' import type { Side } from './side' +export enum OrderType { + LIMIT = 'limit', + MARKET = 'market', + 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 +} + +/** + * 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 +} + +/** + * 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 + /** * 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 +161,33 @@ 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 /** Optional log related to the order cancellation. */ log?: CancelOrderJournalLog } @@ -93,49 +257,11 @@ 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 /** * Options for configuring the order book. @@ -152,26 +278,6 @@ export interface OrderBookOptions { 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 -} - /** * Represents an update to the price of an order. */ @@ -201,14 +307,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 From bce24ce644b2f0d55e108d24faab482d27215310 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 22 Jul 2024 10:33:55 +0200 Subject: [PATCH 04/17] build: release beta script and fix node crypto on webpack build --- .release-it.json | 3 + config/webpack.config.js | 15 +- package-lock.json | 1006 ++++++++++++++++++++++++++++++++++++-- package.json | 4 + 4 files changed, 996 insertions(+), 32 deletions(-) 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/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 38ffe12..b91c65d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,17 +18,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", "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 +4025,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 +4160,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", @@ -4299,6 +4325,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", @@ -4361,6 +4503,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 +4811,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 +5588,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 +5637,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 +5700,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 +5908,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 +5952,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 +5971,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 +6036,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 +6908,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 +7706,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 +7741,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", @@ -9049,6 +9371,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", @@ -9089,6 +9422,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", @@ -9131,6 +9483,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", @@ -9757,10 +10121,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" } @@ -10262,6 +10629,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", @@ -10374,6 +10758,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", @@ -10665,6 +11065,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", @@ -10762,10 +11168,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" @@ -10821,6 +11247,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", @@ -11593,6 +12029,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", @@ -11818,6 +12264,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", @@ -11890,14 +12349,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" @@ -12321,6 +12784,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", @@ -13547,6 +14020,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", @@ -16856,6 +17335,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", @@ -16933,6 +17431,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", @@ -17037,6 +17541,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", @@ -17065,6 +17688,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", @@ -17276,6 +17905,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", @@ -17811,6 +18450,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", @@ -17832,6 +18477,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", @@ -17849,6 +18539,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", @@ -17965,11 +18674,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" } @@ -17996,12 +18706,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", @@ -18041,6 +18780,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", @@ -18654,6 +19416,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", @@ -19210,6 +19982,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", @@ -19219,6 +20011,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", @@ -20369,6 +21172,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", @@ -20397,6 +21211,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", @@ -20424,6 +21256,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", @@ -20883,9 +21727,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": { @@ -21236,6 +22080,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", @@ -21320,6 +22178,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", @@ -21508,6 +22379,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", @@ -21594,10 +22471,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": { @@ -21630,6 +22529,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", @@ -22155,6 +23064,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", @@ -22308,6 +23227,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", @@ -22361,14 +23290,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": { @@ -22666,6 +23596,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", @@ -23520,6 +24460,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..34755e8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "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", @@ -49,17 +50,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", "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" }, From 47e6f5e9deaaf4ade4289955ef456884f53d506e Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 22 Jul 2024 10:34:19 +0200 Subject: [PATCH 05/17] test: add test for stop limit and stop market order --- test/order.test.ts | 278 +++++++++++++++--- test/orderbook.test.ts | 636 +++++++++++++++++++++++++++++++--------- test/orderqueue.test.ts | 57 +++- test/orderside.test.ts | 55 +++- 4 files changed, 830 insertions(+), 196 deletions(-) diff --git a/test/order.test.ts b/test/order.test.ts index e6aa3c7..ab8c5e1 100644 --- a/test/order.test.ts +++ b/test/order.test.ts @@ -1,123 +1,307 @@ 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 + const order = OrderFactory.createOrder({ + id, + type, + side, + size, + price, + time, + timeInForce, + isMaker: false + }) - equal(order instanceof Order, true) + 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) same(order.toObject(), { id, + type, side, - origSize: size, size, + origSize: size, price, time, + timeInForce, isMaker: order.isMaker }) equal( order.toString(), `${id}: + type: ${type} side: ${side} - origSize: ${size} size: ${size} + origSize: ${size} price: ${price} time: ${time} + timeInForce: ${timeInForce} isMaker: ${false as unknown as string}` ) equal( order.toJSON(), JSON.stringify({ id, + type, side, - origSize: size, size, + origSize: size, price, time, + timeInForce, isMaker: false }) ) end() }) -void test('it should create order without passing a date', ({ - equal, - end, - teardown, - same -}) => { - const fakeTimestamp = 1487076708000 - const { now } = Date - teardown(() => (Date.now = now)) - Date.now = (...m) => fakeTimestamp +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.stopPrice, stopPrice) + equal(order.time, time) + same(order.toObject(), { + id, + type, + side, + size, + origSize: size, + stopPrice, + time + }) + equal( + order.toString(), + `${id}: + type: ${type} + side: ${side} + size: ${size} + 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 order = new Order(id, side, size, price) - equal(order instanceof Order, true) + const stopPrice = 4 + const time = Date.now() + const timeInForce = TimeInForce.IOC + const order = OrderFactory.createOrder({ + id, + type, + side, + size, + price, + time, + 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.origSize, size) equal(order.price, price) - equal(order.time, fakeTimestamp) - equal(order.isMaker, false) + equal(order.origSize, size) + equal(order.stopPrice, stopPrice) + equal(order.timeInForce, timeInForce) + equal(order.isMaker, true) + equal(order.time, time) same(order.toObject(), { id, + type, side, - origSize: size, size, + origSize: size, price, - time: fakeTimestamp, - isMaker: false + stopPrice, + timeInForce, + time, + isMaker: true }) equal( order.toString(), `${id}: + type: ${type} side: ${side} - origSize: ${size} size: ${size} + origSize: ${size} price: ${price} - time: ${fakeTimestamp} - isMaker: ${false as unknown as string}` + stopPrice: ${stopPrice} + timeInForce: ${timeInForce} + time: ${time} + isMaker: ${true as unknown as string}` ) - equal( order.toJSON(), JSON.stringify({ id, + type, side, - origSize: size, size, + origSize: size, price, - time: fakeTimestamp, - isMaker: false + 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 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 type = OrderType.STOP_MARKET + const side = Side.BUY + const size = 5 + const stopPrice = 4 + const order = OrderFactory.createOrder({ + type, + side, + size, + stopPrice + }) + equal(order.id, fakeId) + equal(order.time, fakeTimestamp) + same(order.toObject(), { + id: fakeId, + type, + side, + size, + origSize: size, + stopPrice, + time: fakeTimestamp + }) + equal( + order.toString(), + `${fakeId}: + type: ${type} + side: ${side} + size: ${size} + origSize: ${size} + stopPrice: ${stopPrice} + time: ${fakeTimestamp}` + ) + + equal( + order.toJSON(), + JSON.stringify({ + id: fakeId, + type, + side, + size, + 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 +323,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..1df242a 100644 --- a/test/orderbook.test.ts +++ b/test/orderbook.test.ts @@ -1,10 +1,10 @@ -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 { JournalLog, OrderType, TimeInForce } from '../src/types' import { OrderQueue } from '../src/orderqueue' +import { LimitOrder, StopLimitOrder, StopMarketOrder } from '../src/order' const addDepth = ( ob: OrderBook, @@ -13,21 +13,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 +46,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 +59,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 +72,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 +90,112 @@ 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) + // { done, partial, partialQuantityProcessed, quantityLeft, err } + 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) - + // { done, partial, partialQuantityProcessed, quantityLeft, err } = + 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 +203,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() @@ -244,8 +287,8 @@ void test('test market', ({ equal, end }) => { addDepth(ob, '', 2) const process1 = - // { done, partial, partialQuantityProcessed, quantityLeft, err } - ob.market(Side.BUY, 3) + // { done, partial, partialQuantityProcessed, quantityLeft, err } + ob.market({ side: Side.BUY, size: 3 }) equal(process1.err === null, true) equal(process1.quantityLeft, 0) @@ -253,8 +296,8 @@ 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) + // { done, partial, partialQuantityProcessed, quantityLeft, err } = + ob.createOrder({ type: OrderType.MARKET, side: Side.SELL, size: 12 }) equal(process3.done.length, 5) equal(process3.err === null, true) @@ -262,16 +305,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 +322,298 @@ 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() + + 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.ErrInvalidStopPrice) + const wrongStopPrice2 = ob.stopMarket({ + side: Side.BUY, + size: 1, + stopPrice: ob.marketPrice + }) // Same as market price + equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidStopPrice) + // @ts-expect-error invalid side + const wrongOtherOrderOption1 = ob.stopMarket({ 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.ErrInvalidStopPrice) + const wrongStopPrice2 = ob.stopMarket({ + side: Side.SELL, + size: 1, + stopPrice: ob.marketPrice + }) // Same as market price + equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidStopPrice) + + // 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() + + 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.ErrInvalidStopPrice) + 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.ErrInvalidStopPrice) + // @ts-expect-error invalid side + const wrongOtherOrderOption1 = ob.stopLimit({ 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.ErrInvalidStopPrice) + 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.ErrInvalidStopPrice) + + // 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() }) @@ -294,8 +626,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 +669,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 +685,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 +734,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 +750,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 +806,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 +825,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 +868,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 +922,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 +941,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 +960,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 +979,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 +998,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 +1022,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 +1030,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) }) }) 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..0299023 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) @@ -154,8 +167,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 +199,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) From 96408a05b988b8597168e1b2c99b5e45fc88204d Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 22 Jul 2024 11:09:13 +0200 Subject: [PATCH 06/17] docs: update new signatures method + add stop limit and stop market --- README.md | 63 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 5ee639f..8793c9e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Ultra-fast matching engine written in TypeScript - Standard price-time priority - Supports both market and limit orders +- Supports conditional orders (Stop Market and Stop Limit) - Supports time in force GTC, FOK and IOC - Supports order cancelling - Supports order price and/or size updating @@ -56,11 +57,11 @@ const lob = 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) +lob.createOrder({ type: 'limit' | 'market', side: 'buy' | 'sell', size: number, price: number, id?: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) -lob.limit(side: 'buy' | 'sell', orderID: string, size: number, price: number) +lob.limit({ id: string, side: 'buy' | 'sell', size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) -lob.market(side: 'buy' | 'sell', size: number) +lob.market({ side: 'buy' | 'sell', size: number }) lob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number }) @@ -69,38 +70,45 @@ lob.cancel(orderID: string) ## 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()` or `stopMarket()` functions ### Create Order ```js // Create a limit order -createOrder('limit', side: 'buy' | 'sell', size: number, price: number, orderID: string, timeInForce?: 'GTC' | 'FOK' | 'IOC') +createOrder({ type: 'limit', side: 'buy' | 'sell', size: number, price: number, id: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) // Create a market order -createOrder('market', side: 'buy' | 'sell', size: number) +createOrder({ type: 'market', side: 'buy' | 'sell', size: number }) + +// Create a stop limit order +createOrder({ type: 'stop_limit', side: 'buy' | 'sell', size: number, price: number, id: string, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) + +// Create a stop market order +createOrder({ type: 'stop_market', side: 'buy' | 'sell', size: number, stopPrice: number }) ``` ### 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') +limit({ side: 'buy' | 'sell', id: string, size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) ``` For example: ``` -limit("sell", "uniqueID", 55, 100) +limit({ side: "sell", id: "uniqueID", size: 55, price: 100 }) asks: 110 -> 5 110 -> 5 100 -> 1 100 -> 56 @@ -113,7 +121,7 @@ partial - null ``` ``` -limit("buy", "uniqueID", 7, 120) +limit({ side: "buy", id: "uniqueID", size: 7, price: 120 }) asks: 110 -> 5 100 -> 1 @@ -127,7 +135,7 @@ partial - uniqueID order ``` ``` -limit("buy", "uniqueID", 3, 120) +limit({ side: "buy", id: "uniqueID", size: 3, price: 120 }) asks: 110 -> 5 100 -> 1 110 -> 3 @@ -143,19 +151,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) +market({ side: 'buy' | 'sell', size: number }) ``` For example: ``` -market('sell', 6) +market({ side: 'sell', size: 6 }) asks: 110 -> 5 110 -> 5 100 -> 1 100 -> 1 @@ -169,7 +178,7 @@ quantityLeft - 0 ``` ``` -market('buy', 10) +market({ side: 'buy', size: 10 }) asks: 110 -> 5 100 -> 1 @@ -201,7 +210,7 @@ modify(orderID: string, { size: number, price: number }) For example: ``` -limit("sell", "uniqueID", 55, 100) +limit({ side: "sell", id: "uniqueID", size: 55, price: 100 }) asks: 110 -> 5 110 -> 5 100 -> 1 100 -> 56 @@ -272,7 +281,7 @@ After taking the snapshot, you can safely remove all logs preceding the `lastOp` const lob = new OrderBook({ enableJournaling: true}) // after every order save the log to the database -const order = lob.limit("sell", "uniqueID", 55, 100) +const order = lob.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 @@ -306,7 +315,7 @@ By combining snapshots with journaling, you can effectively restore and audit th const lob = 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 = lob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 }) await saveLog(order.log) ``` From 770721b06d75c9255827e3bc06ce570c953986da Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 22 Jul 2024 11:14:11 +0200 Subject: [PATCH 07/17] chore(release): hft-limit-order-book@5.1.0-beta.0 From 28751852a15811b437d46a19f162594d1b55e190 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 22 Jul 2024 11:15:36 +0200 Subject: [PATCH 08/17] 6.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b30db7..b825ada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hft-limit-order-book", - "version": "5.0.0", + "version": "6.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "hft-limit-order-book", - "version": "5.0.0", + "version": "6.0.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 34755e8..f49e36e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hft-limit-order-book", - "version": "5.0.0", + "version": "6.0.0", "description": "Node.js Lmit Order Book for high-frequency trading (HFT).", "author": "Andrea Fassina ", "license": "MIT", From 7e36e53af78dde5991962e317b662eabb2a79b46 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Mon, 22 Jul 2024 11:20:32 +0200 Subject: [PATCH 09/17] chore(release): hft-limit-order-book@6.1.0-beta.0 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92219f8..600d344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## [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/package-lock.json b/package-lock.json index b825ada..439db66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hft-limit-order-book", - "version": "6.0.0", + "version": "6.1.0-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "hft-limit-order-book", - "version": "6.0.0", + "version": "6.1.0-beta.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f49e36e..b859888 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hft-limit-order-book", - "version": "6.0.0", + "version": "6.1.0-beta.0", "description": "Node.js Lmit Order Book for high-frequency trading (HFT).", "author": "Andrea Fassina ", "license": "MIT", From 313aefe3cebef6dd9876e7b9828cdfc54db33f1a Mon Sep 17 00:00:00 2001 From: fasenderos Date: Tue, 23 Jul 2024 10:19:46 +0200 Subject: [PATCH 10/17] fix: use new signatures in benchmark script --- benchmarks/benchmark_lob.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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_ }) } } From 2e7dd678975974a90ebb2921193b6c24b152d67e Mon Sep 17 00:00:00 2001 From: fasenderos Date: Tue, 23 Jul 2024 10:21:10 +0200 Subject: [PATCH 11/17] chore: update husky script --- .husky/.gitignore | 1 - .husky/commit-msg | 4 ---- .husky/common.sh | 8 -------- .husky/pre-commit | 4 ---- package-lock.json | 3 +-- package.json | 5 ++--- 6 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 .husky/.gitignore delete mode 100644 .husky/common.sh 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/package-lock.json b/package-lock.json index 439db66..2df9f1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "hft-limit-order-book", "version": "6.1.0-beta.0", - "hasInstallScript": true, "license": "MIT", "dependencies": { "denque": "2.1.0", @@ -20,7 +19,7 @@ "@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", diff --git a/package.json b/package.json index b859888..b82335d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "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", @@ -39,7 +38,7 @@ "test": "tap", "test:dev": "tap repl w", "test:cov": "tap && tap report lcov", - "prepare": "husky install" + "prepare": "husky" }, "dependencies": { "denque": "2.1.0", @@ -52,7 +51,7 @@ "@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", From 9f062c44d9d490e872c4f8c16287a7140fd1481c Mon Sep 17 00:00:00 2001 From: fasenderos Date: Tue, 23 Jul 2024 10:22:39 +0200 Subject: [PATCH 12/17] refactor: improve code readability on cancelOrder --- src/orderbook.ts | 19 ++----------------- test/orderbook.test.ts | 2 +- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/orderbook.ts b/src/orderbook.ts index c6ffba1..e757938 100644 --- a/src/orderbook.ts +++ b/src/orderbook.ts @@ -583,24 +583,9 @@ export class OrderBook { 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 - } - - // Side SELL + const side = order.side === Side.BUY ? this.bids : this.asks const response: ICancelOrder = { - order: this.asks.remove(order) + order: side.remove(order) } if (this.enableJournaling) { response.log = { diff --git a/test/orderbook.test.ts b/test/orderbook.test.ts index 1df242a..51485d6 100644 --- a/test/orderbook.test.ts +++ b/test/orderbook.test.ts @@ -1117,7 +1117,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) From 41ef939d172ea7f1a93e1e481f1dcffcbca55640 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Tue, 23 Jul 2024 15:10:13 +0200 Subject: [PATCH 13/17] docs: add stop limit and stop market documentations --- README.md | 39 ++++++++++++++++++++++++++++++- src/orderbook.ts | 21 +++++++++++++++++ test/orderbook.test.ts | 52 +++++++++++++++++++++++++++--------------- 3 files changed, 92 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8793c9e..46e7fab 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,16 @@ const lob = 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, id?: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +lob.createOrder({ type: 'limit' | 'market' | 'stop_limit' | 'stop_market', side: 'buy' | 'sell', size: number, price?: number, id?: string, stopPrice?: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) lob.limit({ id: string, side: 'buy' | 'sell', size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) lob.market({ side: 'buy' | 'sell', size: number }) +lob.stopLimit({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) + +lob.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) + lob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number }) lob.cancel(orderID: string) @@ -191,6 +195,39 @@ 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 + */ +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 + */ +stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) +``` + ### Modify an existing order ```js diff --git a/src/orderbook.ts b/src/orderbook.ts index e757938..5f8d6bd 100644 --- a/src/orderbook.ts +++ b/src/orderbook.ts @@ -215,6 +215,15 @@ export class OrderBook { /* c8 ignore stop */ } + /** + * 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 => { return this._stopMarket(options) } @@ -284,6 +293,18 @@ export class OrderBook { /* c8 ignore stop */ } + /** + * 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 => { return this._stopLimit(options) } diff --git a/test/orderbook.test.ts b/test/orderbook.test.ts index 51485d6..51420c1 100644 --- a/test/orderbook.test.ts +++ b/test/orderbook.test.ts @@ -92,8 +92,8 @@ void test('test limit', ({ equal, end }) => { addDepth(ob, '', 2) equal(ob.marketPrice, 0) const process1 = - // { done, partial, partialQuantityProcessed, quantityLeft, err } - ob.limit({ side: Side.BUY, id: 'order-b100', size: 1, price: 100 }) + // { done, partial, partialQuantityProcessed, quantityLeft, err } + ob.limit({ side: Side.BUY, id: 'order-b100', size: 1, price: 100 }) equal(ob.marketPrice, 100) equal(process1.err === null, true) @@ -103,8 +103,8 @@ void test('test limit', ({ equal, end }) => { equal(process1.partialQuantityProcessed, 1) const process2 = - // { done, partial, partialQuantityProcessed, quantityLeft, err } = - ob.limit({ side: Side.BUY, id: 'order-b150', size: 10, price: 150 }) + // { done, partial, partialQuantityProcessed, quantityLeft, err } = + 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') @@ -135,7 +135,6 @@ void test('test limit', ({ equal, end }) => { price: 100 }) equal(process5.err?.message, ERROR.ErrInvalidSide) - const removed = ob.cancel('order-b100') equal(removed === undefined, true) // Test also the createOrder method @@ -287,8 +286,8 @@ void test('test market', ({ equal, end }) => { addDepth(ob, '', 2) const process1 = - // { done, partial, partialQuantityProcessed, quantityLeft, err } - ob.market({ side: Side.BUY, size: 3 }) + // { done, partial, partialQuantityProcessed, quantityLeft, err } + ob.market({ side: Side.BUY, size: 3 }) equal(process1.err === null, true) equal(process1.quantityLeft, 0) @@ -296,8 +295,8 @@ void test('test market', ({ equal, end }) => { // Test also the createOrder method const process3 = - // { done, partial, partialQuantityProcessed, quantityLeft, err } = - ob.createOrder({ type: OrderType.MARKET, side: Side.SELL, size: 12 }) + // { done, partial, partialQuantityProcessed, quantityLeft, err } = + ob.createOrder({ type: OrderType.MARKET, side: Side.SELL, size: 12 }) equal(process3.done.length, 5) equal(process3.err === null, true) @@ -347,10 +346,10 @@ void test('createOrder error', ({ equal, end }) => { }) /** - * Stop-Market Order: - * Buy: marketPrice < stopPrice - * Sell: marketPrice > stopPrice - */ + * Stop-Market Order: + * Buy: marketPrice < stopPrice + * Sell: marketPrice > stopPrice + */ void test('test stop_market order', ({ equal, end }) => { const ob = new OrderBook() @@ -374,8 +373,11 @@ void test('test stop_market order', ({ equal, end }) => { stopPrice: ob.marketPrice }) // Same as market price equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidStopPrice) - // @ts-expect-error invalid side - const wrongOtherOrderOption1 = ob.stopMarket({ side: 'wrong-side', size: 1 }) + 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 @@ -496,16 +498,28 @@ void test('test stop_limit order', ({ equal, end }) => { price: ob.marketPrice }) // Same as market price equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidStopPrice) - // @ts-expect-error invalid side - const wrongOtherOrderOption1 = ob.stopLimit({ side: 'wrong-side', size: 1, price: 10 }) + 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 }) + 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 }) + const wrongOtherOrderOption3 = ob.stopLimit({ + side: Side.BUY, + size: 1, + price: 0 + }) equal(wrongOtherOrderOption3.err != null, true) // Add a stop limit BUY order From 5b193185b5e32ba6d0ad9d0853876708e3c3e23c Mon Sep 17 00:00:00 2001 From: fasenderos Date: Sun, 28 Jul 2024 02:36:07 +0200 Subject: [PATCH 14/17] feat: add support for OCO orders --- README.md | 46 +++++- src/errors.ts | 6 +- src/order.ts | 21 ++- src/orderbook.ts | 228 ++++++++++++++++++++++------ src/stopbook.ts | 28 +++- src/stopqueue.ts | 27 +++- src/stopside.ts | 50 ++++++- src/types.ts | 28 +++- test/order.test.ts | 320 ++++++++++++++++++++++++++++----------- test/orderbook.test.ts | 210 ++++++++++++++++++++++++-- test/orderside.test.ts | 18 +++ test/stopbook.test.ts | 149 ++++++++++++++++++ test/stopqueue.test.ts | 88 +++++++++++ test/stopside.test.ts | 333 +++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1380 insertions(+), 172 deletions(-) create mode 100644 test/stopbook.test.ts create mode 100644 test/stopqueue.test.ts create mode 100644 test/stopside.test.ts diff --git a/README.md b/README.md index 46e7fab..e5f3bab 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Ultra-fast matching engine written in TypeScript - Standard price-time priority - Supports both market and limit orders -- Supports conditional orders (Stop Market and Stop Limit) +- Supports conditional orders (Stop Market, Stop Limit and OCO) - Supports time in force GTC, FOK and IOC - Supports order cancelling - Supports order price and/or size updating @@ -57,7 +57,7 @@ const lob = new OrderBook() Then you'll be able to use next primary functions: ```js -lob.createOrder({ type: 'limit' | 'market' | 'stop_limit' | 'stop_market', side: 'buy' | 'sell', size: number, price?: number, id?: string, stopPrice?: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +lob.createOrder({ type: 'limit' | 'market' | 'stop_limit' | 'stop_market' | 'oco', side: 'buy' | 'sell', size: number, price?: number, id?: string, stopPrice?: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) lob.limit({ id: string, side: 'buy' | 'sell', size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) @@ -65,6 +65,8 @@ lob.market({ side: 'buy' | 'sell', size: number }) lob.stopLimit({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +lob.oco({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' }) + lob.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) lob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number }) @@ -79,17 +81,20 @@ To add an order to the order book you can call the general `createOrder()` funct ### Create Order ```js -// Create a limit order +// Create limit order createOrder({ type: 'limit', side: 'buy' | 'sell', size: number, price: number, id: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) -// Create a market order +// Create market order createOrder({ type: 'market', side: 'buy' | 'sell', size: number }) -// Create a stop limit order +// Create stop limit order createOrder({ type: 'stop_limit', side: 'buy' | 'sell', size: number, price: number, id: string, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) -// Create a stop market order +// Create stop market order createOrder({ type: 'stop_market', side: 'buy' | 'sell', size: number, stopPrice: number }) + +// Create OCO order +createOrder({ type: 'oco', side: 'buy' | 'sell', size: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' }) ``` ### Create Limit Order @@ -228,6 +233,35 @@ stopLimit({ side: 'buy' | 'sell', id: string, size: number, price: number, stopP 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 + */ +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 diff --git a/src/errors.ts b/src/errors.ts index 7f75d61..036b60b 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,13 +1,13 @@ 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', ErrInvalidPriceOrQuantity = 'orderbook: invalid order price or quantity', ErrInvalidQuantity = 'orderbook: invalid order quantity', ErrInvalidSide = "orderbook: given neither 'bid' nor 'ask'", - ErrInvalidStopPrice = 'orderbook: Invalid Stop Price. For Stop-Limit Order (BUY: marketPrice < stopPrice <= price, SELL: marketPrice > stopPrice >= price). For Stop-Market Order (BUY: marketPrice < stopPrice, SELL: marketPrice > stopPrice)', ErrInvalidTimeInForce = "orderbook: supported time in force are 'GTC', 'IOC' and 'FOK'", ErrLimitFOKNotFillable = 'orderbook: limit FOK order not fillable', ErrOrderExists = 'orderbook: order already exists', @@ -33,8 +33,8 @@ export const CustomError = (error?: ERROR | string): Error => { return new Error(ERROR.ErrOrderNotFound) case ERROR.ErrInvalidSide: return new Error(ERROR.ErrInvalidSide) - case ERROR.ErrInvalidStopPrice: - return new Error(ERROR.ErrInvalidStopPrice) + 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 4b25097..5a9c2bb 100644 --- a/src/order.ts +++ b/src/order.ts @@ -75,12 +75,15 @@ export class LimitOrder extends BaseOrder { 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 @@ -108,6 +111,10 @@ export class LimitOrder extends BaseOrder { return this._isMaker } + get ocoStopPrice (): number | undefined { + return this._ocoStopPrice + } + toString = (): string => `${this._id}: type: ${this.type} @@ -170,7 +177,7 @@ export class StopMarketOrder extends BaseOrder { side: this._side, size: this._size, origSize: this._origSize, - stopPrice: this.stopPrice, + stopPrice: this._stopPrice, time: this._time }) } @@ -181,6 +188,8 @@ export class StopLimitOrder extends BaseOrder { 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 @@ -188,6 +197,7 @@ export class StopLimitOrder extends BaseOrder { this._stopPrice = options.stopPrice this._timeInForce = options.timeInForce this._isMaker = options.isMaker + this._isOCO = options.isOCO ?? false } // Getter for order type @@ -220,6 +230,11 @@ export class StopLimitOrder extends BaseOrder { return this._isMaker } + // Getter for order isOCO + get isOCO (): boolean { + return this._isOCO + } + toString = (): string => `${this._id}: type: ${this.type} @@ -241,8 +256,8 @@ export class StopLimitOrder extends BaseOrder { size: this._size, origSize: this._origSize, price: this._price, - stopPrice: this.stopPrice, - timeInForce: this.timeInForce, + stopPrice: this._stopPrice, + timeInForce: this._timeInForce, time: this._time, isMaker: this._isMaker }) diff --git a/src/orderbook.ts b/src/orderbook.ts index 5f8d6bd..d9d32e9 100644 --- a/src/orderbook.ts +++ b/src/orderbook.ts @@ -24,7 +24,9 @@ import { MarketOrderOptions, StopMarketOrderOptions, LimitOrderOptions, - StopLimitOrderOptions + StopLimitOrderOptions, + OCOOrderOptions, + StopOrder } from './types' const validTimeInForce = Object.values(TimeInForce) @@ -86,8 +88,10 @@ export class OrderBook { * @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.timeInForce - Time-in-force supported are: `GTC` (default), `FOK`, `IOC`. 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 @@ -104,6 +108,8 @@ export class OrderBook { * @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 * @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 ( @@ -115,7 +121,9 @@ export class OrderBook { price?: number, orderID?: string, timeInForce?: TimeInForce, - stopPrice?: number + stopPrice?: number, + stopLimitPrice?: number, + stopLimitTimeInForce?: TimeInForce ): IProcessOrder public createOrder ( @@ -125,7 +133,9 @@ export class OrderBook { price?: number, orderID?: string, timeInForce = TimeInForce.GTC, - stopPrice?: number + stopPrice?: number, + stopLimitPrice?: number, + stopLimitTimeInForce = TimeInForce.GTC ): IProcessOrder { let options: CreateOrderOptions // We don't want to test the deprecated signature. @@ -144,7 +154,10 @@ export class OrderBook { id: orderID, timeInForce, // @ts-expect-error - stopPrice + stopPrice, + // @ts-expect-error + stopLimitPrice, + stopLimitTimeInForce } /* c8 ignore stop */ } else if (typeof typeOrOptions === 'object') { @@ -164,6 +177,8 @@ export class OrderBook { return this.stopMarket(options) case OrderType.STOP_LIMIT: return this.stopLimit(options) + case OrderType.OCO: + return this.oco(options) default: return { done: [], @@ -277,9 +292,9 @@ export class OrderBook { price !== undefined ) { return this._limit({ + id: orderID, side: sideOrOptions, size, - id: orderID, price, timeInForce }) @@ -309,6 +324,33 @@ export class OrderBook { return this._stopLimit(options) } + /** + * 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 => { + return this._oco(options) + } + /** * Modify an existing order with given ID. When an order is modified by price or quantity, * it will be deemed as a new entry. Under the price-time-priority algorithm, orders are @@ -475,15 +517,12 @@ export class OrderBook { let quantityToTrade = options.size let iter let sideToProcess: OrderSide - let oppositeSide: Side if (options.side === Side.BUY) { iter = this.asks.minPriceQueue sideToProcess = this.asks - oppositeSide = Side.SELL } else { iter = this.bids.maxPriceQueue sideToProcess = this.bids - oppositeSide = Side.BUY } const priceBefore = this._marketPrice while (quantityToTrade > 0 && sideToProcess.len() > 0) { @@ -497,7 +536,9 @@ export class OrderBook { quantityToTrade = quantityLeft } response.quantityLeft = quantityToTrade - this.executeConditionalOrder(oppositeSide, priceBefore, response) + + this.executeConditionalOrder(options.side, priceBefore, response) + if (this.enableJournaling) { response.log = { opId: ++this._lastOp, @@ -510,7 +551,7 @@ export class OrderBook { } private readonly _limit = ( - options: LimitOrderOptions, + options: LimitOrderOptions & { ocoStopPrice?: number }, incomingResponse?: IProcessOrder ): IProcessOrder => { const response = incomingResponse ?? this.validateLimitOrder(options) @@ -521,7 +562,8 @@ export class OrderBook { options.id, options.size, options.price, - options.timeInForce ?? TimeInForce.GTC + options.timeInForce ?? TimeInForce.GTC, + options.ocoStopPrice ) if (this.enableJournaling && order != null) { response.log = { @@ -566,6 +608,46 @@ export class OrderBook { 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 @@ -574,7 +656,7 @@ export class OrderBook { this.stopBook.add(stopOrder) response.done.push(stopOrder) } else { - response.err = CustomError(ERROR.ErrInvalidStopPrice) + response.err = CustomError(ERROR.ErrInvalidConditionalOrder) } return response } @@ -596,9 +678,15 @@ 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 @@ -608,9 +696,15 @@ export class OrderBook { const response: ICancelOrder = { order: side.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 } @@ -636,26 +730,24 @@ export class OrderBook { orderID: string, size: number, price: number, - timeInForce: TimeInForce + timeInForce: TimeInForce, + ocoStopPrice?: number ): LimitOrder | undefined => { let quantityToTrade = size let sideToProcess: OrderSide let sideToAdd: OrderSide let comparator let iter - let oppositeSide: Side if (side === Side.BUY) { sideToAdd = this.bids sideToProcess = this.asks comparator = this.greaterThanOrEqual iter = this.asks.minPriceQueue - oppositeSide = Side.SELL } else { sideToAdd = this.asks sideToProcess = this.bids comparator = this.lowerThanOrEqual iter = this.bids.maxPriceQueue - oppositeSide = Side.BUY } if (timeInForce === TimeInForce.FOK) { @@ -665,7 +757,6 @@ export class OrderBook { return } } - let bestPrice = iter() const priceBefore = this._marketPrice while ( @@ -684,7 +775,7 @@ export class OrderBook { bestPrice = iter() } - this.executeConditionalOrder(oppositeSide, priceBefore, response) + this.executeConditionalOrder(side, priceBefore, response) let order: LimitOrder if (quantityToTrade > 0) { @@ -696,7 +787,8 @@ export class OrderBook { price, time: Date.now(), timeInForce, - isMaker: true + isMaker: true, + ...(ocoStopPrice !== undefined ? { ocoStopPrice } : {}) }) if (response.done.length > 0) { response.partialQuantityProcessed = size - quantityToTrade @@ -738,44 +830,54 @@ export class OrderBook { } private readonly executeConditionalOrder = ( - oppositeSide: Side, + side: Side, priceBefore: number, response: IProcessOrder ): void => { const pendingOrders = this.stopBook.getConditionalOrders( - oppositeSide, + 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) { - if (headOrder.type === OrderType.STOP_MARKET) { - this._market( - { - id: headOrder.id, - side: headOrder.side, - size: headOrder.size - }, - response - ) - } else { - this._limit( - { - id: headOrder.id, - side: headOrder.side, - size: headOrder.size, - price: headOrder.price, - timeInForce: headOrder.timeInForce - }, - response - ) - } - response.activated.push(headOrder) + 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) }) } } @@ -821,6 +923,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 } @@ -869,6 +995,14 @@ export class OrderBook { response.done.push(canceledOrder.order) } } + // Remove linked OCO Stop Order if any + if (headOrder.ocoStopPrice !== undefined) { + this.stopBook.remove( + headOrder.side, + headOrder.id, + headOrder.ocoStopPrice + ) + } this._marketPrice = headOrder.price } } diff --git a/src/stopbook.ts b/src/stopbook.ts index ef01b16..893cdc0 100644 --- a/src/stopbook.ts +++ b/src/stopbook.ts @@ -17,13 +17,27 @@ export class StopBook { 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 = ( - oppositeSide: Side, - upperBound: number, - lowerBound: number + side: Side, + priceBefore: number, + marketPrice: number ): StopQueue[] => { - const stopSide = oppositeSide === Side.BUY ? this.asks : this.bids - return stopSide.between(upperBound, lowerBound) + const stopSide = side === Side.BUY ? this.bids : this.asks + return stopSide.between(priceBefore, marketPrice) } /** @@ -38,6 +52,7 @@ export class StopBook { let response = false const { type, side, stopPrice } = order if (type === OrderType.STOP_LIMIT) { + // Buy: marketPrice < stopPrice <= price if ( side === Side.BUY && marketPrice < stopPrice && @@ -45,6 +60,7 @@ export class StopBook { ) { response = true } + // Sell: marketPrice > stopPrice >= price if ( side === Side.SELL && marketPrice > stopPrice && @@ -53,7 +69,9 @@ export class StopBook { 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 index bfc7220..23c64d7 100644 --- a/src/stopqueue.ts +++ b/src/stopqueue.ts @@ -11,6 +11,10 @@ export class StopQueue { this._orders = new Denque() } + get price (): number { + return this._price + } + // returns the number of orders in queue len = (): number => { return this._orders.length @@ -18,7 +22,11 @@ export class StopQueue { // remove order from head of queue removeFromHead = (): StopOrder | undefined => { - return this._orders.shift() + // 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 @@ -27,4 +35,21 @@ export class StopQueue { 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 index 66cc8c7..8676fcb 100644 --- a/src/stopside.ts +++ b/src/stopside.ts @@ -2,10 +2,12 @@ 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 = @@ -13,6 +15,7 @@ export class StopSide { ? (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 @@ -27,15 +30,52 @@ export class StopSide { return this._prices[strPrice].append(order) } - // Get orders queue between upper and lower price levels - between = (upperBound: number, lowerBound: number): StopQueue[] => { + // 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( - (_, queue) => { - queues.push(queue) + (price, queue) => { + if ( + (this._side === Side.BUY && price >= lowest) || + (this._side === Side.SELL && price <= highest) + ) { + queues.push(queue) + } }, lowerBound, // Inclusive - upperBound + 1 // Exclusive (so we add +1) + upperBound // Exclusive (so we add +-1 depending on the side) ) return queues } diff --git a/src/types.ts b/src/types.ts index e63c6dd..7f80b1b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ import type { Side } from './side' export enum OrderType { LIMIT = 'limit', MARKET = 'market', + OCO = 'oco', STOP_LIMIT = 'stop_limit', STOP_MARKET = 'stop_market', } @@ -52,11 +53,12 @@ interface ILimitOrderOptions extends InternalBaseOrderOptions { } export interface InternalLimitOrderOptions extends ILimitOrderOptions { type: OrderType.LIMIT + ocoStopPrice?: number } /** * Specific options for a stop market order. -*/ + */ export interface StopMarketOrderOptions extends MarketOrderOptions { stopPrice: number } @@ -74,6 +76,16 @@ export interface StopLimitOrderOptions extends LimitOrderOptions { 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 } /** @@ -141,6 +153,10 @@ export type OrderOptions = | InternalStopLimitOrderOptions | InternalStopMarketOrderOptions +export type StopOrderOptions = + | StopMarketOrderOptions + | StopLimitOrderOptions + | OCOOrderOptions /** * Represents the result of processing an order. */ @@ -188,6 +204,7 @@ export interface CancelOrderOptions { */ export interface ICancelOrder { order: LimitOrder + stopOrder?: StopOrder /** Optional log related to the order cancellation. */ log?: CancelOrderJournalLog } @@ -258,10 +275,11 @@ export type JournalLog = | CancelOrderJournalLog export type CreateOrderOptions = - | { type: OrderType.MARKET } & MarketOrderOptions - | { type: OrderType.LIMIT } & LimitOrderOptions - | { type: OrderType.STOP_MARKET } & StopMarketOrderOptions - | { type: OrderType.STOP_LIMIT } & StopLimitOrderOptions + | ({ 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. diff --git a/test/order.test.ts b/test/order.test.ts index ab8c5e1..04f7a05 100644 --- a/test/order.test.ts +++ b/test/order.test.ts @@ -18,41 +18,44 @@ void test('it should create LimitOrder', ({ equal, same, end }) => { const price = 100 const time = Date.now() const timeInForce = TimeInForce.IOC - 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) - same(order.toObject(), { - id, - type, - side, - size, - origSize: size, - price, - time, - timeInForce, - isMaker: order.isMaker - }) - equal( - order.toString(), - `${id}: + { + 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} @@ -61,10 +64,49 @@ void test('it should create LimitOrder', ({ equal, same, end }) => { time: ${time} timeInForce: ${timeInForce} isMaker: ${false as unknown as string}` - ) - equal( - order.toJSON(), - JSON.stringify({ + ) + 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, @@ -73,9 +115,35 @@ void test('it should create LimitOrder', ({ equal, same, end }) => { price, time, timeInForce, - isMaker: false + 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() }) @@ -146,44 +214,46 @@ void test('it should create StopLimitOrder', ({ equal, same, end }) => { const stopPrice = 4 const time = Date.now() const timeInForce = TimeInForce.IOC - const order = OrderFactory.createOrder({ - id, - type, - side, - size, - price, - time, - stopPrice, - timeInForce, - isMaker: true - }) + { + const order = OrderFactory.createOrder({ + id, + type, + side, + size, + price, + time, + 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) - same(order.toObject(), { - id, - type, - side, - size, - origSize: size, - price, - stopPrice, - timeInForce, - time, - isMaker: true - }) - equal( - order.toString(), - `${id}: + 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} @@ -193,10 +263,56 @@ void test('it should create StopLimitOrder', ({ equal, same, end }) => { timeInForce: ${timeInForce} time: ${time} isMaker: ${true as unknown as string}` - ) - equal( - order.toJSON(), - JSON.stringify({ + ) + 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, @@ -208,11 +324,39 @@ void test('it should create StopLimitOrder', ({ equal, same, end }) => { time, isMaker: true }) - ) - // Price setter - const newPrice = 120 - order.price = newPrice - equal(order.price, newPrice) + 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() }) @@ -230,12 +374,12 @@ void test('it should create order without passing a date or id', ({ teardown(() => (Date.now = now)) // @ts-expect-error cannot assign because is readonly // eslint-disable-next-line - teardown(() => (randomUUID = originalRandomUUID)) + teardown(() => (randomUUID = originalRandomUUID)); Date.now = (...m) => fakeTimestamp // @ts-expect-error cannot assign because is readonly // eslint-disable-next-line - randomUUID = () => fakeId + randomUUID = () => fakeId; const type = OrderType.STOP_MARKET const side = Side.BUY diff --git a/test/orderbook.test.ts b/test/orderbook.test.ts index 51420c1..1fe2da4 100644 --- a/test/orderbook.test.ts +++ b/test/orderbook.test.ts @@ -2,7 +2,12 @@ import { test } from 'tap' import { Side } from '../src/side' import { OrderBook } from '../src/orderbook' import { ERROR } from '../src/errors' -import { JournalLog, OrderType, TimeInForce } from '../src/types' +import { + IProcessOrder, + JournalLog, + OrderType, + TimeInForce +} from '../src/types' import { OrderQueue } from '../src/orderqueue' import { LimitOrder, StopLimitOrder, StopMarketOrder } from '../src/order' @@ -366,13 +371,13 @@ void test('test stop_market order', ({ equal, end }) => { size: 1, stopPrice: ob.marketPrice - 10 }) // Below market price - equal(wrongStopPrice.err?.message, ERROR.ErrInvalidStopPrice) + 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.ErrInvalidStopPrice) + equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder) const wrongOtherOrderOption1 = ob.stopMarket({ // @ts-expect-error invalid side side: 'wrong-side', @@ -415,13 +420,13 @@ void test('test stop_market order', ({ equal, end }) => { size: 1, stopPrice: ob.marketPrice + 10 }) // Above market price - equal(wrongStopPrice.err?.message, ERROR.ErrInvalidStopPrice) + 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.ErrInvalidStopPrice) + equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder) // Add a stop market SELL order const beforeMarketPrice = ob.marketPrice @@ -489,7 +494,7 @@ void test('test stop_limit order', ({ equal, end }) => { stopPrice: ob.marketPrice - 10, // Below market price price: ob.marketPrice }) - equal(wrongStopPrice.err?.message, ERROR.ErrInvalidStopPrice) + equal(wrongStopPrice.err?.message, ERROR.ErrInvalidConditionalOrder) const wrongStopPrice2 = ob.stopLimit({ id: 'fake-id', side: Side.BUY, @@ -497,7 +502,7 @@ void test('test stop_limit order', ({ equal, end }) => { stopPrice: ob.marketPrice, price: ob.marketPrice }) // Same as market price - equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidStopPrice) + equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder) const wrongOtherOrderOption1 = ob.stopLimit({ // @ts-expect-error invalid side side: 'wrong-side', @@ -569,7 +574,7 @@ void test('test stop_limit order', ({ equal, end }) => { stopPrice: ob.marketPrice + 10, // Above market price price: ob.marketPrice }) - equal(wrongStopPrice.err?.message, ERROR.ErrInvalidStopPrice) + equal(wrongStopPrice.err?.message, ERROR.ErrInvalidConditionalOrder) const wrongStopPrice2 = ob.stopLimit({ id: 'fake-id', side: Side.SELL, @@ -577,7 +582,7 @@ void test('test stop_limit order', ({ equal, end }) => { stopPrice: ob.marketPrice, price: ob.marketPrice }) // Same as market price - equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidStopPrice) + equal(wrongStopPrice2.err?.message, ERROR.ErrInvalidConditionalOrder) // Add a stop limit BUY order const beforeMarketPrice = ob.marketPrice @@ -631,6 +636,193 @@ void test('test stop_limit order', ({ equal, end }) => { end() }) +/** + * OCO Order: + * Buy: price < marketPrice < stopPrice + * Sell: price > marketPrice > stopPrice + */ +void test('test oco order', ({ equal, end }) => { + const ob = new OrderBook() + + 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() +}) + void test('test modify', ({ equal, end }) => { const ob = new OrderBook() diff --git a/test/orderside.test.ts b/test/orderside.test.ts index 0299023..5217095 100644 --- a/test/orderside.test.ts +++ b/test/orderside.test.ts @@ -139,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) @@ -287,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() +}) From 606a794f2df88592eb5b9630a8c1b6e918a150ed Mon Sep 17 00:00:00 2001 From: fasenderos Date: Sun, 28 Jul 2024 02:41:12 +0200 Subject: [PATCH 15/17] chore(release): hft-limit-order-book@6.1.0-beta.1 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 600d344..4ec6249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # 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) diff --git a/package-lock.json b/package-lock.json index 2df9f1a..27d17c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hft-limit-order-book", - "version": "6.1.0-beta.0", + "version": "6.1.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "hft-limit-order-book", - "version": "6.1.0-beta.0", + "version": "6.1.0-beta.1", "license": "MIT", "dependencies": { "denque": "2.1.0", diff --git a/package.json b/package.json index b82335d..ee80528 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hft-limit-order-book", - "version": "6.1.0-beta.0", + "version": "6.1.0-beta.1", "description": "Node.js Lmit Order Book for high-frequency trading (HFT).", "author": "Andrea Fassina ", "license": "MIT", From 55d3e11b4b486391c4f9f4647d90d14af3712623 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Thu, 1 Aug 2024 10:17:28 +0200 Subject: [PATCH 16/17] feat: new experimentalConditionalOrders order book option --- README.md | 87 +++++++++++++++++++++++------------------- src/orderbook.ts | 15 +++++++- src/types.ts | 5 +++ test/orderbook.test.ts | 6 +-- 4 files changed, 69 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index e5f3bab..1c326c9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,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) +- 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 @@ -51,50 +51,59 @@ 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' | 'stop_limit' | 'stop_market' | 'oco', side: 'buy' | 'sell', size: number, price?: number, id?: string, stopPrice?: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +ob.createOrder({ type: 'limit' | 'market', side: 'buy' | 'sell', size: number, price?: number, id?: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) -lob.limit({ id: string, side: 'buy' | 'sell', size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +ob.limit({ id: string, side: 'buy' | 'sell', size: number, price: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) -lob.market({ side: 'buy' | 'sell', size: number }) +ob.market({ side: 'buy' | 'sell', size: number }) -lob.stopLimit({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +ob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number }) -lob.oco({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' }) +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' + +const ob = new OrderBook({ experimentalConditionalOrders: true }) + +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.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) +ob.stopLimit({ id: string, side: 'buy' | 'sell', size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) -lob.modify(orderID: string, { side: 'buy' | 'sell', size: number, price: number }) +ob.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) -lob.cancel(orderID: string) +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()`, `market()`, `stopLimit()` or `stopMarket()` 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 limit order -createOrder({ type: 'limit', side: 'buy' | 'sell', size: number, price: number, id: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +ob.createOrder({ type: 'limit', side: 'buy' | 'sell', size: number, price: number, id: string, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) // Create market order -createOrder({ type: 'market', side: 'buy' | 'sell', size: number }) +ob.createOrder({ type: 'market', side: 'buy' | 'sell', size: number }) // Create stop limit order -createOrder({ type: 'stop_limit', side: 'buy' | 'sell', size: number, price: number, id: string, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +ob.createOrder({ type: 'stop_limit', side: 'buy' | 'sell', size: number, price: number, id: string, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) // Create stop market order -createOrder({ type: 'stop_market', side: 'buy' | 'sell', size: number, stopPrice: number }) +ob.createOrder({ type: 'stop_market', side: 'buy' | 'sell', size: number, stopPrice: number }) // Create OCO order -createOrder({ type: 'oco', side: 'buy' | 'sell', size: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' }) +ob.createOrder({ type: 'oco', side: 'buy' | 'sell', size: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' }) ``` ### Create Limit Order @@ -111,13 +120,13 @@ createOrder({ type: 'oco', side: 'buy' | 'sell', size: number, stopPrice: number * @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', id: 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({ side: "sell", id: "uniqueID", size: 55, price: 100 }) +ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 }) asks: 110 -> 5 110 -> 5 100 -> 1 100 -> 56 @@ -130,7 +139,7 @@ partial - null ``` ``` -limit({ side: "buy", id: "uniqueID", size: 7, price: 120 }) +ob.limit({ side: "buy", id: "uniqueID", size: 7, price: 120 }) asks: 110 -> 5 100 -> 1 @@ -144,7 +153,7 @@ partial - uniqueID order ``` ``` -limit({ side: "buy", id: "uniqueID", size: 3, price: 120 }) +ob.limit({ side: "buy", id: "uniqueID", size: 3, price: 120 }) asks: 110 -> 5 100 -> 1 110 -> 3 @@ -167,13 +176,13 @@ partial - 1 order with price 110 * @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({ side: 'sell', size: 6 }) +ob.market({ side: 'sell', size: 6 }) asks: 110 -> 5 110 -> 5 100 -> 1 100 -> 1 @@ -187,7 +196,7 @@ quantityLeft - 0 ``` ``` -market({ side: 'buy', size: 10 }) +ob.market({ side: 'buy', size: 10 }) asks: 110 -> 5 100 -> 1 @@ -215,7 +224,7 @@ quantityLeft - 4 * @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 */ -stopLimit({ side: 'buy' | 'sell', id: string, size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) +ob.stopLimit({ side: 'buy' | 'sell', id: string, size: number, price: number, stopPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC' }) ``` ### Create Stop Market Order @@ -230,7 +239,7 @@ stopLimit({ side: 'buy' | 'sell', id: string, size: number, price: number, stopP * @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 */ -stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) +ob.stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) ``` ### Create OCO (One-Cancels-the-Other) Order @@ -259,7 +268,7 @@ stopMarket({ side: 'buy' | 'sell', size: number, stopPrice: number }) * @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 */ -oco({ side: 'buy' | 'sell', id: string, size: number, price: number, stopPrice: number, stopLimitPrice: number, timeInForce?: 'GTC' | 'FOK' | 'IOC', stopLimitTimeInForce?: 'GTC' | 'FOK' | 'IOC' }) +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 @@ -275,13 +284,13 @@ oco({ side: 'buy' | 'sell', id: string, size: number, price: number, stopPrice: * @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({ side: "sell", id: "uniqueID", size: 55, price: 100 }) +ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 }) asks: 110 -> 5 110 -> 5 100 -> 1 100 -> 56 @@ -290,7 +299,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 @@ -300,7 +309,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 @@ -318,13 +327,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 @@ -349,15 +358,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({ side: "sell", id: "uniqueID", size: 55, price: 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 @@ -367,7 +376,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 @@ -376,17 +385,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({ side: "sell", id: "uniqueID", size: 55, price: 100 }) +const order = ob.limit({ side: "sell", id: "uniqueID", size: 55, price: 100 }) await saveLog(order.log) ``` diff --git a/src/orderbook.ts b/src/orderbook.ts index d9d32e9..8ee94d5 100644 --- a/src/orderbook.ts +++ b/src/orderbook.ts @@ -39,22 +39,26 @@ export class OrderBook { 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) @@ -240,6 +244,8 @@ export class OrderBook { * @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) } @@ -321,6 +327,8 @@ export class OrderBook { * @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) } @@ -348,6 +356,8 @@ export class OrderBook { * @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) } @@ -834,6 +844,7 @@ export class OrderBook { priceBefore: number, response: IProcessOrder ): void => { + if (!this.experimentalConditionalOrders) return const pendingOrders = this.stopBook.getConditionalOrders( side, priceBefore, @@ -996,7 +1007,7 @@ export class OrderBook { } } // Remove linked OCO Stop Order if any - if (headOrder.ocoStopPrice !== undefined) { + if (this.experimentalConditionalOrders && headOrder.ocoStopPrice !== undefined) { this.stopBook.remove( headOrder.side, headOrder.id, diff --git a/src/types.ts b/src/types.ts index 7f80b1b..7c6e057 100644 --- a/src/types.ts +++ b/src/types.ts @@ -294,6 +294,11 @@ export interface OrderBookOptions { enableJournaling?: boolean /** Array of journal logs. */ journal?: JournalLog[] + /** + * Flag to enable experimental Conditional Order (Stop Market, Stop Limit and OCO orders). + * Default to false + */ + experimentalConditionalOrders?: boolean } /** diff --git a/test/orderbook.test.ts b/test/orderbook.test.ts index 1fe2da4..ff89801 100644 --- a/test/orderbook.test.ts +++ b/test/orderbook.test.ts @@ -356,7 +356,7 @@ void test('createOrder error', ({ equal, end }) => { * Sell: marketPrice > stopPrice */ void test('test stop_market order', ({ equal, end }) => { - const ob = new OrderBook() + const ob = new OrderBook({ experimentalConditionalOrders: true }) addDepth(ob, '', 2) // We need to create at least on maket order in order to set @@ -477,7 +477,7 @@ void test('test stop_market order', ({ equal, end }) => { * Sell: marketPrice > stopPrice >= price */ void test('test stop_limit order', ({ equal, end }) => { - const ob = new OrderBook() + const ob = new OrderBook({ experimentalConditionalOrders: true }) addDepth(ob, '', 2) // We need to create at least on maket order in order to set @@ -642,7 +642,7 @@ void test('test stop_limit order', ({ equal, end }) => { * Sell: price > marketPrice > stopPrice */ void test('test oco order', ({ equal, end }) => { - const ob = new OrderBook() + const ob = new OrderBook({ experimentalConditionalOrders: true }) addDepth(ob, '', 2) // We need to create at least on maket order in order to set From 95dcf429f05defcdf5ad45b2877fc10311fc7041 Mon Sep 17 00:00:00 2001 From: fasenderos Date: Thu, 1 Aug 2024 10:19:03 +0200 Subject: [PATCH 17/17] docs: update readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1c326c9..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!