Skip to content

Commit e1fadad

Browse files
authored
Merge pull request #473 from fasenderos/response-order-object
feat: order now will be returned as object BREAKING CHANGE: The OrderBook previously returned an instance of the BaseOrder class (new BaseOrder()) for its orders. With this update, it now returns the underlying plain object instead
2 parents ed47ccb + 2e905e3 commit e1fadad

12 files changed

+298
-149
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@ A `snapshot` represents the state of the order book at a specific point in time.
393393
Snapshots are crucial for restoring the order book to a previous state. The orderbook can restore from a snapshot before processing any journal logs, ensuring consistency and accuracy.
394394
After taking the snapshot, you can safely remove all logs preceding the `lastOp` id.
395395

396+
**Note**: The snapshot of the order book returns an object containing an `array` of `bids` and `asks`, which in turn are arrays of order objects. If the snapshot is saved to the database as a `string`, make sure to pass the snapshot in its original format when initializing the order book. For example, you can achieve this by using `JSON.parse` to convert the string back into its original object form.
397+
396398
```js
397399
const ob = new OrderBook({ enableJournaling: true})
398400

src/order.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type Side,
1414
type TimeInForce,
1515
} from "./types";
16+
import { safeStringify } from "./utils";
1617

1718
abstract class BaseOrder {
1819
readonly _id: string;
@@ -141,7 +142,7 @@ export class LimitOrder extends BaseOrder {
141142
makerQty: ${this._makerQty}
142143
takerQty: ${this._takerQty}`;
143144

144-
toJSON = (): string => JSON.stringify(this.toObject());
145+
toJSON = (): string | null => safeStringify(this.toObject());
145146

146147
toObject = (): ILimitOrder => ({
147148
id: this._id,
@@ -184,7 +185,7 @@ export class StopMarketOrder extends BaseOrder {
184185
stopPrice: ${this._stopPrice}
185186
time: ${this._time}`;
186187

187-
toJSON = (): string => JSON.stringify(this.toObject());
188+
toJSON = (): string | null => safeStringify(this.toObject());
188189

189190
toObject = (): IStopMarketOrder => ({
190191
id: this._id,
@@ -249,10 +250,11 @@ export class StopLimitOrder extends BaseOrder {
249250
size: ${this._size}
250251
price: ${this._price}
251252
stopPrice: ${this._stopPrice}
253+
isOCO: ${this.isOCO}
252254
timeInForce: ${this._timeInForce}
253255
time: ${this._time}`;
254256

255-
toJSON = (): string => JSON.stringify(this.toObject());
257+
toJSON = (): string | null => safeStringify(this.toObject());
256258

257259
toObject = (): IStopLimitOrder => ({
258260
id: this._id,
@@ -261,6 +263,7 @@ export class StopLimitOrder extends BaseOrder {
261263
size: this._size,
262264
price: this._price,
263265
stopPrice: this._stopPrice,
266+
isOCO: this.isOCO,
264267
timeInForce: this._timeInForce,
265268
time: this._time,
266269
});

src/orderbook.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import { StopBook } from "./stopbook";
1212
import {
1313
type CreateOrderOptions,
1414
type ICancelOrder,
15+
type ILimitOrder,
16+
type IOrder,
1517
type IProcessOrder,
1618
type JournalLog,
1719
type LimitOrderOptions,
1820
type MarketOrderOptions,
1921
type OCOOrderOptions,
20-
type Order,
2122
type OrderBookOptions,
2223
OrderType,
2324
type OrderUpdatePrice,
@@ -346,8 +347,8 @@ export class OrderBook {
346347
* @param orderID - The ID of the order to be returned
347348
* @returns The order if exists or `undefined`
348349
*/
349-
public order = (orderID: string): LimitOrder | undefined => {
350-
return this.orders[orderID];
350+
public order = (orderID: string): ILimitOrder | undefined => {
351+
return this.orders[orderID]?.toObject();
351352
};
352353

353354
// Returns price levels and volume at price level
@@ -412,13 +413,13 @@ export class OrderBook {
412413
};
413414

414415
public snapshot = (): Snapshot => {
415-
const bids: Array<{ price: number; orders: LimitOrder[] }> = [];
416-
const asks: Array<{ price: number; orders: LimitOrder[] }> = [];
416+
const bids: Array<{ price: number; orders: ILimitOrder[] }> = [];
417+
const asks: Array<{ price: number; orders: ILimitOrder[] }> = [];
417418
this.bids.priceTree().forEach((price: number, orders: OrderQueue) => {
418-
bids.push({ price, orders: orders.toArray() });
419+
bids.push({ price, orders: orders.toArray().map((o) => o.toObject()) });
419420
});
420421
this.asks.priceTree().forEach((price: number, orders: OrderQueue) => {
421-
asks.push({ price, orders: orders.toArray() });
422+
asks.push({ price, orders: orders.toArray().map((o) => o.toObject()) });
422423
});
423424
return { bids, asks, ts: Date.now(), lastOp: this._lastOp };
424425
};
@@ -534,7 +535,7 @@ export class OrderBook {
534535
isOCO: true,
535536
});
536537
this.stopBook.add(stopLimit);
537-
response.done.push(stopLimit);
538+
response.done.push(stopLimit.toObject());
538539
} else {
539540
response.err = CustomError(ERROR.INVALID_CONDITIONAL_ORDER);
540541
}
@@ -547,7 +548,7 @@ export class OrderBook {
547548
): IProcessOrder => {
548549
if (this.stopBook.validConditionalOrder(this._marketPrice, stopOrder)) {
549550
this.stopBook.add(stopOrder);
550-
response.done.push(stopOrder);
551+
response.done.push(stopOrder.toObject());
551552
} else {
552553
response.err = CustomError(ERROR.INVALID_CONDITIONAL_ORDER);
553554
}
@@ -558,15 +559,17 @@ export class OrderBook {
558559
this._lastOp = snapshot.lastOp;
559560
for (const level of snapshot.bids) {
560561
for (const order of level.orders) {
561-
this.orders[order.id] = order;
562-
this.bids.append(order);
562+
const newOrder = OrderFactory.createOrder(order);
563+
this.orders[newOrder.id] = newOrder;
564+
this.bids.append(newOrder);
563565
}
564566
}
565567

566568
for (const level of snapshot.asks) {
567569
for (const order of level.orders) {
568-
this.orders[order.id] = order;
569-
this.asks.append(order);
570+
const newOrder = OrderFactory.createOrder(order);
571+
this.orders[newOrder.id] = newOrder;
572+
this.asks.append(newOrder);
570573
}
571574
}
572575
};
@@ -586,16 +589,14 @@ export class OrderBook {
586589
delete this.orders[orderID];
587590
const side = order.side === Side.BUY ? this.bids : this.asks;
588591
const response: ICancelOrder = {
589-
order: side.remove(order),
592+
order: side.remove(order)?.toObject(),
590593
};
591594

592595
// Delete OCO Order only when the delete request comes from user
593596
if (!internalDeletion && order.ocoStopPrice !== undefined) {
594-
response.stopOrder = this.stopBook.remove(
595-
order.side,
596-
orderID,
597-
order.ocoStopPrice,
598-
);
597+
response.stopOrder = this.stopBook
598+
.remove(order.side, orderID, order.ocoStopPrice)
599+
?.toObject();
599600
}
600601

601602
if (this.enableJournaling) {
@@ -698,16 +699,16 @@ export class OrderBook {
698699
});
699700
if (response.done.length > 0) {
700701
response.partialQuantityProcessed = size - quantityToTrade;
701-
response.partial = order;
702+
response.partial = order.toObject();
702703
}
703704
this.orders[orderID] = sideToAdd.append(order);
704705
} else {
705706
let totalQuantity = 0;
706707
let totalPrice = 0;
707708

708-
response.done.forEach((order: Order) => {
709+
response.done.forEach((order: IOrder) => {
709710
totalQuantity += order.size;
710-
totalPrice += (order as LimitOrder).price * order.size;
711+
totalPrice += (order as ILimitOrder).price * order.size;
711712
});
712713

713714
if (response.partialQuantityProcessed > 0 && response.partial !== null) {
@@ -728,7 +729,7 @@ export class OrderBook {
728729
takerQty,
729730
makerQty,
730731
});
731-
response.done.push(order);
732+
response.done.push(order.toObject());
732733
}
733734

734735
// If IOC order was not matched completely remove from the order book
@@ -787,7 +788,7 @@ export class OrderBook {
787788
response,
788789
);
789790
}
790-
response.activated.push(stopOrder);
791+
response.activated.push(stopOrder.toObject());
791792
});
792793
}
793794
};
@@ -914,13 +915,14 @@ export class OrderBook {
914915
const headOrder = orderQueue.head();
915916
if (headOrder !== undefined) {
916917
if (response.quantityLeft < headOrder.size) {
917-
response.partial = OrderFactory.createOrder({
918+
const partial = OrderFactory.createOrder({
918919
...headOrder.toObject(),
919920
size: headOrder.size - response.quantityLeft,
920921
});
921-
this.orders[headOrder.id] = response.partial;
922+
response.partial = partial.toObject();
923+
this.orders[headOrder.id] = partial;
922924
response.partialQuantityProcessed = response.quantityLeft;
923-
orderQueue.update(headOrder, response.partial);
925+
orderQueue.update(headOrder, partial);
924926
response.quantityLeft = 0;
925927
} else {
926928
response.quantityLeft = response.quantityLeft - headOrder.size;

src/types.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export interface IStopLimitOrder {
152152
stopPrice: number;
153153
timeInForce: TimeInForce;
154154
time: number;
155+
isOCO: boolean;
155156
}
156157

157158
/**
@@ -168,16 +169,19 @@ export type StopOrderOptions =
168169
| StopLimitOrderOptions
169170
| OCOOrderOptions;
170171

172+
export type IStopOrder = IStopLimitOrder | IStopMarketOrder;
173+
export type IOrder = ILimitOrder | IStopOrder;
174+
171175
/**
172176
* Represents the result of processing an order.
173177
*/
174178
export interface IProcessOrder {
175179
/** Array of fully processed orders. */
176-
done: Order[];
180+
done: IOrder[];
177181
/** Array of activated (stop limit or stop market) orders */
178-
activated: StopOrder[];
182+
activated: IStopOrder[];
179183
/** The partially processed order, if any. */
180-
partial: LimitOrder | null;
184+
partial: ILimitOrder | null;
181185
/** The quantity that has been processed in the partial order. */
182186
partialQuantityProcessed: number;
183187
/** The remaining quantity that needs to be processed. */
@@ -331,8 +335,8 @@ export type CreateOrderOptions =
331335
* Represents a cancel order operation.
332336
*/
333337
export interface ICancelOrder {
334-
order: LimitOrder;
335-
stopOrder?: StopOrder;
338+
order: ILimitOrder;
339+
stopOrder?: IStopOrder;
336340
/** Optional log related to the order cancellation. */
337341
log?: CancelOrderJournalLog;
338342
}
@@ -386,14 +390,14 @@ export interface Snapshot {
386390
/** Price of the ask order */
387391
price: number;
388392
/** List of orders associated with this price */
389-
orders: LimitOrder[];
393+
orders: ILimitOrder[];
390394
}>;
391395
/** List of bid orders, each with a price and a list of associated orders */
392396
bids: Array<{
393397
/** Price of the bid order */
394398
price: number;
395399
/** List of orders associated with this price */
396-
orders: LimitOrder[];
400+
orders: ILimitOrder[];
397401
}>;
398402
/** Unix timestamp representing when the snapshot was taken */
399403
ts: number;

src/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* node:coverage ignore next - Don't know why this line is uncovered */
2+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
3+
export const safeStringify = (value: any): string | null => {
4+
try {
5+
return JSON.stringify(value);
6+
} catch (error) {
7+
return null;
8+
}
9+
};
10+
11+
/* node:coverage ignore next - Don't know why this line is uncovered */
12+
export const safeParse = <T>(value: string): T | null => {
13+
try {
14+
return JSON.parse(value);
15+
} catch (error) {
16+
return null;
17+
}
18+
};

test/order.test.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void test("it should create LimitOrder", () => {
4545
assert.equal(order.makerQty, size);
4646
assert.equal(order.takerQty, 0);
4747
assert.equal(order.ocoStopPrice, undefined);
48-
assert.deepEqual(order.toObject(), {
48+
assert.deepStrictEqual(order.toObject(), {
4949
id,
5050
type,
5151
side,
@@ -115,7 +115,7 @@ void test("it should create LimitOrder", () => {
115115
assert.equal(order.makerQty, size);
116116
assert.equal(order.takerQty, 0);
117117
assert.equal(order.ocoStopPrice, ocoStopPrice);
118-
assert.deepEqual(order.toObject(), {
118+
assert.deepStrictEqual(order.toObject(), {
119119
id,
120120
type,
121121
side,
@@ -181,7 +181,7 @@ void test("it should create StopMarketOrder", () => {
181181
assert.equal(order.size, size);
182182
assert.equal(order.stopPrice, stopPrice);
183183
assert.equal(order.time, time);
184-
assert.deepEqual(order.toObject(), {
184+
assert.deepStrictEqual(order.toObject(), {
185185
id,
186186
type,
187187
side,
@@ -242,13 +242,14 @@ void test("it should create StopLimitOrder", () => {
242242
assert.equal(order.timeInForce, timeInForce);
243243
assert.equal(order.time, time);
244244
assert.equal(order.isOCO, false);
245-
assert.deepEqual(order.toObject(), {
245+
assert.deepStrictEqual(order.toObject(), {
246246
id,
247247
type,
248248
side,
249249
size,
250250
price,
251251
stopPrice,
252+
isOCO: false,
252253
timeInForce,
253254
time,
254255
});
@@ -260,6 +261,7 @@ void test("it should create StopLimitOrder", () => {
260261
size: ${size}
261262
price: ${price}
262263
stopPrice: ${stopPrice}
264+
isOCO: false
263265
timeInForce: ${timeInForce}
264266
time: ${time}`,
265267
);
@@ -272,6 +274,7 @@ void test("it should create StopLimitOrder", () => {
272274
size,
273275
price,
274276
stopPrice,
277+
isOCO: false,
275278
timeInForce,
276279
time,
277280
}),
@@ -306,13 +309,14 @@ void test("it should create StopLimitOrder", () => {
306309
assert.equal(order.timeInForce, timeInForce);
307310
assert.equal(order.time, time);
308311
assert.equal(order.isOCO, true);
309-
assert.deepEqual(order.toObject(), {
312+
assert.deepStrictEqual(order.toObject(), {
310313
id,
311314
type,
312315
side,
313316
size,
314317
price,
315318
stopPrice,
319+
isOCO: true,
316320
timeInForce,
317321
time,
318322
});
@@ -324,6 +328,7 @@ void test("it should create StopLimitOrder", () => {
324328
size: ${size}
325329
price: ${price}
326330
stopPrice: ${stopPrice}
331+
isOCO: true
327332
timeInForce: ${timeInForce}
328333
time: ${time}`,
329334
);
@@ -336,6 +341,7 @@ void test("it should create StopLimitOrder", () => {
336341
size,
337342
price,
338343
stopPrice,
344+
isOCO: true,
339345
timeInForce,
340346
time,
341347
}),
@@ -376,7 +382,7 @@ void test("it should create order without passing a date or id", (t) => {
376382
});
377383
assert.equal(order.id, fakeId);
378384
assert.equal(order.time, fakeTimestamp);
379-
assert.deepEqual(order.toObject(), {
385+
assert.deepStrictEqual(order.toObject(), {
380386
id: fakeId,
381387
type,
382388
side,

0 commit comments

Comments
 (0)