> ## 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.

# Trading on Kalshi

> Trade Kalshi event contracts through Scalar Field — YES/NO symbols, V2 order path, complement-aware history and cash accounting, fees, cancels, and quantity rules.

<Info>**Execution:** `venue.trade()` in chat code, `strategy.execute()` in strategy agents. **Data reference:** [Kalshi Data](/docs/market-data/kalshi-data). Timestamps in `venue.history` and Kalshi market data are New York time (naive); see the timestamp note on Kalshi Data.</Info>

# Overview

Kalshi is a CFTC-regulated exchange for binary event contracts. Winning contracts settle at **\$1**; prices are probabilities in roughly `[0.01, 0.99]`. Scalar Field connects to **your own Kalshi account** — we never create a trader account on your behalf. Cash stays on Kalshi; deposits and withdrawals happen at Kalshi, never through us.

| Specification      | Value                                                    |
| ------------------ | -------------------------------------------------------- |
| Instruments        | Event contracts (YES / NO outcomes)                      |
| Modes              | `KALSHI` (live only; no paper venue)                     |
| Hours              | Near-24/7; weekly trading pause Thu 03:00–05:00 ET       |
| Fill model         | Synthesized IOC for market; GTC for limit                |
| Slippage tolerance | `slippage_bps` on synthesized market orders (default 50) |
| Currency           | USD held on the Kalshi account (`cash`)                  |
| Min quantity       | 0.01 contracts                                           |

# Connecting

