diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d68a5..296740f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- **REST**: `PublicTrade::is_block_trade` — parsed from `GET /markets/trades` + (Kalshi changelog 2026-05-29: public trade responses now flag block + trades and support filtering by block status). Block trades are large + negotiated prints routed off the central order book; exposing the flag + lets trade-flow / microstructure consumers exclude them. The field is + absent on older payloads and defaults to `false` (forward/backward + compatible — no behavioural change for existing consumers). + +### Changed + +- **Internal**: extracted the inline `GET /markets/trades` body parser out + of `KalshiClient::get_trades` into a testable + `api_detail::parse_trades_response`, matching the existing + `parse_deposits_response` / `parse_withdrawals_response` convention. Adds + unit coverage for trade parsing (incl. the new `is_block_trade` flag). + ## [0.4.8] - 2026-05-19 ### Fixed diff --git a/include/kalshi/api.hpp b/include/kalshi/api.hpp index 68cf27c..681bcf5 100644 --- a/include/kalshi/api.hpp +++ b/include/kalshi/api.hpp @@ -165,6 +165,10 @@ struct PublicTrade { std::int32_t count{0}; Side taker_side{Side::Yes}; std::int64_t created_time{0}; + /// True when the trade was executed as a block trade (large negotiated + /// print routed off the central order book). Kalshi added this field on + /// 2026-05-29; absent on older payloads → defaults to false. + bool is_block_trade{false}; }; // ===== Phase 1: Exchange API Models ===== diff --git a/src/api/client.cpp b/src/api/client.cpp index 7265416..b14b514 100644 --- a/src/api/client.cpp +++ b/src/api/client.cpp @@ -921,6 +921,26 @@ std::vector parse_withdrawals_response(std::string_view body) { return parse_portfolio_movements(body, "withdrawals"); } +std::vector parse_trades_response(std::string_view body) { + const std::string buf{body}; + const std::vector trade_objects = extract_array_objects(buf, "trades"); + std::vector trades; + trades.reserve(trade_objects.size()); + for (const std::string& obj : trade_objects) { + PublicTrade t; + t.trade_id = extract_string(obj, "trade_id"); + t.market_ticker = extract_string(obj, "ticker"); + t.yes_price = static_cast(extract_int(obj, "yes_price")); + t.no_price = static_cast(extract_int(obj, "no_price")); + t.count = static_cast(extract_int(obj, "count")); + t.taker_side = parse_side(extract_string(obj, "taker_side")); + t.created_time = extract_int(obj, "created_time"); + t.is_block_trade = extract_bool(obj, "is_block_trade"); + trades.push_back(t); + } + return trades; +} + OrderCancelResult parse_order_cancel_result_response(std::string_view body) { const std::string obj{body}; OrderCancelResult result; @@ -1276,23 +1296,8 @@ Result> KalshiClient::get_trades(const GetTradesP response->status_code}); } - std::vector trades; - std::vector trade_objects = extract_array_objects(response->body, "trades"); - - for (const std::string& obj : trade_objects) { - PublicTrade t; - t.trade_id = extract_string(obj, "trade_id"); - t.market_ticker = extract_string(obj, "ticker"); - t.yes_price = static_cast(extract_int(obj, "yes_price")); - t.no_price = static_cast(extract_int(obj, "no_price")); - t.count = static_cast(extract_int(obj, "count")); - t.taker_side = parse_side(extract_string(obj, "taker_side")); - t.created_time = extract_int(obj, "created_time"); - trades.push_back(t); - } - PaginatedResponse result; - result.items = std::move(trades); + result.items = api_detail::parse_trades_response(response->body); std::string cursor = extract_cursor(response->body); if (!cursor.empty()) { diff --git a/src/api/response_parsers.hpp b/src/api/response_parsers.hpp index ff4972e..2c0413e 100644 --- a/src/api/response_parsers.hpp +++ b/src/api/response_parsers.hpp @@ -24,6 +24,11 @@ namespace kalshi::api_detail { /// Parses the ``orderbooks`` array returned by ``GET /markets/orderbooks``. [[nodiscard]] std::vector parse_orderbooks_response(std::string_view body); +/// Parses the ``trades`` array from ``GET /markets/trades``. Returns an empty +/// vector when the array is missing or empty. The cursor field is read +/// separately by the client method. +[[nodiscard]] std::vector parse_trades_response(std::string_view body); + /// Parses the ``deposits`` array from ``GET /portfolio/deposits``. Returns /// an empty vector when the array is missing or empty. The cursor field /// is read separately by the client method. diff --git a/tests/test_response_parsers.cpp b/tests/test_response_parsers.cpp index 79ac2bb..a3a7036 100644 --- a/tests/test_response_parsers.cpp +++ b/tests/test_response_parsers.cpp @@ -243,6 +243,33 @@ TEST(ResponseParsers, MarketsParseUnopenedArray) { EXPECT_EQ(markets[1].status, kalshi::MarketStatus::Open); } +TEST(ResponseParsers, TradesParseBlockTradeFlag) { + // Kalshi 2026-05-29: public trade responses flag block trades via the + // new boolean ``is_block_trade``. First row carries it true; second row + // omits it (older/non-block print) and must default to false. + const std::string body = R"json({ + "trades": [ + {"trade_id": "t1", "ticker": "KXHIGHDEN-26JUN03-B80", "yes_price": 42, + "no_price": 58, "count": 10, "taker_side": "yes", + "created_time": 1780000000, "is_block_trade": true}, + {"trade_id": "t2", "ticker": "KXHIGHDEN-26JUN03-B80", "yes_price": 40, + "no_price": 60, "count": 5, "taker_side": "no", + "created_time": 1780000100} + ], + "cursor": "" + })json"; + + const std::vector trades = kalshi::api_detail::parse_trades_response(body); + + ASSERT_EQ(trades.size(), 2U); + EXPECT_EQ(trades[0].trade_id, "t1"); + EXPECT_EQ(trades[0].count, 10); + EXPECT_EQ(trades[0].taker_side, kalshi::Side::Yes); + EXPECT_TRUE(trades[0].is_block_trade); + EXPECT_EQ(trades[1].trade_id, "t2"); + EXPECT_FALSE(trades[1].is_block_trade); +} + TEST(ResponseParsers, DepositsParseFinalizedAndPending) { const std::string body = R"json({ "deposits": [