Skip to content
This repository was archived by the owner on May 25, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 3 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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,26 @@ See [this Python example](https://gist.github.com/poly-rodr/44313920481de58d5a3f

**Pro tip**: You only need to set these once per wallet. After that, you can trade freely.

## Tick size and order rejection

The client **caches** each market’s minimum tick size (from `get_tick_size`) for a short time to reduce API calls. When you **create or sign orders** (`create_order`, `create_market_order`), the client always fetches the **current** tick size from the CLOB, so signed orders use the correct value even if the cache is stale.

- **Using `get_tick_size` elsewhere** (e.g. for display or validation): if the market’s tick size may have changed on the CLOB, either call `get_tick_size(token_id, force_refresh=True)` or clear the cache first: `client.clear_tick_size_cache(token_id)` (or `client.clear_tick_size_cache()` for all tokens).
- **Order rejected by the API**: if the server rejects an order and the error is related to tick size or price precision, the client may raise `TickSizeRejectedError` with a message suggesting you clear the tick size cache and retry:
```python
from py_clob_client import ClobClient, TickSizeRejectedError

try:
resp = client.post_order(signed, OrderType.GTC)
except TickSizeRejectedError as e:
# Market tick size may have changed; clear cache and retry
client.clear_tick_size_cache() # or client.clear_tick_size_cache(token_id)
# Re-create and post the order
```

## Notes
- To discover token IDs, use the Markets API Explorer: [Get Markets](https://docs.polymarket.com/developers/gamma-markets-api/get-markets).
- Prices are in dollars from 0.00 to 1.00. Shares are whole or fractional units of the outcome token.
- If an order is rejected due to tick size or price precision, see [Tick size and order rejection](#tick-size-and-order-rejection) above.

See [/example](/examples) for more.
3 changes: 3 additions & 0 deletions py_clob_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .client import ClobClient
from .exceptions import TickSizeRejectedError
from .clob_types import (
ApiCreds,
OrderArgs,
Expand Down Expand Up @@ -38,6 +39,8 @@
__all__ = [
# Main client
"ClobClient",
# Exceptions
"TickSizeRejectedError",
# Core types
"ApiCreds",
"OrderArgs",
Expand Down
108 changes: 72 additions & 36 deletions py_clob_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
MarketOrderArgs,
PostOrdersArgs,
)
from .exceptions import PolyException
from .exceptions import PolyException, PolyApiException, TickSizeRejectedError
from .http_helpers.helpers import (
add_query_trade_params,
add_query_open_orders_params,
Expand Down Expand Up @@ -399,11 +399,22 @@ def get_spreads(self, params: list[BookParams]):
body = [{"token_id": param.token_id} for param in params]
return post("{}{}".format(self.host, GET_SPREADS), data=body)

def get_tick_size(self, token_id: str) -> TickSize:
def get_tick_size(self, token_id: str, force_refresh: bool = False) -> TickSize:
"""
Returns the minimum tick size for the given token (market).

Results are cached for tick_size_ttl seconds. If the order book's tick
size has changed on the CLOB, you may get a stale value until the cache
expires or you force a refresh. When signing orders, the client
automatically uses a fresh tick size; if you use get_tick_size elsewhere
and the market may have changed, pass force_refresh=True or call
clear_tick_size_cache(token_id) first.
"""
cached_at = self.__tick_size_timestamps.get(token_id)

if (
token_id in self.__tick_sizes
not force_refresh
and token_id in self.__tick_sizes
and cached_at is not None
and (time.monotonic() - cached_at) < self.__tick_size_ttl
):
Expand Down Expand Up @@ -438,6 +449,12 @@ def _update_tick_size_from_order_book(self, book: OrderBookSummary):
self.__tick_sizes[book.asset_id] = str(book.tick_size)
self.__tick_size_timestamps[book.asset_id] = time.monotonic()

@staticmethod
def _is_tick_size_related_error(error_msg) -> bool:
"""True if the API error message is likely due to tick size / price precision."""
if error_msg is None:
return False
Comment thread
cursor[bot] marked this conversation as resolved.

def get_neg_risk(self, token_id: str) -> bool:
if token_id in self.__neg_risk:
return self.__neg_risk[token_id]
Expand All @@ -458,9 +475,12 @@ def get_fee_rate_bps(self, token_id: str) -> int:
return fee_rate

def __resolve_tick_size(
self, token_id: str, tick_size: TickSize = None
self,
token_id: str,
tick_size: TickSize = None,
force_refresh: bool = False,
) -> TickSize:
min_tick_size = self.get_tick_size(token_id)
min_tick_size = self.get_tick_size(token_id, force_refresh=force_refresh)
if tick_size is not None:
if is_tick_size_smaller(tick_size, min_tick_size):
raise Exception(
Expand Down Expand Up @@ -498,10 +518,11 @@ def create_order(
"""
self.assert_level_1_auth()

# add resolve_order_options, or similar
# Resolve tick size from CLOB (force refresh to avoid stale cache when signing)
tick_size = self.__resolve_tick_size(
order_args.token_id,
options.tick_size if options else None,
force_refresh=True,
)

if not price_valid(order_args.price, tick_size):
Expand Down Expand Up @@ -545,10 +566,11 @@ def create_market_order(
"""
self.assert_level_1_auth()

# add resolve_order_options, or similar
# Resolve tick size from CLOB (force refresh to avoid stale cache when signing)
tick_size = self.__resolve_tick_size(
order_args.token_id,
options.tick_size if options else None,
force_refresh=True,
)

if order_args.price is None or order_args.price <= 0:
Expand Down Expand Up @@ -604,21 +626,28 @@ def post_orders(self, args: list[PostOrdersArgs]):
serialized_body=json.dumps(body, separators=(",", ":"), ensure_ascii=False),
)
headers = create_level_2_headers(self.signer, self.creds, request_args)
# Builder flow
if self.can_builder_auth():
builder_headers = self._generate_builder_headers(request_args, headers)
if builder_headers is not None:
return post(
"{}{}".format(self.host, POST_ORDERS),
headers=builder_headers,
data=request_args.serialized_body,
)
# send exact serialized bytes
return post(
"{}{}".format(self.host, POST_ORDERS),
headers=headers,
data=request_args.serialized_body,
)
try:
# Builder flow
if self.can_builder_auth():
builder_headers = self._generate_builder_headers(request_args, headers)
if builder_headers is not None:
return post(
"{}{}".format(self.host, POST_ORDERS),
headers=builder_headers,
data=request_args.serialized_body,
)
# send exact serialized bytes
return post(
"{}{}".format(self.host, POST_ORDERS),
headers=headers,
data=request_args.serialized_body,
)
except PolyApiException as e:
if self._is_tick_size_related_error(e.error_msg):
err = TickSizeRejectedError(str(e.error_msg), api_exception=e)
err.__cause__ = e
Comment thread
kingo233 marked this conversation as resolved.
raise err
raise

def post_order(self, order, orderType: OrderType = OrderType.GTC, post_only: bool = False):
"""
Expand All @@ -636,20 +665,27 @@ def post_order(self, order, orderType: OrderType = OrderType.GTC, post_only: boo
serialized_body=json.dumps(body, separators=(",", ":"), ensure_ascii=False),
)
headers = create_level_2_headers(self.signer, self.creds, request_args)
# Builder flow
if self.can_builder_auth():
builder_headers = self._generate_builder_headers(request_args, headers)
if builder_headers is not None:
return post(
"{}{}".format(self.host, POST_ORDER),
headers=builder_headers,
data=request_args.serialized_body,
)
return post(
"{}{}".format(self.host, POST_ORDER),
headers=headers,
data=request_args.serialized_body,
)
try:
# Builder flow
if self.can_builder_auth():
builder_headers = self._generate_builder_headers(request_args, headers)
if builder_headers is not None:
return post(
"{}{}".format(self.host, POST_ORDER),
headers=builder_headers,
data=request_args.serialized_body,
)
return post(
"{}{}".format(self.host, POST_ORDER),
headers=headers,
data=request_args.serialized_body,
)
except PolyApiException as e:
if self._is_tick_size_related_error(e.error_msg):
err = TickSizeRejectedError(str(e.error_msg), api_exception=e)
err.__cause__ = e
raise err
raise

def create_and_post_order(
self, order_args: OrderArgs, options: PartialCreateOrderOptions = None
Expand Down
26 changes: 26 additions & 0 deletions py_clob_client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,29 @@ def __repr__(self):

def __str__(self):
return self.__repr__()


class TickSizeRejectedError(PolyApiException):
"""
Raised when an order is rejected and the error is likely due to tick size /
price precision (e.g. the market's tick size changed on the CLOB). Clear the
tick size cache and retry: client.clear_tick_size_cache() or
client.clear_tick_size_cache(token_id), then create and post the order again.
"""

def __init__(self, msg, api_exception=None):
self.api_exception = api_exception
hint = (
"Clear tick size cache: client.clear_tick_size_cache() or "
"client.clear_tick_size_cache(token_id), then create and post the order again."
)
self.msg = f"{msg}. {hint}"
super().__init__(error_msg=self.msg)
if api_exception is not None:
self.status_code = api_exception.status_code

def __str__(self):
return self.msg

def __repr__(self):
return f"TickSizeRejectedError({self.msg!r})"
Comment thread
cursor[bot] marked this conversation as resolved.