Skip to content

feat(clob): add order-flow helpers for fill estimates, one-call orders, and settlement - #32

Merged
SebastianBoehler merged 14 commits into
SebastianBoehler:mainfrom
yluoc:order-flow-helpers
Oct 10, 2026
Merged

SebastianBoehler merged 14 commits into
SebastianBoehler:mainfrom
yluoc:order-flow-helpers

Conversation

@yluoc

@yluoc yluoc commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the order-flow helpers from the official SDKs:

  • estimate_market_price: walks the book and returns the worst and average price, simulated fill, and fully_fillable. Also works offline on a book you already hold.
  • place_limit_order: GTC, or GTD when expiration is set (at least 180 s ahead), with post_only.
  • place_market_order: FAK/FOK. With worst_price it signs at that bound; without it, it prices from the book and fails a shallow FOK with InsufficientLiquidity before signing.
  • get_trade and wait_for_order_fill_settlement: poll fills until CONFIRMED or FAILED and return the settlement hashes.

Also fixes the book walk starting at the worst level for best-first (OrderbookManager) books. Adds docs/order-flow.md, README usage, and order_flow_example. Existing create_*/post_order APIs are unchanged.

Verification

Commands run and their actual results:

  • ctest -LE live: 60/60 passed, including test_package_consumer and 4 new test executables.
  • scripts/quality.py origin/main: clang-format and clang-tidy passed.
  • POLYMARKET_RUN_LIVE_SMOKE=1 ctest -L live: both live tests passed.
  • Read-only estimates on 3 production books matched in both REST and stream order.

Checks not run and the reason:

  • Real order placement and settlement: not authorized; covered by local fixtures only.
  • Prettier: no Linux Node locally; tables aligned by hand.
  • macOS and Debug: left to CI.

Compatibility and evidence

Public API, binary compatibility, or release implications:

  • New header market_price.hpp, new param and result types, and new ClobClient methods. Not breaking; minor release.
  • SdkErrorCode gains InsufficientLiquidity, Timeout and TransactionFailed (appended), so exhaustive switch statements will warn.

Protocol evidence:

Live outcomes observed were read-only. Everything that signs or posts an order is covered by fake-server fixtures only.

@yluoc

yluoc commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

This PR adds the order-flow helpers our C++ SDK was missing compared with the official Python and TypeScript SDKs: a fill-price estimate, one-call limit and market orders, and a wait until fills settle.

Why we need it. A bot using this SDK had to build orders by hand, couldn't preview what a market order would cost, and had no way to know when a fill was final. After a match, a trade can still fail on-chain, so a bot that updates its position right after posting can think it holds shares it doesn't. wait_for_order_fill_settlement fixes that by waiting until each fill is CONFIRMED or FAILED.

Where it goes further than the official SDKs:

Full fill preview: the estimate returns the average price, shares, collateral and levels touched, not just the worst price. That's what you need for a slippage check.
No network call needed: you can estimate against a book you already stream through OrderbookManager, with no REST request. This matters most on the latency-sensitive path.
Book order doesn't matter: the walk handles both REST and stream ordering. Building this uncovered and fixed a bug where stream books were walked from the worst price.
Exact price cap: a bounded market BUY is signed at exactly worst_price, so a finer tick can't lift a higher ask.
Simpler bound: one worst_price field instead of separate max_price and min_price, so the bound can't be set on the wrong side.

Copy link
Copy Markdown
Owner

Thanks Bill, these helpers are useful, especially the offline estimate, corrected REST/stream book ordering, and exact signed price bound. I reviewed fa978bb20caf6c16cdbda09b219311a036c6d72e. I would hold the merge until the settlement compatibility issues are fixed.

