From 79eca0a3d78492c924a7b3a0c610b92832b1e9d6 Mon Sep 17 00:00:00 2001 From: kartojal Date: Fri, 7 Aug 2026 11:15:24 +0200 Subject: [PATCH 1/3] fix(client): handle tokens without trades --- src/polymarket/clients/async_public.py | 6 +++++- src/polymarket/clients/async_secure.py | 6 +++++- src/polymarket/clients/public.py | 6 +++++- src/polymarket/clients/secure.py | 6 +++++- src/polymarket/models/clob/last_trade.py | 7 ++++++- tests/unit/test_clob_actions.py | 8 ++++++++ 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/polymarket/clients/async_public.py b/src/polymarket/clients/async_public.py index a923d9c..6e12a8d 100644 --- a/src/polymarket/clients/async_public.py +++ b/src/polymarket/clients/async_public.py @@ -1274,7 +1274,11 @@ async def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decima return _clob_actions.parse_spreads(await self._ctx.clob.post_json(path, json=body)) async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: - """Get the most recent trade price for a token.""" + """Get the most recent trade price for a token. + + For a token without trades, ``side`` is ``None`` and ``price`` is the + ``Decimal("0.5")`` placeholder. + """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price( await self._ctx.clob.get_json(path, params=params) diff --git a/src/polymarket/clients/async_secure.py b/src/polymarket/clients/async_secure.py index 10dd6af..0b72230 100644 --- a/src/polymarket/clients/async_secure.py +++ b/src/polymarket/clients/async_secure.py @@ -1868,7 +1868,11 @@ async def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decima return _clob_actions.parse_spreads(await self._ctx.clob.post_json(path, json=body)) async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: - """Get the most recent trade price for a token.""" + """Get the most recent trade price for a token. + + For a token without trades, ``side`` is ``None`` and ``price`` is the + ``Decimal("0.5")`` placeholder. + """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price( await self._ctx.clob.get_json(path, params=params) diff --git a/src/polymarket/clients/public.py b/src/polymarket/clients/public.py index 16cf1bf..2046a70 100644 --- a/src/polymarket/clients/public.py +++ b/src/polymarket/clients/public.py @@ -1070,7 +1070,11 @@ def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decimal]: return _clob_actions.parse_spreads(self._ctx.clob.post_json(path, json=body)) def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: - """Get the most recent trade price for a token.""" + """Get the most recent trade price for a token. + + For a token without trades, ``side`` is ``None`` and ``price`` is the + ``Decimal("0.5")`` placeholder. + """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price(self._ctx.clob.get_json(path, params=params)) diff --git a/src/polymarket/clients/secure.py b/src/polymarket/clients/secure.py index 795c6f5..335c5e9 100644 --- a/src/polymarket/clients/secure.py +++ b/src/polymarket/clients/secure.py @@ -1394,7 +1394,11 @@ def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decimal]: return _clob_actions.parse_spreads(self._ctx.clob.post_json(path, json=body)) def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: - """Get the most recent trade price for a token.""" + """Get the most recent trade price for a token. + + For a token without trades, ``side`` is ``None`` and ``price`` is the + ``Decimal("0.5")`` placeholder. + """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price(self._ctx.clob.get_json(path, params=params)) diff --git a/src/polymarket/models/clob/last_trade.py b/src/polymarket/models/clob/last_trade.py index 9086ede..04eb79c 100644 --- a/src/polymarket/models/clob/last_trade.py +++ b/src/polymarket/models/clob/last_trade.py @@ -11,13 +11,18 @@ class LastTradePrice(BaseModel): price: Decimal - side: OrderSide + side: OrderSide | None @field_validator("price", mode="before") @classmethod def _parse_price(cls, value: object) -> object: return parse_decimal_string(value) + @field_validator("side", mode="before") + @classmethod + def _empty_side_to_none(cls, value: object) -> object: + return None if value == "" else value + class LastTradePriceForToken(BaseModel): token_id: TokenId diff --git a/tests/unit/test_clob_actions.py b/tests/unit/test_clob_actions.py index 5f91bf4..8121df8 100644 --- a/tests/unit/test_clob_actions.py +++ b/tests/unit/test_clob_actions.py @@ -500,6 +500,14 @@ def test_parse_last_trade_price_returns_model() -> None: assert result.side == "BUY" +def test_parse_last_trade_price_normalizes_empty_side() -> None: + result = parse_last_trade_price({"price": "0.5", "side": ""}) + + assert result.price == Decimal("0.5") + assert_type(result.side, OrderSide | None) + assert result.side is None + + def test_parse_last_trade_price_rejects_numeric_price() -> None: with pytest.raises(UnexpectedResponseError): parse_last_trade_price({"price": 0.53, "side": "BUY"}) From dbd05590b925449a6024cfe9dbe28f10f8aecbf0 Mon Sep 17 00:00:00 2001 From: kartojal Date: Fri, 7 Aug 2026 18:52:29 +0200 Subject: [PATCH 2/3] fix(client): return None for tokens without trades --- examples/market_prices.py | 4 ++-- src/polymarket/_internal/actions/clob.py | 19 ++++++++++++++++--- src/polymarket/clients/async_public.py | 5 ++--- src/polymarket/clients/async_secure.py | 5 ++--- src/polymarket/clients/public.py | 5 ++--- src/polymarket/clients/secure.py | 5 ++--- src/polymarket/models/clob/last_trade.py | 7 +------ tests/integration/test_clob_reads.py | 2 +- tests/unit/test_clob_actions.py | 10 +++++----- tests/unit/test_clob_transport.py | 3 ++- 10 files changed, 35 insertions(+), 30 deletions(-) diff --git a/examples/market_prices.py b/examples/market_prices.py index 3c4cfdb..be976e3 100644 --- a/examples/market_prices.py +++ b/examples/market_prices.py @@ -37,8 +37,8 @@ def main() -> None: "buyPrice": buy_price, "midpoint": midpoint, "spread": spread, - "lastTradePrice": last_trade.price, - "lastTradeSide": last_trade.side, + "lastTradePrice": last_trade.price if last_trade is not None else "N/A", + "lastTradeSide": last_trade.side if last_trade is not None else "N/A", } ) diff --git a/src/polymarket/_internal/actions/clob.py b/src/polymarket/_internal/actions/clob.py index 7924f00..3813851 100644 --- a/src/polymarket/_internal/actions/clob.py +++ b/src/polymarket/_internal/actions/clob.py @@ -1,6 +1,6 @@ from collections.abc import Sequence from decimal import Decimal -from typing import Annotated, cast +from typing import Annotated, Literal, cast from pydantic import BeforeValidator, TypeAdapter, ValidationError, field_validator @@ -47,6 +47,16 @@ def _parse_spread(cls, value: object) -> object: return parse_decimal_string(value) +class _LastTradePriceResponse(BaseModel): + price: Decimal + side: OrderSide | Literal[""] + + @field_validator("price", mode="before") + @classmethod + def _parse_price(cls, value: object) -> object: + return parse_decimal_string(value) + + _PRICE_HISTORY_INTERVALS: frozenset[str] = frozenset({"max", "1w", "1d", "6h", "1h"}) _VALID_ORDER_SIDES: frozenset[str] = frozenset({"BUY", "SELL"}) @@ -200,8 +210,11 @@ def build_last_trade_price_request(*, token_id: str) -> tuple[str, dict[str, str return "/last-trade-price", {"token_id": _require_string_token_id(token_id)} -def parse_last_trade_price(data: object) -> LastTradePrice: - return LastTradePrice.parse_response(data) +def parse_last_trade_price(data: object) -> LastTradePrice | None: + response = _LastTradePriceResponse.parse_response(data) + if response.side == "": + return None + return LastTradePrice(price=response.price, side=response.side) def build_last_trade_prices_request( diff --git a/src/polymarket/clients/async_public.py b/src/polymarket/clients/async_public.py index 6e12a8d..3c25f6d 100644 --- a/src/polymarket/clients/async_public.py +++ b/src/polymarket/clients/async_public.py @@ -1273,11 +1273,10 @@ async def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decima path, body = _clob_actions.build_spreads_request(token_ids=token_ids) return _clob_actions.parse_spreads(await self._ctx.clob.post_json(path, json=body)) - async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: + async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: """Get the most recent trade price for a token. - For a token without trades, ``side`` is ``None`` and ``price`` is the - ``Decimal("0.5")`` placeholder. + Returns ``None`` when the token has not traded. """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price( diff --git a/src/polymarket/clients/async_secure.py b/src/polymarket/clients/async_secure.py index 0b72230..5020b43 100644 --- a/src/polymarket/clients/async_secure.py +++ b/src/polymarket/clients/async_secure.py @@ -1867,11 +1867,10 @@ async def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decima path, body = _clob_actions.build_spreads_request(token_ids=token_ids) return _clob_actions.parse_spreads(await self._ctx.clob.post_json(path, json=body)) - async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: + async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: """Get the most recent trade price for a token. - For a token without trades, ``side`` is ``None`` and ``price`` is the - ``Decimal("0.5")`` placeholder. + Returns ``None`` when the token has not traded. """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price( diff --git a/src/polymarket/clients/public.py b/src/polymarket/clients/public.py index 2046a70..95e4825 100644 --- a/src/polymarket/clients/public.py +++ b/src/polymarket/clients/public.py @@ -1069,11 +1069,10 @@ def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decimal]: path, body = _clob_actions.build_spreads_request(token_ids=token_ids) return _clob_actions.parse_spreads(self._ctx.clob.post_json(path, json=body)) - def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: + def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: """Get the most recent trade price for a token. - For a token without trades, ``side`` is ``None`` and ``price`` is the - ``Decimal("0.5")`` placeholder. + Returns ``None`` when the token has not traded. """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price(self._ctx.clob.get_json(path, params=params)) diff --git a/src/polymarket/clients/secure.py b/src/polymarket/clients/secure.py index 335c5e9..9260d7b 100644 --- a/src/polymarket/clients/secure.py +++ b/src/polymarket/clients/secure.py @@ -1393,11 +1393,10 @@ def get_spreads(self, *, token_ids: Sequence[str]) -> dict[TokenId, Decimal]: path, body = _clob_actions.build_spreads_request(token_ids=token_ids) return _clob_actions.parse_spreads(self._ctx.clob.post_json(path, json=body)) - def get_last_trade_price(self, *, token_id: str) -> LastTradePrice: + def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: """Get the most recent trade price for a token. - For a token without trades, ``side`` is ``None`` and ``price`` is the - ``Decimal("0.5")`` placeholder. + Returns ``None`` when the token has not traded. """ path, params = _clob_actions.build_last_trade_price_request(token_id=token_id) return _clob_actions.parse_last_trade_price(self._ctx.clob.get_json(path, params=params)) diff --git a/src/polymarket/models/clob/last_trade.py b/src/polymarket/models/clob/last_trade.py index 04eb79c..9086ede 100644 --- a/src/polymarket/models/clob/last_trade.py +++ b/src/polymarket/models/clob/last_trade.py @@ -11,18 +11,13 @@ class LastTradePrice(BaseModel): price: Decimal - side: OrderSide | None + side: OrderSide @field_validator("price", mode="before") @classmethod def _parse_price(cls, value: object) -> object: return parse_decimal_string(value) - @field_validator("side", mode="before") - @classmethod - def _empty_side_to_none(cls, value: object) -> object: - return None if value == "" else value - class LastTradePriceForToken(BaseModel): token_id: TokenId diff --git a/tests/integration/test_clob_reads.py b/tests/integration/test_clob_reads.py index f35e49d..9fcda64 100644 --- a/tests/integration/test_clob_reads.py +++ b/tests/integration/test_clob_reads.py @@ -152,7 +152,7 @@ async def run() -> dict[TokenId, Decimal]: @pytest.mark.integration def test_async_get_last_trade_price_returns_model(active_clob_token: TokenId) -> None: - async def run() -> LastTradePrice: + async def run() -> LastTradePrice | None: async with AsyncPublicClient() as client: return await client.get_last_trade_price(token_id=active_clob_token) diff --git a/tests/unit/test_clob_actions.py b/tests/unit/test_clob_actions.py index 8121df8..c0b9b5b 100644 --- a/tests/unit/test_clob_actions.py +++ b/tests/unit/test_clob_actions.py @@ -30,7 +30,7 @@ parse_spreads, ) from polymarket.errors import UnexpectedResponseError, UserInputError -from polymarket.models import OrderSide, PriceRequest, TokenId +from polymarket.models import LastTradePrice, OrderSide, PriceRequest, TokenId def test_build_midpoint_request_targets_midpoint_path_with_token_id() -> None: @@ -496,16 +496,16 @@ def test_build_last_trade_price_request_targets_last_trade_price_path() -> None: def test_parse_last_trade_price_returns_model() -> None: result = parse_last_trade_price({"price": "0.53", "side": "BUY"}) + assert result is not None assert result.price == Decimal("0.53") assert result.side == "BUY" -def test_parse_last_trade_price_normalizes_empty_side() -> None: +def test_parse_last_trade_price_returns_none_without_trades() -> None: result = parse_last_trade_price({"price": "0.5", "side": ""}) - assert result.price == Decimal("0.5") - assert_type(result.side, OrderSide | None) - assert result.side is None + assert_type(result, LastTradePrice | None) + assert result is None def test_parse_last_trade_price_rejects_numeric_price() -> None: diff --git a/tests/unit/test_clob_transport.py b/tests/unit/test_clob_transport.py index cb618eb..84ccb52 100644 --- a/tests/unit/test_clob_transport.py +++ b/tests/unit/test_clob_transport.py @@ -230,13 +230,14 @@ async def run() -> dict[TokenId, Decimal]: def test_async_get_last_trade_price_returns_model() -> None: captured: list[httpx.Request] = [] - async def run() -> LastTradePrice: + async def run() -> LastTradePrice | None: async with AsyncPublicClient() as client: _install_async_clob(client, _clob_handler(captured, {"price": "0.53", "side": "BUY"})) return await client.get_last_trade_price(token_id="123") result = asyncio.run(run()) + assert result is not None assert result.price == Decimal("0.53") assert result.side == "BUY" assert urlparse(str(captured[0].url)).path == "/last-trade-price" From e5a0b1963ad5943e087fa1d16d36e2284257d3f8 Mon Sep 17 00:00:00 2001 From: kartojal Date: Fri, 7 Aug 2026 19:57:33 +0200 Subject: [PATCH 3/3] docs(client): clarify sparse last trade batches --- src/polymarket/clients/async_public.py | 6 +++++- src/polymarket/clients/async_secure.py | 6 +++++- src/polymarket/clients/public.py | 6 +++++- src/polymarket/clients/secure.py | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/polymarket/clients/async_public.py b/src/polymarket/clients/async_public.py index 3c25f6d..398f21b 100644 --- a/src/polymarket/clients/async_public.py +++ b/src/polymarket/clients/async_public.py @@ -1286,7 +1286,11 @@ async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: async def get_last_trade_prices( self, *, token_ids: Sequence[str] ) -> tuple[LastTradePriceForToken, ...]: - """Get the most recent trade prices for multiple tokens.""" + """Get the most recent trade prices for multiple tokens. + + Tokens without trades are omitted. Match returned entries by ``token_id``; + the result is not positionally aligned with ``token_ids``. + """ path, body = _clob_actions.build_last_trade_prices_request(token_ids=token_ids) return _clob_actions.parse_last_trade_prices( await self._ctx.clob.post_json(path, json=body) diff --git a/src/polymarket/clients/async_secure.py b/src/polymarket/clients/async_secure.py index 5020b43..0207bdb 100644 --- a/src/polymarket/clients/async_secure.py +++ b/src/polymarket/clients/async_secure.py @@ -1880,7 +1880,11 @@ async def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: async def get_last_trade_prices( self, *, token_ids: Sequence[str] ) -> tuple[LastTradePriceForToken, ...]: - """Get the most recent trade prices for multiple tokens.""" + """Get the most recent trade prices for multiple tokens. + + Tokens without trades are omitted. Match returned entries by ``token_id``; + the result is not positionally aligned with ``token_ids``. + """ path, body = _clob_actions.build_last_trade_prices_request(token_ids=token_ids) return _clob_actions.parse_last_trade_prices( await self._ctx.clob.post_json(path, json=body) diff --git a/src/polymarket/clients/public.py b/src/polymarket/clients/public.py index 95e4825..17ba6ee 100644 --- a/src/polymarket/clients/public.py +++ b/src/polymarket/clients/public.py @@ -1080,7 +1080,11 @@ def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: def get_last_trade_prices( self, *, token_ids: Sequence[str] ) -> tuple[LastTradePriceForToken, ...]: - """Get the most recent trade prices for multiple tokens.""" + """Get the most recent trade prices for multiple tokens. + + Tokens without trades are omitted. Match returned entries by ``token_id``; + the result is not positionally aligned with ``token_ids``. + """ path, body = _clob_actions.build_last_trade_prices_request(token_ids=token_ids) return _clob_actions.parse_last_trade_prices(self._ctx.clob.post_json(path, json=body)) diff --git a/src/polymarket/clients/secure.py b/src/polymarket/clients/secure.py index 9260d7b..8710b6c 100644 --- a/src/polymarket/clients/secure.py +++ b/src/polymarket/clients/secure.py @@ -1404,7 +1404,11 @@ def get_last_trade_price(self, *, token_id: str) -> LastTradePrice | None: def get_last_trade_prices( self, *, token_ids: Sequence[str] ) -> tuple[LastTradePriceForToken, ...]: - """Get the most recent trade prices for multiple tokens.""" + """Get the most recent trade prices for multiple tokens. + + Tokens without trades are omitted. Match returned entries by ``token_id``; + the result is not positionally aligned with ``token_ids``. + """ path, body = _clob_actions.build_last_trade_prices_request(token_ids=token_ids) return _clob_actions.parse_last_trade_prices(self._ctx.clob.post_json(path, json=body))