From 33db6622672f60290343d3f33aea353d36bcc3c3 Mon Sep 17 00:00:00 2001 From: SebastianBoehler <27767932+SebastianBoehler@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:03:45 +0200 Subject: [PATCH] fix(order): apply tick-size rounding --- CMakeLists.txt | 2 +- README.md | 13 +++-- docs/clob-v2-migration.md | 3 ++ include/clob_client.hpp | 2 + include/polymarket/version.hpp | 4 +- src/clob_order_execution.cpp | 37 +++++++++++---- src/order_execution.cpp | 87 +++++++++++++++++++++++++++++++++- src/order_execution.hpp | 16 +++++++ src/order_test.cpp | 32 ++++++++----- tests/test_order_execution.cpp | 22 +++++++++ 10 files changed, 189 insertions(+), 29 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd717bf..03b4678 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16) # Policy for older CMake compatibility in dependencies set(CMAKE_POLICY_VERSION_MINIMUM 3.5) -project(polymarket_cpp_client VERSION 1.2.3 LANGUAGES CXX C) +project(polymarket_cpp_client VERSION 1.2.4 LANGUAGES CXX C) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/README.md b/README.md index d46e812..3c72ecf 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ include(FetchContent) FetchContent_Declare( polymarket_client GIT_REPOSITORY https://github.com/SebastianBoehler/polymarket-cpp-client.git - GIT_TAG v1.2.3 # or any release tag + GIT_TAG v1.2.4 # or any release tag ) FetchContent_MakeAvailable(polymarket_client) @@ -53,11 +53,11 @@ Download pre-built binaries from [Releases](https://github.com/SebastianBoehler/ ```bash # macOS -curl -LO https://github.com/SebastianBoehler/polymarket-cpp-client/releases/download/v1.2.3/polymarket-cpp-client-macos-arm64.tar.gz +curl -LO https://github.com/SebastianBoehler/polymarket-cpp-client/releases/download/v1.2.4/polymarket-cpp-client-macos-arm64.tar.gz tar -xzf polymarket-cpp-client-macos-arm64.tar.gz -C /usr/local # Linux -curl -LO https://github.com/SebastianBoehler/polymarket-cpp-client/releases/download/v1.2.3/polymarket-cpp-client-linux-x64.tar.gz +curl -LO https://github.com/SebastianBoehler/polymarket-cpp-client/releases/download/v1.2.4/polymarket-cpp-client-linux-x64.tar.gz tar -xzf polymarket-cpp-client-linux-x64.tar.gz -C /usr/local ``` @@ -254,6 +254,13 @@ std::cout << "Avg latency: " << stats.avg_latency_ms << "ms\n"; client.stop_heartbeat(); ``` +## Order Precision + +Limit and market order helpers round prices, share sizes, and maker/taker +amounts with Polymarket's tick-size precision rules. `CreateOrderParams` and +`CreateMarketOrderParams` default to `tick_size = "0.01"`; set this from cached +market metadata when trading markets with a different minimum tick size. + ## WebSocket Resilience `WebSocketClient` supports additive production-safety options for market-data diff --git a/docs/clob-v2-migration.md b/docs/clob-v2-migration.md index 4373002..e715dd0 100644 --- a/docs/clob-v2-migration.md +++ b/docs/clob-v2-migration.md @@ -47,6 +47,9 @@ Implemented changes: `deferExec`, and `postOnly`. - The balance/allowance endpoint signs the bare path and appends `signature_type` in the query string. +- Limit and market order amount builders apply the same tick-size keyed + rounding config as the official clients; callers can pass cached `tick_size` + metadata without adding an order-time lookup. Legacy `nonce` and `fee_rate_bps` fields remain on lower-level structs only for source compatibility with existing code that constructs `OrderData` directly; diff --git a/include/clob_client.hpp b/include/clob_client.hpp index 1252d9d..99293f0 100644 --- a/include/clob_client.hpp +++ b/include/clob_client.hpp @@ -119,6 +119,7 @@ namespace polymarket double price; double size; OrderSide side; + std::string tick_size = "0.01"; std::string expiration = "0"; std::string metadata = "0x0000000000000000000000000000000000000000000000000000000000000000"; std::string builder_code = "0x0000000000000000000000000000000000000000000000000000000000000000"; @@ -132,6 +133,7 @@ namespace polymarket double amount; // USDC for BUY, shares for SELL OrderSide side; std::optional price; // Optional price limit + std::string tick_size = "0.01"; std::string metadata = "0x0000000000000000000000000000000000000000000000000000000000000000"; std::string builder_code = "0x0000000000000000000000000000000000000000000000000000000000000000"; }; diff --git a/include/polymarket/version.hpp b/include/polymarket/version.hpp index 622a768..12054f4 100644 --- a/include/polymarket/version.hpp +++ b/include/polymarket/version.hpp @@ -2,8 +2,8 @@ #define POLYMARKET_CLIENT_VERSION_MAJOR 1 #define POLYMARKET_CLIENT_VERSION_MINOR 2 -#define POLYMARKET_CLIENT_VERSION_PATCH 3 -#define POLYMARKET_CLIENT_VERSION "1.2.3" +#define POLYMARKET_CLIENT_VERSION_PATCH 4 +#define POLYMARKET_CLIENT_VERSION "1.2.4" namespace polymarket { diff --git a/src/clob_order_execution.cpp b/src/clob_order_execution.cpp index 96eacc9..03c3edc 100644 --- a/src/clob_order_execution.cpp +++ b/src/clob_order_execution.cpp @@ -55,7 +55,11 @@ namespace polymarket } const auto context = build_execution_context(*this, *order_signer_, funder_address_, sig_type_); - const auto amounts = detail::calculate_limit_order_amounts(params.side, params.price, params.size); + const auto amounts = detail::calculate_limit_order_amounts( + params.side, + params.price, + params.size, + detail::rounding_config_for_tick_size(params.tick_size)); OrderData order_data; order_data.maker = context.maker_address(); @@ -121,15 +125,30 @@ namespace polymarket } } - CreateOrderParams order_params; - order_params.token_id = params.token_id; - order_params.price = price; - order_params.size = params.side == OrderSide::BUY ? params.amount / price : params.amount; - order_params.side = params.side; - order_params.metadata = params.metadata; - order_params.builder_code = params.builder_code; + bool is_neg_risk = false; + auto neg_risk_info = get_neg_risk(params.token_id); + is_neg_risk = neg_risk_info && neg_risk_info->neg_risk; + + const auto context = build_execution_context(*this, *order_signer_, funder_address_, sig_type_); + const auto amounts = detail::calculate_market_order_amounts( + params.side, + params.amount, + price, + detail::rounding_config_for_tick_size(params.tick_size)); + + OrderData order_data; + order_data.maker = context.maker_address(); + order_data.taker = ZERO_ADDRESS; + order_data.token_id = params.token_id; + order_data.maker_amount = to_wei(amounts.maker, 6); + order_data.taker_amount = to_wei(amounts.taker, 6); + order_data.side = params.side; + order_data.signer = context.signer_for_order(); + order_data.metadata = params.metadata; + order_data.builder = params.builder_code; + order_data.signature_type = sig_type_; - return create_order(order_params); + return order_signer_->sign_order(order_data, context.exchange_for(is_neg_risk)); } Result ClobClient::create_market_order_result(const CreateMarketOrderParams ¶ms) diff --git a/src/order_execution.cpp b/src/order_execution.cpp index a24c996..5768ce0 100644 --- a/src/order_execution.cpp +++ b/src/order_execution.cpp @@ -1,10 +1,46 @@ #include "order_execution.hpp" +#include +#include + namespace polymarket::detail { namespace { constexpr const char *ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + double decimal_scale(int decimals) + { + double scale = 1.0; + for (int i = 0; i < decimals; ++i) + { + scale *= 10.0; + } + return scale; + } + + double round_down(double value, int decimals) + { + const double scale = decimal_scale(decimals); + return std::floor(value * scale) / scale; + } + + double round_up(double value, int decimals) + { + const double scale = decimal_scale(decimals); + return std::ceil(value * scale) / scale; + } + + double round_nearest(double value, int decimals) + { + const double scale = decimal_scale(decimals); + return std::round(value * scale) / scale; + } + + double round_order_amount(double value, int decimals) + { + return round_down(round_up(value, decimals + 4), decimals); + } } std::string OrderExecutionContext::maker_address() const @@ -24,11 +60,58 @@ namespace polymarket::detail OrderAmounts calculate_limit_order_amounts(OrderSide side, double price, double size) { + return calculate_limit_order_amounts(side, price, size, rounding_config_for_tick_size("0.01")); + } + + OrderRoundingConfig rounding_config_for_tick_size(const std::string &tick_size) + { + if (tick_size == "0.1") + { + return {1, 2, 3}; + } + if (tick_size == "0.01") + { + return {2, 2, 4}; + } + if (tick_size == "0.001") + { + return {3, 2, 5}; + } + if (tick_size == "0.0001") + { + return {4, 2, 6}; + } + throw std::invalid_argument("unsupported tick size: " + tick_size); + } + + OrderAmounts calculate_limit_order_amounts(OrderSide side, + double price, + double size, + const OrderRoundingConfig &rounding) + { + const double raw_price = round_nearest(price, rounding.price_decimals); + if (side == OrderSide::BUY) + { + const double raw_taker = round_down(size, rounding.size_decimals); + return {round_order_amount(raw_taker * raw_price, rounding.amount_decimals), raw_taker}; + } + + const double raw_maker = round_down(size, rounding.size_decimals); + return {raw_maker, round_order_amount(raw_maker * raw_price, rounding.amount_decimals)}; + } + + OrderAmounts calculate_market_order_amounts(OrderSide side, + double amount, + double price, + const OrderRoundingConfig &rounding) + { + const double raw_price = round_down(price, rounding.price_decimals); + const double raw_maker = round_down(amount, rounding.size_decimals); if (side == OrderSide::BUY) { - return {size * price, size}; + return {raw_maker, round_order_amount(raw_maker / raw_price, rounding.amount_decimals)}; } - return {size, size * price}; + return {raw_maker, round_order_amount(raw_maker * raw_price, rounding.amount_decimals)}; } nlohmann::json signed_order_json(const SignedOrder &order) diff --git a/src/order_execution.hpp b/src/order_execution.hpp index d4c189e..c14d099 100644 --- a/src/order_execution.hpp +++ b/src/order_execution.hpp @@ -25,7 +25,23 @@ namespace polymarket::detail double taker; }; + struct OrderRoundingConfig + { + int price_decimals; + int size_decimals; + int amount_decimals; + }; + + OrderRoundingConfig rounding_config_for_tick_size(const std::string &tick_size); OrderAmounts calculate_limit_order_amounts(OrderSide side, double price, double size); + OrderAmounts calculate_limit_order_amounts(OrderSide side, + double price, + double size, + const OrderRoundingConfig &rounding); + OrderAmounts calculate_market_order_amounts(OrderSide side, + double amount, + double price, + const OrderRoundingConfig &rounding); nlohmann::json signed_order_json(const SignedOrder &order); nlohmann::json order_payload_json(const SignedOrder &order, const std::string &owner, diff --git a/src/order_test.cpp b/src/order_test.cpp index b5f7a0d..53dd4cb 100644 --- a/src/order_test.cpp +++ b/src/order_test.cpp @@ -12,6 +12,7 @@ */ #include "order_signer.hpp" +#include "order_execution.hpp" #include "http_client.hpp" #include #include @@ -356,26 +357,33 @@ int main(int argc, char *argv[]) std::string exchange_address = is_neg_risk ? NEG_RISK_CTF_EXCHANGE : CTF_EXCHANGE; std::cout << " Exchange: " << exchange_address << "\n"; - // Calculate shares for $1 order - match TS client's rounding for tick size 0.01 - // Config: price=2, size=2, amount=4 + // Calculate amounts with the same tick-size precision rules as the official clients. double order_usd = 1.0; - double raw_price = std::floor(best_ask * 100) / 100; // roundDown to 2 decimals - double raw_maker = std::floor(order_usd * 100) / 100; // roundDown to 2 decimals - double raw_taker = raw_maker / raw_price; + const std::string live_order_type = "FAK"; + const auto rounding = detail::rounding_config_for_tick_size("0.01"); + detail::OrderAmounts amounts; - // TS rounding: roundUp to (amount+4)=8 decimals, then roundDown to amount=4 decimals - raw_taker = std::ceil(raw_taker * 100000000) / 100000000; // roundUp to 8 decimals - raw_taker = std::floor(raw_taker * 10000) / 10000; // roundDown to 4 decimals + if (live_order_type == "GTC") + { + const double raw_price = std::floor(best_ask * 100) / 100; + const double shares = std::floor((order_usd / raw_price) * 100) / 100; + amounts = detail::calculate_limit_order_amounts(OrderSide::BUY, best_ask, shares, rounding); + } + else + { + amounts = detail::calculate_market_order_amounts(OrderSide::BUY, order_usd, best_ask, rounding); + } - std::cout << " Placing FAK order: $" << order_usd << " @ " << raw_price << " = " << raw_taker << " shares\n"; + std::cout << " Placing " << live_order_type << " order: $" << amounts.maker + << " for " << amounts.taker << " shares\n"; // Create order OrderData real_order; real_order.maker = funder_address; real_order.taker = "0x0000000000000000000000000000000000000000"; real_order.token_id = yes_token; - real_order.maker_amount = to_wei(raw_maker, 6); - real_order.taker_amount = to_wei(raw_taker, 6); + real_order.maker_amount = to_wei(amounts.maker, 6); + real_order.taker_amount = to_wei(amounts.taker, 6); real_order.side = OrderSide::BUY; real_order.signer = signer.address(); real_order.expiration = "0"; @@ -420,7 +428,7 @@ int main(int argc, char *argv[]) post_body["postOnly"] = false; post_body["order"] = order_obj; post_body["owner"] = creds.api_key; - post_body["orderType"] = "FAK"; + post_body["orderType"] = live_order_type; std::string body_str = post_body.dump(); std::cout << " Full order body:\n" diff --git a/tests/test_order_execution.cpp b/tests/test_order_execution.cpp index d7a7b1a..aa4a673 100644 --- a/tests/test_order_execution.cpp +++ b/tests/test_order_execution.cpp @@ -72,6 +72,28 @@ int main() return 1; } + const auto reported_gtc = detail::calculate_limit_order_amounts(OrderSide::BUY, 0.1700000850000425, 5.8823); + if (!expect_close("reported GTC maker amount", reported_gtc.maker, 0.9996) || + !expect_close("reported GTC taker amount", reported_gtc.taker, 5.88) || + !expect_equal("reported GTC maker wei", to_wei(reported_gtc.maker, 6), "999600") || + !expect_equal("reported GTC taker wei", to_wei(reported_gtc.taker, 6), "5880000")) + { + return 1; + } + + const auto reported_fak = detail::calculate_market_order_amounts( + OrderSide::BUY, + 1.0, + 0.1700000850000425, + detail::rounding_config_for_tick_size("0.01")); + if (!expect_close("reported FAK maker amount", reported_fak.maker, 1.0) || + !expect_close("reported FAK taker amount", reported_fak.taker, 5.8823) || + !expect_equal("reported FAK maker wei", to_wei(reported_fak.maker, 6), "1000000") || + !expect_equal("reported FAK taker wei", to_wei(reported_fak.taker, 6), "5882300")) + { + return 1; + } + const auto sell_amounts = detail::calculate_limit_order_amounts(OrderSide::SELL, 0.42, 10.0); if (!expect_close("sell maker amount", sell_amounts.maker, 10.0) || !expect_close("sell taker amount", sell_amounts.taker, 4.2))