Skip to content

Protected BUY market orders (FAK/FOK with max_price) round the taker amount up, implied price drops below max_price and the order can never match at the ask #291

Description

@zixuanz313

Summary

A protected BUY market order (max_price set, FAK/FOK) whose amount / max_price is not an exact 2-decimal share count gets its taker amount rounded up by _compute_market_order_amounts(..., protect_price=True). The resulting implied price makerAmount / takerAmount lands strictly below max_price. When max_price is the current best ask (the normal way to place a marketable FAK), the order can never match at the ask and is killed unfilled every time.

In our production bot this silently killed roughly two thirds of all FAK orders for a month (about 63k of 96k orders between 2026-08-04 and 2026-09-02). The failures are invisible from the client side: the venue just reports the FAK as unmatched.

Reproduction

from decimal import Decimal as D
from polymarket._internal.actions.orders.market import _compute_market_order_amounts

offered, requested = _compute_market_order_amounts(
    amount=D("1"), price=D("0.07"), side="BUY", tick_size=D("0.01"), protect_price=True,
)
print(offered, requested, D(offered) / D(requested))
# 1000000 14290000 0.06999...  -> implied price 0.069996 < 0.07

amount=1 / price=0.07: raw taker = 14.2857 shares, rounded up to 14.29, maker stays 1.00 USDC, implied price 0.069996. A bid of 0.069996 cannot lift an ask at 0.07, so a FAK sent with max_price=0.07 against a 0.07 ask is dead on arrival.

Same for any price where amount / price has more than 2 decimals: 0.03, 0.06, 0.07, 0.09, 0.11, 0.12 with amount=1 (all 99–100 % unmatched in our order log); 0.10, 0.13–0.30 fill normally. It also hits amounts with sub-cent decimals, because raw_maker = round_down(amount, config.size) truncates the amount to cents first (e.g. 8 shares × 0.962 on a 0.001-tick market = 7.696 → 7.69 → shares rounded up → implied 0.9619…).

Reached from the public API through create_market_order / place_market_order on the secure clients whenever side="BUY" and max_price is set (_prepare_protected_market_order_draft passes protect_price=True).

Verified on polymarket-client 0.7.1 (also present since 0.3.0 as far as we can tell; our unmatched rate jumped from ~10 % to ~70 % the day we upgraded from 0.2.0).

Why this is a bug

protect_price guarantees the effective price never exceeds max_price, which is reasonable. But rounding the taker amount up while keeping the maker amount fixed means the only prices reachable are strictly below max_price whenever the division is inexact. For a marketable order priced exactly at the touch that turns "never pay more than the ask" into "never match the ask".

Suggested fix

When amount / max_price is not an exact 2-decimal share count, three constraints cannot all hold at once:

  1. makerAmount <= amount (never overspend),
  2. shares * price >= $1 (the venue's minimum for market orders),
  3. makerAmount / shares >= max_price (the order can actually match at max_price).

With amount=1, price=0.07: 14.28 shares satisfies 1 and 3 but not 2; 14.29 shares with maker 1.00 satisfies 1 and 2 but not 3; 14.29 shares with maker 1.0003 satisfies 2 and 3 but not 1. The current code picks the second option, which produces an order that is guaranteed never to fill, so honouring 1 and 2 buys nothing.

Two options that do not silently produce a dead order:

  • Round the share count up and set makerAmount = shares * max_price (1.0003 for 14.29 shares). This exceeds amount by less than 0.01 * max_price, i.e. under one cent, which is easy to document; the order matches at the ask and clears the minimum.
  • Or raise a UserInputError telling the caller that amount at this price is not representable and must be increased, so the caller decides.

Either is fine; what should not happen is the present behaviour, where the implied price lands below max_price and the venue reports the order as unmatched with no hint that it could never have matched.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions