Skip to content

mostlyright.markets.kalshi

Kalshi venue wrapper over the venue-free core composition body.

markets.kalshi.training_table(ticker, ...) owns all Kalshi venue knowledge — series/city → settlement station resolution, the label="cli" routing (Kalshi NHIGH/NLOW settle on the NWS CLI product), trades columns — and delegates the entire feature-composition + join to the core private _dataset_impl body. No join logic is duplicated here: the wrapper resolves a station, then forwards to core with label="cli".

outcome=True appends a single label_outcome column. Settlement is metadata-driven: it reads the market’s own strike_type / floor_strike / cap_strike (the Kalshi GetMarket contract), supplied offline through the market= argument, and never infers a direction or bounds from the -T<n> / -B<mid> ticker suffix. parse_ticker resolves the settlement station only; when settlement metadata is absent, settlement raises a typed error rather than guessing.

Kalshi settlement comparison — the Kalshi GetMarket strike_type contract (docs.kalshi.com/api-reference/market/get-market):

  • greater → YES iff value > floor_strike (STRICT)
  • greater_or_equal → YES iff value >= floor_strike
  • less → YES iff value < cap_strike (STRICT)
  • less_or_equal → YES iff value <= cap_strike
  • between → YES iff floor_strike <= value <= cap_strike

The displayed threshold (“87° or above”) is floor_strike + 1 because NWS CLI settlement values are whole °F and greater is strict — a DISPLAY artifact, never a settlement input; settle only ever compares the observed value to floor_strike / cap_strike.

AttributeDescription
labelThe kalshi.label namespace singleton (kalshi.label.settlement(...)).
FunctionDescription
candles(url_or_id, *, interval, from_time, …)Read a Kalshi market’s bucketed price history over a window.
events(*[, series, status, all])List the events under a Kalshi series.
market(url_or_id)Look one Kalshi market up by ticker or by a pasted kalshi.com link.
markets(*[, series, event, status, …])List Kalshi markets, narrowed by series, event, status, or category.
orderbook(url_or_id, *[, depth])Read a Kalshi market’s resting book as it stands right now.
parse_ticker(ticker)Parse a full Kalshi market ticker → (series, measure, station) OFFLINE.
series(*[, category, all])Browse Kalshi’s recurring-contract catalog.
settlement_days(entity, from_date, to_date)The bare Kalshi settlement-day grid (train == live — the skew guard).
trades(url_or_id, *, from_time, to_time)Read a Kalshi market’s executed prints over a window.
training_table(entity, from_date, to_date, *)Kalshi leakage-free supervised table over the label="cli" settlement target.
ExceptionDescription
KalshiTickerErrorA Kalshi market ticker could not be parsed to (series, city, strike).

exception mostlyright.markets.kalshi.KalshiTickerError

Section titled “exception mostlyright.markets.kalshi.KalshiTickerError”

Bases: ValueError

A Kalshi market ticker could not be parsed to (series, city, strike).

Subclasses ValueError so existing pytest.raises(ValueError) guards and callers catching ValueError keep working.

mostlyright.markets.kalshi.candles(url_or_id, , interval, from_time, to_time)

Section titled “mostlyright.markets.kalshi.candles(url_or_id, , interval, from_time, to_time)”

Read a Kalshi market’s bucketed price history over a window.

Works for any window length. Kalshi refuses a request spanning more than 5,000 candlesticks with a hard HTTP 400 — no partial result and no silent truncation — so a month of minute buckets is not one request. This verb splits the window into chunks of 4,500 buckets (a margin under the cap so a boundary rounding at a chunk edge can never trip it), issues them SEQUENTIALLY under the client’s politeness floor, then concatenates, deduplicates the bucket a venue may serve in both adjacent chunks, and sorts ascending.

Timestamps are bucket starts. Kalshi stamps each candle with the period end; the interval is subtracted here so a Kalshi frame and a frame from another venue put the same print in the same bucket. A bucket whose instant the venue did not serve is dropped rather than carried as a NULL timestamp (the column is non-nullable, and a NULL key would collapse every such bucket onto one row in the dedup); when any row is dropped the frame carries df.attrs["rows_dropped_unplaceable"].

Units. open / high / low / close are probabilities in [0, 1]; each *_native carries the venue’s own magnitude in cents, with sub-cent price tiers preserved as fractional cents. Every row also carries the yes-bid and yes-ask OHLC and volume_contracts / open_interest: on an illiquid market the quote legs are often the only signal, so they are never conditional. There is deliberately no column named volume — the unit belongs in the name.

One tier per call. Kalshi serves recent markets from its live tier and older ones from a historical tier whose rows come back stamped source="kalshi.historical". The tier is resolved once, on the first chunk, and every remaining chunk is asked of that same tier — so a returned frame never silently carries two provenances. A market neither tier serves raises rather than returning an empty frame.

  • Parameters:
    • url_or_id (str) – A Kalshi market ticker ("KXHIGHNY-26MAY29-T85") or a kalshi.com market link carrying ?ticker=. The link is parsed offline and never fetched.
    • interval (str) – Bucket width — "1m", "1h" or "1d".
    • from_time (datetime) – tz-aware UTC bounds; from_time must be strictly earlier than to_time.
    • to_time (datetime) – tz-aware UTC bounds; from_time must be strictly earlier than to_time.
  • Return type: DataFrame
  • Returns: A schema.markets.candles.v2 frame, one row per bucket. A window the venue has no candles for returns an empty frame carrying the full column set, never None and never [].
  • Raises:
>>> from datetime import UTC, datetime
>>> window = kalshi.candles(
... "KXHIGHNY-26MAY29-T85",
... interval="1h",
... from_time=datetime(2026, 5, 29, tzinfo=UTC),
... to_time=datetime(2026, 5, 30, tzinfo=UTC),
... )
>>> window["close"].between(0, 1).all()
True

mostlyright.markets.kalshi.events(, series=None, status=None, all=False)

Section titled “mostlyright.markets.kalshi.events(, series=None, status=None, all=False)”

List the events under a Kalshi series.

An event groups the markets that settle on one occurrence — one calendar day of a daily-high series, for instance — so this is the middle tier between series() and markets().

  • Parameters:
    • series (str | None) – Series ticker to narrow to (e.g. "KXHIGHNY").
    • status (str | None) – One of open / closed / settled / unopened. Validated here so a typo is a readable error rather than an HTTP 400 from the venue.
    • all (bool) – Accept every event the venue lists. Required when no other filter is given.
  • Return type: DataFrame
  • Returns: A schema.markets.events.v1 frame, one row per event, with open_time / close_time as tz-aware UTC instants. A query that matches nothing returns an empty frame with the full column set.
  • Raises: ContractError – No filter and no all=True, or an invalid status.

mostlyright.markets.kalshi.fetch_market(ticker, , client=None, sleep_between=None)

Section titled “mostlyright.markets.kalshi.fetch_market(ticker, , client=None, sleep_between=None)”

Fetch the Kalshi market object for ticker (GetMarket).

An opt-in settlement-metadata helper. GET /markets/{ticker} on the same public, no-auth, read-only Kalshi REST API as fetch_candlesticks / fetch_trades / fetch_orderbook. Returns the market object carrying strike_type / floor_strike / cap_strike / result, which a user passes to mostlyright.markets.kalshi.training_table() as market=.

training_table never calls this — it exists so a caller can fetch the metadata themselves and pass it in, so no settlement path makes a hidden fetch. No persistence: the market object is used in-memory and never cached.

Not the same thing as markets.kalshi.market(url_or_id): that verb returns a normalized one-row schema.markets.listing.v1 frame with probability prices and accepts a pasted kalshi.com link, while this function returns the raw venue object the settlement path reads.

  • Parameters:
    • ticker (str) – Full market ticker (e.g. KXHIGHNY-26JUL24-B81.5).
    • client (Client | None) – Optional httpx.Client.
    • sleep_between (float | None) – Per-request polite sleep override.
  • Return type: dict[str, Any]
  • Returns: The Kalshi market object (dict) with the strike metadata.

The kalshi.label namespace singleton (kalshi.label.settlement(...)).

mostlyright.markets.kalshi.market(url_or_id)

Section titled “mostlyright.markets.kalshi.market(url_or_id)”

Look one Kalshi market up by ticker or by a pasted kalshi.com link.

Paste the link from the post you are checking, or type the ticker — both identify the same contract and return the same single row.

The pasted link is never fetched. It is parsed offline for host and shape, the ticker is extracted from it, and the venue request is built from the resolved API address, so a lookalike host cannot steer a request anywhere.

Both tiers are searched: the live tier answers first, and a market older than its retention window falls through to the historical tier and comes back stamped source="kalshi.historical". A ticker neither tier knows raises rather than returning an empty frame — “no such market” and “no data” are different answers and this verb will not conflate them.

Not to be confused with fetch_market(), which returns the raw venue market object (strike_type / floor_strike / cap_strike) that a caller passes to training_table(market=...). This verb returns a normalized one-row schema.markets.listing.v1 frame with probability prices.

  • Parameters: url_or_id (str) – A Kalshi market ticker ("KXHIGHNY-26MAY29-T85") or a kalshi.com market link carrying ?ticker=.
  • Return type: DataFrame
  • Returns: A one-row schema.markets.listing.v1 frame.
  • Raises:
    • ContractErrorurl_or_id is not a valid ticker, or is a link that identifies a series or an event rather than a single market.
    • httpx.HTTPStatusError – Neither tier serves the market.

mostlyright.markets.kalshi.markets(, series=None, event=None, status=None, category=None, all=False, historical=False)

Section titled “mostlyright.markets.kalshi.markets(, series=None, event=None, status=None, category=None, all=False, historical=False)”

List Kalshi markets, narrowed by series, event, status, or category.

Prices come back as probabilities in [0, 1] so a frame from this venue and a frame from another can be concatenated and compared without a unit conversion at the call site. The Polymarket-only columns (market_slug, outcome_token_ids, volume_usd, liquidity_usd) are NULL: Kalshi serves no such value, and this frame would rather say nothing than say something plausible and wrong.

Multivariate event combinations are always excluded. They are a >400,000 row combinatorial universe that no research question asks for, and there is no kwarg to let them back in.

  • Parameters:
    • series (str | None) – Series ticker to narrow to (e.g. "KXHIGHNY").
    • event (str | None) – Event ticker to narrow to (e.g. "KXHIGHNY-26MAY29").
    • status (str | None) – One of open / closed / settled / unopened.
    • category (str | None) – Venue category to narrow to.
    • all (bool) – Accept the full unnarrowed universe. Required when no other filter is given.
    • historical (bool) – Read from the venue’s historical tier instead of the live one, for markets that closed before the live retention window. Requires series, because that tier reaches back far enough that an unnarrowed walk of it is exactly the bulk retrieval the venue’s terms bar. Rows come back stamped source="kalshi.historical".
  • Return type: DataFrame
  • Returns: A schema.markets.listing.v1 frame, one row per market. A query that matches nothing returns an empty frame with the full column set.
  • Raises: ContractError – No filter and no all=True, an invalid status, or historical=True without series.
>>> settled = markets(series="KXHIGHNY", status="settled")
>>> settled["last_price"].between(0, 1).all()
True

mostlyright.markets.kalshi.orderbook(url_or_id, , depth=50)

Section titled “mostlyright.markets.kalshi.orderbook(url_or_id, , depth=50)”

Read a Kalshi market’s resting book as it stands right now.

The frame is LONG: one row per price level, with side and level naming where that row sits. That shape is what lets a Kalshi book and a book from another venue concatenate into one frame and stay unit-honest — a nested bid/ask column pair would have to invent a fill rule for the side with fewer levels, and would silently pair a bid with an unrelated ask. level is 0 at the touch (the highest bid, the lowest ask) and counts outward, assigned here rather than trusted from the wire so the ordering is identical however the venue happened to sort its arrays.

Kalshi quotes YES bids and NO bids, not bids and asks. A resting NO bid at price p is an offer to sell YES at 1 - p, so the NO side is mapped onto the yes-token ASK by ask_price = 1 - no_bid_price. Every row of the returned frame speaks the YES token, on both sides.

A book is a point-in-time read. There is no historical book tier on this venue — a past book cannot be reconstructed from a past price — so every row is stamped source="kalshi" and captured_at is the one instant the snapshot was taken, identical across the whole frame.

  • Parameters:
    • url_or_id (str) – A Kalshi market ticker or a kalshi.com market link carrying ?ticker=. The link is parsed offline and never fetched.
    • depth (int) – Levels to request per side, 1 to 1000.
  • Return type: DataFrame
  • Returns: A schema.markets.orderbook.v2 frame, one row per level. A market with no resting orders returns an empty frame carrying the full column set, never None and never [].
  • Raises:
    • ContractErrorurl_or_id does not identify a single market.
    • ValueErrordepth is outside 1 to 1000. Raised before any request.

mostlyright.markets.kalshi.parse_ticker(ticker)

Section titled “mostlyright.markets.kalshi.parse_ticker(ticker)”

Parse a full Kalshi market ticker → (series, measure, station) OFFLINE.

