Skip to content

fix(client): round protected market BUY shares down so the order can cross at max_price - #292

Open
naruto11eth wants to merge 1 commit into
mainfrom
feature/dev-639-protected-buy-rounding
Open

fix(client): round protected market BUY shares down so the order can cross at max_price#292
naruto11eth wants to merge 1 commit into
mainfrom
feature/dev-639-protected-buy-rounding

Conversation

@naruto11eth

@naruto11eth naruto11eth commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

A market BUY with max_price is signed as two amounts: collateral to spend and shares to receive. The exchange derives the order price as makerAmount / takerAmount and only matches a BUY when that price is at or above the resting ask. When amount / max_price was not exactly representable, the SDK rounded the share count up, which placed the derived price a hair below max_price. An ask resting exactly at max_price, the normal target for a FAK or FOK at the touch, could never be lifted, and the venue simply reported the order as not filled. Most prices are affected: with amount=1 on a 0.01-tick market, every inexact division lands below the cap.

This change rounds the share count down instead, which is the construction the unprotected path and the legacy py-clob-client already use. The derived price now sits at or fractionally above max_price and always below the next tick, so the order lifts an ask at max_price, fills at the resting price, and cannot reach a higher tick. Maker amount stays the cent-rounded amount, so spend never exceeds it and the venue's whole-cent and $1 minimum rules are met whenever amount >= 1.

What changes on the wire

  • Protected BUY: takerAmount drops by one precision step when the division is inexact. 1 @ 0.07 signs 14.2857 shares instead of 14.2858; 100 @ 0.55 signs 181.8181 instead of 181.8182. makerAmount is unchanged.
  • Unprotected BUY and all SELL orders: unchanged. The protect_price flag is removed; SELL amounts never reached the rounding branch (2dp shares times the tick price always fits the amount precision), so one code path now covers both modes.
  • This reverses the rounding direction chosen when protected orders were introduced (feat(client): support protected market orders #105), which encoded takerAmount as a minimum-shares floor at the cost of marketability. The documented contract, fill at max_price or better, is kept; the previously signed floor was never fillable at the cap.

Docs and tests

  • create_market_order / place_market_order docstrings (sync and async) now state what max_price and min_price bound and why the signed price may sit fractionally above max_price.
  • Direct tests on the amount math across all six tick sizes: derived price at or above max_price and below the next tick, maker never exceeds amount, maker on whole cents, taker on the amount tick; the $1 minimum case; exact division and SELL unchanged.
  • Sync-client protected BUY test and a max_spend + max_price test, neither of which existed.

The TypeScript client has the same rounding and gets a matching change separately.

Fixes #291


Note

Medium Risk
Changes signed order amounts for protected market BUYs, which affects fill behavior at exact max_price; logic is well-tested but trading-path regressions would show up as missed or altered fills.

Overview
Fixes protected market BUY orders with max_price that could fail to fill when liquidity sat exactly at the cap.

Amount encoding: _compute_market_order_amounts now always floors the share (taker) amount after precision trimming, instead of rounding shares up when protect_price=True. The exchange prices BUYs as maker / taker and only matches at or above the ask; rounding shares up pushed the derived price slightly below max_price, so touch liquidity was unreachable. Flooring keeps the encoded price at or fractionally above max_price (within one tick). The protect_price flag is removed so protected and unprotected paths share one rounding rule; SELL behavior is unchanged.

Wire impact: Inexact divisions lower takerAmount by one precision step (e.g. 100 @ 0.55181_818_100 vs 181_818_200); makerAmount is unchanged.

Docs & tests: create_market_order / place_market_order docstrings (sync + async) document price bounds, sub-tick encoded price, and possible unspent dust. New parametrized unit tests on amount math, max_spend + max_price, and a sync protected-BUY assertion.

Reviewed by Cursor Bugbot for commit 6a77970. Bugbot is set up for automated code reviews on this repo. Configure here.

…cross at max_price

The exchange prices a BUY as makerAmount / takerAmount and only matches
when that is at or above the ask. Rounding the share count up put the
price a hair below max_price, so a FAK or FOK sent at the touch could
never lift an ask resting there. Rounding down keeps it at or fractionally
above max_price and below the next tick, the same construction the
unprotected path and the legacy clients use. Maker amount, SELL orders and
unprotected BUYs are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant