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
91 changes: 87 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
limit (PUT `/portfolio/order_groups/{id}/limit`).
- New `get_api_limits()` endpoint to retrieve API tier and rate limits
(GET `/account/limits`).
- `contracts_limit_fp` fixed-point string field on `CreateOrderGroupRequest`,
`UpdateOrderGroupLimitRequest`, `GetOrderGroupResponse`, and `OrderGroupSummary`.
The integer `contracts_limit` field is now optional (provide one or both).
- New `OrderGroupUpdates` WebSocket channel for order group lifecycle events
(`OrderGroupUpdateData`, `OrderGroupEventType`).
- Fixed-point `_fp` fields added to WebSocket message types: `delta_fp` on
- Subaccount support across order operations: `cancel_order_for_subaccount()`,
`delete_order_group_for_subaccount()`, `reset_order_group_for_subaccount()`,
`trigger_order_group_for_subaccount()` convenience methods on `KalshiClient`.
Optional `subaccount` field added to `CreateOrderRequest`, `AmendOrderRequest`,
`DecreaseOrderRequest`, and `CreateOrderGroupRequest`.
- Per-order subaccount support in batch cancels via new `BatchCancelOrderItem`
type and `BatchCancelOrdersRequest::with_orders()`. New
`cancel_orders_with_items()` method on `BatchManager`.
- `subaccount_number` field on `Order` and `Fill` response types.
- Subaccount support for RFQ/Quote operations: `subaccount` field on
`CreateRfqRequest` and `CreateQuoteRequest`.
- `target_cost_dollars` field on `CreateRfqRequest`, `Rfq`, and `Quote` as a
dollar-denominated fixed-point string (replaces deprecated
`target_cost_centi_cents`).
- `side` and `expires_in_seconds` on `CreateRfqRequest`; `side` and `expires_ts`
on `Rfq` and `Quote` response types.
- Partial acceptance support: `contracts` field on `AcceptQuoteRequest` and
`CreateQuoteRequest`.
- Fixed-point `_fp` fields on REST API types: `count_fp` on `CreateOrderRequest`
and `AmendOrderRequest`, `reduce_by_fp`/`reduce_to_fp` on
`DecreaseOrderRequest`, `queue_position_fp` on `QueuePosition` and
`OrderQueuePositionResponse`, `contracts_fp` on `CreateRfqRequest`/`Rfq`/`Quote`,
`contracts_limit_fp` on `CreateOrderGroupRequest`/`UpdateOrderGroupLimitRequest`/
`GetOrderGroupResponse`/`OrderGroupSummary`.
- Fixed-point `_fp` fields on WebSocket message types: `delta_fp` on
`OrderbookDeltaData`, `volume_fp`/`open_interest_fp` on `TickerData`,
`count_fp` on `TradeData`, `count_fp`/`post_position_fp` on `FillData`,
and `position_fp`/`volume_fp` on `MarketPositionData`.
- `Settled` variant for `MarketStatus` enum and `Scalar` variant for
`MarketResult` enum.
- Integer cent price fields on `Market`: `yes_bid`, `yes_ask`, `no_bid`,
`no_ask`, `last_price`, `previous_yes_bid`, `previous_yes_ask`,
`previous_price`, `notional_value`, `liquidity`.
- Market metadata fields: `category`, `response_price_units`, `risk_limit_cents`,
`tick_size`, `updated_time`, `cap_count`, `fractional_trading_enabled`,
`settlement_value`, `expiration_value`.
- `min_updated_ts` filter on `GetMarketsParams` for querying markets by metadata
update time.
- `name` and `subaccount` fields on `CreateOrderGroupRequest`; `name`, `status`,
`created_time` on `OrderGroupSummary`.
- `fee_cost` on `Fill` for exchange fee cost as a fixed-point dollar string.
- `market_result` and `total_cost` on `MarketPosition`.
- `status` field on `Event`, `Milestone`, and `StructuredTarget`.
- Series metadata fields: `category`, `status`, `tags`, `settlement_sources`
(now typed as `Vec<SettlementSource>`), `contract_url`, `contract_terms_url`,
`product_metadata`, `fee_type`, `fee_multiplier`, `additional_prohibitions`.
- `client_transfer_id` on `TransferBetweenSubaccountsRequest` for idempotent
transfers.
- Multivariate collection enrichments: new `CollectionEvent` and
`AssociatedEvent` types; `events`, `open_date`, `close_date`,
`associated_events`, `is_ordered`, `size_min`, `size_max`,
`functional_description` fields on `MultivariateEventCollection`.
- Selected-market support for multivariate lookups: `selected_markets` field and
`with_selected_markets()` constructor on `CreateMarketInCollectionRequest` and
`LookupTickersRequest`.
- `lookback_seconds` filter on `GetLookupHistoryParams`.

### Fixed

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

### Changed

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

### Deprecated

- `CreateOrderRequest::sell_position_floor()` — use `reduce_only` instead. Only
accepts value of 0.
- `Rfq::target_cost_as_dollars()` and `Quote::rfq_target_cost_as_dollars()` —
use the `target_cost_dollars` / `rfq_target_cost_dollars` fields directly.
- `BatchCancelOrdersRequest::new()` and `try_new()` — use `with_orders()` /
`try_with_orders()` for per-order subaccount support.
- `target_cost_centi_cents` on RFQ/Quote types — use `target_cost_dollars`
instead.
- `Order.queue_position` — always returns 0; use the `get_order_queue_position`
endpoint instead.
- `Fill.price` and `Trade.price` — use `yes_price` / `no_price` instead.
- `Fill.trade_id`, `Fill.market_ticker`, `Fill.ts` — legacy field names.
- `Event.category` — use series-level category instead.
- `MarketPosition.resting_orders_count` — deprecated by the API.

## [0.2.0] - 2026-01-18

### Added
Expand Down
12 changes: 8 additions & 4 deletions examples/batch_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
//! Run with: cargo run --example batch_orders

use kalshi_trade_rs::{
Action, BatchCancelOrdersRequest, BatchCreateOrdersRequest, CreateOrderRequest,
GetMarketsParams, KalshiClient, KalshiConfig, MarketFilterStatus, OrderType, Side, TimeInForce,
cents_to_dollars,
Action, BatchCancelOrderItem, BatchCancelOrdersRequest, BatchCreateOrdersRequest,
CreateOrderRequest, GetMarketsParams, KalshiClient, KalshiConfig, MarketFilterStatus,
OrderType, Side, TimeInForce, cents_to_dollars,
};
use std::time::{SystemTime, UNIX_EPOCH};

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

let cancel_request = BatchCancelOrdersRequest::new(successful_order_ids.clone());
let cancel_items: Vec<BatchCancelOrderItem> = successful_order_ids
.iter()
.map(BatchCancelOrderItem::new)
.collect();
let cancel_request = BatchCancelOrdersRequest::with_orders(cancel_items);
let cancel_response = client.batch_cancel_orders(cancel_request).await?;

