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.
| 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 ninjagit clone https://github.com/microsoft/vcpkg.git ~/vcpkg && ~/vcpkg/bootstrap-vcpkg.sh -disableMetricsVCPKG_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"' >> ~/.zshrcIf 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++/v1If 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.staleDependencies 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 devOr as one workflow preset:
cmake --workflow --preset dev| 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 --versioncatch_discover_tests registers each TEST_CASE with CTest individually:
ctest --preset dev -R "microprice" --output-on-failureOr drive the Catch2 binary directly by tag:
./build/dev/bin/eventbook_tests "[book]"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.
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.yamlconfig/local.yaml, .env, *.pem, and *.key are all git-ignored.
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).
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 6Observed 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 KXMVECROSSCATEGORYreturns 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.
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.
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 300M2'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.
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| # | 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.
- Architecture
- Data dictionary
- Experiment protocol - frozen before collection
- Research report - skeleton, no results yet
- Extensions - explicitly out of scope for v1
- AGENTS.md - working agreement for AI agents on this repository
MIT. See LICENSE.