# Order book

> Keep an order book from a depth feed with createOrderBook — snapshot, incremental updates with sequence checks, grouping — and show it as stacked or side-by-side grid rows.

A depth feed sends a snapshot of the book, then the price levels that change. `createOrderBook` from `bo-grid/realtime` keeps that book: it applies the changes as they arrive, checks their sequence numbers, and reads back the best levels a side when a frame draws, merged into price buckets on request and shaped as rows for the grid.

The book below is read once a frame. Sizes flash as levels change, the depth bars show the running total from the spread outward, and Group merges levels into buckets.

Example: An order book (order-book.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';
  import { createOrderBook, type BookLevelInput, type BookRow } from 'bo-grid/realtime';

  // A book kept from a depth feed: a snapshot, then the levels that change.
  const book = createOrderBook({ maxLevels: 500 });
  const size = () => Math.ceil(Math.random() * 40) / 10;
  const price = (side: 'bid' | 'ask', depth: number) => Math.round((side === 'bid' ? 99.9 - depth * 0.1 : 100.1 + depth * 0.1) * 10) / 10;
  const side = (s: 'bid' | 'ask'): BookLevelInput[] => Array.from({ length: 30 }, (_, i) => [price(s, i), size()]);
  book.snapshot({ bids: side('bid'), asks: side('ask') });

  let group = $state(0);
  let rows = $state.raw<BookRow[]>([]);
  const columns: ColumnDef[] = [
    { type: 'number', key: 'price', header: 'Price', decimals: 1, width: 100, cellClass: (_v, r) => (r as BookRow).side },
    { type: 'number', key: 'size', header: 'Size', decimals: 1, flash: 'auto' },
    { type: 'number', key: 'share', header: 'Depth', flex: 1, format: (_v, r) => (r ? (r as BookRow).total.toFixed(1) : ''), dataBar: { min: 0, max: 1, color: 'color-mix(in srgb, var(--bo-amber) 45%, transparent)' } },
  ];

  $effect(() => {
    rows = book.rows(6, { group }); // a new grouping reads at once
  });

  $effect(() => {
    let seen = book.version;
    let frame = 0;
    const read = () => {
      // Read once a frame, not once a message: version bumps on every visible change.
      if (book.version !== seen) {
        seen = book.version;
        rows = book.rows(6, { group });
      }
      frame = requestAnimationFrame(read);
    };
    frame = requestAnimationFrame(read);
    const feed = setInterval(() => {
      const s = Math.random() < 0.5 ? 'bid' : 'ask';
      const level: BookLevelInput = [price(s, Math.floor(Math.random() * 10)), Math.random() < 0.15 ? 0 : size()]; // size 0 removes a level
      book.update(s === 'bid' ? { bids: [level] } : { asks: [level] });
    }, 60);
    return () => {
      cancelAnimationFrame(frame);
      clearInterval(feed);
    };
  });
</script>

<label class="bar">Group levels <select bind:value={group}><option value={0}>none</option><option value={0.5}>0.5</option><option value={1}>1</option></select></label>
<div class="book"><Grid {rows} {columns} rowHeight={28} height={12 * 28} cellSelection={false} /></div>

<style>
  .bar {
    display: block;
    margin: 0 0 8px;
    font-size: 12px;
  }
  select {
    font: inherit;
  }
  .book :global(.c.ask) { color: var(--bo-down); }
  .book :global(.c.bid) { color: var(--bo-up); }
</style>
```

## Keeping the book

```ts
import { createOrderBook } from 'bo-grid/realtime';

const book = createOrderBook({ maxLevels: 1000 });
book.snapshot({ bids, asks, seq: snap.lastUpdateId }); // e.g. from REST

socket.onmessage = (e) => {
  const m = JSON.parse(e.data);
  // [price, size] levels, as numbers or numeric strings; a size of 0 removes a level
  const result = book.update({ bids: m.b, asks: m.a, from: m.U, seq: m.u });
  if (result === 'gap') resnapshot(); // a message was missed
};
```

`update` returns `'applied'`, `'stale'` or `'gap'`:

- **`seq`** is the update's last sequence number. An update that is not newer than the last one applied is dropped as `'stale'`, as a feed's diffs from before the snapshot must be.
- **`from`** is the first sequence number the update covers, for a feed that numbers its updates in an unbroken run. When it is past the one after the last applied, `update` returns `'gap'` and applies nothing: take a new snapshot.
- Without `from`, a missed update goes unnoticed unless it crosses the book: `crossed` turns true when the best bid reaches the best ask.
- `snapshot` always replaces the whole book, whatever its `seq`. `clear()` empties it and forgets the sequence.

## Reading it once a frame

Apply updates as they arrive, and read the book when a frame draws. `version` bumps on every change a read can see, so an unchanged book costs nothing:

```ts
let seen = -1;
function frame() {
  if (book.version !== seen) {
    seen = book.version;
    rows = book.rows(20, { group: 0.5 }); // stacked: asks above bids
  }
  requestAnimationFrame(frame);
}
```

Three reads, each up to `n` levels a side:

- **`rows(n)`**: stacked rows, the asks (farthest first) above the bids, each with `side`, `price`, `size`, `total` (depth from the spread outward), `share` (that depth as a share of the deeper side, for bars on one scale) and `best`.
- **`pairs(n)`**: the nth bid beside the nth ask in one row (`bidPrice`, `bidSize`, `bidTotal`, `bidShare`, then the same for the ask), for a side-by-side book.
- **`top(n)`**: the levels themselves, `{ bids, asks }` as `[price, size]` pairs, best first. The same levels feed a [depth ladder](https://bonguynvan.github.io/bo-grid/docs/depth-ladder/): `bids={top.bids} asks={top.asks}`.

`best()` gives the best bid and ask, and `levels` counts each side.

## Showing it in the grid

```svelte
<Grid {rows} rowClass={(r) => String(r.side)} columns={[
  { type: 'number', key: 'price', header: 'Price' },
  { type: 'volume', key: 'size', header: 'Size', flash: 'auto' },
  { type: 'number', key: 'share', header: 'Depth', format: (_v, r) => String(r?.total ?? ''), dataBar: { min: 0, max: 1 } },
]} height={480} />
```

- In `rows`, a level keeps its row id while it stays in the book, so a size column with `flash: 'auto'` flashes the levels that changed, not every row the book moved past. Each grouping has ids of its own, so switching it flashes nothing.
- In `pairs`, the row id is the depth (0 at the spread), so a row's level changes as the book moves: don't flash those columns by value.
- A `dataBar` on `share` with `min: 0, max: 1` draws the depth bars of both sides on one scale.

## Options

- **`group`** (a read option) merges levels into buckets of that size: 0.1, 1 or 10 on a crypto book. Bids round down and asks up, so the two sides never meet in one bucket.
- **`maxLevels`** keeps only a side's best levels and drops the far ones, in batches, for a feed that never removes them. A dropped level comes back only with the next snapshot, so keep it well above the depth you show.
- Prices compare to 1e-8, as the depth ladder's do: scale a book quoted finer than that.

The order book example, with a stacked and a side-by-side book from one feed:

Live example: https://bonguynvan.github.io/bo-grid/examples/orderbook/ (source: https://github.com/bonguynvan/bo-grid/blob/main/src/demo/examples/)

## Reference

`createOrderBook(options?)` returns an `OrderBook`. Its option is `maxLevels` (read at most this many levels a side).

| Member | What it does |
| --- | --- |
| `snapshot({ bids, asks, seq? })` | Replaces the whole book. |
| `update({ bids?, asks?, seq?, from? })` | Applies changed levels; returns `'applied'`, `'stale'` or `'gap'`. |
| `rows(n, { group? })` | Stacked rows (`BookRow`): asks, then bids. |
| `pairs(n, { group? })` | Side-by-side rows (`BookPairRow`). |
| `top(n, { group? })` | `{ bids, asks }`, best first. |
| `best()` | `{ bid?, ask? }`. |
| `crossed` | Whether the best bid is at or above the best ask. |
| `levels` | `{ bids, asks }`: levels a side. |
| `version` | Bumps on every change a read can see. |
| `seq` | The last sequence number applied. |
| `clear()` | Empties the book. |

---
Page: https://bonguynvan.github.io/bo-grid/docs/order-book/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
