๐ Prediction markets with sportsdataverse-py
This page is the executed notebook
18_prediction_markets_intro.ipynb: download it to run it yourself.
Sportsbooks quote odds; prediction markets quote prices. A YES contract that pays $1 and trades at
$0.62 is a 62% implied probability, set by whoever is trading it. sportsdataverse.odds wraps the
public market-data APIs of the two biggest venues:
| Venue | Functions | Auth |
|---|---|---|
| Kalshi | kalshi_* | none for market data |
| Polymarket | polymarket_gamma_* (catalog), polymarket_clob_* (order book) | none |
Sportsbook odds from The Odds API live in 12_odds_intro. This notebook only reads public prices;
it places no orders.
๐งฐ The toolboxโ
| Function | What it gives you |
|---|---|
kalshi_exchange_status | Is the exchange open for trading? |
kalshi_series_list | Market families (series) in a category |
kalshi_events | Events, filterable by series and status |
kalshi_markets | Markets of an event, with bid / ask / last price |
kalshi_orderbook | Order book of one market |
kalshi_trades | Public trades |
polymarket_gamma_events | Events, filterable by tag (sports, ...) |
polymarket_gamma_markets | Markets with outcomes, prices and volume |
polymarket_clob_midpoint | Midpoint of one outcome token's book |
๐ Setupโ
pip install sportsdataverse
The Kalshi and Polymarket helpers take their paging arguments (limit, offset) as strings, the way
the APIs document them.
import json
import matplotlib.pyplot as plt
import polars as pl
from sportsdataverse import odds
pl.Config.set_tbl_rows(10)
pl.Config.set_fmt_str_lengths(60)
polars.config.Config
Provider feeds are seasonal and occasionally rate-limited, so a tiny safe() helper runs each call
defensively: you get the frame when the feed is up, and a one-line skip when it isn't, naming whether
the answer was nothing there (NoDataError) or the fetch failed (AssetFetchError). ๐
from sportsdataverse.errors import AssetFetchError, NoDataError
def safe(label, thunk):
try:
out = thunk()
print(f"โ
{label}")
return out
except (NoDataError, AssetFetchError) as e:
print(f"\u23ed\ufe0f {label}: {type(e).__name__}: {e}")
return None
๐ฆ Kalshiโ
Kalshi groups markets as series (a market family, such as single NFL games), events (one game) and markets (one contract). Start with the exchange status and the sports series.
safe("Kalshi status", odds.kalshi_exchange_status)
โ
Kalshi status
shape: (1, 4)
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ exchange_active โ exchange_index_statuses โ intra_exchange_transfers_acti โ trading_active โ
โ --- โ --- โ ve โ --- โ
โ bool โ str โ --- โ bool โ
โ โ โ bool โ โ
โโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโก
โ true โ [{"description": "Default", โ true โ true โ
โ โ "exchange_active": true, โ โ โ
โ โ "exchanโฆ โ โ โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโ
series = safe("Kalshi sports series", lambda: odds.kalshi_series_list(category="Sports"))
(series.filter(pl.col("title").str.contains("(?i)NFL|NBA|NHL|college"))
.select("ticker", "title", "frequency").head(10)
if series is not None and series.height else "series unavailable")
โ
Kalshi sports series
shape: (10, 3)
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ ticker โ title โ frequency โ
โ --- โ --- โ --- โ
โ str โ str โ str โ
โโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโก
โ KXNCAAMBBIG12REG โ Men's College Basketball Big 12 Regular Season Champion โ custom โ
โ KXNFLDRAFTSELECT โ NFL Draft Exact Selection โ custom โ
โ KXNCAAF1HFT โ College Football 1st Half Fulltime Result โ custom โ
โ KXNCAAWBTOTAL โ Women's College Basketball Total Points โ custom โ
โ KXNHLNORRIS โ NHL James Norris Memorial Trophy โ annual โ
โ KXNCAAFFIRSTTDTEAM โ College Football First Team to Score a Touchdown โ custom โ
โ KXNFLCAREERRSHTDS โ NFL Career Rushing Touchdowns โ custom โ
โ KXNFLCAREERPASSYDS โ NFL Career Passing Yards โ custom โ
โ KXNBA2QTOTAL โ NBA 2nd Quarter Total โ custom โ
โ KXNBAGAMES โ NBA games โ daily โ
โโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโ
One NFL game market and its order book ๐โ
KXNFLGAME is the single-game NFL series. Take its first open event, list the markets
(kalshi_markets), then read one market's book
(kalshi_orderbook). The book holds bids only, on both
sides; prices and sizes arrive as strings, so we cast them before plotting.
events = safe("Kalshi NFL events", lambda: odds.kalshi_events(limit="10", status="open", series_ticker="KXNFLGAME"))
event = events["event_ticker"][0] if events is not None and events.height else None
mkts = safe(f"markets {event}", lambda: odds.kalshi_markets(event_ticker=event)) if event else None
cols = ["ticker", "title", "yes_bid_dollars", "yes_ask_dollars", "last_price_dollars", "volume_fp"]
mkts.select([c for c in cols if c in mkts.columns]) if mkts is not None and mkts.height else "no open NFL events"
โ
Kalshi NFL events
โ
markets KXNFLGAME-26OCT26DALPHI
shape: (2, 6)
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ ticker โ title โ yes_bid_dollar โ yes_ask_dollar โ last_price_dol โ volume_fp โ
โ --- โ --- โ s โ s โ lars โ --- โ
โ str โ str โ --- โ --- โ --- โ str โ
โ โ โ str โ str โ str โ โ
โโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโก
โ KXNFLGAME-26OCT โ Philadelphia โ 0.3100 โ 0.3200 โ 0.3200 โ 721704.90 โ
โ 26DALPHI-PHI โ wins โ โ โ โ โ
โ KXNFLGAME-26OCT โ Dallas wins โ 0.6700 โ 0.6800 โ 0.6800 โ 556008.54 โ
โ 26DALPHI-DAL โ โ โ โ โ โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโ
ticker = mkts["ticker"][0] if mkts is not None and mkts.height else None
book = safe(f"order book {ticker}", lambda: odds.kalshi_orderbook(ticker)) if ticker else None
if book is not None and book.height:
fig, ax = plt.subplots(figsize=(8, 4))
book = book.with_columns(pl.col("price", "size").cast(pl.Float64))
yes = book.filter(pl.col("side") == "yes")
# A NO bid at p is an offer to sell YES at 1 - p, so both sides fit on one YES-price axis.
no = book.filter(pl.col("side") == "no").with_columns(price=1 - pl.col("price"))
ax.bar(yes["price"], yes["size"], width=0.008, color="#2ca02c", alpha=0.7, label="YES bids")
ax.bar(no["price"], no["size"], width=0.008, color="#d62728", alpha=0.7, label="YES offers (NO bids)")
ax.set_yscale("log")
ax.set_xlabel("YES price in dollars (= implied probability)")
ax.set_ylabel("contracts (log scale)")
ax.set_title(f"Kalshi order book: {ticker}")
ax.legend()
plt.show()
else:
print("no order book to draw")
โ
order book KXNFLGAME-26OCT26DALPHI-PHI

trades = safe(f"trades {ticker}", lambda: odds.kalshi_trades(limit="20", ticker=ticker)) if ticker else None
trades.head() if trades is not None and trades.height else "no recent trades"
โ
trades KXNFLGAME-26OCT26DALPHI-PHI
shape: (5, 10)
โโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ count_fp โ created_t โ is_block_ โ no_price_ โ โฆ โ taker_sid โ ticker โ trade_id โ yes_price โ
โ --- โ ime โ trade โ dollars โ โ e โ --- โ --- โ _dollars โ
โ str โ --- โ --- โ --- โ โ --- โ str โ str โ --- โ
โ โ str โ bool โ str โ โ str โ โ โ str โ
โโโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโโโโโชโโโโชโโโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโโโโโก
โ 14.91 โ 2026-10-2 โ false โ 0.6800 โ โฆ โ yes โ KXNFLGAME โ 0724ab06- โ 0.3200 โ
โ โ 6T08:23:2 โ โ โ โ โ -26OCT26D โ 0b29-83ff โ โ
โ โ 1.219164Z โ โ โ โ โ ALPHI-PHI โ -908c-143 โ โ
โ โ โ โ โ โ โ โ 3676aadb8 โ โ
โ 13.60 โ 2026-10-2 โ false โ 0.6800 โ โฆ โ yes โ KXNFLGAME โ 0724ab04- โ 0.3200 โ
โ โ 6T08:22:5 โ โ โ โ โ -26OCT26D โ 9589-bb73 โ โ
โ โ 6.209996Z โ โ โ โ โ ALPHI-PHI โ -fac8-d87 โ โ
โ โ โ โ โ โ โ โ 33fc7cacc โ โ
โ 74.57 โ 2026-10-2 โ false โ 0.6800 โ โฆ โ yes โ KXNFLGAME โ 0724ab0a- โ 0.3200 โ
โ โ 6T08:22:1 โ โ โ โ โ -26OCT26D โ d069-a4cb โ โ
โ โ 9.463875Z โ โ โ โ โ ALPHI-PHI โ -0c9f-96b โ โ
โ โ โ โ โ โ โ โ 2df68711c โ โ
โ 5.00 โ 2026-10-2 โ false โ 0.6800 โ โฆ โ yes โ KXNFLGAME โ 0724ab09- โ 0.3200 โ
โ โ 6T08:22:0 โ โ โ โ โ -26OCT26D โ 97e9-8d3b โ โ
โ โ 7.376608Z โ โ โ โ โ ALPHI-PHI โ -0d60-b19 โ โ
โ โ โ โ โ โ โ โ abf761e48 โ โ
โ 5.62 โ 2026-10-2 โ false โ 0.6900 โ โฆ โ no โ KXNFLGAME โ 0724a83f- โ 0.3100 โ
โ โ 6T08:22:0 โ โ โ โ โ -26OCT26D โ ed09-b1ce โ โ
โ โ 5.713199Z โ โ โ โ โ ALPHI-PHI โ -5a65-27c โ โ
โ โ โ โ โ โ โ โ 3abf8693f โ โ
โโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโ
๐ฎ Polymarketโ
Polymarket has two public hosts: Gamma (the catalog of events and markets) and the CLOB (the order
books). polymarket_gamma_events with
tag_slug="sports" lists the open events Polymarket tags as sports (its own tagging, so an
odd non-sport event can slip in); volume is in USDC.
pev = safe("Polymarket sports events",
lambda: odds.polymarket_gamma_events(limit="20", closed="false", tag_slug="sports"))
if pev is not None and pev.height:
top = pev.with_columns(pl.col("volume").cast(pl.Float64, strict=False)).sort("volume").tail(12)
fig, ax = plt.subplots(figsize=(8, 6))
ax.barh([t[:45] for t in top["title"]], top["volume"] / 1e6, color="#3b5bdb")
ax.set_xlabel("volume (million USDC)")
ax.set_title("Open Polymarket sports events by volume")
plt.tight_layout()
plt.show()
else:
print("no Polymarket sports events")
โ
Polymarket sports events

From catalog to order book ๐โ
A Gamma market lists its outcomes, their prices, and the CLOB token id of each outcome
(clob_token_ids, a JSON list as text). That token id is what
polymarket_clob_midpoint takes.
pmk = safe("Polymarket markets by volume",
lambda: odds.polymarket_gamma_markets(limit="5", closed="false", order="volume", ascending="false"))
pmk.select("question", "outcomes", "outcome_prices", "volume") if pmk is not None and pmk.height else None
โ
Polymarket markets by volume
shape: (5, 4)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ question โ outcomes โ outcome_prices โ volume โ
โ --- โ --- โ --- โ --- โ
โ str โ str โ str โ str โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโโโโโโโโโโชโโโโโโโโโโโโก
โ Will York City FC win on โ ["Yes", "No"] โ ["0.295", "0.705"] โ 99.997143 โ
โ 2026-10-03? โ โ โ โ
โ Sheffield Wednesday FC vs. โ ["Over", "Under"] โ ["0.17", "0.83"] โ 99.996667 โ
โ Cambridge United FC: O/U 4.5 โ โ โ โ
โ Al Ahli Saudi FC vs. Al Gharafa โ ["Over", "Under"] โ ["0.17", "0.83"] โ 99.996667 โ
โ SC: O/U 5.5 โ โ โ โ
โ Spread: Como 1907 (-1.5) โ ["Como 1907", "Torino FC"] โ ["0.315", "0.685"] โ 99.992259 โ
โ Rot-Weiss Essen vs. RB Leipzig: โ ["Over", "Under"] โ ["0.17", "0.83"] โ 99.991177 โ
โ O/U 5.5 โ โ โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโ
if pmk is not None and pmk.height and pmk["clob_token_ids"][0]:
token = json.loads(pmk["clob_token_ids"][0])[0]
mid = safe("CLOB midpoint", lambda: odds.polymarket_clob_midpoint(token))
out = mid
else:
out = "no token id to look up"
out
โ
CLOB midpoint
shape: (1, 1)
โโโโโโโโโ
โ mid โ
โ --- โ
โ str โ
โโโโโโโโโก
โ 0.295 โ
โโโโโโโโโ
๐ Where to nextโ
- Compare a market's price with a sportsbook's no-vig probability from
12_odds_intro. kalshi_series/kalshi_event/kalshi_marketfetch one record by ticker;polymarket_clob_book/polymarket_clob_priceread the full book or one side.- Full reference: the Odds pages in the sidebar.