Station-only and grammar-agnostic. Resolves the series → measure(high/low) + city → settlement station via the parity-critical catalog whitelist, and captures the strike token verbatim without interpreting it. It accepts every real ticker shape, including a between ticker such as KXHIGHNY-26JUL24-B81.5. Settlement direction/bounds come from the market’s own strike_type / floor_strike / cap_strike metadata, threaded on later via _apply_market_metadata(), never from this token.

  • Parameters: ticker (str) – The full market ticker (<SERIES>-<DATECODE>-<STRIKE>).
  • Return type: _ParsedTicker
  • Returns: A _ParsedTicker with settlement metadata unset (None).
  • Raises: KalshiTickerError – the ticker doesn’t decompose into series / date / strike, the series isn’t a HIGH/LOW weather series, the strike token is empty, or the city isn’t in the settlement whitelist.

mostlyright.markets.kalshi.series(, category=None, all=False)

Section titled “mostlyright.markets.kalshi.series(, category=None, all=False)”

Browse Kalshi’s recurring-contract catalog.

A series is the concept a researcher thinks in — “the NYC daily high family” — and each series owns the events and markets beneath it. Start here, then narrow with events() and markets().

This endpoint answers with everything, in one response. Measured in July 2026: 12,330 series, 16,277,583 bytes uncompressed / 2.29 MB gzipped, in 0.66 s. It ignores limit and returns no cursor, so category= is the only narrowing that actually shrinks the payload ("Economics" returns 630 rows). httpx negotiates gzip automatically, so the 2.29 MB figure is what crosses the wire. Those numbers are what the venue served when they were taken and can move as the catalog grows.

  • Parameters:
    • category (str | None) – Venue category to narrow to (e.g. "Economics"). Matched against the category column of the returned frame.
    • all (bool) – Accept the full unnarrowed catalog. Required when category is omitted — see the error raised in that case.
  • Return type: DataFrame
  • Returns: A schema.markets.series.v1 frame, one row per series. A category the venue knows nothing about returns an empty frame with the full column set, never None.
  • Raises: ContractError – Neither category nor all=True was given.
>>> economics = series(category="Economics")
>>> economics.columns.tolist()[:3]
['venue', 'series_ticker', 'title']

mostlyright.markets.kalshi.settlement_days(entity, from_date, to_date)

Section titled “mostlyright.markets.kalshi.settlement_days(entity, from_date, to_date)”

The bare Kalshi settlement-day grid (train == live — the skew guard).

Two positional dates (inclusive ends). entity is a full Kalshi ticker (or a list). Each ticker resolves its settlement station via the parity-critical catalog whitelist (parse_ticker — no duplicated station logic); the grid is one row per LST settlement day with no y columns.

Every returned row keeps its originating ticker as the leading column (before station and the date day field), so concatenating several contracts never collapses provenance — a per-row ticker survives the concat and each day is attributable to the contract that minted it.

The caller names the contracts; the factory mints the calendar for the settlement station(s) they existed under. PIT-correct rolling-feature transforms are OUT of scope (computed inside sources or by users downstream at their own risk).

  • Parameters:
    • entity (str | list[str] | tuple[str, ...]) – A full Kalshi ticker, or a list of tickers for a long-format panel.
    • from_date (str) – Inclusive start date.
    • to_date (str) – Inclusive end date.
  • Return type: DataFrame
>>> grid = settlement_days("KXHIGHNY", "2025-01-06", "2025-01-12")
>>> grid.columns[:3].tolist()
['ticker', 'station', 'date']

mostlyright.markets.kalshi.trades(url_or_id, , from_time, to_time)

Section titled “mostlyright.markets.kalshi.trades(url_or_id, , from_time, to_time)”

Read a Kalshi market’s executed prints over a window.

Where candles() bucketizes, this returns the tape itself: one row per fill, ordered oldest first. The window kwargs are the same from_time / to_time grammar every window-scoped verb in the SDK uses — tz-aware UTC instants, timestamp grain, half-open — and they reach the venue as its own UNIX-second bounds.

Units. price is a probability in [0, 1] for the YES side; price_native carries the venue’s cents. The printed NO price is 1 - yes by construction, so carrying it would be a second unit rather than new information.

“volume_usd“ is NULL on every Kalshi row. The venue serves no USD notional for a fill, and multiplying a contract count by a price would publish a number the venue never did. Absent is not zero. The column exists so a Kalshi frame and a Polymarket frame — where the value IS derivable and is carried alongside both of its inputs — concatenate into one tape.

