Skip to content

Commit a1c3d50

Browse files
committed
feat: batch API updates — subaccounts, _fp fields, model enrichments, docs
Add subaccount support across orders, order groups, RFQs, and batch cancels. Add fixed-point _fp fields to REST and WS types. Enrich Market, Series, Event, Multivariate, and other models with new API fields. Update CHANGELOG and WS README to reflect all changes.
1 parent 1001d2d commit a1c3d50

21 files changed

+995
-88
lines changed

CHANGELOG.md

Lines changed: 77 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,39 @@ 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+
- `target_cost_centi_cents` on RFQ/Quote types — use `target_cost_dollars`
117+
instead.
118+
46119
## [0.2.0] - 2026-01-18
47120

48121
### Added

examples/rfq_verify.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ 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()
102+
.target_cost_as_dollars()
103103
.map(|d| format!("${:.2} target", d))
104104
.unwrap_or_else(|| "unknown size".to_string()),
105105
};

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: 62 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

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

543602
/// Check if an API error message indicates a transient/retryable error.
@@ -792,6 +851,7 @@ mod tests {
792851
self_trade_prevention_type: None,
793852
order_group_id: None,
794853
cancel_order_on_pause: None,
854+
subaccount_number: None,
795855
}
796856
}
797857

0 commit comments

Comments
 (0)