> ## Documentation Index
> Fetch the complete documentation index at: https://scalarfield.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Kalshi Prediction Markets

> Search Kalshi event contracts, inspect live order books, pull OHLC candlesticks, read the public trade tape, and load market resolution fields — from Kalshi's public Trade API.

<Info>**Python functions:** `screenKalshiMarkets()`, `screenKalshiEvents()`, `getKalshiMarket()`, `getKalshiOrderbook()`, `getKalshiCandles()`, `getKalshiTrades()`</Info>

| Specification | Value |
| - | - |
| Delivery Frequency | continuous |
| Data Frequency | event-driven (trades), bar-aggregated (candles) |
| Coverage | Live Kalshi markets plus archived settled markets/trades via Kalshi's historical API (rolling \~3-month live window) |
| Candle intervals | 1 minute, 1 hour, 1 day |
| Availability | Free (public API; no Kalshi login required) |

# Product Overview

Kalshi Data exposes Kalshi's public event-contract markets through Scalar Field. Markets are binary YES/NO contracts priced as probabilities. `screenKalshiMarkets(query=...)` searches live Kalshi public APIs — with a query, Kalshi's `/v1/search/series` index; without, `GET /markets` (one page each). Pass `symbol` (YES) or `symbol_no` (NO) into `venue.trade()` and strategy agents.

Kalshi's order book returns **bids only** on each leg. A YES ask is derived as `1 − best NO bid` (and vice versa). Candlesticks try the live series path first and fail over to `GET /historical/markets/{ticker}/candlesticks` for markets that have rolled off the live set. Intervals with no trades are returned with null OHLC, volume 0, and populated open interest.

<Info>**Timestamps:** Naive request datetimes (`YYYY-MM-DD` / `YYYY-MM-DDTHH:MM:SS`) are **New York time** and converted to UTC before calling Kalshi. Clock-time fields on responses (`close_time`, `created_time`, `settlement_ts`, candle index `end_period`, trade `created_time`) are converted to timezone-naive New York time. **Unix-second values stay Unix:** request ints, candle column `end_period_ts`, and candle `df.attrs["last_ts"]` (that attr is the last `end_period_ts`, not a New York datetime). Trade `df.attrs["last_ts"]` is New York time — same name, different type.</Info>

<Note>Kalshi is a tradable venue on Scalar Field. See [Trading on Kalshi](/docs/trading/venues/kalshi) for connect flow, order semantics, and settlement.</Note>

## Functions at a Glance

| Function | Purpose |
| - | - |
| `screenKalshiMarkets(...)` | Search/filter markets by series, event, status, close time |
| `screenKalshiEvents(...)` | List events, optionally with nested markets and `mutually_exclusive` |
| `getKalshiMarket(ticker)` | Full public Market payload, including settlement / resolution fields |
| `getKalshiOrderbook(ticker)` | Live YES/NO book with derived asks |
| `getKalshiCandles(ticker, start, end, period_interval)` | OHLC bars (1 / 60 / 1440 minutes); live then historical |
| `getKalshiTrades(ticker)` | Public trade tape with `is_complete` / `last_ts` / `next_cursor` |

Kalshi does **not** publish participant-level research APIs (no public wallets, positions, fills-by-trader, or leaderboards). That is a regulated-exchange constraint, not a missing wrapper. Research here is market-level: books, tape, candles, and resolution metadata.

# Querying the Data

## `screenKalshiMarkets()`

```python theme={null}
from scalarlib import screenKalshiMarkets

markets = screenKalshiMarkets(query="bitcoin", closed=False, limit=50)
print(markets[["ticker", "symbol", "title", "yes_bid", "volume"]].head())
```

Returns a DataFrame with `ticker`, `symbol` (YES), `symbol_no` (NO), bids/asks, volume, open interest, `close_time` (expiration, New York time), `created_time` (New York time), `price_ranges`, and settlement columns `result`, `settlement_ts` (New York time), `settlement_value_dollars`, `expiration_value`. Filter expiration with `start_date` / `end_date` (`YYYY-MM-DD`, New York calendar days).

### Text search vs status filters

Kalshi exposes two discovery APIs that **cannot be combined in one call**:

| Path | Kalshi endpoint | Text `query` | `status` values |
| - | - | - | - |
| Topic search | `/v1/search/series` | Yes (fuzzy relevance) | **`open`** or **`settled` only** |
| Structural browse | `GET /markets` | **No** | `open`, `paused`, `unopened`, `closed`, `settled` |

`screenKalshiMarkets` enforces this split:

* **With `query`** — use `closed=False` (default open) or `status="settled"`. Raises `ValueError` if combined with `closed=True` or `status` in `"paused"`, `"unopened"`, `"closed"`.
* **Without `query`** — use `status="paused"`, `"unopened"`, `"closed"`, `"settled"`, or `closed=True` (closed + settled union). Narrow with `series_ticker`, `event_ticker`, or `tickers`.

Kalshi lifecycle mapping ([market lifecycle](https://docs.kalshi.com/getting_started/market_lifecycle)): filter `paused` → row status `inactive`; `closed` → past close, not yet `finalized`; `settled` → `finalized`. Prefer `status="closed"` or `status="settled"` over `closed=True` when you need one slice.

`status` is a **query** filter: `"open"` (default; `None` is the same as omit), `"closed"`, `"settled"`, `"unopened"`, `"paused"`, or aliases `"active"` / `"finalized"`. Other values raise `ValueError`; a non-string raises `TypeError`. Ignored when `closed` is set.

Search-index rows may leave settlement columns null even after a market has determined. Use `getKalshiMarket(ticker)` for the authoritative Market object.

## `screenKalshiEvents()`

```python theme={null}
from scalarlib import screenKalshiEvents

events = screenKalshiEvents(status="open", limit=50)
```

`status` query values: `"open"`, `"closed"`, `"settled"`, `"unopened"`, or omit/`None` for any. Matches events that have at least one child market in that state ([Kalshi event status filter](https://docs.kalshi.com/api-reference/events/get-events)). `"unopened"` is events whose child markets are still `initialized`. Other values, including `"active"` and `"finalized"`, raise `ValueError` before the request. Event-row `status` is often null.

This is **one page**. Kalshi's `GET /events` hard-caps each page at **200**. `limit` above 200 raises `ValueError` (it is not silently truncated). Walk further pages with `cursor=df.attrs["next_cursor"]` until `df.attrs["is_complete"]` is true.

```python theme={null}
page = screenKalshiEvents(status="open", limit=200)
print(len(page), page.attrs["is_complete"], page.attrs["next_cursor"])
if not page.attrs["is_complete"]:
    more = screenKalshiEvents(
        status="open", limit=200, cursor=page.attrs["next_cursor"],
    )
```

`mutually_exclusive` is Kalshi's event flag: **true** means only one child market can resolve YES; **false** means more than one child can resolve YES. It is not inferred by Scalar Field.

## `getKalshiMarket()`

```python theme={null}
from scalarlib import getKalshiMarket

market = getKalshiMarket("KXHIGHNY-26AUG10-T60")
print(market["status"], market["result"], market["settlement_ts"])
print(market["settlement_value_dollars"], market["expiration_value"])
print(market["rules_primary"])
```

This is the resolution dataset. It calls `GET /markets/{ticker}`, then `GET /historical/markets/{ticker}` when the live ticker has rolled off Kalshi's \~3-month settled-market cutoff.

Fields come from [Kalshi's Market schema](https://docs.kalshi.com/api-reference/market/get-market) and [market lifecycle](https://docs.kalshi.com/getting_started/market_lifecycle):

| Field | Meaning |
| - | - |
| `result` | `yes` / `no` / `scalar` after determination; empty string before |
| `settlement_value_dollars` | YES/LONG payout in dollars; filled after determination. Binary winners pay **\$1** |
| `settlement_ts` | Timestamp when the market reached `finalized` (New York time, naive) |
| `expiration_value` | Observed value considered for settlement (not a news-source URL) |
| `status` | `initialized`, `active`, `inactive`, `closed`, `determined`, `disputed`, `amended`, `finalized` |
| `rules_primary` / `rules_secondary` | Current settlement-rule text |

`disputed` / `amended` are lifecycle states on this object, not a separate dispute feed. Kalshi does not publish a settlement-rule revision log — `rules_*` is the current text. There is no `resolution_source` field in the Trade API.

## `getKalshiOrderbook()`

```python theme={null}
from scalarlib import getKalshiOrderbook

book = getKalshiOrderbook("KXHIGHNY-26AUG10-T60")
yes, no = book["yes"], book["no"]
print(yes["bid"], yes["ask"])
if yes["bid"] is not None and yes["ask"] is not None:
    spread = yes["ask"] - yes["bid"]
```

Kalshi's raw orderbook is bids only ([Orderbook responses](https://docs.kalshi.com/getting_started/orderbook_responses)):

* YES bid at `X` ≡ NO ask at `1 − X` (same size)
* NO bid at `Y` ≡ YES ask at `1 − Y` (same size)

The data server already applies that conversion. `yes.bids` / `no.bids` are `[[price, size], ...]` best-first.

| Situation | Result |
| - | - |
| Both legs have bids | `yes.ask = 1 − best NO bid`; YES spread = `yes.ask − yes.bid` |
| Empty NO bids | `yes.ask` and `no.bid` are `None` — do not invent a quote |
| Empty YES bids | `yes.bid` and `no.ask` are `None` |
| Multi-level book | Each YES bid level `[p, s]` is a NO ask at `1 − p` for size `s` (and vice versa). Sizes match; do not assume the same number of displayed levels after a `depth` trim |
| Rounding | Complement is `round(1 − p, 4)` dollars. Kalshi still rejects off-grid limit prices on submit — snap with the market's `price_ranges` |
| Fees | Derived asks are **pre-fee**. A complementary pair that sums to \$1 is not a fee-adjusted arbitrage signal |

## `getKalshiCandles()`

```python theme={null}
from scalarlib import getKalshiCandles

df = getKalshiCandles(
    "KXHIGHNY-26AUG10-T60",
    start="2026-08-01",
    end="2026-08-10",
    period_interval=60,
)
print(df.attrs["source"], df.attrs["is_complete"])
```

`period_interval` must be `1`, `60`, or `1440`. Columns: `end_period_ts`, `open`, `high`, `low`, `close`, `volume`, `open_interest`. The index is `end_period` (New York time, timezone-naive). `end_period_ts` remains Unix seconds (UTC instant). Naive `start` / `end` strings are New York time. Prices are the **YES trade** distribution from Kalshi (`price.open_dollars` / `high` / `low` / `close`, or historical `price.open` / `high` / `low` / `close` — both map into the same columns).

### Sparse / zero-trade intervals

Kalshi returns a row for intervals with no trades. That is valid, not a gap in the series.

| Field | Empty interval |
| - | - |
| `open`, `high`, `low`, `close` | `null` (pandas `NaN`) — Kalshi documents these as nullable when there was no trade in the period |
| `volume` | `0` |
| `open_interest` | Populated (OI at `end_period_ts`) |

Do **not** forward-fill null OHLC unless that is an explicit choice for your analysis. This wrapper does not substitute `previous_dollars` into OHLC.

Kalshi can also prepend a synthetic candle when `include_latest_before_start=true` (null OHLC, `previous_price` set). `getKalshiCandles` does not pass that flag.

### Live vs historical coverage

Candlesticks for markets that **settled before** Kalshi's rolling live cutoff (`market_settled_ts` from `GET /historical/cutoff`, target window **\~3 months**) are only on `GET /historical/markets/{ticker}/candlesticks`. `getKalshiCandles` calls the live series path first and **fails over** to that historical endpoint when live is 404/empty.

There is no documented hard row cap on the candlestick endpoint (it is a time-range query, not a `limit` page). `df.attrs["is_complete"]` is True after a successful time-range response. `df.attrs["source"]` is `"live"` or `"historical"`. `df.attrs["last_ts"]` is the last `end_period_ts` in the frame — **Unix seconds, not New York time**. Use the `end_period` index when you want the clock time. This is not the same as trade `last_ts` (a New York datetime).

## `getKalshiTrades()`

```python theme={null}
from scalarlib import getKalshiTrades

tape = getKalshiTrades("KXHIGHNY-26AUG10-T60", limit=100)
if not tape.attrs["is_complete"]:
    more = getKalshiTrades(
        "KXHIGHNY-26AUG10-T60",
        limit=100,
        cursor=tape.attrs["next_cursor"],
        source=tape.attrs["source"],
    )
```

Public tape from `GET /markets/trades`, then `GET /historical/trades` when the live cursor is exhausted and `limit` still has room (`include_historical=True` by default). Columns: `trade_id`, `ticker`, `symbol`, `outcome`, `price`, `yes_price`, `no_price`, `count`, `taker_side`, `created_time` (New York time, naive).

`yes_price` + `no_price` = 1. `outcome` / `price` are the **taker's** `taker_outcome_side` and that leg's price — the same complement encoding as private fills (sell-YES prints as NO at `1 - yes_price`). `taker_side` is the legacy YES/NO of the aggressive order.

Optional filters: `min_ts` / `max_ts` (naive New York time or Unix seconds; converted to UTC for Kalshi), `cursor`, `source` (`live` / `historical` / omit for auto).

### Pagination, completeness, retention

| Rule | Behavior |
| - | - |
| `limit` | Default 100, **capped at 1000**. Kalshi's page size is 1–1000 (default 100). |
| What `limit=1000` means | Up to 1,000 matching trades from live, then historical if live is exhausted. It is not a silent “lifetime tape” dump — check `is_complete`. |
| Cursor | Kalshi is cursor-paginated (`cursor` empty ⇒ no further page on that source). Resume with `df.attrs["next_cursor"]` and `df.attrs["source"]`. |
| `df.attrs["is_complete"]` | True when the last queried source returned no further cursor. |
| `df.attrs["last_ts"]` | Oldest `created_time` in this page, as naive **New York** datetime (not Unix seconds — unlike candle `last_ts`). When incomplete, pass it as `max_ts` (same `min_ts`) to continue backwards, or use `next_cursor`. |
| Live vs archive | Trades older than `trades_created_ts` (\~3 months) live on `GET /historical/trades`. This helper **does** query that endpoint unless `include_historical=False` or `source="live"`. |
| User fills | `GET /historical/fills` is authenticated and **not** the public tape. Use `venue.history(account="KALSHI")`. |
| Latency | Kalshi documents a short delay before REST reflects matching-engine events. No numeric SLA. |

# Related

* [Trading on Kalshi](/docs/trading/venues/kalshi)
* [Polymarket Data](/docs/market-data/polymarket-data)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.