10. The order book
The engine ships three order-book implementations, one per market-data depth level, and the export layer mirrors them behind a single handle type. A book is always created for one declared contract, enforces that contract's tick grid on every price it accepts, and consumes one flat message format. What differs between the three levels is exactly how much microstructure each one retains — nothing else: the message protocol, the price discipline and the error surface are identical across all three.
Three depths, one model
The three levels form a strict containment hierarchy. Each level keeps everything the previous one keeps and adds one further degree of resolution.
- L1 aggregates each side as a whole. It keeps the side valuation — the sum of price times quantity over every resting order — plus an order registry keyed by order id, used to validate incoming messages and reject those that reference an unknown originating order. There are no price levels, no queues and no per-level storage of any kind.
- L2 splits each side into price levels. The price of every resting order — and therefore its exact contribution to the side's value — is known, but an order's position inside its level's queue is not.
- L3 keeps the full FIFO queue of orders inside each price level. This is complete per-order depth: every order's price, quantity and queue position are recoverable.
A useful mental picture is a triangle. The best bid and offer sit at the apex; the altitude dropped from that apex splits the triangle into the bid side and the ask side. Descending from the apex along either side means walking away from the best price into progressively deeper levels. L1 sees only the two half-triangles as solid shapes, L2 sees the horizontal slices through them, and L3 sees the individual orders queued inside each slice.
| Level | Side aggregate | Price levels | FIFO queue per level |
|---|---|---|---|
| L1 | yes | no | no |
| L2 | yes | yes | no |
| L3 | yes | yes | yes |
BBO is a message, not a book method
The level split above follows how real exchange feeds are structured, and one consequence of that research is deliberate: no book in this engine derives the best bid and offer for you, and L1 refuses level reads entirely.
The industry evidence is consistent. NASDAQ TotalView-ITCH 5.0 is a per-order depth feed — add, cancel, delete, execute, replace — with no separate top-of-book message inside it; the BBO is reconstructed from the book, and NASDAQ sells top of book as a distinct product. IEX is the cleanest example: it ships two separate protocols, TOPS carrying the top of book (best bid and ask with aggregated size, plus the last trade) and DEEP carrying the depth of book (aggregated size at every price level, plus the last trade). CME MDP 3.0 offers Market by Price and Market by Order views, and its top of book is simply level 1 of the price-aggregated book — again no dedicated message.
The conclusion these feeds share: a size-bearing BBO — best price together with the volume resting at it — is always the top of a price-aggregated ladder. No real product publishes "a per-side total with no price grouping, plus a BBO", because maintaining a correct BBO incrementally requires a sorted price structure, and a sorted price structure is a ladder — the defining property of L2. The engine therefore draws the same line: L1, which has no ladder, provides no BBO, and its level readers refuse with a diagnostic that names L2 and L3 as the levels that do. The tse_book_imbalance_to_bound, tse_book_imbalance_to_depth, tse_book_side_stats_to_bound and tse_book_levels_top calls all carry this restriction.
The top of book instead travels the way exchanges ship it — as a separate protocol message. In the export surface that is the bid/ask tick (TseTickBidAsk: bid, bidVolume, ask, askVolume, last), pushed through an adapter of type tse_md_bidask, mirroring the IEX TOPS/DEEP split: the top-of-book feed and the depth feed are two feeds.
- Nasdaq TotalView-ITCH 5.0 specification
- IEX DEEP (depth of book) introduction alert
- IEX TOPS / SNAP specification
- CME MDP 3.0 — Market by Price, Multiple Depth Book
- CME MDP 3.0 — Central Limit Order Book
Price discipline
Every book — L1, L2 and L3 alike — runs the same tick pipeline on every price it receives: check conformance, reject what is far from the grid, snap what is near, and only then touch the book.
Prices ride in book messages as doubles in price units, while the contract's tick size is a fix-point value in units of one millionth of a price unit (the engine's FinValue representation; tse_finvalue_scale returns the exponent). A price is conformant when its distance to the nearest grid point is at most one raw millionth-unit. This one-unit tolerance exists to absorb the binary-representation error of the double-to-fix-point conversion: a value such as 2.05 lands one raw unit off the exact grid point and must still be accepted. A conformant price is never stored raw — it is snapped to the nearest tick on accept, and price levels are keyed by the snapped price, so two inputs near the same grid point always land on the same level.
A price farther than one raw unit from the grid is rejected outright: the message is refused, a log entry records the rejection, and the book is left unchanged. The check runs on the new and replace paths — a modify carries no price, only quantity.
Two ordering guarantees complete the discipline. First, a replace is validated before the resting order is pulled: a non-conformant replace is rejected as a whole, the old order stays resting, and the side valuation does not move. Second, an emptied price level is removed entirely — when a cancel, executed or replace takes the last quantity off a level, the level disappears from the book: it no longer appears in level reads and contributes nothing to statistics. L2 and L3 behave identically in both respects.
Messages
Books consume one flat structure, TseBookMessage — a tagged union in spirit: the kind field selects which of the remaining fields are read, and the unused fields are ignored.
The numeric values of kind are the engine's message codes, and the gap is deliberate: the value 5 is the engine-protocol "execution" message, which books do not accept — hence the jump from tse_book_message_replace (4) to tse_book_message_executed (6).
structSize must equal sizeof(TseBookMessage). A raw C caller sets it explicitly; both wrappers fill it for you — the C++ tse::BookMessage has no such field at all, and in Python make_book_message, Book.apply and the push_book_* methods stamp it before every call. A wrong structSize is a validation error and the message never reaches the book.
Reads come back through two structures. TseBookSideStats carries the aggregate of one side: notional, orderCount and contracts. TseBookLevel is one price-level row, and its index field deserves attention: it is the tick distance from the best price of the side, not an ordinal row number. In skip mode the indexes arrive sparse — only occupied ticks produce rows; in include mode they are dense, empty ticks are materialized as rows with present equal to zero, and a depth argument measures a fixed price distance rather than a row count.
| Field | Type |
|---|---|
| structSize | size_t |
| kind | TseBookMessageKind |
| tsNanoseconds | int64_t |
| messageId | uint64_t |
| orderId | uint64_t |
| price | double |
| quantity | double |
| txnSide | TseSide |
| Kind | Fields read |
|---|---|
tse_book_message_new | orderId, price, quantity, txnSide |
tse_book_message_modify | orderId, quantity |
tse_book_message_cancel | messageId only |
tse_book_message_replace | orderId, price, quantity, txnSide |
tse_book_message_executed | orderId, price, quantity, txnSide |
TseBookMessageKind enumerates those kinds: the value column is the engine code, the three name columns the spelling in each language.
| Value | C | C++ | Python |
|---|---|---|---|
| 1 | tse_book_message_new | new_ | New |
| 2 | tse_book_message_modify | modify | Modify |
| 3 | tse_book_message_cancel | cancel | Cancel |
| 4 | tse_book_message_replace | replace | Replace |
| 6 | tse_book_message_executed | executed | Executed |
| Field | Type |
|---|---|
| notional | double |
| orderCount | int64_t |
| contracts | double |
| Field | Type |
|---|---|
| index | uint64_t |
| price | double |
| notional | double |
| orderCount | int64_t |
| contracts | double |
| present | int32_t |
TseBookMissingLevel selects between the two modes above, and is spelled in each language as follows.
| Value | C | C++ | Python |
|---|---|---|---|
| 0 | tse_book_missing_level_skip | skip | Skip |
| 1 | tse_book_missing_level_include | include | Include |
The C surface
All book functions live in tse_book.h. They fall into seven groups.
Lifecycle. A book is created for a contract already declared on the account with tse_add_contract. TseBookLevelKind is tse_book_l1, tse_book_l2 or tse_book_l3 (values 1, 2, 3). tse_book_create returns NULL on failure with the reason in tse_last_error(account); every later failure on the created book is reported through the book's own tse_book_last_error.
TSE_API TseBookHandle tse_book_create(TseAccountHandle account, char const* contractSymbol, TseBookLevelKind kind);
TSE_API void tse_book_destroy(TseBookHandle book);
TSE_API char const* tse_book_last_error(TseBookHandle book);
Applying messages. tse_book_apply validates before it mutates: an unknown kind, a wrong structSize or a price off the tick grid is an error and the book stays unchanged.
TSE_API TseStatus tse_book_apply(TseBookHandle book, TseBookMessage const* message);
The imbalance family. Whole-book imbalance is defined over total contracts as (bid − ask) / (bid + ask). The bounded form restricts each side to a price band — [bidBound, best bid] on the bid side and [best ask, askBound] on the ask side; the depth form takes the top rows of each side under a missing-level mode. Both restricted forms require L2 or L3.
TSE_API TseStatus tse_book_imbalance(TseBookHandle book, double* outValue);
TSE_API TseStatus tse_book_imbalance_to_bound(TseBookHandle book, double bidBound, double askBound, double* outValue);
TSE_API TseStatus tse_book_imbalance_to_depth(TseBookHandle book, size_t depth, TseBookMissingLevel mode, double* outValue);
Side statistics. The side argument is tse_side_long for the bid and tse_side_short for the ask. The to-bound form requires L2 or L3.
TSE_API TseStatus tse_book_side_stats(TseBookHandle book, TseSide side, TseBookSideStats* outStats);
TSE_API TseStatus tse_book_side_stats_to_bound(TseBookHandle book, TseSide side, double bound, TseBookSideStats* outStats);
Level access. tse_book_levels_top returns the top rows of one side, best price first, using the capacity-in / count-out convention: pass the capacity of outLevels in *inoutCount, and on return it holds the number of rows written. Pass a null outLevels to query the row count first. Requires L2 or L3 — on L1 this is the refusal described above.
TSE_API TseStatus tse_book_levels_top(TseBookHandle book, TseSide side, size_t depth, TseBookMissingLevel mode, TseBookLevel* outLevels, size_t* inoutCount);
Feeding a book from an adapter. Book messages can be pushed through a market-data adapter of type tse_md_book (created with tse_market_create); the message then reaches every input bound to that adapter-and-contract pair.
TSE_API TseStatus tse_market_push_book_by_name(TseMarketHandle market, char const* contractName, TseBookMessage const* message);
TSE_API TseStatus tse_market_push_book_by_id(TseMarketHandle market, uint64_t contractId, TseBookMessage const* message);
Book inputs. Two input builders wire book data into the signal chain. tse_add_input_book takes a user processor of type TseBookInputProcessor — it receives each book message and decides what to store into the input's storage; to maintain a book inside the processor, create one with tse_book_create and apply the messages with tse_book_apply. tse_add_input_book_imbalance is the compiled-in alternative: it feeds every message into the given book and stores the whole-book imbalance per message, with zero callback crossings — the built-in mirror of an imbalance strategy.
typedef int (*TseBookInputProcessor)(TseStorageHandle storage, char const* contractId, TseBookMessage const* message, void* userData);
TSE_API TseStatus tse_add_input_book(TseAccountHandle account, char const* label, int32_t dataLength, TseDuration duration, TseBookInputProcessor processor, void* userData, TseMarketHandle market, int coreId, char const* const* contractSymbols, size_t contractCount);
TSE_API TseStatus tse_add_input_book_imbalance(TseAccountHandle account, char const* label, int32_t dataLength, TseDuration duration, TseBookHandle book, TseMarketHandle market, int coreId, char const* const* contractSymbols, size_t contractCount);
C++ and Python
The C++ wrapper exposes the book as tse::Book, a non-owning handle: the Account owns the book, and the handle becomes invalid when the account is destroyed. The enumerations are tse::BookLevelKind (l1, l2, l3), tse::BookMessageKind (new_, modify, cancel, replace, executed — same engine codes, same gap at 5) and tse::BookMissingLevel (skip, include). Errors surface as exceptions carrying the tse_book_last_error text.
namespace tse {
class Book final {
public:
void apply(BookMessage const& message) const &;
double imbalance() const &;
double imbalanceToBound(double bidBound, double askBound) const &;
double imbalanceToDepth(std::size_t depth, BookMissingLevel mode) const &;
BookSideStats sideStats(Side side) const &;
BookSideStats sideStatsToBound(Side side, double bound) const &;
std::vector<BookLevel> levelsTop(Side side, std::size_t depth, BookMissingLevel mode) const &;
TseBookHandle handle() const noexcept;
};
}
On the account, Account::createBook(contractSymbol, kind) builds the book, Account::addInputBook takes a tse::BookInputProcessor — an std::function<bool(Storage const&, std::string const&, BookMessage const&)> — and Account::addInputBookImbalance takes a Book const&. Adapter feeding is Market::pushBook, overloaded on contract name and contract id.
Book createBook(std::string contractSymbol, BookLevelKind kind) &;
void addInputBook(std::string label, std::int32_t dataLength, Duration duration, BookInputProcessor processor, Market const& market, std::vector<std::string> contractSymbols, int coreId = -1) &;
void addInputBookImbalance(std::string label, std::int32_t dataLength, Duration duration, Book const& book, Market const& market, std::vector<std::string> contractSymbols, int coreId = -1) &;
void pushBook(std::string contractName, BookMessage const& message) const &;
void pushBook(std::uint64_t contractId, BookMessage const& message) const &;
The Python module mirrors the same surface. The enumerations are BookLevelKind (L1, L2, L3), BookMessageKind (New, Modify, Cancel, Replace, Executed) and BookMissingLevel (Skip, Include). make_book_message builds a TseBookMessage with structSize pre-filled; Book methods raise TseError with the tse_book_last_error text on failure, and levels_top performs the count query and the row fetch for you, returning a list of TseBookLevel rows.
book = account.create_book("AAPL", tse.BookLevelKind.L3)
message = tse.make_book_message(tse.BookMessageKind.New, ts_ns, message_id, order_id, price, quantity, tse.Side.Long)
book.apply(message)
whole = book.imbalance()
banded = book.imbalance_to_bound(bid_bound, ask_bound)
deep = book.imbalance_to_depth(depth, tse.BookMissingLevel.Include)
stats = book.side_stats(tse.Side.Long)
bounded = book.side_stats_to_bound(tse.Side.Short, bound)
rows = book.levels_top(tse.Side.Long, depth, tse.BookMissingLevel.Skip)
market.push_book_by_name("AAPL", message)
market.push_book_by_id(contract_id, message)
account.add_input_book(label, data_length, duration, processor, market, ["AAPL"])
account.add_input_book_imbalance(label, data_length, duration, book, market, ["AAPL"])
Finally, the fix-point converters translate between doubles and the engine's scaled-integer price representation — useful whenever raw tick sizes or scaled prices cross the boundary. The scaled integer carries units of ten to the minus scale of one price unit.
TSE_API int32_t tse_finvalue_scale(void);
TSE_API int64_t tse_finvalue_from_double(double value);
TSE_API double tse_finvalue_to_double(int64_t scaled);
std::int32_t finValueScale() noexcept;
std::int64_t finValueFromDouble(double value) noexcept;
double finValueToDouble(std::int64_t scaled) noexcept;
scale = tse.fin_value_scale()
scaled = tse.fin_value_from_double(2.05)
value = tse.fin_value_to_double(scaled)
Version 5.0.0.0