Skip to content

Commit b5a8f72

Browse files
authored
Merge pull request #27 from pbeets/feat/batch1-api-updates
feat: batch API updates — subaccounts, _fp fields, model enrichments
2 parents b2e9b35 + 2164600 commit b5a8f72

22 files changed

+1052
-107
lines changed

CHANGELOG.md

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
limit (PUT `/portfolio/order_groups/{id}/limit`).
1616
- New `get_api_limits()` endpoint to retrieve API tier and rate limits
1717
(GET `/account/limits`).
18-
- `contracts_limit_fp` fixed-point string field on `CreateOrderGroupRequest`,
19-
`UpdateOrderGroupLimitRequest`, `GetOrderGroupResponse`, and `OrderGroupSummary`.
20-
The integer `contracts_limit` field is now optional (provide one or both).
2118
- New `OrderGroupUpdates` WebSocket channel for order group lifecycle events
2219
(`OrderGroupUpdateData`, `OrderGroupEventType`).
23-
- Fixed-point `_fp` fields added to WebSocket message types: `delta_fp` on
20+
- Subaccount support across order operations: `cancel_order_for_subaccount()`,
21+
`delete_order_group_for_subaccount()`, `reset_order_group_for_subaccount()`,
22+
`trigger_order_group_for_subaccount()` convenience methods on `KalshiClient`.
23+
Optional `subaccount` field added to `CreateOrderRequest`, `AmendOrderRequest`,
24+
`DecreaseOrderRequest`, and `CreateOrderGroupRequest`.
25+
- Per-order subaccount support in batch cancels via new `BatchCancelOrderItem`
26+
type and `BatchCancelOrdersRequest::with_orders()`. New
27+
`cancel_orders_with_items()` method on `BatchManager`.
28+
- `subaccount_number` field on `Order` and `Fill` response types.
29+
- Subaccount support for RFQ/Quote operations: `subaccount` field on
30+
`CreateRfqRequest` and `CreateQuoteRequest`.
31+
- `target_cost_dollars` field on `CreateRfqRequest`, `Rfq`, and `Quote` as a
32+
dollar-denominated fixed-point string (replaces deprecated
33+
`target_cost_centi_cents`).
34+
- `side` and `expires_in_seconds` on `CreateRfqRequest`; `side` and `expires_ts`
35+
on `Rfq` and `Quote` response types.
36+
- Partial acceptance support: `contracts` field on `AcceptQuoteRequest` and
37+
`CreateQuoteRequest`.
38+
- Fixed-point `_fp` fields on REST API types: `count_fp` on `CreateOrderRequest`
39+
and `AmendOrderRequest`, `reduce_by_fp`/`reduce_to_fp` on
40+
`DecreaseOrderRequest`, `queue_position_fp` on `QueuePosition` and
41+
`OrderQueuePositionResponse`, `contracts_fp` on `CreateRfqRequest`/`Rfq`/`Quote`,
42+
`contracts_limit_fp` on `CreateOrderGroupRequest`/`UpdateOrderGroupLimitRequest`/
43+
`GetOrderGroupResponse`/`OrderGroupSummary`.
44+
- Fixed-point `_fp` fields on WebSocket message types: `delta_fp` on
2445
`OrderbookDeltaData`, `volume_fp`/`open_interest_fp` on `TickerData`,
2546
`count_fp` on `TradeData`, `count_fp`/`post_position_fp` on `FillData`,
2647
and `position_fp`/`volume_fp` on `MarketPositionData`.
48+
- `Settled` variant for `MarketStatus` enum and `Scalar` variant for
49+
`MarketResult` enum.
50+
- Integer cent price fields on `Market`: `yes_bid`, `yes_ask`, `no_bid`,
51+
`no_ask`, `last_price`, `previous_yes_bid`, `previous_yes_ask`,
52+
`previous_price`, `notional_value`, `liquidity`.
53+
- Market metadata fields: `category`, `response_price_units`, `risk_limit_cents`,
54+
`tick_size`, `updated_time`, `cap_count`, `fractional_trading_enabled`,
55+
`settlement_value`, `expiration_value`.
56+
- `min_updated_ts` filter on `GetMarketsParams` for querying markets by metadata
57+
update time.
58+
- `name` and `subaccount` fields on `CreateOrderGroupRequest`; `name`, `status`,
59+
`created_time` on `OrderGroupSummary`.
60+
- `fee_cost` on `Fill` for exchange fee cost as a fixed-point dollar string.
61+
- `market_result` and `total_cost` on `MarketPosition`.
62+
- `status` field on `Event`, `Milestone`, and `StructuredTarget`.
63+
- Series metadata fields: `category`, `status`, `tags`, `settlement_sources`
64+
(now typed as `Vec<SettlementSource>`), `contract_url`, `contract_terms_url`,
65+
`product_metadata`, `fee_type`, `fee_multiplier`, `additional_prohibitions`.
66+
- `client_transfer_id` on `TransferBetweenSubaccountsRequest` for idempotent
67+
transfers.
68+
- Multivariate collection enrichments: new `CollectionEvent` and
69+
`AssociatedEvent` types; `events`, `open_date`, `close_date`,
70+
`associated_events`, `is_ordered`, `size_min`, `size_max`,
71+
`functional_description` fields on `MultivariateEventCollection`.
72+
- Selected-market support for multivariate lookups: `selected_markets` field and
73+
`with_selected_markets()` constructor on `CreateMarketInCollectionRequest` and
74+
`LookupTickersRequest`.
75+
- `lookback_seconds` filter on `GetLookupHistoryParams`.
2776

2877
### Fixed
2978

@@ -34,15 +83,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3483

3584
### Changed
3685

86+
- **Breaking:** `AmendOrderRequest::new()` no longer requires `client_order_id`
87+
and `updated_client_order_id` — these are now optional. Use
88+
`AmendOrderRequest::with_client_order_ids()` for the previous behavior.
89+
- **Breaking:** `BatchCancelOrdersRequest::ids` is now `Option<Vec<String>>`.
90+
Existing code using `.ids` directly must unwrap. Use
91+
`BatchCancelOrdersRequest::with_orders()` for the new per-order subaccount
92+
format.
93+
- **Breaking:** `Rfq::target_cost_dollars()` renamed to
94+
`Rfq::target_cost_as_dollars()` to avoid conflict with the new
95+
`target_cost_dollars` field.
96+
- **Breaking:** `Quote::rfq_target_cost_dollars()` renamed to
97+
`Quote::rfq_target_cost_as_dollars()`.
98+
- **Breaking:** `CreateMarketInCollectionRequest::variables` and
99+
`LookupTickersRequest::variables` changed from `HashMap` to `Option<HashMap>`
100+
to support the new `selected_markets` alternative.
37101
- **Breaking:** Removed `initialized` field from `OrderbookSummary`. The
38102
aggregator now guarantees that summaries are only produced for fully
39103
initialized orderbooks, making the field redundant. Remove any
40104
`summary.initialized` checks from your code.
105+
- `CreateRfqRequest::with_target_cost_dollars()` now sends the dollar amount via
106+
the `target_cost_dollars` field instead of converting to centi-cents.
41107
- `Channel::requires_market_ticker()` now returns `true` only for
42108
`OrderbookDelta`. Other market data channels (`Ticker`, `Trade`,
43109
`MarketLifecycle`, `Multivariate`) support subscribing with an empty ticker
44110
list to receive updates for all markets.
45111

112+
### Deprecated
113+
114+
- `CreateOrderRequest::sell_position_floor()` — use `reduce_only` instead. Only
115+
accepts value of 0.
116+
- `Rfq::target_cost_as_dollars()` and `Quote::rfq_target_cost_as_dollars()`
117+
use the `target_cost_dollars` / `rfq_target_cost_dollars` fields directly.
118+
- `BatchCancelOrdersRequest::new()` and `try_new()` — use `with_orders()` /
119+
`try_with_orders()` for per-order subaccount support.
120+
- `target_cost_centi_cents` on RFQ/Quote types — use `target_cost_dollars`
121+
instead.
122+
- `Order.queue_position` — always returns 0; use the `get_order_queue_position`
123+
endpoint instead.
124+
- `Fill.price` and `Trade.price` — use `yes_price` / `no_price` instead.
125+
- `Fill.trade_id`, `Fill.market_ticker`, `Fill.ts` — legacy field names.
126+
- `Event.category` — use series-level category instead.
127+
- `MarketPosition.resting_orders_count` — deprecated by the API.
128+
46129
## [0.2.0] - 2026-01-18
47130

48131
### Added

examples/batch_orders.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
//! Run with: cargo run --example batch_orders
1111
1212
use kalshi_trade_rs::{
13-
Action, BatchCancelOrdersRequest, BatchCreateOrdersRequest, CreateOrderRequest,
14-
GetMarketsParams, KalshiClient, KalshiConfig, MarketFilterStatus, OrderType, Side, TimeInForce,
15-
cents_to_dollars,
13+
Action, BatchCancelOrderItem, BatchCancelOrdersRequest, BatchCreateOrdersRequest,
14+
CreateOrderRequest, GetMarketsParams, KalshiClient, KalshiConfig, MarketFilterStatus,
15+
OrderType, Side, TimeInForce, cents_to_dollars,
1616
};
1717
use std::time::{SystemTime, UNIX_EPOCH};
1818

@@ -171,7 +171,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
171171
println!("=== Batch Cancel Orders ===");
172172
println!("Canceling {} orders...", successful_order_ids.len());
173173

174-
let cancel_request = BatchCancelOrdersRequest::new(successful_order_ids.clone());
174+
let cancel_items: Vec<BatchCancelOrderItem> = successful_order_ids
175+
.iter()
176+
.map(BatchCancelOrderItem::new)
177+
.collect();
178+
let cancel_request = BatchCancelOrdersRequest::with_orders(cancel_items);
175179
let cancel_response = client.batch_cancel_orders(cancel_request).await?;
176180

177181
println!("\nBatch cancel results:");

examples/rfq_verify.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
9999
let size = match rfq.contracts {
100100
Some(c) if c > 0 => format!("{} contracts", c),
101101
_ => rfq
102-
.target_cost_dollars()
103-
.map(|d| format!("${:.2} target", d))
102+
.target_cost_dollars
103+
.as_deref()
104+
.map(|d| format!("${} target", d))
104105
.unwrap_or_else(|| "unknown size".to_string()),
105106
};
106107
println!(

examples/trading.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
175175
.as_nanos();
176176
let new_client_order_id = format!("example-amended-{}", ts);
177177

178-
let amend_request = AmendOrderRequest::new(
178+
let amend_request = AmendOrderRequest::with_client_order_ids(
179179
&market.ticker,
180180
Side::Yes,
181181
Action::Buy,

src/api/order_groups.rs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,59 @@ pub async fn list_order_groups(
4242
}
4343

4444
/// Deletes an order group and cancels all orders within it.
45-
pub async fn delete_order_group(http: &HttpClient, order_group_id: &str) -> Result<()> {
46-
let path = format!("/portfolio/order_groups/{}", encode_id(order_group_id));
45+
pub async fn delete_order_group(
46+
http: &HttpClient,
47+
order_group_id: &str,
48+
subaccount: Option<i32>,
49+
) -> Result<()> {
50+
let path = match subaccount {
51+
Some(sub) => format!(
52+
"/portfolio/order_groups/{}?subaccount={}",
53+
encode_id(order_group_id),
54+
sub
55+
),
56+
None => format!("/portfolio/order_groups/{}", encode_id(order_group_id)),
57+
};
4758
http.delete(&path).await
4859
}
4960

5061
/// Resets the matched contracts counter to zero, re-enabling order placement.
51-
pub async fn reset_order_group(http: &HttpClient, order_group_id: &str) -> Result<()> {
52-
let path = format!(
53-
"/portfolio/order_groups/{}/reset",
54-
encode_id(order_group_id)
55-
);
62+
pub async fn reset_order_group(
63+
http: &HttpClient,
64+
order_group_id: &str,
65+
subaccount: Option<i32>,
66+
) -> Result<()> {
67+
let path = match subaccount {
68+
Some(sub) => format!(
69+
"/portfolio/order_groups/{}/reset?subaccount={}",
70+
encode_id(order_group_id),
71+
sub
72+
),
73+
None => format!(
74+
"/portfolio/order_groups/{}/reset",
75+
encode_id(order_group_id)
76+
),
77+
};
5678
http.put_empty_json(&path).await
5779
}
5880

5981
/// Triggers an order group, cancelling all orders within it.
60-
pub async fn trigger_order_group(http: &HttpClient, order_group_id: &str) -> Result<()> {
61-
let path = format!(
62-
"/portfolio/order_groups/{}/trigger",
63-
encode_id(order_group_id)
64-
);
82+
pub async fn trigger_order_group(
83+
http: &HttpClient,
84+
order_group_id: &str,
85+
subaccount: Option<i32>,
86+
) -> Result<()> {
87+
let path = match subaccount {
88+
Some(sub) => format!(
89+
"/portfolio/order_groups/{}/trigger?subaccount={}",
90+
encode_id(order_group_id),
91+
sub
92+
),
93+
None => format!(
94+
"/portfolio/order_groups/{}/trigger",
95+
encode_id(order_group_id)
96+
),
97+
};
6598
http.put_empty_json(&path).await
6699
}
67100

src/api/orders.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@ pub async fn get_order(http: &HttpClient, order_id: &str) -> Result<OrderRespons
2424
http.get(&path).await
2525
}
2626

27-
pub async fn cancel_order(http: &HttpClient, order_id: &str) -> Result<CancelOrderResponse> {
28-
let path = format!("/portfolio/orders/{}", order_id);
27+
pub async fn cancel_order(
28+
http: &HttpClient,
29+
order_id: &str,
30+
subaccount: Option<i32>,
31+
) -> Result<CancelOrderResponse> {
32+
let path = match subaccount {
33+
Some(sub) => format!("/portfolio/orders/{}?subaccount={}", order_id, sub),
34+
None => format!("/portfolio/orders/{}", order_id),
35+
};
2936
http.delete_with_response(&path).await
3037
}
3138

src/batch.rs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ use crate::{
4747
KalshiClient,
4848
error::{Error, MAX_BATCH_SIZE, Result},
4949
models::{
50-
BatchCancelOrderResult, BatchCancelOrdersRequest, BatchCreateOrdersRequest,
51-
BatchOrderResult, CreateOrderRequest, Order,
50+
BatchCancelOrderItem, BatchCancelOrderResult, BatchCancelOrdersRequest,
51+
BatchCreateOrdersRequest, BatchOrderResult, CreateOrderRequest, Order,
5252
},
5353
};
5454

@@ -510,6 +510,7 @@ impl<'a> BatchManager<'a> {
510510
}
511511

512512
// Send the batch with retry logic
513+
#[allow(deprecated)]
513514
let request = BatchCancelOrdersRequest::new(chunk.to_vec());
514515
let client = self.client;
515516
match self
@@ -538,6 +539,65 @@ impl<'a> BatchManager<'a> {
538539
error: None,
539540
}
540541
}
542+
543+
/// Cancel multiple orders with per-order subaccount support.
544+
///
545+
/// Like [`cancel_orders`](Self::cancel_orders) but accepts
546+
/// [`BatchCancelOrderItem`] entries, allowing each cancellation to target
547+
/// a different subaccount.
548+
pub async fn cancel_orders_with_items(
549+
&self,
550+
items: Vec<BatchCancelOrderItem>,
551+
) -> BatchOperationResult<AggregatedCancelResponse> {
552+
if items.is_empty() {
553+
return BatchOperationResult {
554+
completed: AggregatedCancelResponse { orders: vec![] },
555+
error: None,
556+
};
557+
}
558+
559+
let mut all_results = Vec::with_capacity(items.len());
560+
561+
for chunk in items.chunks(MAX_BATCH_SIZE) {
562+
let cost = chunk.len() as f64 * CANCEL_ORDER_COST;
563+
564+
let wait_time = {
565+
let mut limiter = self.rate_limiter.lock().await;
566+
limiter.consume(cost)
567+
};
568+
569+
if !wait_time.is_zero() {
570+
tokio::time::sleep(wait_time).await;
571+
}
572+
573+
let request = BatchCancelOrdersRequest::with_orders(chunk.to_vec());
574+
let client = self.client;
575+
match self
576+
.execute_with_retry(|| {
577+
let req = request.clone();
578+
async move { client.batch_cancel_orders(req).await }
579+
})
580+
.await
581+
{
582+
Ok(response) => all_results.extend(response.orders),
583+
Err(e) => {
584+
return BatchOperationResult {
585+
completed: AggregatedCancelResponse {
586+
orders: all_results,
587+
},
588+
error: Some(e),
589+
};
590+
}
591+
}
592+
}
593+
594+
BatchOperationResult {
595+
completed: AggregatedCancelResponse {
596+
orders: all_results,
597+
},
598+
error: None,
599+
}
600+
}
541601
}
542602

543603
/// Check if an API error message indicates a transient/retryable error.
@@ -792,6 +852,7 @@ mod tests {
792852
self_trade_prevention_type: None,
793853
order_group_id: None,
794854
cancel_order_on_pause: None,
855+
subaccount_number: None,
795856
}
796857
}
797858

0 commit comments

Comments
 (0)