Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: uv sync --all-extras

- name: Run mypy
run: uv run mypy polymarket_us tests/types/order_snapshot.py
run: uv run mypy polymarket_us tests/types

test:
runs-on: ubuntu-latest
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,25 @@ asyncio.run(main())
| `markets.bbo(slug)` | Get best bid/offer |
| `markets.settlement(slug)` | Get settlement price |

#### Market response type migration

The response types now match the existing JSON returned by both sync and async
clients; runtime responses are unchanged. Typed callers should read book and BBO
data through `marketData`. Settlement uses `slug` and a numeric `settlement`,
replacing the previous `marketSlug`, `settlementPrice`, and `settledAt` declarations.

```python
book = client.markets.book("btc-100k")["marketData"]
bbo = client.markets.bbo("btc-100k")["marketData"]
settlement = client.markets.settlement("btc-100k")
slug = settlement["slug"]
settlement_price = settlement["settlement"]
```

With `AsyncPolymarketUS`, await each method call before reading these keys.
Handle `None` for book `stats` and `transactTime`, and BBO `bestBid`, `bestAsk`, and
`lastTradePx`. Books also support `MARKET_STATE_CLOSED`.

### Orders (Authenticated)

| Method | Description |
Expand Down
12 changes: 6 additions & 6 deletions polymarket_us/resources/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from polymarket_us.resource import APIResource, AsyncAPIResource
from polymarket_us.types import (
GetMarketBBOResponse,
GetMarketBookResponse,
GetMarketResponse,
GetMarketsResponse,
MarketBBO,
MarketBook,
MarketSettlement,
MarketsListParams,
)
Expand All @@ -26,11 +26,11 @@ def retrieve_by_slug(self, slug: str) -> GetMarketResponse:
"""Get a market by slug."""
return self._client.get(f"/v1/market/slug/{slug}")

def book(self, slug: str) -> MarketBook:
def book(self, slug: str) -> GetMarketBookResponse:
"""Get order book for a market."""
return self._client.get(f"/v1/markets/{slug}/book")

def bbo(self, slug: str) -> MarketBBO:
def bbo(self, slug: str) -> GetMarketBBOResponse:
"""Get best bid/offer for a market."""
return self._client.get(f"/v1/markets/{slug}/bbo")

Expand All @@ -54,11 +54,11 @@ async def retrieve_by_slug(self, slug: str) -> GetMarketResponse:
"""Get a market by slug."""
return await self._client.get(f"/v1/market/slug/{slug}")

async def book(self, slug: str) -> MarketBook:
async def book(self, slug: str) -> GetMarketBookResponse:
"""Get order book for a market."""
return await self._client.get(f"/v1/markets/{slug}/book")

async def bbo(self, slug: str) -> MarketBBO:
async def bbo(self, slug: str) -> GetMarketBBOResponse:
"""Get best bid/offer for a market."""
return await self._client.get(f"/v1/markets/{slug}/bbo")

Expand Down
4 changes: 4 additions & 0 deletions polymarket_us/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
Tag,
)
from polymarket_us.types.markets import (
GetMarketBBOResponse,
GetMarketBookResponse,
GetMarketResponse,
GetMarketsResponse,
MarketBBO,
Expand Down Expand Up @@ -113,6 +115,8 @@
"MarketsListParams",
"GetMarketsResponse",
"GetMarketResponse",
"GetMarketBookResponse",
"GetMarketBBOResponse",
# Orders
"OrderType",
"OrderSide",
Expand Down
28 changes: 20 additions & 8 deletions polymarket_us/types/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class MarketStats(TypedDict, total=False):


MarketState = Literal[
"MARKET_STATE_CLOSED",
"MARKET_STATE_OPEN",
"MARKET_STATE_PREOPEN",
"MARKET_STATE_SUSPENDED",
Expand All @@ -72,29 +73,28 @@ class MarketBook(TypedDict, total=False):
bids: list[OrderBookLevel]
offers: list[OrderBookLevel]
state: MarketState
stats: MarketStats
transactTime: str
stats: MarketStats | None
transactTime: str | None


class MarketBBO(TypedDict, total=False):
"""Best bid/offer for a market."""

marketSlug: str
bestBid: Amount
bestAsk: Amount
bestBid: Amount | None
bestAsk: Amount | None
bidDepth: int
askDepth: int
lastTradePx: Amount
lastTradePx: Amount | None
sharesTraded: str
openInterest: str


class MarketSettlement(TypedDict):
"""Market settlement information."""

marketSlug: str
settlementPrice: Amount
settledAt: str
slug: str
settlement: float


class MarketsListParams(PaginationParams, total=False):
Expand Down Expand Up @@ -126,3 +126,15 @@ class GetMarketResponse(TypedDict):
"""Response for getting a single market."""

market: MarketDetail


class GetMarketBookResponse(TypedDict):
"""Response for getting a market's order book."""

marketData: MarketBook


class GetMarketBBOResponse(TypedDict):
"""Response for getting a market's best bid/offer."""

marketData: MarketBBO
213 changes: 116 additions & 97 deletions tests/test_markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

import httpx
import pytest
from pytest_httpx import HTTPXMock

from polymarket_us import PolymarketUS
from polymarket_us import AsyncPolymarketUS, PolymarketUS
from polymarket_us.types import GetMarketBBOResponse, GetMarketBookResponse, MarketSettlement


class TestMarketsList:
Expand Down Expand Up @@ -93,108 +95,125 @@ def test_uses_correct_path_with_slug(
assert "/v1/market/slug/btc-100k" in url


class TestMarketsBook:
"""Tests for markets.book()."""

@pytest.fixture
def client(self) -> PolymarketUS:
return PolymarketUS()

@patch.object(httpx.Client, "request")
def test_get_order_book(self, mock_request: MagicMock, client: PolymarketUS) -> None:
"""Should get order book."""
mock_response = MagicMock()
mock_response.is_success = True
mock_response.text = '{"marketSlug": "btc-100k", "bids": [], "offers": []}'
mock_response.json.return_value = {
@pytest.fixture(params=[False, True], ids=["populated", "empty"])
def book_response(request: pytest.FixtureRequest) -> GetMarketBookResponse:
if request.param:
return {
"marketData": {
"marketSlug": "btc-100k",
"bids": [],
"offers": [],
"state": "MARKET_STATE_CLOSED",
"stats": None,
"transactTime": None,
}
}
return {
"marketData": {
"marketSlug": "btc-100k",
"bids": [{"px": {"value": "0.55", "currency": "USD"}, "qty": "100"}],
"offers": [{"px": {"value": "0.56", "currency": "USD"}, "qty": "80"}],
"state": "MARKET_STATE_OPEN",
"stats": {"lastTradePx": {"value": "0.55", "currency": "USD"}},
"transactTime": "2026-09-21T12:00:00Z",
}
mock_request.return_value = mock_response

book = client.markets.book("btc-100k")

assert book["marketSlug"] == "btc-100k"
assert "bids" in book
assert "offers" in book

@patch.object(httpx.Client, "request")
def test_uses_correct_path(self, mock_request: MagicMock, client: PolymarketUS) -> None:
"""Should use correct path."""
mock_response = MagicMock()
mock_response.is_success = True
mock_response.text = '{"bids": [], "offers": []}'
mock_response.json.return_value = {"bids": [], "offers": []}
mock_request.return_value = mock_response

client.markets.book("test-market")

call_args = mock_request.call_args
url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs.get("url")
assert "/v1/markets/test-market/book" in url


class TestMarketsBBO:
"""Tests for markets.bbo()."""

@pytest.fixture
def client(self) -> PolymarketUS:
return PolymarketUS()

@patch.object(httpx.Client, "request")
def test_get_best_bid_offer(self, mock_request: MagicMock, client: PolymarketUS) -> None:
"""Should get best bid/offer."""
mock_response = MagicMock()
mock_response.is_success = True
mock_response.text = '{"marketSlug": "btc-100k"}'
mock_response.json.return_value = {
}


@pytest.fixture(params=[False, True], ids=["populated", "empty"])
def bbo_response(request: pytest.FixtureRequest) -> GetMarketBBOResponse:
if request.param:
return {
"marketData": {
"marketSlug": "btc-100k",
"bestBid": None,
"bestAsk": None,
"lastTradePx": None,
"bidDepth": 0,
"askDepth": 0,
"sharesTraded": "",
"openInterest": "",
}
}
return {
"marketData": {
"marketSlug": "btc-100k",
"bestBid": {"value": "0.55", "currency": "USD"},
"bestAsk": {"value": "0.56", "currency": "USD"},
"lastTradePx": {"value": "0.55", "currency": "USD"},
"bidDepth": 1,
"askDepth": 1,
"sharesTraded": "100",
"openInterest": "80",
}
mock_request.return_value = mock_response

bbo = client.markets.bbo("btc-100k")

assert "bestBid" in bbo
assert "bestAsk" in bbo

@patch.object(httpx.Client, "request")
def test_uses_correct_path(self, mock_request: MagicMock, client: PolymarketUS) -> None:
"""Should use correct path."""
mock_response = MagicMock()
mock_response.is_success = True
mock_response.text = "{}"
mock_response.json.return_value = {}
mock_request.return_value = mock_response

client.markets.bbo("test-market")

call_args = mock_request.call_args
url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs.get("url")
assert "/v1/markets/test-market/bbo" in url


class TestMarketsSettlement:
"""Tests for markets.settlement()."""

@pytest.fixture
def client(self) -> PolymarketUS:
return PolymarketUS()

@patch.object(httpx.Client, "request")
def test_uses_correct_path(self, mock_request: MagicMock, client: PolymarketUS) -> None:
"""Should use correct path."""
mock_response = MagicMock()
mock_response.is_success = True
mock_response.text = "{}"
mock_response.json.return_value = {}
mock_request.return_value = mock_response

client.markets.settlement("settled-market")

call_args = mock_request.call_args
url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs.get("url")
assert "/v1/markets/settled-market/settlement" in url
}


def test_book_returns_wire_response(
httpx_mock: HTTPXMock, book_response: GetMarketBookResponse
) -> None:
httpx_mock.add_response(
method="GET",
url="https://gateway.polymarket.us/v1/markets/btc-100k/book",
json=book_response,
)
with PolymarketUS() as client:
assert client.markets.book("btc-100k") == book_response


async def test_async_book_returns_wire_response(
httpx_mock: HTTPXMock, book_response: GetMarketBookResponse
) -> None:
httpx_mock.add_response(
method="GET",
url="https://gateway.polymarket.us/v1/markets/btc-100k/book",
json=book_response,
)
async with AsyncPolymarketUS() as client:
assert await client.markets.book("btc-100k") == book_response


def test_bbo_returns_wire_response(
httpx_mock: HTTPXMock, bbo_response: GetMarketBBOResponse
) -> None:
httpx_mock.add_response(
method="GET", url="https://gateway.polymarket.us/v1/markets/btc-100k/bbo", json=bbo_response
)
with PolymarketUS() as client:
assert client.markets.bbo("btc-100k") == bbo_response


async def test_async_bbo_returns_wire_response(
httpx_mock: HTTPXMock, bbo_response: GetMarketBBOResponse
) -> None:
httpx_mock.add_response(
method="GET", url="https://gateway.polymarket.us/v1/markets/btc-100k/bbo", json=bbo_response
)
async with AsyncPolymarketUS() as client:
assert await client.markets.bbo("btc-100k") == bbo_response


@pytest.mark.parametrize("settlement", [0, 0.5, 1])
def test_settlement_returns_numeric_price(httpx_mock: HTTPXMock, settlement: float) -> None:
response: MarketSettlement = {"slug": "btc-100k", "settlement": settlement}
httpx_mock.add_response(
method="GET",
url="https://gateway.polymarket.us/v1/markets/btc-100k/settlement",
json=response,
)
with PolymarketUS() as client:
assert client.markets.settlement("btc-100k") == response


@pytest.mark.parametrize("settlement", [0, 0.5, 1])
async def test_async_settlement_returns_numeric_price(
httpx_mock: HTTPXMock, settlement: float
) -> None:
response: MarketSettlement = {"slug": "btc-100k", "settlement": settlement}
httpx_mock.add_response(
method="GET",
url="https://gateway.polymarket.us/v1/markets/btc-100k/settlement",
json=response,
)
async with AsyncPolymarketUS() as client:
assert await client.markets.settlement("btc-100k") == response
Loading
Loading