Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ jobs:
- uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d # v1.2.24
with:
key: macos-release
# 13.3 is the documented minimum; std::to_chars(double) needs it.
- name: Build
run: >-
make build CMAKE_ARGS="-G Ninja -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-DKALSHI_WARNINGS_AS_ERRORS=ON"
-DKALSHI_WARNINGS_AS_ERRORS=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3"
- name: Test
run: make test

Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
view over the start of a longer buffer was counted from the bytes after
it. Counting also no longer copies each order: 20 orders now take 6
allocations instead of 26.
- A connection the peer reset can no longer end the process with SIGPIPE.
`WebSocketClient` relied on libwebsockets ignoring SIGPIPE process-wide,
which an application that restores the default handler undoes. Its sockets
now set `SO_NOSIGPIPE` where the OS has it (macOS and the BSDs, which signal
the whole process), and its network thread blocks SIGPIPE (Linux, which
signals the writing thread). Callbacks on that thread see SIGPIPE blocked.
- On macOS, the benchmarks' `allocs` and `alloc_bytes` no longer count the
allocation that recording the `instructions` counter makes.

### Changed

- `KALSHI_NATIVE_ARCH` stops at configure time when the compiler rejects
`-march=native` for the target, as in universal macOS builds, instead of
failing mid-build.
- `make lint`, `make format`, and `make tidy` find Homebrew's keg-only
`llvm@18` without `PATH` changes.
- README is shorter and states the macOS 13.3 minimum deployment target, which
CI now builds against.

## [0.6.1] - 2026-09-25

