Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
```

Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/clob-v2-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions include/clob_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -132,6 +133,7 @@ namespace polymarket
double amount; // USDC for BUY, shares for SELL
OrderSide side;
std::optional<double> price; // Optional price limit
std::string tick_size = "0.01";
std::string metadata = "0x0000000000000000000000000000000000000000000000000000000000000000";
std::string builder_code = "0x0000000000000000000000000000000000000000000000000000000000000000";
};
Expand Down
4 changes: 2 additions & 2 deletions include/polymarket/version.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
37 changes: 28 additions & 9 deletions src/clob_order_execution.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<SignedOrder> ClobClient::create_market_order_result(const CreateMarketOrderParams &params)
Expand Down
87 changes: 85 additions & 2 deletions src/order_execution.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
#include "order_execution.hpp"

#include <cmath>
#include <stdexcept>

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
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions src/order_execution.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 20 additions & 12 deletions src/order_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

#include "order_signer.hpp"
#include "order_execution.hpp"
#include "http_client.hpp"
#include <nlohmann/json.hpp>
#include <iostream>
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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"
Expand Down
22 changes: 22 additions & 0 deletions tests/test_order_execution.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading