{
 "nbformat": 4,
 "nbformat_minor": 5,
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-0",
   "metadata": {},
   "source": [
    "# Polymarket API in Python\n",
    "\n",
    "One market, the order book for one of its outcomes, the fee that market charges, and a separate Vultax research table. Public data only: no key, no account, no orders, and no package to install.\n",
    "\n",
    "[Read the tutorial](https://vultax.com/research/polymarket-api-python-tutorial) · [Polymarket's market data documentation](https://docs.polymarket.com/market-data/overview) · [Fee calculator](https://vultax.com/tools/polymarket-fee-calculator)\n",
    "\n",
    "The outputs below were captured on 20 September 2026 at 21:05 UTC by running these cells in order in a plain Python 3.12.3 process. Run them again and the prices, sizes and times will be whatever the market shows then."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-1",
   "metadata": {},
   "source": [
    "## 1. The functions\n",
    "\n",
    "This cell is `polymarket_snapshot.py` without its command-line entry point. Run it once; it only defines functions and makes no request."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-2",
   "metadata": {},
   "source": [
    "#!/usr/bin/env python3\n",
    "\"\"\"Read one Polymarket market and one order book, plus a separate Vultax research table.\n",
    "\n",
    "Python 3.10+, standard library only. No credentials, account access or orders.\n",
    "\n",
    "  python3 polymarket_snapshot.py --list 10            find a market slug (1 request)\n",
    "  python3 polymarket_snapshot.py --slug MARKET_SLUG   save a snapshot (3 requests)\n",
    "\"\"\"\n",
    "import argparse\n",
    "import csv\n",
    "import io\n",
    "import json\n",
    "import math\n",
    "import sys\n",
    "from datetime import datetime, timezone\n",
    "from pathlib import Path\n",
    "from urllib.error import HTTPError, URLError\n",
    "from urllib.parse import urlencode, urlparse\n",
    "from urllib.request import Request, urlopen\n",
    "\n",
    "MAX_BYTES = 4 * 1024 * 1024\n",
    "GAMMA = \"https://gamma-api.polymarket.com\"\n",
    "CLOB = \"https://clob.polymarket.com\"\n",
    "STUDY = \"https://vultax.com/research/best-polymarket-traders-leaderboards\"\n",
    "DATASET_ID = \"section-activity-is-the-first-filter-chart\"\n",
    "FEE_CALCULATOR = \"https://vultax.com/tools/polymarket-fee-calculator\"\n",
    "\n",
    "\n",
    "class ExampleError(Exception):\n",
    "    \"\"\"A problem the reader can act on. Printed as one line, without a traceback.\"\"\"\n",
    "\n",
    "\n",
    "def now():\n",
    "    return datetime.now(timezone.utc).isoformat()\n",
    "\n",
    "\n",
    "def fetch_json(url):\n",
    "    request = Request(url, headers={\"User-Agent\": \"Vultax-Public-Research-Example/1.1\", \"Accept\": \"application/json\"})\n",
    "    try:\n",
    "        with urlopen(request, timeout=20) as response:\n",
    "            body = response.read(MAX_BYTES + 1)\n",
    "            if len(body) > MAX_BYTES:\n",
    "                raise ExampleError(f\"The response from {url} exceeds this example's 4 MiB limit\")\n",
    "            return json.loads(body), {\"url\": url, \"retrievedAt\": now(), \"httpStatus\": response.status}\n",
    "    except HTTPError as error:\n",
    "        retry_after = error.headers.get(\"Retry-After\")\n",
    "        hint = f\"; wait {retry_after} seconds before trying again\" if retry_after else \"\"\n",
    "        raise ExampleError(f\"HTTP {error.code} from {url}{hint}. This example never retries on its own.\") from error\n",
    "    except (URLError, TimeoutError) as error:\n",
    "        raise ExampleError(f\"Could not reach {urlparse(url).netloc}: {getattr(error, 'reason', error)}. Check the connection and run it again.\") from error\n",
    "    except json.JSONDecodeError as error:\n",
    "        raise ExampleError(f\"{url} did not return JSON\") from error\n",
    "\n",
    "\n",
    "def array(value, field):\n",
    "    parsed = json.loads(value) if isinstance(value, str) else value\n",
    "    if not isinstance(parsed, list):\n",
    "        raise ValueError(f\"{field} must be an array or a JSON-encoded array\")\n",
    "    return parsed\n",
    "\n",
    "\n",
    "def finite_number(value):\n",
    "    if value is None or isinstance(value, bool) or value == \"\":\n",
    "        return None\n",
    "    try:\n",
    "        result = float(value)\n",
    "    except (TypeError, ValueError):\n",
    "        return None\n",
    "    return result if math.isfinite(result) else None\n",
    "\n",
    "\n",
    "def slug_from(value):\n",
    "    \"\"\"Accepts a slug or a pasted polymarket.com address and returns its last path segment.\"\"\"\n",
    "    text = value.strip()\n",
    "    if \"://\" in text or text.startswith(\"polymarket.com\"):\n",
    "        path = urlparse(text if \"://\" in text else \"https://\" + text).path\n",
    "        text = path.rstrip(\"/\").rsplit(\"/\", 1)[-1]\n",
    "    if not text or any(character.isspace() for character in text):\n",
    "        raise ExampleError(\"Pass a market slug such as will-x-happen-by-2026, or the market's polymarket.com address\")\n",
    "    return text\n",
    "\n",
    "\n",
    "def normalize_market(market):\n",
    "    labels = array(market.get(\"outcomes\"), \"outcomes\")\n",
    "    token_ids = array(market.get(\"clobTokenIds\"), \"clobTokenIds\")\n",
    "    raw_prices = market.get(\"outcomePrices\")\n",
    "    prices = [None] * len(labels) if raw_prices is None else array(raw_prices, \"outcomePrices\")\n",
    "    if not labels or len(labels) != len(token_ids) or len(labels) != len(prices):\n",
    "        raise ValueError(\"Outcome, price and token arrays do not align; refusing to guess their mapping\")\n",
    "    if any(not isinstance(token, str) or not token.isdigit() for token in token_ids):\n",
    "        raise ValueError(\"Token IDs must remain decimal strings; numeric IDs can lose precision\")\n",
    "    outcomes = []\n",
    "    for label, token, raw_price in zip(labels, token_ids, prices):\n",
    "        price = finite_number(raw_price)\n",
    "        if price is not None and not 0 <= price <= 1:\n",
    "            raise ValueError(\"Outcome price is outside [0, 1]\")\n",
    "        outcomes.append({\"label\": str(label), \"tokenId\": token, \"gammaPrice\": price})\n",
    "    return {\n",
    "        \"id\": str(market[\"id\"]), \"conditionId\": market.get(\"conditionId\"), \"slug\": market.get(\"slug\"),\n",
    "        \"question\": market.get(\"question\"), \"sourceUpdatedAt\": market.get(\"updatedAt\"),\n",
    "        \"active\": market.get(\"active\"), \"closed\": market.get(\"closed\"),\n",
    "        \"acceptingOrders\": market.get(\"acceptingOrders\"), \"outcomes\": outcomes,\n",
    "        \"feesEnabled\": market.get(\"feesEnabled\"), \"feeType\": market.get(\"feeType\"), \"feeSchedule\": market.get(\"feeSchedule\"),\n",
    "    }\n",
    "\n",
    "\n",
    "def choose_outcome(outcomes, wanted):\n",
    "    \"\"\"The first outcome by default, under its real label. Index 0 is never assumed to mean Yes.\"\"\"\n",
    "    if wanted is None:\n",
    "        return outcomes[0]\n",
    "    matches = [row for row in outcomes if row[\"label\"].casefold() == wanted.casefold()]\n",
    "    if len(matches) != 1:\n",
    "        labels = \", \".join(repr(row[\"label\"]) for row in outcomes)\n",
    "        raise ExampleError(f\"--outcome {wanted!r} does not name exactly one outcome of this market. Its outcomes are: {labels}\")\n",
    "    return matches[0]\n",
    "\n",
    "\n",
    "def normalize_book(book, token_id, condition_id):\n",
    "    if str(book.get(\"asset_id\")) != token_id or book.get(\"market\") != condition_id:\n",
    "        raise ValueError(\"Book identity does not match the requested outcome and condition\")\n",
    "    def levels(side):\n",
    "        result = []\n",
    "        for level in book.get(side, []):\n",
    "            price, size = finite_number(level.get(\"price\")), finite_number(level.get(\"size\"))\n",
    "            if price is None or size is None or not 0 < price < 1 or size <= 0:\n",
    "                raise ValueError(f\"Invalid {side} level; refusing to silently discard it\")\n",
    "            result.append({\"price\": price, \"size\": size})\n",
    "        return result\n",
    "    bids, asks = levels(\"bids\"), levels(\"asks\")\n",
    "    best_bid = max((row[\"price\"] for row in bids), default=None)\n",
    "    best_ask = min((row[\"price\"] for row in asks), default=None)\n",
    "    spread = round(best_ask - best_bid, 6) if best_bid is not None and best_ask is not None else None\n",
    "    stamp = finite_number(book.get(\"timestamp\"))\n",
    "    return {\"tokenId\": token_id, \"conditionId\": condition_id, \"sourceTimestampMs\": book.get(\"timestamp\"),\n",
    "            \"sourceTimestamp\": datetime.fromtimestamp(stamp / 1000, timezone.utc).isoformat() if stamp else None,\n",
    "            \"bestBid\": best_bid, \"bestAsk\": best_ask, \"spread\": spread,\n",
    "            \"crossedBook\": spread < 0 if spread is not None else None,\n",
    "            \"bidLevels\": len(bids), \"askLevels\": len(asks),\n",
    "            \"bestBidSize\": sum(row[\"size\"] for row in bids if row[\"price\"] == best_bid) if bids else None,\n",
    "            \"bestAskSize\": sum(row[\"size\"] for row in asks if row[\"price\"] == best_ask) if asks else None}\n",
    "\n",
    "\n",
    "def taker_fee(shares, price, rate):\n",
    "    \"\"\"Polymarket's published formula, rounded once to five decimal places.\"\"\"\n",
    "    return round(shares * rate * price * (1 - price) + 1e-12, 5)\n",
    "\n",
    "\n",
    "def fee_estimate(market, book, shares=100):\n",
    "    \"\"\"A taker fee for the best ask, from the market's own fee fields. Anything unknown stays unknown.\"\"\"\n",
    "    schedule = market.get(\"feeSchedule\") if isinstance(market.get(\"feeSchedule\"), dict) else None\n",
    "    rate = finite_number(schedule.get(\"rate\")) if schedule else None\n",
    "    if market.get(\"feesEnabled\") is not True:\n",
    "        return {\"shares\": shares, \"takerFee\": 0.0, \"basis\": \"feesEnabled is not true on this market\", \"calculator\": FEE_CALCULATOR}\n",
    "    if rate == 0:\n",
    "        return {\"shares\": shares, \"takerFee\": 0.0, \"basis\": \"this market's rate is 0\", \"calculator\": FEE_CALCULATOR}\n",
    "    if rate is None or schedule.get(\"exponent\") != 1:\n",
    "        return {\"shares\": shares, \"takerFee\": None, \"basis\": \"fees are enabled but the rate is missing or the exponent is not 1; not estimated\"}\n",
    "    if book is None or book[\"bestAsk\"] is None:\n",
    "        return {\"shares\": shares, \"rate\": rate, \"takerFee\": None, \"basis\": \"no ask to price the example at\"}\n",
    "    return {\"shares\": shares, \"price\": book[\"bestAsk\"], \"rate\": rate, \"takerFee\": taker_fee(shares, book[\"bestAsk\"], rate),\n",
    "            \"basis\": \"feeSchedule.rate on this market, exponent 1, at the best ask; an estimate, not a quote\",\n",
    "            \"calculator\": f\"{FEE_CALCULATOR}?rate={rate:g}&shares={shares}&price={book['bestAsk'] * 100:g}\"}\n",
    "\n",
    "\n",
    "def select_research(document):\n",
    "    if document.get(\"articleUrl\") != STUDY:\n",
    "        raise ValueError(\"Unexpected research article identity\")\n",
    "    dataset = next((row for row in document.get(\"datasets\", []) if row.get(\"id\") == DATASET_ID), None)\n",
    "    if dataset is None or dataset.get(\"isLive\") is not False:\n",
    "        raise ValueError(\"Expected published snapshot dataset is absent\")\n",
    "    if not dataset.get(\"version\") or not dataset.get(\"measurement\"):\n",
    "        raise ValueError(\"Research version or measurement context is missing\")\n",
    "    if any(len(row) != len(dataset[\"columns\"]) for row in dataset[\"rows\"]):\n",
    "        raise ValueError(\"Research columns and rows do not align\")\n",
    "    return dataset\n",
    "\n",
    "\n",
    "def csv_cell(value):\n",
    "    text = \"\" if value is None else str(value)\n",
    "    if isinstance(value, str) and text.lstrip().startswith((\"=\", \"+\", \"-\", \"@\")):\n",
    "        return \"'\" + text\n",
    "    return text\n",
    "\n",
    "\n",
    "def render_csv(columns, rows):\n",
    "    output = io.StringIO(newline=\"\")\n",
    "    writer = csv.writer(output)\n",
    "    writer.writerow(columns)\n",
    "    writer.writerows([[csv_cell(cell) for cell in row] for row in rows])\n",
    "    return output.getvalue()\n",
    "\n",
    "\n",
    "def list_markets(count):\n",
    "    url = f\"{GAMMA}/markets?\" + urlencode({\"limit\": count, \"active\": \"true\", \"closed\": \"false\", \"order\": \"volume24hr\", \"ascending\": \"false\"})\n",
    "    markets, _receipt = fetch_json(url)\n",
    "    if not isinstance(markets, list):\n",
    "        raise ExampleError(\"Gamma did not return a list of markets\")\n",
    "    print(f\"{len(markets)} open markets by 24-hour volume, read {now()}\\n\")\n",
    "    for market in markets:\n",
    "        schedule = market.get(\"feeSchedule\") if isinstance(market.get(\"feeSchedule\"), dict) else {}\n",
    "        fee = \"no fee\" if market.get(\"feesEnabled\") is not True or schedule.get(\"rate\") == 0 else f\"rate {schedule.get('rate')}\"\n",
    "        print(f\"{market.get('slug')}\\n    {market.get('question')}  [{fee}]\")\n",
    "    print(\"\\nNext: python3 polymarket_snapshot.py --slug ONE_OF_THE_SLUGS_ABOVE\")\n",
    "\n",
    "\n",
    "def find_market(slug):\n",
    "    markets, receipt = fetch_json(f\"{GAMMA}/markets?\" + urlencode({\"slug\": slug}))\n",
    "    if isinstance(markets, list) and len(markets) == 1 and markets[0].get(\"slug\") == slug:\n",
    "        return markets[0], receipt\n",
    "    # Not a market. If it names an event, show that event's markets and stop; never pick one for the reader.\n",
    "    events, _ = fetch_json(f\"{GAMMA}/events?\" + urlencode({\"slug\": slug}))\n",
    "    children = [m.get(\"slug\") for m in (events[0].get(\"markets\") or [])] if isinstance(events, list) and len(events) == 1 else []\n",
    "    if children:\n",
    "        shown = \"\\n  \".join(children[:20])\n",
    "        raise ExampleError(f\"{slug!r} is an event with {len(children)} markets, not one market. Pass one of its market slugs:\\n  {shown}\")\n",
    "    raise ExampleError(f\"No market has the exact slug {slug!r}. Run with --list 10 to see current slugs.\")\n",
    "\n",
    "\n",
    "def snapshot(slug, wanted_outcome, output):\n",
    "    raw_market, gamma_receipt = find_market(slug)\n",
    "    market = normalize_market(raw_market)\n",
    "    outcome = choose_outcome(market[\"outcomes\"], wanted_outcome)\n",
    "    book = None\n",
    "    book_receipt = {\"status\": \"unavailable\", \"reason\": \"market is not confirmed open for orders\"}\n",
    "    if market[\"active\"] is True and market[\"closed\"] is False and market[\"acceptingOrders\"] is True:\n",
    "        book_url = f\"{CLOB}/book?\" + urlencode({\"token_id\": outcome[\"tokenId\"]})\n",
    "        try:\n",
    "            raw_book, book_receipt = fetch_json(book_url)\n",
    "            book = normalize_book(raw_book, outcome[\"tokenId\"], market[\"conditionId\"])\n",
    "        except (ExampleError, ValueError) as error:\n",
    "            book_receipt = {\"url\": book_url, \"retrievedAt\": now(), \"status\": \"unavailable\", \"reason\": str(error)}\n",
    "    research, research_receipt = fetch_json(STUDY + \"/data.json\")\n",
    "    dataset = select_research(research)\n",
    "    result = {\"schemaVersion\": \"vultax-public-api-example-v2\", \"generatedAt\": now(),\n",
    "              \"market\": market, \"bookOutcome\": outcome[\"label\"], \"book\": book,\n",
    "              \"feeEstimate\": fee_estimate(market, book), \"researchDataset\": dataset,\n",
    "              \"receipts\": {\"gamma\": gamma_receipt, \"book\": book_receipt, \"research\": research_receipt},\n",
    "              \"limitations\": \"Sequential public snapshots, not an atomic quote or executable trade. Gamma price is separate from CLOB bid/ask. Missing values remain null. The fee is an estimate from the market's reported settings. Research is historical and is not joined to this market.\"}\n",
    "    output.mkdir(parents=True, exist_ok=False)\n",
    "    (output / \"snapshot.json\").write_text(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False) + \"\\n\", encoding=\"utf-8\")\n",
    "    (output / \"outcomes.csv\").write_text(render_csv([\"market_id\", \"condition_id\", \"outcome\", \"token_id\", \"gamma_price\", \"source_updated_at\"],\n",
    "        [[market[\"id\"], market[\"conditionId\"], row[\"label\"], row[\"tokenId\"], row[\"gammaPrice\"], market[\"sourceUpdatedAt\"]] for row in market[\"outcomes\"]]), encoding=\"utf-8\", newline=\"\")\n",
    "    (output / \"research.csv\").write_text(render_csv([column[\"name\"] for column in dataset[\"columns\"]], dataset[\"rows\"]), encoding=\"utf-8\", newline=\"\")\n",
    "    return result\n",
    "\n",
    "\n",
    "def describe(result, output):\n",
    "    market, book, fee = result[\"market\"], result[\"book\"], result[\"feeEstimate\"]\n",
    "    show = lambda value: \"none\" if value is None else f\"{value:g}\"\n",
    "    print(market[\"question\"])\n",
    "    for row in market[\"outcomes\"]:\n",
    "        print(f\"  {row['label']:<24} Gamma price {show(row['gammaPrice'])}\")\n",
    "    if book is None:\n",
    "        print(f\"Order book for {result['bookOutcome']!r}: unavailable ({result['receipts']['book'].get('reason')})\")\n",
    "    else:\n",
    "        bid = f\"bid {book['bestBid']:g} ({book['bestBidSize']:,.0f} shares)\" if book[\"bestBid\"] is not None else \"no bids\"\n",
    "        ask = f\"ask {book['bestAsk']:g} ({book['bestAskSize']:,.0f} shares)\" if book[\"bestAsk\"] is not None else \"no asks\"\n",
    "        spread = f\"spread {book['spread']:g}\" if book[\"spread\"] is not None else \"no spread without both sides\"\n",
    "        print(f\"Order book for {result['bookOutcome']!r}: {bid}, {ask}, {spread}; book time {book['sourceTimestamp']}\")\n",
    "    if fee[\"takerFee\"] is None:\n",
    "        print(f\"Taker fee for {fee['shares']} shares: not estimated ({fee['basis']})\")\n",
    "    elif \"rate\" in fee:\n",
    "        print(f\"Taker fee for {fee['shares']} shares at the {fee['price']:g} ask: ${fee['takerFee']:g} (this market's rate is {fee['rate']:g}; an estimate, not a quote)\")\n",
    "    else:\n",
    "        print(f\"Taker fee for {fee['shares']} shares: $0 ({fee['basis']})\")\n",
    "    print(f\"Research table: {len(result['researchDataset']['rows'])} rows, {result['researchDataset']['measurement'].get('population')}, kept separate from this market\")\n",
    "    print(f\"Saved snapshot.json, outcomes.csv and research.csv to {output}\")\n",
    "\n",
    "\n",
    "def main():\n",
    "    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)\n",
    "    mode = parser.add_mutually_exclusive_group(required=True)\n",
    "    mode.add_argument(\"--slug\", help=\"Exact market slug, or the market's polymarket.com address\")\n",
    "    mode.add_argument(\"--list\", type=int, metavar=\"N\", help=\"Print the N most-traded open markets (1 to 100) with their slugs, then stop\")\n",
    "    parser.add_argument(\"--outcome\", help=\"Outcome label whose book to read, for example No (default: the first outcome)\")\n",
    "    parser.add_argument(\"--output\", type=Path, help=\"New output directory; an existing one is never overwritten\")\n",
    "    args = parser.parse_args()\n",
    "    try:\n",
    "        if args.list is not None:\n",
    "            if not 1 <= args.list <= 100:\n",
    "                parser.error(\"--list takes a number from 1 to 100\")\n",
    "            return list_markets(args.list)\n",
    "        output = args.output or Path(\"polymarket-example-\" + datetime.now(timezone.utc).strftime(\"%Y%m%dT%H%M%S%fZ\"))\n",
    "        if output.exists():\n",
    "            parser.error(\"Output directory already exists; choose a new path\")\n",
    "        describe(snapshot(slug_from(args.slug), args.outcome, output), output)\n",
    "    except (ExampleError, ValueError) as error:\n",
    "        sys.exit(f\"error: {error}\")\n",
    "\n"
   ],
   "execution_count": 1,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "id": "cell-3",
   "metadata": {},
   "source": [
    "## 2. Find a market\n",
    "\n",
    "A market is addressed by its slug. This lists the most-traded open markets with their slugs and what each charges a taker. One request."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-4",
   "metadata": {},
   "source": [
    "list_markets(5)"
   ],
   "execution_count": 2,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "5 open markets by 24-hour volume, read 2026-09-20T21:05:52.266726+00:00\n",
      "\n",
      "will-united-russia-er-gain-the-most-seats-in-the-next-russian-parliamentary-election\n",
      "    Will United Russia (ER) gain the most seats in the next Russian parliamentary election?  [no fee]\n",
      "nfl-gb-nyj-2026-09-20\n",
      "    Packers vs. Jets  [no fee]\n",
      "nfl-was-dal-2026-09-20\n",
      "    Commanders vs. Cowboys  [no fee]\n",
      "nfl-cle-tb-2026-09-20\n",
      "    Browns vs. Buccaneers  [no fee]\n",
      "nfl-no-bal-2026-09-20-spread-home-8pt5\n",
      "    Spread: BAL (-8.5)  [no fee]\n",
      "\n",
      "Next: python3 polymarket_snapshot.py --slug ONE_OF_THE_SLUGS_ABOVE\n"
     ]
    }
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-5",
   "metadata": {},
   "source": [
    "## 3. Save a snapshot\n",
    "\n",
    "Replace `MARKET_SLUG` with any slug from the list, or paste a whole polymarket.com address. `OUTCOME = None` reads the first outcome under its real label; set it to a label such as `\"No\"` to choose another. Three requests: the market, one order book, one research table. The files go to a new folder, and an existing folder is never overwritten."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-6",
   "metadata": {},
   "source": [
    "MARKET_SLUG = \"another-fed-rate-hike-in-2026\"\n",
    "OUTCOME = None\n",
    "\n",
    "folder = Path(\"polymarket-example-\" + datetime.now(timezone.utc).strftime(\"%Y%m%dT%H%M%SZ\"))\n",
    "try:\n",
    "    result = snapshot(slug_from(MARKET_SLUG), OUTCOME, folder)\n",
    "    describe(result, folder)\n",
    "except (ExampleError, ValueError) as error:\n",
    "    print(\"error:\", error)"
   ],
   "execution_count": 3,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Another Fed rate hike in 2026?\n",
      "  Yes                      Gamma price 0.855\n",
      "  No                       Gamma price 0.145\n",
      "Order book for 'Yes': bid 0.85 (3,316 shares), ask 0.86 (542 shares), spread 0.01; book time 2026-09-20T21:05:45.721000+00:00\n",
      "Taker fee for 100 shares at the 0.86 ask: $0.602 (this market's rate is 0.05; an estimate, not a quote)\n",
      "Research table: 5 rows, 7,178 wallets, kept separate from this market\n",
      "Saved snapshot.json, outcomes.csv and research.csv to polymarket-example-20260920T210552Z\n"
     ]
    }
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-7",
   "metadata": {},
   "source": [
    "## 4. The order book and the fee\n",
    "\n",
    "The best bid and ask are found by value, not by position in the reply. A side with no orders stays `None`; it is never written as zero. The fee estimate uses the rate this market reports, not its category, and says why when it cannot be estimated."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-8",
   "metadata": {},
   "source": [
    "print(json.dumps({\"book\": result[\"book\"], \"feeEstimate\": result[\"feeEstimate\"]}, indent=2))"
   ],
   "execution_count": 4,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "{\n",
      "  \"book\": {\n",
      "    \"tokenId\": \"834988939366537589681339435844742816680296103724655311351481456389958777625\",\n",
      "    \"conditionId\": \"0x9b1a03b41a1a68c209e9c3caf3d8af3f878420d6dbe8cb8afffd66049e73b114\",\n",
      "    \"sourceTimestampMs\": \"1789938345721\",\n",
      "    \"sourceTimestamp\": \"2026-09-20T21:05:45.721000+00:00\",\n",
      "    \"bestBid\": 0.85,\n",
      "    \"bestAsk\": 0.86,\n",
      "    \"spread\": 0.01,\n",
      "    \"crossedBook\": false,\n",
      "    \"bidLevels\": 43,\n",
      "    \"askLevels\": 12,\n",
      "    \"bestBidSize\": 3316.47,\n",
      "    \"bestAskSize\": 542.04\n",
      "  },\n",
      "  \"feeEstimate\": {\n",
      "    \"shares\": 100,\n",
      "    \"price\": 0.86,\n",
      "    \"rate\": 0.05,\n",
      "    \"takerFee\": 0.602,\n",
      "    \"basis\": \"feeSchedule.rate on this market, exponent 1, at the best ask; an estimate, not a quote\",\n",
      "    \"calculator\": \"https://vultax.com/tools/polymarket-fee-calculator?rate=0.05&shares=100&price=86\"\n",
      "  }\n",
      "}\n"
     ]
    }
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-9",
   "metadata": {},
   "source": [
    "## 5. The research table stays separate\n",
    "\n",
    "The third request fetched a published table from a Vultax study. It describes a historical population of wallets, not the market above, so it is saved beside the market files and never merged into them. Its window, population and version travel with it."
   ]
  },
  {
   "cell_type": "code",
   "id": "cell-10",
   "metadata": {},
   "source": [
    "dataset = result[\"researchDataset\"]\n",
    "print(dataset[\"title\"])\n",
    "print(dataset[\"measurement\"][\"population\"], \"·\", dataset[\"measurement\"][\"temporalCoverage\"])\n",
    "for label, value in dataset[\"rows\"]:\n",
    "    print(f\"  {label:>7} closed trades: {value}% of wallets in profit\")\n",
    "print(\"version\", dataset[\"version\"][:12], \"·\", dataset[\"sectionUrl\"])"
   ],
   "execution_count": 5,
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Share of wallets profitable in July 2026, by trades in the month\n",
      "7,178 wallets · 2026-07\n",
      "        1 closed trades: 12.2% of wallets in profit\n",
      "      2–5 closed trades: 40.8% of wallets in profit\n",
      "     6–20 closed trades: 63.7% of wallets in profit\n",
      "   21–100 closed trades: 63.2% of wallets in profit\n",
      "     100+ closed trades: 65.9% of wallets in profit\n",
      "version 6364106ffef0 · https://vultax.com/research/best-polymarket-traders-leaderboards#section-activity-is-the-first-filter\n"
     ]
    }
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-11",
   "metadata": {},
   "source": [
    "## What this is not\n",
    "\n",
    "Three requests made one after another describe three nearby moments, not one instant. A visible ask does not guarantee a later fill, and the fee is an estimate from the market's reported settings. Nothing here trades. For more than a handful of markets, read Polymarket's pagination and real-time documentation before raising the request rate, and record a failed run as a gap rather than a flat line."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 }
}
