{ "cells": [ { "cell_type": "markdown", "id": "5b2c990f", "metadata": {}, "source": [ "# 📈 Prediction markets with `sportsdataverse-py`\n", "\n", "Sportsbooks quote odds; prediction markets quote prices. A YES contract that pays $1 and trades at\n", "$0.62 is a 62% implied probability, set by whoever is trading it. `sportsdataverse.odds` wraps the\n", "public market-data APIs of the two biggest venues:\n", "\n", "| Venue | Functions | Auth |\n", "|---|---|---|\n", "| [Kalshi](https://kalshi.com) | `kalshi_*` | none for market data |\n", "| [Polymarket](https://polymarket.com) | `polymarket_gamma_*` (catalog), `polymarket_clob_*` (order book) | none |\n", "\n", "Sportsbook odds from The Odds API live in `12_odds_intro`. This notebook only reads public prices;\n", "it places no orders." ] }, { "cell_type": "markdown", "id": "4c59c1a6", "metadata": {}, "source": [ "## 🧰 The toolbox\n", "\n", "| Function | What it gives you |\n", "|---|---|\n", "| [`kalshi_exchange_status`](../odds/reference/kalshi.md#kalshi_exchange_status) | Is the exchange open for trading? |\n", "| [`kalshi_series_list`](../odds/reference/kalshi.md#kalshi_series_list) | Market families (series) in a category |\n", "| [`kalshi_events`](../odds/reference/kalshi.md#kalshi_events) | Events, filterable by series and status |\n", "| [`kalshi_markets`](../odds/reference/kalshi.md#kalshi_markets) | Markets of an event, with bid / ask / last price |\n", "| [`kalshi_orderbook`](../odds/reference/kalshi.md#kalshi_orderbook) | Order book of one market |\n", "| [`kalshi_trades`](../odds/reference/kalshi.md#kalshi_trades) | Public trades |\n", "| [`polymarket_gamma_events`](../odds/reference/polymarket.md#polymarket_gamma_events) | Events, filterable by tag (`sports`, ...) |\n", "| [`polymarket_gamma_markets`](../odds/reference/polymarket.md#polymarket_gamma_markets) | Markets with outcomes, prices and volume |\n", "| [`polymarket_clob_midpoint`](../odds/reference/polymarket.md#polymarket_clob_midpoint) | Midpoint of one outcome token's book |" ] }, { "cell_type": "markdown", "id": "5a389d02", "metadata": {}, "source": [ "## 🔌 Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "The Kalshi and Polymarket helpers take their paging arguments (`limit`, `offset`) as strings, the way\n", "the APIs document them." ] }, { "cell_type": "code", "execution_count": null, "id": "976c9843", "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", "import matplotlib.pyplot as plt\n", "import polars as pl\n", "\n", "from sportsdataverse import odds\n", "\n", "pl.Config.set_tbl_rows(10)\n", "pl.Config.set_fmt_str_lengths(60)" ] }, { "cell_type": "markdown", "id": "3910b441", "metadata": {}, "source": [ "Provider feeds are seasonal and occasionally rate-limited, so a tiny `safe()` helper runs each call\n", "defensively: you get the frame when the feed is up, and a one-line skip when it isn't, naming whether\n", "the answer was *nothing there* (`NoDataError`) or *the fetch failed* (`AssetFetchError`). 🛟" ] }, { "cell_type": "code", "execution_count": null, "id": "38197e04", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " try:\n", " out = thunk()\n", " print(f\"✅ {label}\")\n", " return out\n", " except (NoDataError, AssetFetchError) as e:\n", " print(f\"\\u23ed\\ufe0f {label}: {type(e).__name__}: {e}\")\n", " return None" ] }, { "cell_type": "markdown", "id": "302862f4", "metadata": {}, "source": [ "## 🏦 Kalshi\n", "\n", "Kalshi groups markets as series (a market family, such as single NFL games), events (one game) and\n", "markets (one contract). Start with the exchange status and the sports series." ] }, { "cell_type": "code", "execution_count": null, "id": "082be961", "metadata": {}, "outputs": [], "source": [ "safe(\"Kalshi status\", odds.kalshi_exchange_status)" ] }, { "cell_type": "code", "execution_count": null, "id": "52586f63", "metadata": {}, "outputs": [], "source": [ "series = safe(\"Kalshi sports series\", lambda: odds.kalshi_series_list(category=\"Sports\"))\n", "(series.filter(pl.col(\"title\").str.contains(\"(?i)NFL|NBA|NHL|college\"))\n", " .select(\"ticker\", \"title\", \"frequency\").head(10)\n", " if series is not None and series.height else \"series unavailable\")" ] }, { "cell_type": "markdown", "id": "085c4cfb", "metadata": {}, "source": [ "### One NFL game market and its order book 📊\n", "\n", "`KXNFLGAME` is the single-game NFL series. Take its first open event, list the markets\n", "([`kalshi_markets`](../odds/reference/kalshi.md#kalshi_markets)), then read one market's book\n", "([`kalshi_orderbook`](../odds/reference/kalshi.md#kalshi_orderbook)). The book holds bids only, on both\n", "sides; prices and sizes arrive as strings, so we cast them before plotting." ] }, { "cell_type": "code", "execution_count": null, "id": "e35064d4", "metadata": {}, "outputs": [], "source": [ "events = safe(\"Kalshi NFL events\", lambda: odds.kalshi_events(limit=\"10\", status=\"open\", series_ticker=\"KXNFLGAME\"))\n", "event = events[\"event_ticker\"][0] if events is not None and events.height else None\n", "mkts = safe(f\"markets {event}\", lambda: odds.kalshi_markets(event_ticker=event)) if event else None\n", "cols = [\"ticker\", \"title\", \"yes_bid_dollars\", \"yes_ask_dollars\", \"last_price_dollars\", \"volume_fp\"]\n", "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\"" ] }, { "cell_type": "code", "execution_count": null, "id": "19f173b7", "metadata": {}, "outputs": [], "source": [ "ticker = mkts[\"ticker\"][0] if mkts is not None and mkts.height else None\n", "book = safe(f\"order book {ticker}\", lambda: odds.kalshi_orderbook(ticker)) if ticker else None\n", "if book is not None and book.height:\n", " fig, ax = plt.subplots(figsize=(8, 4))\n", " book = book.with_columns(pl.col(\"price\", \"size\").cast(pl.Float64))\n", " yes = book.filter(pl.col(\"side\") == \"yes\")\n", " # A NO bid at p is an offer to sell YES at 1 - p, so both sides fit on one YES-price axis.\n", " no = book.filter(pl.col(\"side\") == \"no\").with_columns(price=1 - pl.col(\"price\"))\n", " ax.bar(yes[\"price\"], yes[\"size\"], width=0.008, color=\"#2ca02c\", alpha=0.7, label=\"YES bids\")\n", " ax.bar(no[\"price\"], no[\"size\"], width=0.008, color=\"#d62728\", alpha=0.7, label=\"YES offers (NO bids)\")\n", " ax.set_yscale(\"log\")\n", " ax.set_xlabel(\"YES price in dollars (= implied probability)\")\n", " ax.set_ylabel(\"contracts (log scale)\")\n", " ax.set_title(f\"Kalshi order book: {ticker}\")\n", " ax.legend()\n", " plt.show()\n", "else:\n", " print(\"no order book to draw\")" ] }, { "cell_type": "code", "execution_count": null, "id": "dc0dd596", "metadata": {}, "outputs": [], "source": [ "trades = safe(f\"trades {ticker}\", lambda: odds.kalshi_trades(limit=\"20\", ticker=ticker)) if ticker else None\n", "trades.head() if trades is not None and trades.height else \"no recent trades\"" ] }, { "cell_type": "markdown", "id": "22a401d2", "metadata": {}, "source": [ "## 🔮 Polymarket\n", "\n", "Polymarket has two public hosts: Gamma (the catalog of events and markets) and the CLOB (the order\n", "books). [`polymarket_gamma_events`](../odds/reference/polymarket.md#polymarket_gamma_events) with\n", "`tag_slug=\"sports\"` lists the open events Polymarket tags as sports (its own tagging, so an\n", "odd non-sport event can slip in); volume is in USDC." ] }, { "cell_type": "code", "execution_count": null, "id": "bc55f938", "metadata": {}, "outputs": [], "source": [ "pev = safe(\"Polymarket sports events\",\n", " lambda: odds.polymarket_gamma_events(limit=\"20\", closed=\"false\", tag_slug=\"sports\"))\n", "if pev is not None and pev.height:\n", " top = pev.with_columns(pl.col(\"volume\").cast(pl.Float64, strict=False)).sort(\"volume\").tail(12)\n", " fig, ax = plt.subplots(figsize=(8, 6))\n", " ax.barh([t[:45] for t in top[\"title\"]], top[\"volume\"] / 1e6, color=\"#3b5bdb\")\n", " ax.set_xlabel(\"volume (million USDC)\")\n", " ax.set_title(\"Open Polymarket sports events by volume\")\n", " plt.tight_layout()\n", " plt.show()\n", "else:\n", " print(\"no Polymarket sports events\")" ] }, { "cell_type": "markdown", "id": "5c4d6462", "metadata": {}, "source": [ "### From catalog to order book 🔗\n", "\n", "A Gamma market lists its outcomes, their prices, and the CLOB token id of each outcome\n", "(`clob_token_ids`, a JSON list as text). That token id is what\n", "[`polymarket_clob_midpoint`](../odds/reference/polymarket.md#polymarket_clob_midpoint) takes." ] }, { "cell_type": "code", "execution_count": null, "id": "5385cf95", "metadata": {}, "outputs": [], "source": [ "pmk = safe(\"Polymarket markets by volume\",\n", " lambda: odds.polymarket_gamma_markets(limit=\"5\", closed=\"false\", order=\"volume\", ascending=\"false\"))\n", "pmk.select(\"question\", \"outcomes\", \"outcome_prices\", \"volume\") if pmk is not None and pmk.height else None" ] }, { "cell_type": "code", "execution_count": null, "id": "c0078ee0", "metadata": {}, "outputs": [], "source": [ "if pmk is not None and pmk.height and pmk[\"clob_token_ids\"][0]:\n", " token = json.loads(pmk[\"clob_token_ids\"][0])[0]\n", " mid = safe(\"CLOB midpoint\", lambda: odds.polymarket_clob_midpoint(token))\n", " out = mid\n", "else:\n", " out = \"no token id to look up\"\n", "out" ] }, { "cell_type": "markdown", "id": "c8ae7f51", "metadata": {}, "source": [ "## 🎉 Where to next\n", "\n", "- Compare a market's price with a sportsbook's no-vig probability from `12_odds_intro`.\n", "- `kalshi_series` / `kalshi_event` / `kalshi_market` fetch one record by ticker;\n", " `polymarket_clob_book` / `polymarket_clob_price` read the full book or one side.\n", "- Full reference: the **Odds** pages in the sidebar." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }