diff --git a/CHANGELOG.md b/CHANGELOG.md index aefbccf..0f036b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- New `trigger_order_group()` endpoint to manually trigger an order group's + auto-cancel (PUT `/portfolio/order_groups/{id}/trigger`). +- New `update_order_group_limit()` endpoint to change an order group's contracts + 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 + `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`. + ### Fixed - `OrderbookAggregator` now drops delta messages that arrive before a snapshot diff --git a/examples/order_groups.rs b/examples/order_groups.rs index f3e9268..5381c6a 100644 --- a/examples/order_groups.rs +++ b/examples/order_groups.rs @@ -12,6 +12,7 @@ use kalshi_trade_rs::{ Action, CreateOrderGroupRequest, CreateOrderRequest, GetMarketsParams, GetOrderGroupsParams, KalshiClient, KalshiConfig, MarketFilterStatus, OrderType, Side, TimeInForce, + UpdateOrderGroupLimitRequest, }; #[tokio::main] @@ -108,14 +109,31 @@ async fn main() -> Result<(), Box> { println!("Page 1: {} groups", page1.order_groups.len()); println!(); - // 6. Reset Order Group + // 6. Update Order Group Limit + println!("=== Update Order Group Limit ==="); + println!("Updating contracts limit to 20...\n"); + let update_request = UpdateOrderGroupLimitRequest::new(20); + client + .update_order_group_limit(order_group_id, update_request) + .await?; + println!("Limit updated to 20."); + println!(); + + // 7. Reset Order Group println!("=== Reset Order Group ==="); println!("Resetting matched contracts counter...\n"); client.reset_order_group(order_group_id).await?; println!("Reset complete."); println!(); - // 7. Delete Order Group + // 8. Trigger Order Group + println!("=== Trigger Order Group ==="); + println!("Triggering order group (cancels all orders)...\n"); + client.trigger_order_group(order_group_id).await?; + println!("Order group triggered."); + println!(); + + // 9. Delete Order Group println!("=== Delete Order Group ==="); println!("Deleting the order group (this cancels all orders)...\n"); client.delete_order_group(order_group_id).await?; @@ -146,7 +164,9 @@ async fn main() -> Result<(), Box> { println!(" get_order_group(id) - Get order group details"); println!(" list_order_groups() - List all order groups"); println!(" list_order_groups_with_params() - List with pagination"); + println!(" update_order_group_limit(id, r) - Update contracts limit"); println!(" reset_order_group(id) - Reset matched contracts counter"); + println!(" trigger_order_group(id) - Trigger auto-cancel manually"); println!(" delete_order_group(id) - Delete group and cancel orders"); println!(); diff --git a/src/api.rs b/src/api.rs index 1d1353a..ae94abd 100644 --- a/src/api.rs +++ b/src/api.rs @@ -3,6 +3,7 @@ //! These modules contain endpoint-specific logic. The public API is exposed //! through flat methods on [`KalshiClient`](crate::KalshiClient). +pub(crate) mod account; pub(crate) mod api_keys; pub(crate) mod communications; pub(crate) mod events; diff --git a/src/api/README.md b/src/api/README.md index 2677446..dfa3a3b 100644 --- a/src/api/README.md +++ b/src/api/README.md @@ -21,7 +21,8 @@ Complete reference for all Kalshi REST API endpoints supported by this library. |----------|-----------|----------| | Exchange | 5 | 100% | | Orders | 10 | 100% | -| Order Groups | 5 | 100% | +| Order Groups | 7 | 100% | +| Account | 1 | 100% | | Portfolio | 5 | 100% | | Subaccounts | 5 | 100% | | Markets | 6 | 100% | @@ -36,7 +37,7 @@ Complete reference for all Kalshi REST API endpoints supported by this library. | Structured Targets | 2 | 100% | | Incentive Programs | 1 | 100% | | FCM | 2 | 100% | -| **Total** | **75** | **100%** | +| **Total** | **78** | **100%** | --- @@ -82,11 +83,23 @@ Complete reference for all Kalshi REST API endpoints supported by this library. | ✅ | GET | `/portfolio/order_groups` | `list_order_groups()` | | | ✅ | DELETE | `/portfolio/order_groups/{id}` | `delete_order_group()` | Cancels all orders in group | | ✅ | PUT | `/portfolio/order_groups/{id}/reset` | `reset_order_group()` | Resets contracts counter | +| ✅ | PUT | `/portfolio/order_groups/{id}/trigger` | `trigger_order_group()` | Triggers auto-cancel | +| ✅ | PUT | `/portfolio/order_groups/{id}/limit` | `update_order_group_limit()` | Updates contracts limit | **Source**: `src/api/order_groups.rs` --- +## Account API + +| Status | Method | Endpoint | Rust Function | Notes | +|--------|--------|----------|---------------|-------| +| ✅ | GET | `/account/limits` | `get_api_limits()` | API tier and rate limits | + +**Source**: `src/api/account.rs` + +--- + ## Portfolio API | Status | Method | Endpoint | Rust Function | Notes | diff --git a/src/api/account.rs b/src/api/account.rs new file mode 100644 index 0000000..74e2d8b --- /dev/null +++ b/src/api/account.rs @@ -0,0 +1,8 @@ +//! Account API endpoints. + +use crate::{client::HttpClient, error::Result, models::ApiTierLimitsResponse}; + +/// Returns the user's API tier and rate limits. +pub async fn get_api_limits(http: &HttpClient) -> Result { + http.get("/account/limits").await +} diff --git a/src/api/order_groups.rs b/src/api/order_groups.rs index f8d28a1..bf4779d 100644 --- a/src/api/order_groups.rs +++ b/src/api/order_groups.rs @@ -7,7 +7,7 @@ use crate::{ error::Result, models::{ CreateOrderGroupRequest, CreateOrderGroupResponse, GetOrderGroupResponse, - GetOrderGroupsParams, OrderGroupsResponse, + GetOrderGroupsParams, OrderGroupsResponse, UpdateOrderGroupLimitRequest, }, }; @@ -55,3 +55,25 @@ pub async fn reset_order_group(http: &HttpClient, order_group_id: &str) -> Resul ); 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) + ); + http.put_empty_json(&path).await +} + +/// Updates the contracts limit for an order group. +pub async fn update_order_group_limit( + http: &HttpClient, + order_group_id: &str, + request: UpdateOrderGroupLimitRequest, +) -> Result<()> { + let path = format!( + "/portfolio/order_groups/{}/limit", + encode_id(order_group_id) + ); + http.put_no_response(&path, &request).await +} diff --git a/src/client.rs b/src/client.rs index e0ebf66..88d57bb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,29 +6,29 @@ pub use websocket::WebSocketClient; use crate::{ api::{ - api_keys, communications, events, exchange, fcm, incentive_programs, live_data, markets, - milestones, multivariate, order_groups, orders, portfolio, search, series, + account, api_keys, communications, events, exchange, fcm, incentive_programs, live_data, + markets, milestones, multivariate, order_groups, orders, portfolio, search, series, structured_targets, subaccounts, }, auth::KalshiConfig, error::Result, models::{ AcceptQuoteRequest, AmendOrderRequest, AmendOrderResponse, ApiKeysResponse, - BalanceResponse, BatchCancelOrdersRequest, BatchCancelOrdersResponse, - BatchCandlesticksResponse, BatchCreateOrdersRequest, BatchCreateOrdersResponse, - BatchLiveDataResponse, CancelOrderResponse, CandlesticksResponse, CommunicationsIdResponse, - CreateApiKeyRequest, CreateApiKeyResponse, CreateMarketInCollectionRequest, - CreateMarketInCollectionResponse, CreateOrderGroupRequest, CreateOrderGroupResponse, - CreateOrderRequest, CreateQuoteRequest, CreateRfqRequest, CreateSubaccountRequest, - CreateSubaccountResponse, DecreaseOrderRequest, DeleteApiKeyResponse, - EventCandlesticksResponse, EventForecastPercentileHistoryResponse, EventMetadataResponse, - EventResponse, EventsResponse, ExchangeAnnouncementsResponse, ExchangeScheduleResponse, - ExchangeStatusResponse, FeeChangesResponse, FillsResponse, FiltersBySportResponse, - GenerateApiKeyRequest, GenerateApiKeyResponse, GetBatchCandlesticksParams, - GetBatchLiveDataParams, GetCandlesticksParams, GetEventCandlesticksParams, - GetEventForecastPercentileHistoryParams, GetEventParams, GetEventsParams, - GetFcmOrdersParams, GetFcmPositionsParams, GetFeeChangesParams, GetFillsParams, - GetLookupHistoryParams, GetMarketsParams, GetMilestonesParams, + ApiTierLimitsResponse, BalanceResponse, BatchCancelOrdersRequest, + BatchCancelOrdersResponse, BatchCandlesticksResponse, BatchCreateOrdersRequest, + BatchCreateOrdersResponse, BatchLiveDataResponse, CancelOrderResponse, + CandlesticksResponse, CommunicationsIdResponse, CreateApiKeyRequest, CreateApiKeyResponse, + CreateMarketInCollectionRequest, CreateMarketInCollectionResponse, CreateOrderGroupRequest, + CreateOrderGroupResponse, CreateOrderRequest, CreateQuoteRequest, CreateRfqRequest, + CreateSubaccountRequest, CreateSubaccountResponse, DecreaseOrderRequest, + DeleteApiKeyResponse, EventCandlesticksResponse, EventForecastPercentileHistoryResponse, + EventMetadataResponse, EventResponse, EventsResponse, ExchangeAnnouncementsResponse, + ExchangeScheduleResponse, ExchangeStatusResponse, FeeChangesResponse, FillsResponse, + FiltersBySportResponse, GenerateApiKeyRequest, GenerateApiKeyResponse, + GetBatchCandlesticksParams, GetBatchLiveDataParams, GetCandlesticksParams, + GetEventCandlesticksParams, GetEventForecastPercentileHistoryParams, GetEventParams, + GetEventsParams, GetFcmOrdersParams, GetFcmPositionsParams, GetFeeChangesParams, + GetFillsParams, GetLookupHistoryParams, GetMarketsParams, GetMilestonesParams, GetMultivariateCollectionsParams, GetMultivariateEventsParams, GetOrderGroupResponse, GetOrderGroupsParams, GetOrderbookParams, GetOrdersParams, GetPositionsParams, GetQueuePositionsParams, GetQuoteResponse, GetRfqResponse, GetSettlementsParams, @@ -43,7 +43,7 @@ use crate::{ SettlementsResponse, StructuredTargetResponse, StructuredTargetsResponse, SubaccountBalancesResponse, SubaccountTransfersResponse, TagsByCategoriesResponse, TradesResponse, TransferBetweenSubaccountsRequest, TransferResponse, - UserDataTimestampResponse, + UpdateOrderGroupLimitRequest, UserDataTimestampResponse, }, }; @@ -1085,6 +1085,71 @@ impl KalshiClient { order_groups::reset_order_group(&self.http, order_group_id).await } + /// Trigger an order group. + /// + /// Triggers the order group, cancelling all orders within it as if the + /// contracts limit had been hit. + /// + /// # Arguments + /// + /// * `order_group_id` - The ID of the order group to trigger + /// + /// # Example + /// + /// ```ignore + /// client.trigger_order_group("og_123").await?; + /// println!("Triggered order group"); + /// ``` + pub async fn trigger_order_group(&self, order_group_id: &str) -> Result<()> { + order_groups::trigger_order_group(&self.http, order_group_id).await + } + + /// Update the contracts limit for an order group. + /// + /// Changes the maximum number of contracts that can be matched within + /// this group before auto-cancel is triggered. + /// + /// # Arguments + /// + /// * `order_group_id` - The ID of the order group to update + /// * `request` - The new contracts limit + /// + /// # Example + /// + /// ```ignore + /// use kalshi_trade_rs::UpdateOrderGroupLimitRequest; + /// + /// let request = UpdateOrderGroupLimitRequest::new(200); + /// client.update_order_group_limit("og_123", request).await?; + /// println!("Updated order group limit"); + /// ``` + pub async fn update_order_group_limit( + &self, + order_group_id: &str, + request: UpdateOrderGroupLimitRequest, + ) -> Result<()> { + order_groups::update_order_group_limit(&self.http, order_group_id, request).await + } + + // ========================================================================= + // Account API + // ========================================================================= + + /// Get API tier and rate limits. + /// + /// Returns the user's API tier and associated rate limits. + /// + /// # Example + /// + /// ```ignore + /// let limits = client.get_api_limits().await?; + /// println!("Tier: {}", limits.usage_tier); + /// println!("Read limit: {}", limits.read_limit); + /// ``` + pub async fn get_api_limits(&self) -> Result { + account::get_api_limits(&self.http).await + } + // ========================================================================= // Candlesticks API // ========================================================================= diff --git a/src/client/http.rs b/src/client/http.rs index 1c9dfa2..92ea4fa 100644 --- a/src/client/http.rs +++ b/src/client/http.rs @@ -191,6 +191,17 @@ impl HttpClient { response.json::().await.map_err(Error::Http) } + /// Make a PUT request with a JSON body, expecting no response body. + /// + /// # Arguments + /// * `path` - The API path + /// * `body` - The request body to serialize as JSON + pub async fn put_no_response(&self, path: &str, body: &B) -> Result<()> { + let request = self.build_request(Method::PUT, path)?.json(body); + self.execute(request).await?; + Ok(()) + } + /// Make a PUT request with an empty body and deserialize the response. /// /// # Arguments diff --git a/src/lib.rs b/src/lib.rs index 78fb559..c895056 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,8 +53,8 @@ pub use client::{Environment, HttpClient, KalshiClient}; pub use error::{DisconnectReason, Error, MAX_BATCH_SIZE, Result}; pub use models::{ AcceptQuoteRequest, Action, AmendOrderRequest, AmendOrderResponse, Announcement, - AnnouncementStatus, AnnouncementType, ApiKey, ApiKeysResponse, BalanceResponse, - BatchCancelOrderResult, BatchCancelOrdersRequest, BatchCancelOrdersResponse, + AnnouncementStatus, AnnouncementType, ApiKey, ApiKeysResponse, ApiTierLimitsResponse, + BalanceResponse, BatchCancelOrderResult, BatchCancelOrdersRequest, BatchCancelOrdersResponse, BatchCandlesticksResponse, BatchCreateOrdersRequest, BatchCreateOrdersResponse, BatchLiveDataResponse, BatchOrderError, BatchOrderResult, CancelOrderResponse, Candlestick, CandlestickPeriod, CandlesticksResponse, CommunicationsIdResponse, CompetitionFilter, @@ -81,8 +81,8 @@ pub use models::{ Series, SeriesFeeChange, SeriesListResponse, SeriesResponse, Settlement, SettlementStatus, SettlementsResponse, Side, SportFilter, StandardHoursPeriod, StrikeType, StructuredTarget, StructuredTargetResponse, StructuredTargetsResponse, TagsByCategoriesResponse, TakerSide, - TimeInForce, Trade, TradesResponse, TradingSession, UserDataTimestampResponse, - cents_to_dollars, + TimeInForce, Trade, TradesResponse, TradingSession, UpdateOrderGroupLimitRequest, + UserDataTimestampResponse, cents_to_dollars, }; // Re-export WebSocket types for convenience diff --git a/src/models.rs b/src/models.rs index caf125f..d1e4061 100644 --- a/src/models.rs +++ b/src/models.rs @@ -3,6 +3,7 @@ //! All monetary values are in cents unless noted otherwise. //! Fields ending in `_dollars` are fixed-point dollar strings. +mod account; mod api_key; mod balance; mod common; @@ -27,6 +28,7 @@ mod structured_target; mod subaccount; // Re-export all public types +pub use account::ApiTierLimitsResponse; pub use api_key::{ ApiKey, ApiKeysResponse, CreateApiKeyRequest, CreateApiKeyResponse, DeleteApiKeyResponse, GenerateApiKeyRequest, GenerateApiKeyResponse, @@ -81,7 +83,7 @@ pub use order::{ }; pub use order_group::{ CreateOrderGroupRequest, CreateOrderGroupResponse, GetOrderGroupResponse, GetOrderGroupsParams, - OrderGroupSummary, OrderGroupsResponse, + OrderGroupSummary, OrderGroupsResponse, UpdateOrderGroupLimitRequest, }; pub use position::{EventPosition, GetPositionsParams, MarketPosition, PositionsResponse}; pub use search::{ diff --git a/src/models/account.rs b/src/models/account.rs new file mode 100644 index 0000000..f6cad95 --- /dev/null +++ b/src/models/account.rs @@ -0,0 +1,16 @@ +//! Account models and response types. + +use serde::{Deserialize, Serialize}; + +/// Response from GET /account/limits. +/// +/// Contains information about the user's API tier and rate limits. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiTierLimitsResponse { + /// The user's API usage tier (e.g., "standard", "premier"). + pub usage_tier: String, + /// Maximum read requests per second. + pub read_limit: i64, + /// Maximum write requests per second. + pub write_limit: i64, +} diff --git a/src/models/order_group.rs b/src/models/order_group.rs index 900f60b..87107a3 100644 --- a/src/models/order_group.rs +++ b/src/models/order_group.rs @@ -12,11 +12,17 @@ use super::query::QueryBuilder; pub struct CreateOrderGroupRequest { /// The maximum number of contracts that can be matched within this group. /// When this limit is hit, all orders in the group are cancelled. - pub contracts_limit: i64, + /// Provide `contracts_limit` or `contracts_limit_fp` (or both — if both + /// are provided they must agree). + #[serde(skip_serializing_if = "Option::is_none")] + pub contracts_limit: Option, + /// The maximum number of contracts (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub contracts_limit_fp: Option, } impl CreateOrderGroupRequest { - /// Create a new order group request. + /// Create a new order group request from an integer count. /// /// # Arguments /// @@ -40,7 +46,19 @@ impl CreateOrderGroupRequest { if contracts_limit < 1 { return Err(crate::error::Error::InvalidContractsLimit(contracts_limit)); } - Ok(Self { contracts_limit }) + Ok(Self { + contracts_limit: Some(contracts_limit), + contracts_limit_fp: None, + }) + } + + /// Create a new order group request from a fixed-point string. + #[must_use] + pub fn from_fp(contracts_limit_fp: impl Into) -> Self { + Self { + contracts_limit: None, + contracts_limit_fp: Some(contracts_limit_fp.into()), + } } } @@ -51,6 +69,61 @@ pub struct CreateOrderGroupResponse { pub order_group_id: String, } +/// Request body for PUT /portfolio/order_groups/{order_group_id}/limit. +/// +/// Provide `contracts_limit` or `contracts_limit_fp` (or both — if both are +/// provided they must agree). The limit is a rolling 15-second window. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateOrderGroupLimitRequest { + /// The new maximum number of contracts for this group. + #[serde(skip_serializing_if = "Option::is_none")] + pub contracts_limit: Option, + /// The new maximum number of contracts (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub contracts_limit_fp: Option, +} + +impl UpdateOrderGroupLimitRequest { + /// Create a new update order group limit request from an integer count. + /// + /// # Arguments + /// + /// * `contracts_limit` - New maximum contracts before auto-cancel (must be >= 1) + /// + /// # Panics + /// + /// Panics if `contracts_limit` is less than 1. + /// Use [`try_new`](Self::try_new) for fallible construction. + #[must_use] + pub fn new(contracts_limit: i64) -> Self { + Self::try_new(contracts_limit).expect("invalid contracts limit") + } + + /// Create a new update order group limit request with validation. + /// + /// # Errors + /// + /// Returns an error if `contracts_limit` is less than 1. + pub fn try_new(contracts_limit: i64) -> crate::error::Result { + if contracts_limit < 1 { + return Err(crate::error::Error::InvalidContractsLimit(contracts_limit)); + } + Ok(Self { + contracts_limit: Some(contracts_limit), + contracts_limit_fp: None, + }) + } + + /// Create a new update order group limit request from a fixed-point string. + #[must_use] + pub fn from_fp(contracts_limit_fp: impl Into) -> Self { + Self { + contracts_limit: None, + contracts_limit_fp: Some(contracts_limit_fp.into()), + } + } +} + /// Response from GET /portfolio/order_groups/{order_group_id}. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetOrderGroupResponse { @@ -58,6 +131,12 @@ pub struct GetOrderGroupResponse { pub is_auto_cancel_enabled: bool, /// List of order IDs that belong to this order group. pub orders: Vec, + /// Contracts limit for this order group. + #[serde(default)] + pub contracts_limit: Option, + /// Contracts limit (fixed-point decimal string). + #[serde(default)] + pub contracts_limit_fp: Option, } /// An order group summary (used in list response). @@ -67,6 +146,12 @@ pub struct OrderGroupSummary { pub id: String, /// Whether auto-cancel is enabled for this order group. pub is_auto_cancel_enabled: bool, + /// Contracts limit for this order group. + #[serde(default)] + pub contracts_limit: Option, + /// Contracts limit (fixed-point decimal string). + #[serde(default)] + pub contracts_limit_fp: Option, } /// Query parameters for GET /portfolio/order_groups. @@ -122,12 +207,22 @@ mod tests { #[test] fn test_create_order_group_request() { let req = CreateOrderGroupRequest::new(100); - assert_eq!(req.contracts_limit, 100); + assert_eq!(req.contracts_limit, Some(100)); let json = serde_json::to_string(&req).unwrap(); assert_eq!(json, r#"{"contracts_limit":100}"#); } + #[test] + fn test_create_order_group_request_from_fp() { + let req = CreateOrderGroupRequest::from_fp("100.00"); + assert_eq!(req.contracts_limit, None); + assert_eq!(req.contracts_limit_fp.as_deref(), Some("100.00")); + + let json = serde_json::to_string(&req).unwrap(); + assert_eq!(json, r#"{"contracts_limit_fp":"100.00"}"#); + } + #[test] fn test_get_order_groups_params_query_string() { let params = GetOrderGroupsParams::new(); diff --git a/src/orderbook/aggregator.rs b/src/orderbook/aggregator.rs index 18b5bb5..807493c 100644 --- a/src/orderbook/aggregator.rs +++ b/src/orderbook/aggregator.rs @@ -471,6 +471,7 @@ mod tests { delta: 50, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; agg.handle_delta(&delta, Some(1)); @@ -610,6 +611,7 @@ mod tests { delta: 75, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }, Some(1), @@ -621,6 +623,7 @@ mod tests { delta: -200, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }, Some(2), @@ -648,6 +651,7 @@ mod tests { delta: 100, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }, Some(1), diff --git a/src/orderbook/state.rs b/src/orderbook/state.rs index e33d5d5..4d0a1ff 100644 --- a/src/orderbook/state.rs +++ b/src/orderbook/state.rs @@ -236,6 +236,7 @@ mod tests { delta: 100, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; @@ -255,6 +256,7 @@ mod tests { delta: 100, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; state.apply_delta(&delta1); @@ -266,6 +268,7 @@ mod tests { delta: 50, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; let new_qty = state.apply_delta(&delta2); @@ -283,6 +286,7 @@ mod tests { delta: 100, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; state.apply_delta(&delta1); @@ -294,6 +298,7 @@ mod tests { delta: -30, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; let new_qty = state.apply_delta(&delta2); @@ -311,6 +316,7 @@ mod tests { delta: 100, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; state.apply_delta(&delta1); @@ -322,6 +328,7 @@ mod tests { delta: -100, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; let new_qty = state.apply_delta(&delta2); @@ -340,6 +347,7 @@ mod tests { delta: 50, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; state.apply_delta(&delta1); @@ -351,6 +359,7 @@ mod tests { delta: -100, side: Side::Yes, price_dollars: None, + delta_fp: None, client_order_id: None, }; let new_qty = state.apply_delta(&delta2); diff --git a/src/ws.rs b/src/ws.rs index 3f1128d..1d60945 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -173,9 +173,9 @@ pub use client::{KalshiStreamClient, KalshiStreamHandle}; pub use command::{CommunicationsSharding, SubscribeResult, UnsubscribeResult}; pub use message::{ Action, CommunicationData, FillData, MarketLifecycleData, MarketLifecycleEventType, - MarketPositionData, MultivariateLookupData, MveLeg, OrderbookDeltaData, OrderbookSnapshotData, - PriceLevel, QuoteAcceptedData, QuoteData, RfqData, RfqDeletedData, Side, StreamMessage, - StreamUpdate, TickerData, TradeData, + MarketPositionData, MultivariateLookupData, MveLeg, OrderGroupEventType, OrderGroupUpdateData, + OrderbookDeltaData, OrderbookSnapshotData, PriceLevel, QuoteAcceptedData, QuoteData, RfqData, + RfqDeletedData, Side, StreamMessage, StreamUpdate, TickerData, TradeData, }; /// Connection strategy for the WebSocket client. diff --git a/src/ws/README.md b/src/ws/README.md index 499a635..eefb1f6 100644 --- a/src/ws/README.md +++ b/src/ws/README.md @@ -19,9 +19,9 @@ Complete reference for WebSocket streaming supported by this library. | Category | Channels | Coverage | |----------|----------|----------| | Public Channels | 4 | 100% | -| Authenticated Channels | 3 | 100% | +| Authenticated Channels | 4 | 100% | | Other Channels | 1 | 100% | -| **Total** | **8** | **100%** | +| **Total** | **9** | **100%** | --- @@ -60,6 +60,7 @@ These channels require valid API credentials to subscribe. | ✅ | `fill` | `Channel::Fill` | `FillData` | User order fill notifications | | ✅ | `market_positions` | `Channel::MarketPositions` | `MarketPositionData` | Real-time position updates | | ✅ | `communications` | `Channel::Communications` | `CommunicationData` | RFQ and quote notifications | +| ✅ | `order_group_updates` | `Channel::OrderGroupUpdates` | `OrderGroupUpdateData` | Order group lifecycle events | **Source**: `src/ws/channel.rs` @@ -89,6 +90,7 @@ All message types are defined in `src/ws/message.rs`: | `MarketPositionData` | Position update | `user_id`, `market_ticker`, `position`, `position_cost`, `realized_pnl` | | `MarketLifecycleData` | Lifecycle event | `event_type`, `market_ticker`, timestamps, `result` | | `CommunicationData` | RFQ/Quote events | Tagged enum: `RfqCreated`, `RfqDeleted`, `QuoteCreated`, `QuoteAccepted` | +| `OrderGroupUpdateData` | Order group events | `order_group_id`, `event_type`, `contracts_limit_fp` | ### System Messages diff --git a/src/ws/channel.rs b/src/ws/channel.rs index 6f3eb46..5f878b3 100644 --- a/src/ws/channel.rs +++ b/src/ws/channel.rs @@ -21,6 +21,8 @@ pub enum Channel { /// Market state changes and event lifecycle #[serde(rename = "market_lifecycle_v2")] MarketLifecycle, + /// Order group lifecycle events (requires authentication) + OrderGroupUpdates, /// Multivariate collection lookup notifications Multivariate, } @@ -30,7 +32,7 @@ impl Channel { pub fn requires_auth(&self) -> bool { matches!( self, - Self::Fill | Self::MarketPositions | Self::Communications + Self::Fill | Self::MarketPositions | Self::Communications | Self::OrderGroupUpdates ) } @@ -55,6 +57,7 @@ impl Channel { Self::MarketPositions => "market_positions", Self::Communications => "communications", Self::MarketLifecycle => "market_lifecycle_v2", + Self::OrderGroupUpdates => "order_group_updates", Self::Multivariate => "multivariate", } } diff --git a/src/ws/client.rs b/src/ws/client.rs index b189b46..b53ffd1 100644 --- a/src/ws/client.rs +++ b/src/ws/client.rs @@ -1289,6 +1289,8 @@ mod tests { price_dollars: None, yes_bid_dollars: None, no_bid_dollars: None, + volume_fp: None, + open_interest_fp: None, }), }; diff --git a/src/ws/message.rs b/src/ws/message.rs index 4f937d3..4a0b8de 100644 --- a/src/ws/message.rs +++ b/src/ws/message.rs @@ -7,6 +7,29 @@ use crate::ws::Channel; // Re-export common types from models to avoid duplication pub use crate::models::{Action, Side}; +/// Order group event types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum OrderGroupEventType { + Created, + Triggered, + Reset, + Deleted, + LimitUpdated, +} + +/// Order group update data for order group lifecycle events. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderGroupUpdateData { + /// The order group identifier. + pub order_group_id: String, + /// Type of order group event. + pub event_type: OrderGroupEventType, + /// Contracts limit (fixed-point decimal string), present on limit updates. + #[serde(skip_serializing_if = "Option::is_none")] + pub contracts_limit_fp: Option, +} + /// Market lifecycle event types. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -56,6 +79,8 @@ pub enum StreamMessage { MarketLifecycle(MarketLifecycleData), /// RFQ or quote communication. Communication(CommunicationData), + /// Order group update notification. + OrderGroupUpdate(OrderGroupUpdateData), /// Multivariate collection lookup notification. MultivariateLookup(MultivariateLookupData), /// Connection was closed cleanly. @@ -126,6 +151,9 @@ pub struct OrderbookDeltaData { pub delta: i64, /// Side of the orderbook being updated. pub side: Side, + /// Delta (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub delta_fp: Option, /// Price in dollar format. #[serde(skip_serializing_if = "Option::is_none")] pub price_dollars: Option, @@ -164,6 +192,12 @@ pub struct TickerData { /// No bid in dollars. #[serde(skip_serializing_if = "Option::is_none")] pub no_bid_dollars: Option, + /// Volume (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub volume_fp: Option, + /// Open interest (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub open_interest_fp: Option, } /// Trade data for public trade notifications. @@ -181,6 +215,9 @@ pub struct TradeData { pub taker_side: Side, /// Unix timestamp in seconds. pub ts: i64, + /// Count (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub count_fp: Option, /// Yes price formatted in dollars. #[serde(skip_serializing_if = "Option::is_none")] pub yes_price_dollars: Option, @@ -208,6 +245,9 @@ pub struct FillData { pub yes_price_dollars: String, /// Number of contracts filled. pub count: i64, + /// Count (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub count_fp: Option, /// Action type (buy or sell). pub action: Action, /// Unix timestamp in seconds. @@ -218,6 +258,9 @@ pub struct FillData { /// Position after this fill. #[serde(skip_serializing_if = "Option::is_none")] pub post_position: Option, + /// Position after this fill (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub post_position_fp: Option, /// Side that was purchased. #[serde(skip_serializing_if = "Option::is_none")] pub purchased_side: Option, @@ -247,6 +290,12 @@ pub struct MarketPositionData { /// Total volume traded. #[serde(skip_serializing_if = "Option::is_none")] pub volume: Option, + /// Position (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub position_fp: Option, + /// Volume (fixed-point decimal string). + #[serde(skip_serializing_if = "Option::is_none")] + pub volume_fp: Option, } /// Additional metadata included with market creation events. @@ -527,6 +576,9 @@ impl StreamMessage { .map(|data| StreamMessage::Communication(CommunicationData::QuoteCreated(data))), "quote_accepted" => serde_json::from_value::(value) .map(|data| StreamMessage::Communication(CommunicationData::QuoteAccepted(data))), + // Order group update notifications + "order_group_updates" => serde_json::from_value::(value) + .map(StreamMessage::OrderGroupUpdate), // Multivariate lookup notifications "multivariate_lookup" => serde_json::from_value::(value) .map(StreamMessage::MultivariateLookup),