Skip to content

Commit f6465b1

Browse files
authored
Merge pull request #474 from fasenderos/snapshot-with-stopbook
feat: add stop orders on snapshot
2 parents c477dbf + c48ae23 commit f6465b1

File tree

9 files changed

+289
-8
lines changed

9 files changed

+289
-8
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ A `snapshot` represents the state of the order book at a specific point in time.
387387

388388
- `asks`: List of ask orders, each with a `price` and a list of associated `orders`.
389389
- `bids`: List of bid orders, each with a `price` and a list of associated `orders`.
390+
- `stopBook`: an object with `bids` and `asks` properties related to every `StopOrder` in the orderbook.
390391
- `ts`: A timestamp indicating when the snapshot was taken, in Unix timestamp format.
391392
- `lastOp`: The id of the last operation included in the snapshot
392393

@@ -405,7 +406,7 @@ await saveLog(order.log)
405406
// ... after some time take a snapshot of the order book and save it on the database
406407

407408
const snapshot = ob.snapshot()
408-
await saveSnapshot(snapshot)
409+
await saveSnapshot(JSON.stringify(snapshot))
409410

410411
// 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
411412
await removePreviousLogs(snapshot.lastOp)
@@ -414,7 +415,7 @@ await removePreviousLogs(snapshot.lastOp)
414415
const logs = await getLogs()
415416
const snapshot = await getSnapshot()
416417

417-
const ob = new OrderBook({ snapshot, journal: log, enableJournaling: true })
418+
const ob = new OrderBook({ snapshot: JSON.parse(snapshot), journal: log, enableJournaling: true })
418419
```
419420

420421
### Journal Logs

src/orderbook.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,8 @@ export class OrderBook {
421421
this.asks.priceTree().forEach((price: number, orders: OrderQueue) => {
422422
asks.push({ price, orders: orders.toArray().map((o) => o.toObject()) });
423423
});
424-
return { bids, asks, ts: Date.now(), lastOp: this._lastOp };
424+
const stopBook = this.stopBook.snapshot();
425+
return { bids, asks, stopBook, ts: Date.now(), lastOp: this._lastOp };
425426
};
426427

427428
private readonly _market = (
@@ -572,6 +573,28 @@ export class OrderBook {
572573
this.asks.append(newOrder);
573574
}
574575
}
576+
577+
if (snapshot.stopBook?.bids?.length > 0) {
578+
for (const level of snapshot.stopBook.bids) {
579+
for (const order of level.orders) {
580+
// @ts-expect-error // TODO fix types
581+
const newOrder = OrderFactory.createOrder(order);
582+
// @ts-expect-error // TODO fix types
583+
this.stopBook.add(newOrder);
584+
}
585+
}
586+
}
587+
588+
if (snapshot.stopBook?.asks?.length > 0) {
589+
for (const level of snapshot.stopBook.asks) {
590+
for (const order of level.orders) {
591+
// @ts-expect-error // TODO fix types
592+
const newOrder = OrderFactory.createOrder(order);
593+
// @ts-expect-error // TODO fix types
594+
this.stopBook.add(newOrder);
595+
}
596+
}
597+
}
575598
};
576599

577600
/**

src/stopbook.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* node:coverage ignore next - Don't know why first and last line of each file count as uncovered */
22
import type { StopQueue } from "./stopqueue";
33
import { StopSide } from "./stopside";
4-
import { OrderType, Side, type StopOrder } from "./types";
4+
import { type IStopOrder, OrderType, Side, type StopOrder } from "./types";
55

66
export class StopBook {
77
private readonly bids: StopSide;
@@ -76,5 +76,17 @@ export class StopBook {
7676
}
7777
return response;
7878
};
79+
80+
snapshot = () => {
81+
const bids: Array<{ price: number; orders: IStopOrder[] }> = [];
82+
const asks: Array<{ price: number; orders: IStopOrder[] }> = [];
83+
this.bids.priceTree().forEach((price: number, orders: StopQueue) => {
84+
bids.push({ price, orders: orders.toArray().map((o) => o.toObject()) });
85+
});
86+
this.asks.priceTree().forEach((price: number, orders: StopQueue) => {
87+
asks.push({ price, orders: orders.toArray().map((o) => o.toObject()) });
88+
});
89+
return { bids, asks };
90+
};
7991
/* node:coverage ignore next - Don't know why first and last line of each file count as uncovered */
8092
}

src/stopqueue.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,9 @@ export class StopQueue {
5252
}
5353
return deletedOrder;
5454
};
55+
56+
toArray = (): StopOrder[] => {
57+
return this._orders.toArray();
58+
};
5559
/* node:coverage ignore next - Don't know why first and last line of each file count as uncovered */
5660
}

src/stopside.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,9 @@ export class StopSide {
7777
);
7878
return queues;
7979
};
80+
81+
priceTree = (): createRBTree.Tree<number, StopQueue> => {
82+
return this._priceTree;
83+
};
8084
/* node:coverage ignore next - Don't know why first and last line of each file count as uncovered */
8185
}

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,16 @@ export interface Snapshot {
399399
/** List of orders associated with this price */
400400
orders: ILimitOrder[];
401401
}>;
402+
stopBook: {
403+
asks: Array<{
404+
price: number;
405+
orders: IStopOrder[];
406+
}>;
407+
bids: Array<{
408+
price: number;
409+
orders: IStopOrder[];
410+
}>;
411+
};
402412
/** Unix timestamp representing when the snapshot was taken */
403413
ts: number;
404414
/** The id of the last operation inserted in the orderbook */

test/orderbook.test.ts

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ERROR, ErrorCodes, ErrorMessages } from "../src/errors";
44
import type { LimitOrder } from "../src/order";
55
import { OrderBook } from "../src/orderbook";
66
import type { OrderQueue } from "../src/orderqueue";
7+
import type { StopQueue } from "../src/stopqueue";
78
import {
89
type ILimitOrder,
910
type IProcessOrder,
@@ -12,6 +13,7 @@ import {
1213
type JournalLog,
1314
OrderType,
1415
Side,
16+
type StopOrder,
1517
TimeInForce,
1618
} from "../src/types";
1719

@@ -1561,12 +1563,48 @@ void test("orderbook replayJournal test wrong journal", () => {
15611563
});
15621564

15631565
void test("orderbook test snapshot", () => {
1564-
const ob = new OrderBook();
1566+
const ob = new OrderBook({ experimentalConditionalOrders: true });
1567+
const addStopOrder = (
1568+
side: Side,
1569+
orderId: string,
1570+
stopPrice: number,
1571+
): void => {
1572+
ob.createOrder({
1573+
id: orderId,
1574+
type: OrderType.STOP_LIMIT,
1575+
side,
1576+
size: 10,
1577+
price: stopPrice,
1578+
stopPrice,
1579+
timeInForce: TimeInForce.GTC,
1580+
});
1581+
};
1582+
// Inizialize order book with some orders
15651583
addDepth(ob, "", 10);
1584+
1585+
// Add some stop orders
1586+
// Start with SELL side
1587+
addStopOrder(Side.SELL, "sell-1", 110);
1588+
addStopOrder(Side.SELL, "sell-2", 110); // Same price as before
1589+
addStopOrder(Side.SELL, "sell-3", 120);
1590+
addStopOrder(Side.SELL, "sell-4", 130);
1591+
addStopOrder(Side.SELL, "sell-5", 140);
1592+
1593+
// Test BUY side
1594+
addStopOrder(Side.BUY, "buy-1", 100);
1595+
addStopOrder(Side.BUY, "buy-2", 100); // Same price as before
1596+
addStopOrder(Side.BUY, "buy-3", 90);
1597+
addStopOrder(Side.BUY, "buy-4", 80);
1598+
addStopOrder(Side.BUY, "buy-5", 70);
1599+
15661600
const snapshot = ob.snapshot();
15671601

15681602
assert.equal(Array.isArray(snapshot.asks), true);
15691603
assert.equal(Array.isArray(snapshot.bids), true);
1604+
1605+
assert.equal(Array.isArray(snapshot.stopBook.asks), true);
1606+
assert.equal(Array.isArray(snapshot.stopBook.bids), true);
1607+
15701608
assert.equal(typeof snapshot.ts, "number");
15711609
snapshot.asks.forEach((level) => {
15721610
assert.equal(typeof level.price, "number");
@@ -1591,15 +1629,84 @@ void test("orderbook test snapshot", () => {
15911629
assert.equal(order.origSize, 10);
15921630
});
15931631
});
1632+
1633+
snapshot.stopBook.asks.forEach((level) => {
1634+
assert.equal(typeof level.price, "number");
1635+
assert.equal(Array.isArray(level.orders), true);
1636+
level.orders.forEach((order) => {
1637+
assert.equal(typeof order.id, "string");
1638+
assert.equal(order.type, OrderType.STOP_LIMIT);
1639+
assert.equal(order.side, Side.BUY);
1640+
assert.equal(order.size, 10);
1641+
// @ts-expect-error we know exists for IStopLimitOrder
1642+
assert.equal(typeof order.price, "number");
1643+
assert.equal(typeof order.stopPrice, "number");
1644+
});
1645+
});
1646+
1647+
snapshot.stopBook.bids.forEach((level) => {
1648+
assert.equal(typeof level.price, "number");
1649+
assert.equal(Array.isArray(level.orders), true);
1650+
level.orders.forEach((order) => {
1651+
assert.equal(typeof order.id, "string");
1652+
assert.equal(order.type, OrderType.STOP_LIMIT);
1653+
assert.equal(order.side, Side.BUY);
1654+
assert.equal(order.size, 10);
1655+
// @ts-expect-error we know exists for IStopLimitOrder
1656+
assert.equal(typeof order.price, "number");
1657+
assert.equal(typeof order.stopPrice, "number");
1658+
});
1659+
});
15941660
});
15951661

15961662
void test("orderbook restore from snapshot", () => {
15971663
// Create a new orderbook with 3 orders for price levels and make a snapshot
15981664
const journal: JournalLog[] = [];
1599-
const ob = new OrderBook({ enableJournaling: true });
1665+
const ob = new OrderBook({
1666+
enableJournaling: true,
1667+
experimentalConditionalOrders: true,
1668+
});
1669+
1670+
const addStopOrder = (
1671+
side: Side,
1672+
orderId: string,
1673+
stopPrice: number,
1674+
): void => {
1675+
ob.createOrder({
1676+
id: orderId,
1677+
type: OrderType.STOP_LIMIT,
1678+
side,
1679+
size: 10,
1680+
price: stopPrice,
1681+
stopPrice,
1682+
timeInForce: TimeInForce.GTC,
1683+
});
1684+
};
1685+
1686+
// Inizialize order book with some orders
16001687
addDepth(ob, "first-run-", 10, journal);
1601-
// addDepth(ob, "second-run-", 10, journal);
1602-
// addDepth(ob, "third-run-", 10, journal);
1688+
addDepth(ob, "second-run-", 10, journal);
1689+
addDepth(ob, "third-run-", 10, journal);
1690+
1691+
// Add some stop orders
1692+
// Test BUY side
1693+
addStopOrder(Side.BUY, "buy-1", 100);
1694+
addStopOrder(Side.BUY, "buy-2", 100); // Same price as before
1695+
addStopOrder(Side.BUY, "buy-3", 90);
1696+
addStopOrder(Side.BUY, "buy-4", 80);
1697+
addStopOrder(Side.BUY, "buy-5", 70);
1698+
1699+
// Start with SELL side
1700+
const prevMarketprice = ob.marketPrice;
1701+
// @ts-expect-error we should hack the marketPrice in order to let stop order to be executed
1702+
ob._marketPrice = 150;
1703+
addStopOrder(Side.SELL, "sell-1", 110);
1704+
addStopOrder(Side.SELL, "sell-2", 110); // Same price as before
1705+
addStopOrder(Side.SELL, "sell-3", 120);
1706+
addStopOrder(Side.SELL, "sell-4", 130);
1707+
addStopOrder(Side.SELL, "sell-5", 140);
1708+
// @ts-expect-error restore marketPrice to the original market price
1709+
ob._marketPrice = prevMarketprice;
16031710

16041711
const snapshot = ob.snapshot();
16051712
{
@@ -1608,6 +1715,30 @@ void test("orderbook restore from snapshot", () => {
16081715

16091716
assert.equal(ob.toString(), ob2.toString());
16101717
assert.deepStrictEqual(ob.depth(), ob2.depth());
1718+
assert.equal(
1719+
// @ts-expect-error these are private properties
1720+
ob.stopBook.bids
1721+
.priceTree()
1722+
.values.map((queue) => queue.toArray())
1723+
.join(","),
1724+
// @ts-expect-error these are private properties
1725+
ob2.stopBook.bids
1726+
.priceTree()
1727+
.values.map((queue) => queue.toArray())
1728+
.join(","),
1729+
);
1730+
assert.equal(
1731+
// @ts-expect-error these are private properties
1732+
ob.stopBook.asks
1733+
.priceTree()
1734+
.values.map((queue) => queue.toArray())
1735+
.join(","),
1736+
// @ts-expect-error these are private properties
1737+
ob2.stopBook.asks
1738+
.priceTree()
1739+
.values.map((queue) => queue.toArray())
1740+
.join(","),
1741+
);
16111742

16121743
// @ts-expect-error these are private properties
16131744
Object.entries(ob.orders).forEach(([key, order]) => {
@@ -1661,10 +1792,42 @@ void test("orderbook restore from snapshot", () => {
16611792
);
16621793
});
16631794

1795+
const prevStopBook: Record<number, StopOrder[]> = {};
1796+
const restoredStopBook: Record<number, StopOrder[]> = {};
1797+
1798+
// @ts-expect-error these are private properties
1799+
ob.stopBook.asks.priceTree().forEach((price: number, level: StopQueue) => {
1800+
prevStopBook[price] = level.toArray();
1801+
});
1802+
1803+
// @ts-expect-error these are private properties
1804+
ob.stopBook.bids.priceTree().forEach((price: number, level: StopQueue) => {
1805+
prevStopBook[price] = level.toArray();
1806+
});
1807+
1808+
// @ts-expect-error these are private properties
1809+
ob2.stopBook.asks.priceTree().forEach((price: number, level: StopQueue) => {
1810+
restoredStopBook[price] = level.toArray();
1811+
});
1812+
1813+
// @ts-expect-error these are private properties
1814+
ob2.stopBook.bids.priceTree().forEach((price: number, level: StopQueue) => {
1815+
restoredStopBook[price] = level.toArray();
1816+
});
1817+
1818+
Object.entries(prevStopBook).forEach(([price, orders]) => {
1819+
assert.deepStrictEqual(
1820+
orders.map((order) => order.toObject()),
1821+
restoredStopBook[price].map((order) => order.toObject()),
1822+
);
1823+
});
1824+
16641825
// Compare also the snapshot from the original order book and the restored one
16651826
const snapshot2 = ob2.snapshot();
16661827
assert.deepStrictEqual(snapshot.asks, snapshot2.asks);
16671828
assert.deepStrictEqual(snapshot.bids, snapshot2.bids);
1829+
assert.deepStrictEqual(snapshot.stopBook.asks, snapshot2.stopBook.asks);
1830+
assert.deepStrictEqual(snapshot.stopBook.bids, snapshot2.stopBook.bids);
16681831
}
16691832

16701833
{

0 commit comments

Comments
 (0)