println!("\nBatch cancel results:");
Expand Down
5 changes: 3 additions & 2 deletions examples/rfq_verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let size = match rfq.contracts {
Some(c) if c > 0 => format!("{} contracts", c),
_ => rfq
.target_cost_dollars()
.map(|d| format!("${:.2} target", d))
.target_cost_dollars
.as_deref()
.map(|d| format!("${} target", d))
.unwrap_or_else(|| "unknown size".to_string()),
};
println!(
Expand Down
2 changes: 1 addition & 1 deletion examples/trading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.as_nanos();
let new_client_order_id = format!("example-amended-{}", ts);

let amend_request = AmendOrderRequest::new(
let amend_request = AmendOrderRequest::with_client_order_ids(
&market.ticker,
Side::Yes,
Action::Buy,
Expand Down
57 changes: 45 additions & 12 deletions src/api/order_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,59 @@ pub async fn list_order_groups(
}

/// Deletes an order group and cancels all orders within it.
pub async fn delete_order_group(http: &HttpClient, order_group_id: &str) -> Result<()> {
let path = format!("/portfolio/order_groups/{}", encode_id(order_group_id));
pub async fn delete_order_group(
http: &HttpClient,
order_group_id: &str,
subaccount: Option<i32>,
) -> Result<()> {
let path = match subaccount {
Some(sub) => format!(
"/portfolio/order_groups/{}?subaccount={}",
encode_id(order_group_id),
sub
),
None => format!("/portfolio/order_groups/{}", encode_id(order_group_id)),
};
http.delete(&path).await
}

/// Resets the matched contracts counter to zero, re-enabling order placement.
pub async fn reset_order_group(http: &HttpClient, order_group_id: &str) -> Result<()> {
let path = format!(
"/portfolio/order_groups/{}/reset",
encode_id(order_group_id)
);
pub async fn reset_order_group(
http: &HttpClient,
order_group_id: &str,
subaccount: Option<i32>,
) -> Result<()> {
let path = match subaccount {
Some(sub) => format!(
"/portfolio/order_groups/{}/reset?subaccount={}",
encode_id(order_group_id),
sub
),
None => format!(
"/portfolio/order_groups/{}/reset",
encode_id(order_group_id)
),
};
http.put_empty_json(&path).await
}

/// Triggers an order group, cancelling all orders within it.
pub async fn trigger_order_group(http: &HttpClient, order_group_id: &str) -> Result<()> {
let path = format!(
"/portfolio/order_groups/{}/trigger",
encode_id(order_group_id)
);
pub async fn trigger_order_group(
http: &HttpClient,
order_group_id: &str,
subaccount: Option<i32>,
) -> Result<()> {
let path = match subaccount {
Some(sub) => format!(
"/portfolio/order_groups/{}/trigger?subaccount={}",
encode_id(order_group_id),
sub
),
None => format!(
"/portfolio/order_groups/{}/trigger",
encode_id(order_group_id)
),
};
http.put_empty_json(&path).await
}

Expand Down
11 changes: 9 additions & 2 deletions src/api/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ pub async fn get_order(http: &HttpClient, order_id: &str) -> Result<OrderRespons
http.get(&path).await
}

pub async fn cancel_order(http: &HttpClient, order_id: &str) -> Result<CancelOrderResponse> {
let path = format!("/portfolio/orders/{}", order_id);
pub async fn cancel_order(
http: &HttpClient,
order_id: &str,
subaccount: Option<i32>,
) -> Result<CancelOrderResponse> {
let path = match subaccount {
Some(sub) => format!("/portfolio/orders/{}?subaccount={}", order_id, sub),
None => format!("/portfolio/orders/{}", order_id),
};
http.delete_with_response(&path).await
}

Expand Down
65 changes: 63 additions & 2 deletions src/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ use crate::{
KalshiClient,
error::{Error, MAX_BATCH_SIZE, Result},
models::{
BatchCancelOrderResult, BatchCancelOrdersRequest, BatchCreateOrdersRequest,
BatchOrderResult, CreateOrderRequest, Order,
BatchCancelOrderItem, BatchCancelOrderResult, BatchCancelOrdersRequest,
BatchCreateOrdersRequest, BatchOrderResult, CreateOrderRequest, Order,
},
};

Expand Down Expand Up @@ -510,6 +510,7 @@ impl<'a> BatchManager<'a> {
}

// Send the batch with retry logic
#[allow(deprecated)]
let request = BatchCancelOrdersRequest::new(chunk.to_vec());
let client = self.client;
match self
Expand Down Expand Up @@ -538,6 +539,65 @@ impl<'a> BatchManager<'a> {
error: None,
}
}

/// Cancel multiple orders with per-order subaccount support.
///
/// Like [`cancel_orders`](Self::cancel_orders) but accepts
/// [`BatchCancelOrderItem`] entries, allowing each cancellation to target
/// a different subaccount.
pub async fn cancel_orders_with_items(
&self,
items: Vec<BatchCancelOrderItem>,
) -> BatchOperationResult<AggregatedCancelResponse> {
if items.is_empty() {
return BatchOperationResult {
completed: AggregatedCancelResponse { orders: vec![] },
error: None,
};
}

let mut all_results = Vec::with_capacity(items.len());

for chunk in items.chunks(MAX_BATCH_SIZE) {
let cost = chunk.len() as f64 * CANCEL_ORDER_COST;

let wait_time = {
let mut limiter = self.rate_limiter.lock().await;
limiter.consume(cost)
};

if !wait_time.is_zero() {
tokio::time::sleep(wait_time).await;
}

let request = BatchCancelOrdersRequest::with_orders(chunk.to_vec());
let client = self.client;
match self
.execute_with_retry(|| {
let req = request.clone();
async move { client.batch_cancel_orders(req).await }
})
.await
{
Ok(response) => all_results.extend(response.orders),
Err(e) => {
return BatchOperationResult {
completed: AggregatedCancelResponse {
orders: all_results,
},
error: Some(e),
};
}
}
}

BatchOperationResult {
completed: AggregatedCancelResponse {
orders: all_results,
},
error: None,
}
}
}

/// Check if an API error message indicates a transient/retryable error.
Expand Down Expand Up @@ -792,6 +852,7 @@ mod tests {
self_trade_prevention_type: None,
order_group_id: None,
cancel_order_on_pause: None,
subaccount_number: None,
}
}

Expand Down
Loading