One tier per call. The live tier answers first; a market older than its retention window falls through to the historical tape and every row comes back stamped source="kalshi.historical". The window is applied in this verb on both tiers: the historical tape accepts no server-side bounds at all, and the live tape’s min_ts/max_ts are inclusive where this verb’s window is half-open — so a print landing exactly on to_time would otherwise be in or out depending on which tier answered.

A print whose instant the venue did not serve is dropped, and counted. traded_at is non-nullable, and a fill that cannot be placed on a timeline cannot be claimed to be inside the window either. When any row is dropped the frame carries df.attrs["rows_dropped_unplaceable"]; the key is absent when nothing was dropped.

Supersedes the private _kalshi_trades.fills tape, which stays in place for its existing callers and keeps emitting the older row contract.

  • Parameters:
    • url_or_id (str) – A Kalshi market ticker or a kalshi.com market link carrying ?ticker=. The link is parsed offline and never fetched.
    • from_time (datetime) – tz-aware UTC bounds; from_time must be strictly earlier than to_time.
    • to_time (datetime) – tz-aware UTC bounds; from_time must be strictly earlier than to_time.
  • Return type: DataFrame
  • Returns: A schema.markets.trades.v2 frame, one row per fill, ascending by traded_at. A quiet window returns an empty frame carrying the full column set, never None and never [].
  • Raises:
    • ContractErrorurl_or_id does not identify a single market.
    • TypeError – either bound is a naive datetime.
    • ValueErrorfrom_time is not earlier than to_time.
    • RuntimeError – The tape ran past the client’s page cap. Narrow the window rather than raising the cap — this verb does not swallow it, because a truncated tape that reads as a complete one is worse than an error.

mostlyright.markets.kalshi.training_table(entity, from_date, to_date, , outcome=False, market=None, features=None)

Section titled “mostlyright.markets.kalshi.training_table(entity, from_date, to_date, , outcome=False, market=None, features=None)”

Kalshi leakage-free supervised table over the label="cli" settlement target.

A thin delegator with no duplicated join logic. It resolves the settlement station from entity (the full Kalshi ticker, via the parity-critical catalog whitelist), then forwards the whole feature-composition + join to core research.dataset with label="cli" (Kalshi NHIGH/NLOW settle on the NWS CLI product). Two positional dates.

“outcome=True“ is offline and metadata-driven. It appends a single binary label_outcome computed from the market’s own strike_type / floor_strike / cap_strike, supplied via the market= argument. The -T<n> / -B<mid> ticker suffix is never used to infer the settlement direction. When market= is absent, outcome=True raises a typed ContractError — it makes no network call and never guesses. Fetch the metadata yourself with the opt-in mostlyright.markets._kalshi_client.fetch_market() and pass it as market=; training_table never calls it for you.

This is the markets analogue of weather.training_table(); it composes the domain day-grid factory + sources (kalshi.settlement_days() / kalshi.label.settlement() + the core observation join). The old kalshi.dataset() name is a deprecation shim routing here — dataset is reserved for the catalog noun.

Trades: live trade columns are attached via the dedicated mostlyright.markets._kalshi_trades module (candles/fills/orderbook), not this settlement-join wrapper. The core "trades" feature is not available through the station dataset path; features=["trades"] raises the documented core ValueError.

  • Parameters:
    • entity (str) – The full Kalshi market ticker (KXHIGHNY-25MAY26-T86 / KXHIGHNY-26JUL24-B81.5). Its series → settlement station.
    • from_date (str) – YYYY-MM-DD window bounds (forwarded verbatim).
    • to_date (str) – YYYY-MM-DD window bounds (forwarded verbatim).
    • outcome (bool) – When True, append a binary label_outcome (Int64, 0/1/NA) column, computed from market= per the Kalshi GetMarket strike_type contract (see _settle_metadata()).
    • market (dict[str, Any] | None) – The Kalshi GetMarket object (strike_type / floor_strike / cap_strike), supplied offline by the caller. Required when outcome=True; ignored otherwise.
    • features (list[str] | tuple[str, ...] | None) – Extra core feature names forwarded verbatim to the core composition.
  • Return type: DataFrame
  • Returns: The core dataset(label="cli", ...) frame (⊇ byte-identical core columns) plus, when outcome=True, the label_outcome column.
  • Raises:
    • KalshiTickerError – the ticker doesn’t parse or its city isn’t in the settlement whitelist.
    • ContractErroroutcome=True and the market= metadata is absent or carries an unsupported/incomplete strike_type (refuses; no network, no guess).