1. Sign in at [kalshi.com/account/profile](https://kalshi.com/account/profile) and click **Create API Key**. Kalshi shows the private key exactly once — copy both the API Key ID and the PEM private key.
2. From Portfolio → Kalshi (or the venue card), open Connect and paste the API Key ID and private key.
3. Scalar Field validates the key against Kalshi, then stores it Fernet-encrypted for trading and portfolio reads.
4. Disconnect removes our stored copy only; your key stays active on Kalshi.

The stored key is never granted `write::transfer`. Perpetual futures (Kalshi margin) are not supported in this integration.

# Execution and accounting model

This is the contract for audit-grade use of `venue.trade`, `venue.history`, `venue.account`, and `venue.cancel` on Kalshi. Field meanings come from [Kalshi's Trade API](https://docs.kalshi.com/getting_started/order_direction): V2 orders are a **single YES book**, and fill `outcome_side` / `book_side` describe **directional exposure**, not a second price scale.

## Symbol model

Discover markets with `screenKalshiMarkets()`. Do not invent tickers.

| Field       | Meaning                      | Example                     |
| ----------- | ---------------------------- | --------------------------- |
| `ticker`    | Kalshi market identity       | `KXHIGHNY-26AUG10-T60`      |
| `symbol`    | YES (stated) outcome handle  | `K:KXHIGHNY-26AUG10-T60`    |
| `symbol_no` | NO (opposite) outcome handle | `K:KXHIGHNY-26AUG10-T60:NO` |

Pass `symbol` to trade YES and `symbol_no` to trade NO. Bare tickers are treated as the YES leg. Positions, fills, and history use the same handles.

Kalshi stores a **signed** `position_fp` per market: positive = YES, negative = NO. Scalar Field splits that into one position row per held outcome, matching Polymarket's one-row-per-outcome shape.

## Book identities

Kalshi's public order book returns **bids only** on each leg. Asks are implied by complementarity ([Kalshi orderbook docs](https://docs.kalshi.com/getting_started/orderbook_responses)):

```
YES ask = 1 − best NO bid
NO ask  = 1 − best YES bid
```

The V2 create path quotes that same book from the YES leg only:

* `bid` = buy YES (economically sell NO)
* `ask` = sell YES (economically buy NO)

`venue.trade` converts your `(action, outcome, limit_price)` into that YES-leg `(side, price)` before submit. `avg_price` on the trade response is converted back to the **outcome you passed in**.

## Quantity rules

Kalshi contract counts are fixed-point strings with **minimum granularity 0.01** ([Fixed-Point Representation](https://docs.kalshi.com/getting_started/fixed_point_migration)). Requests accept 0–2 decimal places; responses emit 2 decimals.

This integration **truncates toward zero** (`ROUND_DOWN`) to 0.01 before submit. If the result is 0, the order is rejected locally — Kalshi never sees it.

```python theme={null}
venue.trade("buy", symbol, 0.01, account="KALSHI")    # valid
venue.trade("buy", symbol, 0.019, account="KALSHI")   # submitted as 0.01
venue.trade("buy", symbol, 0.001, account="KALSHI")   # rejected: rounds to 0
```

More than two decimal places are not rounded to nearest; they are truncated. Size from a dollar budget with `qty = int(budget / price * 100) / 100`, then confirm the resting/filled qty — do not assume the exchange kept extra precision.

## Order support

Kalshi's V2 create path (`POST /portfolio/events/orders`) requires `price` and `time_in_force`. There is **no native market order type**.

| Type                                        | Supported?            | What we submit                                                                                                                                                                                                          |
| ------------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Market (`order_type` omitted or `"market"`) | Yes, synthesized      | Opposite-side touch, widened by `slippage_bps`, snapped to `price_ranges`, **IOC**. Unfilled remainder is cancelled, not left resting — status `PARTIALLY_FILLED` or `FAILED` (`NO_FILL`).                              |
| Limit                                       | Yes                   | Snapped conservatively onto `price_ranges` (bid rounds down, ask rounds up). **GTC only.**                                                                                                                              |
| Time-in-force                               | Not caller-selectable | Market → `immediate_or_cancel`. Limit → `good_till_canceled`. Passing `day` / `ioc` / `fok` on a limit fails with `TIF_NOT_SUPPORTED`. Kalshi itself also supports `fill_or_kill`; this integration does not expose it. |
| Stop / stop-limit                           | No                    | `ORDER_TYPE_NOT_SUPPORTED`                                                                                                                                                                                              |

Off-grid limit prices are **snapped**, not rejected at our layer. Kalshi may still reject a price that falls outside the market's `price_ranges` after snapping.

During the Thursday 03:00–05:00 ET pause, **placement fails** with `TRADING_PAUSED`; **cancels still work**. Resting GTC orders are submitted with `cancel_order_on_pause=false`, so they survive the pause.

Settlement is automatic. Winning contracts pay \$1; there is no `redeem` call (`REDEEM_NOT_SUPPORTED`). Settled positions appear in `venue.history(category="settlement")`.

## Cash vs buying power

Kalshi's `GET /portfolio/balance` returns `balance_dollars` (documented as **available USD**) and `portfolio_value` (open-position valuation in cents). It does **not** return a buying-power field.

`venue.account(account="KALSHI")` therefore looks like:

```python theme={null}
{
  "name": "KALSHI",
  "cash": 32.9335,            # balance_dollars
  "portfolio_value": ...,     # cash + marked positions
  "buying_power": None,       # omitted — do not coerce to 0
  "positions": [...],
  "open_orders": [...],
}
```

When `buying_power` is missing or `None`, use **`cash` as the available USD balance**. Clients that do `buying_power or 0` will conclude the account cannot trade.

`cash` is USD on the Kalshi account. It does not include position mark-to-market; that lives in `positions[].market_value` / `portfolio_value`.

## Resting-order cash reservation

Kalshi does not publish a `reserved_cash` (or equivalent) on the direct-member balance payload. `GET /portfolio/total_resting_order_value` is documented as **FCM-only** and is not used here.

Observed behavior, consistent with that API:

* A resting GTC bid appears in `open_orders` immediately.
* `cash` (`balance_dollars`) often **does not decrease** while the order rests.
* The matching engine still enforces availability; a bid that cannot be collateralized is rejected at place or fill time.

Do not treat `cash` as “unreserved buying power” and stack resting bids that sum to more than `cash` without checking `open_orders`. A workable available-cash estimate when `buying_power` is null:

```python theme={null}
acct = venue.account(account="KALSHI")["accounts"][0]
cash = acct["cash"]
reserved = sum(
    (o["remaining_qty"] or 0) * (o["price"] or 0)
    for o in acct.get("open_orders") or []
    if (o.get("side") or "").lower() == "buy"
)
available = max(cash - reserved, 0)   # estimate only; Kalshi may already have reserved
```

Treat that as an estimate. Kalshi's availability check is authoritative.

## History semantics and cash formulas

This is the accounting trap.

Kalshi fills carry both `yes_price_dollars` and `no_price_dollars` (they sum to 1) plus canonical direction fields ([Order direction](https://docs.kalshi.com/getting_started/order_direction)):

| Kalshi field   | Meaning                                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------------------------- |
| `action`       | Legacy `buy` / `sell`. Deprecated; still present until at least May 28, 2026.                                        |
| `outcome_side` | Outcome the fill **positions you for**. Buy-YES and sell-NO → `yes`. Buy-NO and sell-YES → `no`.                     |
| `book_side`    | Same bit in book vocabulary: `bid` ≡ `yes`, `ask` ≡ `no`. V2 quotes the YES leg: `bid` pays YES, `ask` receives YES. |
| `count_fp`     | Contracts filled.                                                                                                    |
| `fee_cost`     | Net fee on that fill (trade fee + rounding − rebate).                                                                |

Unified `venue.history(category="trade", account="KALSHI")` maps those as:

| History field | Source                                         | What it is                                                                                |
| ------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `side`        | Kalshi `action`                                | `"buy"` or `"sell"`                                                                       |
| `outcome`     | `outcome_side`                                 | `"YES"` or `"NO"` — **exposure after the fill**, not necessarily the contract you clicked |
| `symbol`      | ticker + `outcome_side`                        | `K:TICKER` or `K:TICKER:NO`                                                               |
| `price`       | `yes_price` if outcome is YES, else `no_price` | Recorded **outcome-leg** price                                                            |
| `qty`         | `count_fp`                                     | Contracts                                                                                 |
| `amount`      | `qty * price`                                  | **Quoted notional of the recorded leg**, unsigned                                         |
| `fees`        | `fee_cost`                                     | Net Kalshi fee on the fill                                                                |
| `multiplier`  | always `1`                                     |                                                                                           |

The general venue invariant `amount == qty * price * multiplier` holds. **`amount` is not cash.**

Closing a held YES contract at a YES price of \$0.43 is a sell-YES, which Kalshi encodes as `outcome_side=no`. History then looks like:

```python theme={null}
{
  "side": "sell",
  "outcome": "NO",
  "price": 0.57,      # no_price = 1 - 0.43
  "qty": 1,
  "amount": 0.57,     # quoted NO notional — not proceeds
  "fees": ...,
}
```

Gross cash credited by the YES book is the YES price:

```
1 - 0.57 = 0.43
```

`qty * price` (0.57) is the wrong proceeds figure.

### Quoted notional vs cash

| Quantity        | How to read it                                                               |
| --------------- | ---------------------------------------------------------------------------- |
| Quoted notional | `amount` = `qty * price` on the **recorded outcome leg**                     |
| YES-leg price   | `price` if `outcome == "YES"` else `1 - price`                               |
| Gross cash      | Signed YES-leg notional (below)                                              |
| Fees            | `fees` from history (authoritative). Do not use `venue.trade()` `fee_amount` |
| Net cash        | Gross cash − `fees`                                                          |

Reconstruct cash from a history row. V2 cash follows the YES book: **bid pays `yes_price`, ask receives `yes_price`**.

```python theme={null}
def kalshi_cash(row):
    qty = row["qty"]
    price = row["price"]          # outcome-leg recorded price
    fees = row.get("fees") or 0
    outcome = (row.get("outcome") or "YES").upper()
    side = (row.get("side") or "buy").lower()

    yes_price = price if outcome == "YES" else (1 - price)
    # bid ≡ buy YES or sell NO; ask ≡ sell YES or buy NO
    is_bid = (side == "buy" and outcome == "YES") or (side == "sell" and outcome == "NO")
    gross = -qty * yes_price if is_bid else +qty * yes_price
    return gross, gross - fees
```

### Worked examples (1 contract, ignore fees first)

| Intent                         | `side` | `outcome` | Recorded `price` | `amount` | Gross cash                |
| ------------------------------ | ------ | --------- | ---------------- | -------- | ------------------------- |
| Buy YES at \$0.43              | `buy`  | `YES`     | 0.43             | 0.43     | **−0.43**                 |
| Close YES at \$0.43 (sell YES) | `sell` | `NO`      | 0.57             | 0.57     | **+0.43** = `+(1 − 0.57)` |
| Buy NO at \$0.57               | `buy`  | `NO`      | 0.57             | 0.57     | **+0.43** = `+(1 − 0.57)` |
| Close NO at \$0.57 (sell NO)   | `sell` | `YES`     | 0.43             | 0.43     | **−0.43**                 |

Buy-NO opening a short YES **credits** the YES price. That is the V2 single-book identity: buying NO is selling YES. Terminal PnL still matches "paid $0.57 for NO" once the $1 complementary payoff is included at settlement.

Net cash is always `gross − fees`.

Do **not** use:

```python theme={null}
sell_proceeds = qty * price          # wrong on complement-encoded sells
buy_cost = qty * price               # wrong when outcome is NO
```

Settlement rows (`category: "settlement"`) are different: `amount` is `revenue` (payout), `closed_pnl` is already `revenue − cost_basis − fees`, and `price` is payout per contract.

Portfolio `venue.history` merges Kalshi **live** fills/settlements with `/historical/*` so completed activity older than the rolling \~3-month live cutoff is not dropped. Public market-data helpers do **not** do that merge — see [Kalshi Data](/docs/market-data/kalshi-data).

## Fees

Kalshi charges exchange fees plus a **rounding fee** so balances stay on the member's precision grid ($0.0001 for direct members). Net fee = trade fee + rounding − rebate, always ≥ $0 ([Fee rounding](https://docs.kalshi.com/getting_started/fee_rounding)).

| Surface                                             | Fee field                                                                                                                              | Authoritative?                                                                                           |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `POST /portfolio/events/orders` (our `venue.trade`) | `average_fee_paid` × `fill_count`, returned as `fees`. Omitted by Kalshi when `fill_count == 0`. Not copied onto unified `fee_amount`. | **No.** Immediate, possibly incomplete. Resting limits that fill later will not show the final fee here. |
| `GET /portfolio/fills` / `/historical/fills`        | `fee_cost` → history `fees`                                                                                                            | **Yes.** Use reconciled trade history for fee and cash accounting.                                       |

Kalshi order responses may omit finalized fees. Use `venue.history(category="trade", account="KALSHI")` for authoritative fee and cash accounting. Scalar Field does not add a platform fee on Kalshi; `fees` on history is Kalshi's `fee_cost`.

There is a short delay before fills, balances, and positions show up on REST ([Get User Data Timestamp](https://docs.kalshi.com/api-reference/exchange/get-user-data-timestamp)). After a fill, prefer the order response for `filled_qty` / `avg_price`, then re-read `venue.history` / `venue.account` rather than treating the synchronous `fees` field as final.

## Cancel

`venue.cancel(broker_order_id, account="KALSHI")` is supported. The primary path is Kalshi **V2**: `DELETE /portfolio/events/orders/{order_id}`.

Success:

| `status`           | Meaning                                                  |
| ------------------ | -------------------------------------------------------- |
| `CANCELLED`        | Resting remainder cancelled, zero fills on the read-back |
| `PARTIALLY_FILLED` | Some qty filled before the cancel; remainder cancelled   |

Pass `symbol` when you have it (`market_ticker` is required for V2 auto-routing). Cancels still work during the weekly trading pause.

Failure is `status="FAILED"` with `reason` set to the exception string. Typical cases:

| Situation                         | What you see today                                                                                                                                                                                                                                                      |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unknown / already-cancelled order | V2 often 404s. The client then retries legacy `DELETE /portfolio/orders/{id}`. That V1 path is deprecated and may return **HTTP 410**. `reason` can contain that raw deprecated-endpoint error — it is not a stable `ORDER_ALREADY_CANCELLED` / `TRADE_NOT_FOUND` code. |
| Not connected                     | `NO_KALSHI_ACCOUNT`                                                                                                                                                                                                                                                     |

Do not parse 410 / V1 deprecation text as “cancel is unsupported”. Resting-order cancel on V2 works. Treat any `FAILED` after a cancel as “confirm via `open_orders` / `get_order`” rather than retrying blindly.

## Errors

Normalized reasons this integration emits itself:

| `reason`                                 | When                                                             |
| ---------------------------------------- | ---------------------------------------------------------------- |
| `NO_KALSHI_ACCOUNT`                      | No connected key                                                 |
| `TRADING_PAUSED`                         | Weekly Thu 03:00–05:00 ET (placement only)                       |
| `TIF_NOT_SUPPORTED`                      | Resting limit with TIF other than GTC                            |
| `ORDER_TYPE_NOT_SUPPORTED`               | Stop / stop-limit                                                |
| `REDEEM_NOT_SUPPORTED`                   | `action="redeem"`                                                |
| Quantity rounds to 0                     | `Quantity {qty} rounds to 0 contracts (minimum 0.01)`            |
| Unknown / closed market                  | `Unknown Kalshi market …` / `… is {status} and cannot be traded` |
| Empty book on a synthesized market order | `… has no liquidity on the {bid/ask} side`                       |
| IOC matched nothing                      | `NO_FILL (immediate-or-cancel found no liquidity)`               |

Everything else (insufficient balance, invalid price after snap, unknown order, already cancelled) is forwarded from Kalshi as `FAILED` + the HTTP/error body. Those strings are **not** a stable enum.

# Usage

End-to-end: find a market, buy YES, verify, close the held qty, account for complement-encoded history, confirm flat.

```python theme={null}
from scalarlib import screenKalshiMarkets, getKalshiOrderbook, venue

markets = screenKalshiMarkets(query="bitcoin", closed=False, limit=20)
row = markets.iloc[0]
yes = row["symbol"]          # human YES handle
no = row["symbol_no"]
ticker = row["ticker"]

book = getKalshiOrderbook(ticker)
print(book["yes"]["bid"], book["yes"]["ask"])   # ask = 1 - best NO bid

# Buy 1 YES (market = synthesized IOC)
buy = venue.trade("buy", yes, 1, account="KALSHI")
print(buy["status"], buy["filled_qty"], buy["avg_price"], buy.get("fees"))

acct = venue.account(account="KALSHI")["accounts"][0]
print("cash", acct["cash"], "buying_power", acct.get("buying_power"))  # buying_power is None
pos = next(p for p in acct["positions"] if p["symbol"] == buy["symbol"])
print(pos["qty"], pos["outcome"], pos["avg_price"])

# Close the actual held qty (do not assume 1.00 if the IOC partial-filled)
sell = venue.trade("sell", pos["symbol"], pos["qty"], account="KALSHI")

hist = venue.history(category="trade", account="KALSHI", symbol=pos["symbol"], limit=20)
for t in hist:
    yes_px = t["price"] if t.get("outcome") == "YES" else (1 - t["price"])
    is_bid = (t["side"] == "buy" and t.get("outcome") == "YES") or (
        t["side"] == "sell" and t.get("outcome") == "NO"
    )
    gross = -t["qty"] * yes_px if is_bid else +t["qty"] * yes_px
    net = gross - (t.get("fees") or 0)
    print(t["side"], t.get("outcome"), t["price"], t["amount"], "cash", gross, "net", net)

acct = venue.account(account="KALSHI")["accounts"][0]
assert not any(p["symbol"] == pos["symbol"] for p in acct["positions"])
assert not any(o.get("symbol") == pos["symbol"] for o in acct.get("open_orders") or [])
```

Limit on the NO leg:

```python theme={null}
resp = venue.trade(
    "buy", no, 5,
    account="KALSHI", order_type="limit", limit_price=0.42,
)
# Resting: status PENDING, cash typically unchanged, order in open_orders
venue.cancel(resp["broker_order_id"], account="KALSHI", symbol=no)
```

In strategy agents, use `strategy.execute(symbol, target_qty)` with `brokerage_account="KALSHI"`.

# Data Functions

Market data is public (no Kalshi connection required). Full reference, including sparse candles and live-vs-historical coverage: [Kalshi Data](/docs/market-data/kalshi-data).

| Function                                                | Purpose                                                                                                      |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `screenKalshiMarkets(...)`                              | Discover/filter markets. Query `status`: `"open"` (default; `None` same as omit) / `"closed"` / `"settled"`  |
| `screenKalshiEvents(...)`                               | Discover events. Query `status`: `"open"` / `"closed"` / `"settled"` / omit; other values raise `ValueError` |
| `getKalshiMarket(ticker)`                               | Full public Market payload (result, settlement\_ts, rules, disputed/amended)                                 |
| `getKalshiOrderbook(ticker)`                            | Live YES/NO book (asks derived)                                                                              |
| `getKalshiCandles(ticker, start, end, period_interval)` | OHLC (1 / 60 / 1440 min). Empty intervals have null OHLC. Live then historical                               |
| `getKalshiTrades(ticker)`                               | Public trade tape with `is_complete` / `next_cursor`; live then historical                                   |

# Notes

* Weekly maintenance pause: Thursday 03:00–05:00 ET — order placement fails with `TRADING_PAUSED`; cancels still work.
* The stored API key cannot withdraw or transfer funds (`write::transfer` is never granted).
* Perpetual futures (margin) are not supported in this integration.
* Orders, fills, and settlements older than Kalshi's \~3-month live cutoff are merged from `/historical/*` on **portfolio** reads (`venue.history`, positions, resting/completed orders). Public `getKalshiTrades` / `getKalshiCandles` also fail over to `GET /historical/trades` and `GET /historical/markets/{ticker}/candlesticks`.

# Related

* [Kalshi Data](/docs/market-data/kalshi-data)
* [Polymarket vs Kalshi](/docs/trading/venues/polymarket-vs-kalshi)
* [Supported Brokerages](/docs/trading/supported-brokerages)
* [Strategies](/docs/trading/strategies)
