Skip to content

Repository files navigation

EventBook

A C++20 event-driven platform that records and reconstructs Kalshi L2 order books, measures short-horizon price formation, and compares execution strategies under explicit latency, fee, partial-fill, and queue-position assumptions.

Status: M0 - repository and C++ foundation. The build, dependency, and test pipeline work end to end. No market data code exists yet. See Milestones.

This project is read-only with respect to the exchange. No code path submits, amends, or cancels an order. The execution simulator produces simulated fills replayed against recorded historical depth; they are never real fills.

Requirements

Tool Version used Notes
CMake 4.4.2 3.25+ required for the preset schema
Ninja 1.13.2 Generator for all presets
vcpkg any recent Dependency manager, manifest mode
C++ compiler AppleClang 17 Any C++20 compiler should work
brew install cmake ninja
git clone https://github.com/microsoft/vcpkg.git ~/vcpkg && ~/vcpkg/bootstrap-vcpkg.sh -disableMetrics

VCPKG_ROOT must be set, because the presets read it to locate the toolchain file. Add this to your shell profile:

echo 'export VCPKG_ROOT="$HOME/vcpkg"' >> ~/.zshrc

macOS: stale Command Line Tools headers

If any C++ compile fails with fatal error: 'algorithm' file not found, your Command Line Tools install has a leftover empty libc++ directory that shadows the real SDK headers. Check it:

ls /Library/Developer/CommandLineTools/usr/include/c++/v1

If that lists only a handful of __functional_03-style files instead of ~185 headers, remove the stale directory so clang falls through to the SDK:

sudo mv /Library/Developer/CommandLineTools/usr/include/c++/v1 /Library/Developer/CommandLineTools/usr/include/c++/v1.stale

Build and test

Dependencies are declared in vcpkg.json and installed automatically on the first configure, so the first build takes a few minutes.

cmake --preset dev && cmake --build --preset dev && ctest --preset dev

Or as one workflow preset:

cmake --workflow --preset dev

Presets

Preset Build type What it is for
dev Debug Day-to-day work. ASan + UBSan, warnings as errors. Use this by default.
release RelWithDebInfo Benchmarks and any performance number that gets reported.
coverage Debug llvm-cov line coverage.

Binaries land in build/<preset>/bin/.

./build/dev/bin/collect --version

Running one test

catch_discover_tests registers each TEST_CASE with CTest individually:

ctest --preset dev -R "microprice" --output-on-failure

Or drive the Catch2 binary directly by tag:

./build/dev/bin/eventbook_tests "[book]"

Layout

include/eventbook/   public headers, one directory per subsystem
src/                 eventbook_core - all domain logic lives here
apps/                thin executables; argument parsing and wiring only
tests/               unit, property, and integration tests + small fixtures
docs/                architecture, data dictionary, protocol, report
config/              example.yaml; copy to config/local.yaml (git-ignored)
data/raw/            recorded journals (git-ignored)
data/derived/        generated feature datasets (git-ignored)
results/             generated tables and figures (git-ignored)

All logic lives in eventbook_core so that every code path is reachable from tests. apps/ stays thin on purpose.

Configuration and credentials

Credentials are never committed and never read from a tracked file. They come from the environment:

Variable Meaning
EVENTBOOK_KALSHI_KEY_ID Kalshi API key id
EVENTBOOK_KALSHI_KEY_PATH Path to the RSA private key used for request signing

Everything else lives in a YAML file. Start from config/example.yaml:

cp config/example.yaml config/local.yaml

config/local.yaml, .env, *.pem, and *.key are all git-ignored.

Dependencies

Added only when a milestone needs them, each with a stated reason.

Package Purpose Scope Added
fmt Formatting runtime M0
spdlog Structured logging runtime M0
cli11 Command-line parsing runtime M0
catch2 Test framework test M0
openssl TLS 1.2/1.3 client runtime M1
boost-beast HTTP/1.1 messages over Asio runtime M1
nlohmann-json Market metadata parsing runtime M1

openssl is what makes an HTTPS conversation possible at all: Kalshi is TLS-only and sends HSTS. It replaces writing a TLS stack, which is not a thing anyone should do. It will also supply RSA-PSS SHA-256 signing when M2 needs authenticated WebSocket access — REST market data is public and needs none.

boost-beast provides HTTP/1.1 framing (headers, chunked transfer, keep-alive) and, in M2, WebSocket framing. It replaces a hand-written HTTP parser, which is a well-known source of security bugs. libcurl would serve REST alone more simply, but has no WebSocket client, so the project would end up carrying two network stacks; Beast covers both against one Asio model. Verified that Kalshi negotiates HTTP/1.1 cleanly, since Beast does not speak HTTP/2.

Both are runtime-only and neither appears in a public header — BeastHttpTransport hides them behind a pimpl, so they are linked PRIVATE and nothing downstream pays their compile cost. They do make the first vcpkg configure noticeably slower.

nlohmann-json handles market metadata: a few thousand objects parsed once at startup, where ergonomics matter more than throughput. It is deliberately not the parser for the M2 WebSocket feed, which is high-volume and gets simdjson. Two JSON libraries is a considered choice, not an oversight — AGENTS.md sets exactly this split.

Planned, deliberately not installed yet so the build stays fast: simdjson (high-volume parsing, M2), zstd (journal compression, M3), and a YAML library (M1).

Describing the market universe

scout is M1's deliverable: discover markets over REST, apply the frozen inclusion rule, and report both halves — what survived and what was excluded, with a reason for each.

./build/release/bin/scout --series KXFED --show-events 6

Observed against the live API:

  • 87 KXFED markets across only 6 events. Sibling strikes on one FOMC decision move together, so the independent sample size is 6, not 87. This is why M5 partitions by event.
  • Over 250,000 markets are open at once, so an unscoped sweep hits the page budget. That is the correct outcome, not a limitation: version one studies one recurring series, and a truncated sweep of everything would be biased toward whatever the venue lists first.
  • The default listing is dominated by multivariate combo markets. scout --series KXMVECROSSCATEGORY returns 111 markets, 111 excluded, 0 eligible.

Exclusions are split into structural (this software cannot represent the market) and selection (we chose not to study it). Only the second kind needs to be pre-registered and restated beside a result — which is why they are reported separately rather than as one number.

Credentials

REST market data is public and needs none. The WebSocket requires authentication even for public channels — an anonymous connect returns 401 token_authentication_failure — so M2 onward needs a Kalshi API key.

Generate one in Kalshi account settings; you get a key ID and an RSA private key that is shown once and never again. Prefer the demo environment for development.

export EVENTBOOK_KALSHI_KEY_ID="your-key-id"
export EVENTBOOK_KALSHI_KEY_PATH="$HOME/.config/eventbook/kalshi.pem"

The private key is referenced by path, never by value. An environment variable holding key material shows up in ps, in container inspection output, and in crash reports; a path leaks only a filename. *.pem, *.key, .env, and config/local.yaml are all git-ignored, and no test ever uses a real credential — the signing tests generate a throwaway key pair at run time.

Requests are signed with RSA-PSS over SHA-256 (MGF1-SHA-256, salt length equal to the digest length), over the string timestamp_ms + "GET" + path. There is no way to sign anything but a GET: a valid signature is exactly what would make an order request acceptable to the venue, so the absence of a sign_post() is a stronger guarantee than the absence of a caller.

Recording a market

collect reconstructs one market's order book from the live WebSocket and reports data-quality counters. Credentials required.

./build/release/bin/collect -m KXBTCD-26AUG2517-T79499.99 -d 1800 --report-every 300

M2's acceptance run, 30 minutes on a Bitcoin daily-threshold market:

elapsed 1799.9s
connections / reconnects 1 / 0
messages 35,988 (20.0/s), 9.57 MB
snapshots / deltas / trades 1 / 35,961 / 24
sequence gaps 0
rejected deltas 0
parse failures 0
time with an invalid book 0.000s
data quality clean

One snapshot and 35,961 deltas applied without a single rejection means the sequence stayed consecutive for half an hour, every price sat on the market's tick grid, and no delta ever drove a level negative.

Pick a market with actual activity first — KXFED-27APR-T4.25 produced zero deltas in twelve seconds, so recording it would prove the socket stays open and nothing else.

--simulate-gap-after N deliberately skips a sequence number to exercise gap detection against real traffic. It corrupts the local stream on purpose and must never be used while recording data intended for research.

Live smoke tests

ctest never touches the network. The live tests are built but not registered with CTest, so an offline machine or a venue outage cannot turn into a red build. They are read-only, send no credentials, and cannot issue a write. Run them by hand:

./build/dev/bin/eventbook_live_tests

Milestones

# Deliverable Status
M0 Repository and C++ foundation done
M1 Domain types and REST market discovery done
M2 One-market WebSocket vertical slice done
M3 Journal and deterministic replay
M4 Feature dataset and descriptive research
M5 Price-formation experiment
M6 Execution simulator
M7 Adaptive execution and final study
M8 Portfolio hardening

The first vertical slice after setup: connect to one Kalshi market, record its snapshot/delta/trade messages, reconstruct the current book, and replay the captured session to the same final state.

Documentation

License

MIT. See LICENSE.

About

C++20 event-driven platform that records and reconstructs Kalshi L2 order books, measures short-horizon price formation, and compares execution strategies under explicit latency, fee, partial-fill, and queue-position assumptions.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages