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 iffvalue > floor_strike(STRICT)greater_or_equal→ YES iffvalue >= floor_strikeless→ YES iffvalue < cap_strike(STRICT)less_or_equal→ YES iffvalue <= cap_strikebetween→ YES ifffloor_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.
Module Attributes
Section titled “Module Attributes”| Attribute | Description |
|---|---|
label | The kalshi.label namespace singleton (kalshi.label.settlement(...)). |
Functions
Section titled “Functions”| Function | Description |
|---|---|
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. |
Exceptions
Section titled “Exceptions”| Exception | Description |
|---|---|
KalshiTickerError | A 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_timemust be strictly earlier thanto_time. - to_time (
datetime) – tz-aware UTC bounds;from_timemust be strictly earlier thanto_time.
- url_or_id (
- Return type:
DataFrame - Returns:
A
schema.markets.candles.v2frame, one row per bucket. A window the venue has no candles for returns an empty frame carrying the full column set, neverNoneand never[]. - Raises:
- ContractError –
url_or_iddoes not identify a single market. - TypeError – either bound is a naive datetime.
- ValueError –
from_timeis not earlier thanto_time. - UnsupportedResolutionError –
intervalis outside the supported set. Raised before any request.
- ContractError –
Example
Section titled “Example”>>> 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()Truemostlyright.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 ofopen/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.
- series (
- Return type:
DataFrame - Returns:
A
schema.markets.events.v1frame, one row per event, withopen_time/close_timeas 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 invalidstatus.
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:
- Return type:
dict[str,Any] - Returns: The Kalshi market object (dict) with the strike metadata.
mostlyright.markets.kalshi.label
Section titled “mostlyright.markets.kalshi.label”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.v1frame. - Raises:
- ContractError –
url_or_idis 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.
- ContractError –
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 ofopen/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. Requiresseries, 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 stampedsource="kalshi.historical".
- series (
- Return type:
DataFrame - Returns:
A
schema.markets.listing.v1frame, 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 invalidstatus, orhistorical=Truewithoutseries.
Example
Section titled “Example”>>> settled = markets(series="KXHIGHNY", status="settled")>>> settled["last_price"].between(0, 1).all()Truemostlyright.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:
- Return type:
DataFrame - Returns:
A
schema.markets.orderbook.v2frame, one row per level. A market with no resting orders returns an empty frame carrying the full column set, neverNoneand never[]. - Raises:
- ContractError –
url_or_iddoes not identify a single market. - ValueError –
depthis outside 1 to 1000. Raised before any request.
- ContractError –
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
_ParsedTickerwith 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:
- Return type:
DataFrame - Returns:
A
schema.markets.series.v1frame, one row per series. A category the venue knows nothing about returns an empty frame with the full column set, neverNone. - Raises:
ContractError – Neither
categorynorall=Truewas given.
Example
Section titled “Example”>>> 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:
- Return type:
DataFrame
Example
Section titled “Example”>>> 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_timemust be strictly earlier thanto_time. - to_time (
datetime) – tz-aware UTC bounds;from_timemust be strictly earlier thanto_time.
- url_or_id (
- Return type:
DataFrame - Returns:
A
schema.markets.trades.v2frame, one row per fill, ascending bytraded_at. A quiet window returns an empty frame carrying the full column set, neverNoneand never[]. - Raises:
- ContractError –
url_or_iddoes not identify a single market. - TypeError – either bound is a naive datetime.
- ValueError –
from_timeis not earlier thanto_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.
- ContractError –
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-DDwindow bounds (forwarded verbatim). - to_date (
str) –YYYY-MM-DDwindow bounds (forwarded verbatim). - outcome (
bool) – WhenTrue, append a binarylabel_outcome(Int64, 0/1/NA) column, computed frommarket=per the Kalshi GetMarketstrike_typecontract (see_settle_metadata()). - market (
dict[str,Any] |None) – The Kalshi GetMarket object (strike_type/floor_strike/cap_strike), supplied offline by the caller. Required whenoutcome=True; ignored otherwise. - features (
list[str] |tuple[str,...] |None) – Extra core feature names forwarded verbatim to the core composition.
- entity (
- Return type:
DataFrame - Returns:
The core
dataset(label="cli", ...)frame (⊇ byte-identical core columns) plus, whenoutcome=True, thelabel_outcomecolumn. - Raises:
- KalshiTickerError – the ticker doesn’t parse or its city isn’t in the settlement whitelist.
- ContractError –
outcome=Trueand themarket=metadata is absent or carries an unsupported/incompletestrike_type(refuses; no network, no guess).