Expand Down
19 changes: 17 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ option(KALSHI_BUILD_EXAMPLES "Build the examples" ${PROJECT_IS_TOP_LEVEL})
option(KALSHI_BUILD_BENCHMARKS "Build Google Benchmark targets" OFF)
option(KALSHI_USE_SYSTEM_GLAZE "Use an installed Glaze instead of fetching it" OFF)
option(KALSHI_ENABLE_LTO "Enable interprocedural optimization for project targets" OFF)
option(KALSHI_NATIVE_ARCH "Compile project targets with -march=native" OFF)
option(KALSHI_NATIVE_ARCH "Tune project targets for this machine's CPU (-march=native)" OFF)
option(KALSHI_ENABLE_CLANG_TIDY "Run clang-tidy while compiling project targets" OFF)
option(KALSHI_WARNINGS_AS_ERRORS "Treat project warnings as errors" OFF)
option(KALSHI_ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
Expand All @@ -32,8 +32,23 @@ if(KALSHI_ENABLE_SANITIZERS AND KALSHI_ENABLE_THREAD_SANITIZER)
message(FATAL_ERROR "AddressSanitizer and ThreadSanitizer require separate builds")
endif()

# -march=native names the build machine's CPU, so compilers reject it when
# targeting another architecture, as cross and universal macOS builds do.
if(KALSHI_NATIVE_ARCH AND NOT MSVC)
include(CheckCXXCompilerFlag)
# Probe on every configure: the answer changes with CMAKE_OSX_ARCHITECTURES.
unset(KALSHI_MARCH_NATIVE_WORKS CACHE)
check_cxx_compiler_flag(-march=native KALSHI_MARCH_NATIVE_WORKS)
if(NOT KALSHI_MARCH_NATIVE_WORKS)
message(FATAL_ERROR "KALSHI_NATIVE_ARCH needs a build for this machine's CPU, "
"but the compiler rejects -march=native for this target")
endif()
endif()

if(KALSHI_ENABLE_CLANG_TIDY)
find_program(KALSHI_CLANG_TIDY_EXECUTABLE NAMES clang-tidy-18 clang-tidy REQUIRED)
# CI uses clang-tidy 18. Homebrew's llvm@18 is keg-only, so look there too.
find_program(KALSHI_CLANG_TIDY_EXECUTABLE NAMES clang-tidy-18 clang-tidy
HINTS /opt/homebrew/opt/llvm@18/bin /usr/local/opt/llvm@18/bin REQUIRED)
endif()

if(KALSHI_ENABLE_LTO)
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ pipx install cmake && pipx ensurepath # then open a new shell
make test
```

On macOS, Homebrew's `llvm@18` provides clang-format 18 without putting it on
`PATH`, and PyYAML goes in a virtual environment:
On macOS, Homebrew's `llvm@18` provides clang-format and clang-tidy 18, which
`make` finds on its own. PyYAML goes in a virtual environment:

```bash
brew install cmake ninja pkg-config openssl libwebsockets llvm@18
python3 -m venv .venv && .venv/bin/pip install pyyaml
export CLANG_FORMAT="$(brew --prefix llvm@18)/bin/clang-format" PYTHON=.venv/bin/python
export PYTHON=.venv/bin/python
make test lint
```

Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ BUILD_TYPE ?= Release
CMAKE_ARGS ?=
JOBS ?= $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
PYTHON ?= python3
CLANG_FORMAT ?= $(shell command -v clang-format-18 2>/dev/null || command -v clang-format 2>/dev/null)
# Homebrew's llvm@18 is keg-only, so it is not on PATH.
CLANG_FORMAT ?= $(firstword $(shell command -v clang-format-18 2>/dev/null) \
$(wildcard /opt/homebrew/opt/llvm@18/bin/clang-format /usr/local/opt/llvm@18/bin/clang-format) \
$(shell command -v clang-format 2>/dev/null))
CLANG_FORMAT_MAJOR := 18
# Tracked and new C++ sources that exist on disk (deleted files are skipped).
CPP_SOURCES = git ls-files -z --cached --others --exclude-standard '*.cpp' '*.hpp' | \
Expand Down
160 changes: 73 additions & 87 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,31 @@
[![CI](https://github.com/Reddimus/kalshi-cpp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reddimus/kalshi-cpp/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/Reddimus/kalshi-cpp)](https://github.com/Reddimus/kalshi-cpp/releases)

A C++23 client for Kalshi's Predictions API. It covers every REST operation in
Kalshi's OpenAPI document and streams market data over WebSockets. Requests are
signed with Ed25519 or RSA-PSS keys, prices stay exact fixed-point strings, and
every call returns `std::expected<T, kalshi::Error>` instead of throwing.
An unofficial C++23 client for Kalshi's Predictions API. It covers every
[REST operation](docs/operations.md) and [WebSocket channel](docs/channels.md).
A generator builds it from Kalshi's OpenAPI and AsyncAPI documents in
[`spec/`](https://github.com/Reddimus/kalshi-cpp/tree/main/spec), so type and
field names match [Kalshi's API docs](https://docs.kalshi.com). Kalshi's
separate Margin and Perpetuals API is out of scope. The
[API reference](https://reddimus.github.io/kalshi-cpp/) lists every type and method.

The client is generated from Kalshi's OpenAPI and AsyncAPI documents in
[`spec/`](https://github.com/Reddimus/kalshi-cpp/tree/main/spec), so type and field names match
[Kalshi's API reference](https://docs.kalshi.com). The
[API reference for this library](https://reddimus.github.io/kalshi-cpp/) is
built from its headers. Kalshi's separate Margin and Perpetuals API is out of
scope.

## Build
## Install

You need a C++23 compiler, CMake 3.31+, OpenSSL 3, libcurl, and libwebsockets.
CI tests GCC 13 on Ubuntu 24.04, Apple Clang on Apple silicon, and MSVC on
Windows, where [`vcpkg.json`](https://github.com/Reddimus/kalshi-cpp/blob/main/vcpkg.json)
supplies the dependencies. On macOS, the deployment target must be 13.3 or later.

```bash
# macOS
brew install cmake openssl curl libwebsockets pkg-config
brew install cmake openssl libwebsockets pkg-config

# Ubuntu 24.04, whose apt CMake is older than 3.31
sudo apt install build-essential pkg-config pipx libssl-dev libcurl4-openssl-dev libwebsockets-dev
pipx install cmake && pipx ensurepath # then open a new shell

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure
```

## Use it from CMake
Add the library to your CMake project:

```cmake
include(FetchContent)
Expand All @@ -44,7 +39,8 @@ target_link_libraries(myapp PRIVATE kalshi::kalshi)
```

After `cmake --install`, `find_package(kalshi CONFIG REQUIRED)` provides the
same `kalshi::kalshi` target.
same target. Until 1.0, a minor release may break the API.
[CHANGELOG.md](CHANGELOG.md) has migration notes.

## Read market data

Expand All @@ -58,7 +54,7 @@ int main() {
kalshi::KalshiClient client{kalshi::HttpClient{}};

kalshi::GetMarketsParams params;
params.series_ticker = "KXHIGHNY";
params.series_ticker = "KXHIGHNY"; // New York City's daily high temperature
params.status = kalshi::GetMarketsStatus::Open;

kalshi::Result<kalshi::GetMarketsResponse> page = client.get_markets(params);
Expand All @@ -74,20 +70,21 @@ int main() {

`kalshi::collect_pages` follows cursors when you want every page.

## Authenticate
## Errors

Create a key under API keys at <https://kalshi.com/account/profile> and save
the private key file Kalshi gives you. Kalshi recommends Ed25519 keys. If the
page offers to use your own public key, you can generate the pair locally so
the private key never leaves your machine:
Calls that can fail return `kalshi::Result<T>`, which is
`std::expected<T, kalshi::Error>`, instead of throwing. `Error::code` classifies
the failure, such as `RateLimited` or `NetworkError`. When Kalshi rejects a
request, `Error::http_status` and `Error::api_code` hold its HTTP status and
error code.

```bash
openssl genpkey -algorithm ed25519 -out kalshi.key
openssl pkey -in kalshi.key -pubout # paste this public key into Kalshi
```
## Authenticate

Create an API key at <https://kalshi.com/account/profile>. Save the private key
file and note the key ID. `Signer` loads unencrypted RSA and Ed25519 PEM keys.

```cpp
kalshi::Result<kalshi::Signer> signer = kalshi::Signer::from_pem_file(key_id, "kalshi.key");
kalshi::Result<kalshi::Signer> signer = kalshi::Signer::from_pem_file("your-key-id", "kalshi.key");
if (!signer) {
std::cerr << signer.error().message << '\n';
return 1;
Expand All @@ -96,49 +93,30 @@ kalshi::KalshiClient client{kalshi::HttpClient{*signer}}; // Signer is cheap t
kalshi::Result<kalshi::GetBalanceResponse> balance = client.get_balance();
```

`ClientConfig::for_environment(kalshi::Environment::Demo)` points the client at
Kalshi's demo exchange, which uses separate keys and play money.
Kalshi's demo exchange uses play money and its own keys. To use it, pass
`kalshi::ClientConfig::for_environment(kalshi::Environment::Demo)` to
`HttpClient` after the signer. `kalshi::WsConfig::for_environment` does the same
for `WebSocketClient`.

## Place an order

This places a real order unless `client` points at the demo exchange.

```cpp
kalshi::CreateOrderV2Request order;
order.ticker = "KXHIGHNY-26SEP25-T70";
order.side = kalshi::BookSide::Bid;
order.count = "10.00";
order.price = "0.5600";
order.side = kalshi::BookSide::Bid; // Bid buys Yes, Ask sells Yes
order.count = "10.00"; // contracts
order.price = "0.5600"; // dollars
order.time_in_force = kalshi::TimeInForce::GoodTillCanceled;
order.self_trade_prevention_type = kalshi::SelfTradePreventionType::TakerAtCross;

kalshi::Result<kalshi::CreateOrderV2Response> placed = client.create_order(order);
```

The client checks required fields and fixed-point strings before sending, so
`order.price = "56c"` fails locally with `InvalidRequest` instead of reaching the
exchange.

## Errors

`Error::code` says what went wrong: `InvalidRequest`, `AuthenticationError`,
`NotFound`, `RateLimited`, `ServerError`, `NetworkError`, `ParseError`,
`SigningError`, or `InvalidKey`. `Error::http_status` and `Error::api_code` carry
Kalshi's status and error code, and `Error::message` includes its explanation.

## Retries and rate limits

Both are transports you stack under the client:

```cpp
std::shared_ptr<kalshi::HttpClient> http = std::make_shared<kalshi::HttpClient>(*signer);
std::shared_ptr<kalshi::RateLimitedTransport> paced =
std::make_shared<kalshi::RateLimitedTransport>(http, kalshi::RateLimitConfig{});
kalshi::KalshiClient client{std::make_shared<kalshi::RetryingTransport>(paced)};
```

`RetryingTransport` repeats a write only after a 429, so an order is never sent
twice. `RateLimitConfig` defaults to Kalshi's Basic tier. For your account's
budgets, pass the results of `get_account_api_limits()` and
`get_account_endpoint_costs()` to `rate_limit_config()`.
Counts and prices are fixed-point strings, not doubles. The client checks them
and the required fields before sending, so `order.price = "56c"` fails locally
with `InvalidRequest`.

## Stream updates

Expand All @@ -155,35 +133,43 @@ kalshi::Result<kalshi::ws::Subscription> book = ws.subscribe(
kalshi::Result<void> connected = ws.connect();
```

The client reconnects after a dropped connection and resubscribes with each
subscription's current markets. A `ws::Subscription` handle stays the same
throughout, and every `ws::Update` names the subscription it belongs to. When
an order book sequence number is skipped, the client reports the gap to
`on_error` and requests fresh snapshots. [docs/channels.md](docs/channels.md)
lists each channel's message types.
`connect()` waits until the connection opens or fails. Callbacks then run on the
client's network thread until you call `disconnect()` or destroy `ws`, so keep
your program running. After a dropped connection, the client reconnects,
resubscribes, and keeps your `ws::Subscription` handles valid. When it sees a
skipped order book sequence number, it reports the gap to `on_error` and
requests fresh snapshots. [docs/channels.md](docs/channels.md) lists each
channel's message types.

## Examples
## Retries and rate limits

| Program | What it does |
| --- | --- |
| [`market_data`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/market_data.cpp) | Markets, an order book, and candlesticks, without a key |
| [`portfolio`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/portfolio.cpp) | Balance, positions, and resting orders |
| [`place_and_cancel_order`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/place_and_cancel_order.cpp) | A resting order and its cancel, on the demo exchange only |
| [`stream_orderbook`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/stream_orderbook.cpp) | A live local order book that recovers from gaps and reconnects |
Both are transports you stack under the client:

Put `KALSHI_API_KEY_ID`, `KALSHI_API_KEY_FILE`, and optionally `KALSHI_ENV=demo`
in `.env`, then run `make run-portfolio`.
```cpp
std::shared_ptr<kalshi::HttpClient> http = std::make_shared<kalshi::HttpClient>(*signer);
std::shared_ptr<kalshi::RateLimitedTransport> paced =
std::make_shared<kalshi::RateLimitedTransport>(http, kalshi::RateLimitConfig{});
kalshi::KalshiClient client{std::make_shared<kalshi::RetryingTransport>(paced)};
```

## Develop
By default, `RetryingTransport` repeats a POST, PUT, or DELETE only after a 429,
so it never places an order twice. `RateLimitConfig{}` matches Kalshi's Basic
tier. To match your account's limits, pass the results of
`get_account_api_limits()` and `get_account_endpoint_costs()` to
`kalshi::rate_limit_config()`.

```bash
make format lint test # before every commit
make sanitize tsan tidy # ASan/UBSan, ThreadSanitizer, clang-tidy
make consumers bench # packaging check, benchmarks
make codegen # after updating a spec in spec/
make docs # API reference in build-docs/html
```
## Examples

[`examples/`](https://github.com/Reddimus/kalshi-cpp/tree/main/examples) has
four programs: public market data, your portfolio, an order placed and canceled
on the demo exchange, and a live order book. From a clone,
`make run-market_data` runs the first one without a key. For the others,
`make run-<name>` loads your key from `.env`, as the
[examples README](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/README.md)
shows.

## Contributing

[CONTRIBUTING.md](CONTRIBUTING.md) covers the workflow and release steps.
[CHANGELOG.md](CHANGELOG.md) lists changes and migration notes.
Report security issues as described in [SECURITY.md](SECURITY.md).
[CONTRIBUTING.md](CONTRIBUTING.md) covers building from source, tests, code
generation, and releases. Report security issues as
[SECURITY.md](SECURITY.md) describes.
5 changes: 4 additions & 1 deletion benchmarks/benchmarks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,17 @@ std::uint64_t instructions_retired() noexcept {
class LoopReport {
public:
void report(benchmark::State& state) const {
// Read both before touching state.counters, whose map allocates.
const std::uint64_t instructions = instructions_retired();
#ifdef KALSHI_COUNT_ALLOCATIONS
const kalshi::test::AllocationCount used = kalshi::test::allocations() - start_;
#endif
if (instructions_ != 0 && instructions > instructions_) {
state.counters["instructions"] =
benchmark::Counter(static_cast<double>(instructions - instructions_),
benchmark::Counter::kAvgIterations);
}
#ifdef KALSHI_COUNT_ALLOCATIONS
const kalshi::test::AllocationCount used = kalshi::test::allocations() - start_;
state.counters["allocs"] =
benchmark::Counter(static_cast<double>(used.count), benchmark::Counter::kAvgIterations);
state.counters["alloc_bytes"] =
Expand Down
5 changes: 3 additions & 2 deletions include/kalshi/websocket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ struct WsConfig {
///
/// Subscriptions survive reconnects: the client resubscribes with the same
/// parameters and keeps each `ws::Subscription` handle, while the server's
/// `sid` changes. Callbacks run on the client's network thread, except the
/// Disconnected state change from `disconnect()`, which runs on the caller's.
/// `sid` changes. Callbacks run on the client's network thread, which blocks
/// SIGPIPE on POSIX systems, except the Disconnected state change from
/// `disconnect()`, which runs on the caller's.
/// They may call any method, including destroying the client, but should
/// return quickly. Every method is thread-safe.
class WebSocketClient {
Expand Down
Loading
Loading