Skip to content

Commit

Permalink
feat: add error codes
Browse files Browse the repository at this point in the history
  • Loading branch information
fasenderos committed Aug 2, 2024
1 parent e1b5825 commit 5a538d5
Show file tree
Hide file tree
Showing 17 changed files with 169 additions and 116 deletions.
71 changes: 46 additions & 25 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IError } from './types'

export enum ERROR {
DEFAULT = 'DEFAULT',
INSUFFICIENT_QUANTITY = 'INSUFFICIENT_QUANTITY',
Expand All @@ -16,32 +18,50 @@ export enum ERROR {
ORDER_NOT_FOUND = 'ORDER_NOT_FOUND',
}

export const ErrorCodes: Record<ERROR, number> = {
// 10xx General issues
[ERROR.DEFAULT]: 1000,

// 11xx Request issues
[ERROR.INVALID_ORDER_TYPE]: 1100,
[ERROR.INVALID_SIDE]: 1101,
[ERROR.INVALID_QUANTITY]: 1102,
[ERROR.INVALID_PRICE]: 1103,
[ERROR.INVALID_PRICE_OR_QUANTITY]: 1104,
[ERROR.INVALID_TIF]: 1105,
[ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE]: 1106,
[ERROR.LIMIT_ORDER_POST_ONLY]: 1107,
[ERROR.INVALID_CONDITIONAL_ORDER]: 1108,
[ERROR.ORDER_ALREDY_EXISTS]: 1109,
[ERROR.ORDER_NOT_FOUND]: 1110,

// 12xx Internal error
[ERROR.INSUFFICIENT_QUANTITY]: 1200,
[ERROR.INVALID_PRICE_LEVEL]: 1201,
[ERROR.INVALID_JOURNAL_LOG]: 1201
}

export const ErrorMessages: Record<ERROR, string> = {
[ERROR.DEFAULT]: 'Something wrong',
[ERROR.INSUFFICIENT_QUANTITY]:
'orderbook: insufficient quantity to calculate price',
[ERROR.INVALID_CONDITIONAL_ORDER]:
'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)',
[ERROR.INVALID_ORDER_TYPE]:
"orderbook: supported order type are 'limit' and 'market'",
[ERROR.INVALID_PRICE]: 'orderbook: invalid order price',
[ERROR.INVALID_PRICE_LEVEL]: 'orderbook: invalid order price level',
[ERROR.INVALID_PRICE_OR_QUANTITY]:
'orderbook: invalid order price or quantity',
[ERROR.INVALID_QUANTITY]: 'orderbook: invalid order quantity',
[ERROR.INVALID_SIDE]: "orderbook: given neither 'bid' nor 'ask'",
[ERROR.INVALID_TIF]:
"orderbook: supported time in force are 'GTC', 'IOC' and 'FOK'",
[ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE]:
'orderbook: limit FOK order not fillable',
[ERROR.LIMIT_ORDER_POST_ONLY]:
'orderbook: Post-only order rejected because would execute immediately',
[ERROR.ORDER_ALREDY_EXISTS]: 'orderbook: order already exists',
[ERROR.ORDER_NOT_FOUND]: 'orderbook: order not found',
[ERROR.INVALID_JOURNAL_LOG]: 'journal: invalid journal log format'
[ERROR.INSUFFICIENT_QUANTITY]: 'Insufficient quantity to calculate price',
[ERROR.INVALID_CONDITIONAL_ORDER]: 'Stop-Limit Order (BUY: marketPrice < stopPrice <= price, SELL: marketPrice > stopPrice >= price). Stop-Market Order (BUY: marketPrice < stopPrice, SELL: marketPrice > stopPrice). OCO order (BUY: price < marketPrice < stopPrice, SELL: price > marketPrice > stopPrice)',
[ERROR.INVALID_ORDER_TYPE]: "Supported order type are 'limit' and 'market'",
[ERROR.INVALID_PRICE]: 'Invalid order price',
[ERROR.INVALID_PRICE_LEVEL]: 'Invalid order price level',
[ERROR.INVALID_PRICE_OR_QUANTITY]: 'Invalid order price or quantity',
[ERROR.INVALID_QUANTITY]: 'Invalid order quantity',
[ERROR.INVALID_SIDE]: "Invalid side: must be either 'sell' or 'buy'",
[ERROR.INVALID_TIF]: "Invalid TimeInForce: must be one of 'GTC', 'IOC' or 'FOK'",
[ERROR.LIMIT_ORDER_FOK_NOT_FILLABLE]: 'Limit FOK order not fillable',
[ERROR.LIMIT_ORDER_POST_ONLY]: 'Post-only limit order rejected because would execute immediately',
[ERROR.ORDER_ALREDY_EXISTS]: 'Order already exists',
[ERROR.ORDER_NOT_FOUND]: 'Order not found',
[ERROR.INVALID_JOURNAL_LOG]: 'Invalid journal log format'
}

class CustomErrorFactory extends Error {
export class OrderBookError implements IError {
message: string
code: number
constructor (error?: ERROR | string) {
let errorMessage: string
if (error != null && ErrorMessages[error as ERROR] != null) {
Expand All @@ -50,10 +70,11 @@ class CustomErrorFactory extends Error {
const customMessage = error === undefined || error === '' ? '' : `: ${error}`
errorMessage = `${ErrorMessages.DEFAULT}${customMessage}`
}
super(errorMessage)
this.message = errorMessage
this.code = ErrorCodes[error as ERROR ?? ERROR.DEFAULT]
}
}

export const CustomError = (error?: ERROR | string): Error => {
return new CustomErrorFactory(error)
export const CustomError = (error?: ERROR | string): OrderBookError => {
return new OrderBookError(error)
}
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { OrderBook } from './orderbook'
import { Side } from './side'
import { OrderType } from './types'
import { OrderType, Side } from './types'
import type {
CreateOrderOptions,
ICancelOrder,
Expand Down
6 changes: 3 additions & 3 deletions src/order.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CustomError, ERROR } from './errors'
import type { Side } from './side'
import {
ILimitOrder,
IStopLimitOrder,
Expand All @@ -9,9 +8,9 @@ import {
InternalStopLimitOrderOptions,
InternalStopMarketOrderOptions,
TimeInForce,
OrderType
OrderType,
type Side
} from './types'

import { randomUUID } from 'node:crypto'

abstract class BaseOrder {
Expand Down Expand Up @@ -284,6 +283,7 @@ export const OrderFactory = {
case OrderType.STOP_MARKET:
return new StopMarketOrder(options) as any
default:
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw CustomError(ERROR.INVALID_ORDER_TYPE)
}
}
Expand Down
14 changes: 10 additions & 4 deletions src/orderbook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ERROR, CustomError } from './errors'
import { ERROR, CustomError, type OrderBookError } from './errors'
import {
LimitOrder,
OrderFactory,
Expand All @@ -7,7 +7,6 @@ import {
} from './order'
import { OrderQueue } from './orderqueue'
import { OrderSide } from './orderside'
import { Side } from './side'
import { StopBook } from './stopbook'
import {
Order,
Expand All @@ -26,7 +25,8 @@ import {
LimitOrderOptions,
StopLimitOrderOptions,
OCOOrderOptions,
StopOrder
StopOrder,
Side
} from './types'

const validTimeInForce = Object.values(TimeInForce)
Expand Down Expand Up @@ -65,6 +65,7 @@ export class OrderBook {
}
// Than replay from journal log
if (journal != null) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (!Array.isArray(journal)) throw CustomError(ERROR.INVALID_JOURNAL_LOG)
// If a snapshot is available be sure to remove logs before the last restored operation
if (snapshot != null && snapshot.lastOp > 0) {
Expand Down Expand Up @@ -477,7 +478,7 @@ export class OrderBook {
size: number
): {
price: number
err: null | Error
err: null | OrderBookError
} => {
let price = 0
let err = null
Expand Down Expand Up @@ -918,6 +919,7 @@ export class OrderBook {
case 'm': {
const { side, size } = log.o
if (side == null || size == null) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw CustomError(ERROR.INVALID_JOURNAL_LOG)
}
this.market({ side, size })
Expand All @@ -926,6 +928,7 @@ export class OrderBook {
case 'l': {
const { side, id, size, price, timeInForce } = log.o
if (side == null || id == null || size == null || price == null) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw CustomError(ERROR.INVALID_JOURNAL_LOG)
}
this.limit({
Expand All @@ -938,16 +941,19 @@ export class OrderBook {
break
}
case 'd':
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (log.o.orderID == null) throw CustomError(ERROR.INVALID_JOURNAL_LOG)
this.cancel(log.o.orderID)
break
case 'u':
if (log.o.orderID == null || log.o.orderUpdate == null) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw CustomError(ERROR.INVALID_JOURNAL_LOG)
}
this.modify(log.o.orderID, log.o.orderUpdate)
break
default:
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw CustomError(ERROR.INVALID_JOURNAL_LOG)
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/orderside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import createRBTree from 'functional-red-black-tree'
import { CustomError, ERROR } from './errors'
import { LimitOrder, OrderFactory } from './order'
import { OrderQueue } from './orderqueue'
import { Side } from './side'
import type { OrderUpdatePrice, OrderUpdateSize } from './types'
import {
Side,
type OrderUpdatePrice,
type OrderUpdateSize
} from './types'

export class OrderSide {
private _priceTree: createRBTree.Tree<number, OrderQueue>
Expand Down Expand Up @@ -69,6 +72,7 @@ export class OrderSide {
const price = order.price
const strPrice = price.toString()
if (this._prices[strPrice] === undefined) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw CustomError(ERROR.INVALID_PRICE_LEVEL)
}
this._prices[strPrice].remove(order)
Expand Down
4 changes: 0 additions & 4 deletions src/side.ts

This file was deleted.

3 changes: 1 addition & 2 deletions src/stopbook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Side } from './side'
import { StopQueue } from './stopqueue'
import { StopSide } from './stopside'
import { OrderType, StopOrder } from './types'
import { OrderType, Side, StopOrder } from './types'

export class StopBook {
private readonly bids: StopSide
Expand Down
4 changes: 2 additions & 2 deletions src/stopside.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import createRBTree from 'functional-red-black-tree'
import { Side } from './side'
import { StopQueue } from './stopqueue'
import { StopOrder } from './types'
import { Side, StopOrder } from './types'
import { CustomError, ERROR } from './errors'

export class StopSide {
Expand Down Expand Up @@ -34,6 +33,7 @@ export class StopSide {
remove = (id: string, stopPrice: number): StopOrder | undefined => {
const strPrice = stopPrice.toString()
if (this._prices[strPrice] === undefined) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw CustomError(ERROR.INVALID_PRICE_LEVEL)
}
const deletedOrder = this._prices[strPrice].remove(id)
Expand Down
15 changes: 13 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { OrderBookError } from './errors'
import { LimitOrder, StopLimitOrder, StopMarketOrder } from './order'
import type { Side } from './side'

export enum Side {
BUY = 'buy',
SELL = 'sell',
}

export enum OrderType {
LIMIT = 'limit',
Expand All @@ -15,6 +20,11 @@ export enum TimeInForce {
FOK = 'FOK',
}

export interface IError {
code: number
message: string
}

export type StopOrder = StopLimitOrder | StopMarketOrder
export type Order = LimitOrder | StopOrder

Expand Down Expand Up @@ -158,6 +168,7 @@ export type StopOrderOptions =
| StopMarketOrderOptions
| StopLimitOrderOptions
| OCOOrderOptions

/**
* Represents the result of processing an order.
*/
Expand All @@ -173,7 +184,7 @@ export interface IProcessOrder {
/** The remaining quantity that needs to be processed. */
quantityLeft: number
/** The error encountered during order processing, if any. */
err: Error | null
err: OrderBookError | null
/** Optional journal log entry related to the order processing. */
log?: JournalLog
}
Expand Down
4 changes: 3 additions & 1 deletion test/error.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { test } from 'tap'
import { CustomError, ErrorMessages } from '../src/errors'
import { CustomError, ErrorCodes, ErrorMessages } from '../src/errors'

void test('Test default CustomError', ({ equal, end }) => {
const a = CustomError()
equal(a.message, ErrorMessages.DEFAULT)
equal(a.code, ErrorCodes.DEFAULT)
const b = CustomError('foo')
equal(b.message, `${ErrorMessages.DEFAULT}: foo`)
equal(a.code, ErrorCodes.DEFAULT)
end()
})
10 changes: 4 additions & 6 deletions test/order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {
StopLimitOrder,
StopMarketOrder
} from '../src/order'
import { Side } from '../src/side'
import { OrderType, TimeInForce } from '../src/types'
import { ErrorMessages } from '../src/errors'
import { OrderType, Side, TimeInForce } from '../src/types'
import { ErrorCodes, ErrorMessages } from '../src/errors'

void test('it should create LimitOrder', ({ equal, same, end }) => {
const id = 'fakeId'
Expand Down Expand Up @@ -477,9 +476,8 @@ void test('test invalid order type', (t) => {
timeInForce
})
} catch (error) {
if (error instanceof Error) {
t.equal(error?.message, ErrorMessages.INVALID_ORDER_TYPE)
}
t.equal(error?.message, ErrorMessages.INVALID_ORDER_TYPE)
t.equal(error?.code, ErrorCodes.INVALID_ORDER_TYPE)
}
t.end()
})
Loading

0 comments on commit 5a538d5

Please sign in to comment.