Behavior findings

  1. [P1] Normalize REST trade statuses before checking settlement. In clob_order_settlement.cpp:19–26, only plain CONFIRMED and FAILED are recognized. The trade parser preserves REST values such as TRADE_STATUS_CONFIRMED and TRADE_STATUS_FAILED, so both remain pending and eventually return Timeout. A local fixture reproduced this for both prefixed terminal statuses; plain CONFIRMED succeeded. The official SDK model at the cited revision normalizes the prefix, and its settlement tests use prefixed statuses. Please normalize before both terminal-state and all-failed classification, and add confirmed, failed, and mixed-fill regressions.

  2. [P2] Allow an unavailable transaction hash while polling. The new lookup at clob_order_settlement.cpp:74 reuses a parser that rejects transaction_hash: null. A pending MATCHED_NOT_BROADCASTED fixture therefore returned Parse immediately. The official SDK declares this field nullable. Please treat null as an unavailable hash and continue polling. This parser restriction predates the PR, but the new waiter inherits it. I have not measured how often this payload occurs live.

  3. [P2] Make rate-limit retry sleeps respect the settlement deadline. The lookup at clob_order_settlement.cpp:67–68 uses generic read(), whose retry loop does not receive the deadline. With timeout 0ms, HTTP 429, and Retry-After: 0.3, a local fixture returned success after 306ms. This contradicts the documented promise that the waiter never sleeps past its deadline. Please bound retry waits by the remaining settlement budget. HTTP request duration is a separate limitation and should be stated separately.

Error handling and smaller follow-ups

  1. [P2] Preserve the original metadata/book error. At market_price.cpp:344–352, and in the corresponding placement lookups, optional legacy methods discard the underlying error. The helpers then fabricate a retryable HttpTransport error. Both a /book HTTP 404 and an HTTP 200 with malformed JSON reproduced status 0, an empty response excerpt, and retryable=true. Please propagate the underlying SdkError through result-returning internal lookups, so callers can distinguish API rejection, parsing failure, and temporary transport failure.

  2. [P3] Validate OrderSide in the offline estimator. At market_price.cpp:294–311, an invalid enum value such as static_cast<OrderSide>(2) returns success. Book selection treats it as SELL while amount calculation treats it as BUY. Please reject values other than BUY or SELL before calculation or network access. The existing signer already rejects invalid sides.

  3. Minor maintainability note: the new market_price.cpp is 356 lines and combines arithmetic, traversal, result conversion, and network orchestration. Moving the ClobClient network method into a separate implementation file would give a natural split around the repository's soft 300-line limit. This is not a merge blocker.

Validation: all four CI jobs pass. Independently, the macOS Release build completed for the affected targets and order_flow_example; all eight focused tests and test_package_consumer passed. The extra fixtures above exposed cases absent from the current tests. No funded orders or authenticated production settlement were tested.

The documented restriction to the initial trade_ids, including immediate return for delayed orders without IDs, is consistent with the helper's scope. I did not find material scope creep in the placement helpers.

@yluoc

yluoc commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

@SebastianBoehler all issues are fixed. Let me know if you have other concerns. Thanks.

@SebastianBoehler

Copy link
Copy Markdown
Owner

@yluoc Thanks Bill. I double-checked current head c2e668521a79184b18692f5f0a62ba1218b8f67d and rebuilt/reran the local fixtures. The other fixes look good. One part of point 4 still seems incomplete:

[P2] Metadata errors are still lost inside delegated order creation. Initial tick/book lookups now preserve their errors, but these placement branches still use the legacy optional lookups:

  • Tick refresh: cache tick 0.01, then place a limit order or bounded market order at 0.965. If the refreshed /tick-size request returns HTTP 404 or malformed HTTP 200, the original error is lost.
  • Explicit tick: supply tick_size="0.01" and price/bound 0.5. A failed minimum-tick lookup has the same result.
  • Neg-risk lookup: leave neg_risk unset. An HTTP 404 or malformed /neg-risk response also loses its cause.

I reproduced all 12 combinations: both placement helpers, all three branches, and both failure responses. Each returns:

HttpTransport, http_status=0, retryable=true, endpoint=/order, empty response excerpt

The placement helpers delegate to create_order_result / create_market_order_result. Their metadata resolvers still use optional get_tick_size / get_neg_risk, and the catch handlers replace the cause with a generic transport error.

Could you preserve the original SdkError through these creation paths and add public-placement regressions? A permanent 404 should remain ApiResponse with status/body and retryable=false; malformed HTTP 200 should remain Parse. This prevents callers from treating permanent API or parsing failures as temporary network failures.

I would hold the merge for this follow-up.

@SebastianBoehler
SebastianBoehler merged commit 151c440 into SebastianBoehler:main Oct 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants