kalshi_rust/portfolio/mod.rs
1//! Portfolio management and trading operations.
2//!
3//! This module provides comprehensive access to the Kalshi trading API, allowing you to
4//! place orders, manage positions, track fills, and monitor your portfolio. All portfolio
5//! operations require authentication.
6//!
7//! # Overview
8//!
9//! The portfolio module is the core of trading on Kalshi. It encompasses:
10//!
11//! - **Order Management**: Create, cancel, amend, and query orders
12//! - **Position Tracking**: Monitor your holdings across markets and events
13//! - **Fill History**: Track executed trades and transaction history
14//! - **Balance Queries**: Check your account balance
15//! - **Batch Operations**: Execute multiple orders or cancellations in a single request
16//! - **Order Groups**: Manage related orders with shared limits
17//!
18//! # Quick Start - Placing an Order
19//!
20//! ```rust,ignore
21//! use kalshi::{Kalshi, TradingEnvironment, Action, Side, OrderType};
22//!
23//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
24//! let kalshi = Kalshi::new(
25//! TradingEnvironment::DemoMode,
26//! "your-key-id",
27//! "path/to/private.pem"
28//! ).await?;
29//!
30//! // Place a limit order to buy 10 YES contracts at 55 cents
31//! let order = kalshi.create_order(
32//! Action::Buy, // Buy or Sell
33//! None, // client_order_id (auto-generated if None)
34//! 10, // count (number of contracts)
35//! Side::Yes, // Yes or No
36//! "HIGHNY-24JAN15-T50".to_string(), // ticker
37//! OrderType::Limit, // Market or Limit
38//! None, // buy_max_cost
39//! None, // expiration_ts
40//! Some(55), // yes_price (cents)
41//! None, // no_price
42//! None, // sell_position_floor
43//! None, // yes_price_dollars
44//! None, // no_price_dollars
45//! None, // time_in_force
46//! None, // post_only
47//! None, // reduce_only
48//! None, // self_trade_prevention_type
49//! None, // order_group_id
50//! None, // cancel_order_on_pause
51//! ).await?;
52//!
53//! println!("Order created: {}", order.order_id);
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! # Key Concepts
59//!
60//! ## Order Types
61//!
62//! - **Limit Orders**: Specify a price; order only executes at that price or better
63//! - **Market Orders**: Execute immediately at the current market price
64//!
65//! ## Order Sides
66//!
67//! - **Yes**: Betting that the event will occur
68//! - **No**: Betting that the event will not occur
69//!
70//! ## Pricing
71//!
72//! Prices can be specified in two ways:
73//! - **Cents**: Integer values (0-100), e.g., `yes_price: Some(55)` means 55 cents
74//! - **Dollars**: String values (0.00-1.00), e.g., `yes_price_dollars: Some("0.55")`
75//!
76//! ## Order Status Lifecycle
77//!
78//! 1. **Pending**: Order submitted but not yet confirmed
79//! 2. **Resting**: Order is active in the orderbook
80//! 3. **Executed**: Order completely filled
81//! 4. **Canceled**: Order canceled before complete execution
82//!
83//! # Common Workflows
84//!
85//! ## Check Your Balance
86//!
87//! ```rust,ignore
88//! # use kalshi::Kalshi;
89//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
90//! let balance_cents = kalshi.get_balance().await?;
91//! println!("Balance: ${:.2}", balance_cents as f64 / 100.0);
92//! # Ok(())
93//! # }
94//! ```
95//!
96//! ## View Your Open Orders
97//!
98//! ```rust,ignore
99//! # use kalshi::{Kalshi, OrderStatus};
100//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
101//! let (cursor, orders) = kalshi.get_orders(
102//! None, // ticker filter
103//! None, // event_ticker filter
104//! None, // min_ts
105//! None, // max_ts
106//! Some(OrderStatus::Resting), // only resting orders
107//! Some(100), // limit
108//! None, // cursor
109//! ).await?;
110//!
111//! for order in orders {
112//! println!("Order {}: {} {} @ {}",
113//! order.order_id,
114//! order.count.unwrap_or(0),
115//! order.ticker,
116//! order.yes_price.unwrap_or(0)
117//! );
118//! }
119//! # Ok(())
120//! # }
121//! ```
122//!
123//! ## Cancel an Order
124//!
125//! ```rust,ignore
126//! # use kalshi::Kalshi;
127//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
128//! let order_id = "some-order-id";
129//! let (canceled_order, reduced_by) = kalshi.cancel_order(order_id).await?;
130//! println!("Canceled {} contracts", reduced_by);
131//! # Ok(())
132//! # }
133//! ```
134//!
135//! ## View Your Positions
136//!
137//! ```rust,ignore
138//! # use kalshi::Kalshi;
139//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
140//! let (cursor, event_positions, market_positions) = kalshi.get_positions(
141//! Some(50), // limit
142//! None, // cursor
143//! None, // settlement_status
144//! None, // ticker
145//! None, // event_ticker
146//! None, // count_filter
147//! ).await?;
148//!
149//! for position in market_positions {
150//! println!("Position: {} contracts in {}",
151//! position.position,
152//! position.ticker
153//! );
154//! }
155//! # Ok(())
156//! # }
157//! ```
158//!
159//! ## Track Your Fills
160//!
161//! ```rust,ignore
162//! # use kalshi::Kalshi;
163//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
164//! let (cursor, fills) = kalshi.get_fills(
165//! None, // ticker
166//! None, // order_id
167//! None, // min_ts
168//! None, // max_ts
169//! Some(20), // limit
170//! None, // cursor
171//! ).await?;
172//!
173//! for fill in fills {
174//! println!("Fill: {} {} contracts on {} at {} cents",
175//! fill.action,
176//! fill.count,
177//! fill.ticker,
178//! fill.yes_price
179//! );
180//! }
181//! # Ok(())
182//! # }
183//! ```
184//!
185//! # Advanced Features
186//!
187//! ## Batch Operations
188//!
189//! Create or cancel multiple orders in a single API call for better performance:
190//!
191//! ```rust,ignore
192//! # use kalshi::{Kalshi, OrderCreationField, Action, Side, OrderType};
193//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
194//! let orders = vec![
195//! OrderCreationField {
196//! action: Action::Buy,
197//! count: 5,
198//! side: Side::Yes,
199//! ticker: "MARKET-1".to_string(),
200//! input_type: OrderType::Limit,
201//! yes_price: Some(50),
202//! // ... other fields
203//! # client_order_id: None,
204//! # buy_max_cost: None,
205//! # expiration_ts: None,
206//! # no_price: None,
207//! # sell_position_floor: None,
208//! # yes_price_dollars: None,
209//! # no_price_dollars: None,
210//! # time_in_force: None,
211//! # post_only: None,
212//! # reduce_only: None,
213//! # self_trade_prevention_type: None,
214//! # order_group_id: None,
215//! # cancel_order_on_pause: None,
216//! },
217//! // ... more orders (up to 20 per batch)
218//! ];
219//!
220//! let results = kalshi.batch_create_order(orders).await?;
221//! for (i, result) in results.iter().enumerate() {
222//! match result {
223//! Ok(order) => println!("Order {}: Created {}", i, order.order_id),
224//! Err(e) => println!("Order {}: Failed - {:?}", i, e),
225//! }
226//! }
227//! # Ok(())
228//! # }
229//! ```
230//!
231//! ## Order Groups
232//!
233//! Manage related orders with shared contract limits:
234//!
235//! ```rust,ignore
236//! # use kalshi::Kalshi;
237//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
238//! // Create an order group with a 100-contract limit
239//! let group = kalshi.create_order_group(100).await?;
240//!
241//! // Orders in this group will share the 100-contract limit
242//! let order = kalshi.create_order(
243//! // ... order parameters ...
244//! # kalshi::Action::Buy,
245//! # None,
246//! # 10,
247//! # kalshi::Side::Yes,
248//! # "TICKER".to_string(),
249//! # kalshi::OrderType::Limit,
250//! # None, None, Some(50), None, None, None, None, None, None, None, None,
251//! Some(group.id.clone()), // order_group_id
252//! None,
253//! ).await?;
254//! # Ok(())
255//! # }
256//! ```
257//!
258//! ## Amend Orders
259//!
260//! Modify price or quantity without canceling and re-creating:
261//!
262//! ```rust,ignore
263//! # use kalshi::{Kalshi, Side, Action};
264//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
265//! let response = kalshi.amend_order(
266//! "order-id",
267//! "TICKER",
268//! Side::Yes,
269//! Action::Buy,
270//! "original-client-id",
271//! "updated-client-id",
272//! Some(60), // new yes_price
273//! None, // no_price
274//! None, // yes_price_dollars
275//! None, // no_price_dollars
276//! Some(15), // new count
277//! ).await?;
278//!
279//! println!("Amended order from {} to {}", response.old_order.order_id, response.order.order_id);
280//! # Ok(())
281//! # }
282//! ```
283//!
284//! # Error Handling
285//!
286//! All portfolio methods return `Result<T, KalshiError>`. Common errors include:
287//!
288//! - **UserInputError**: Invalid parameters (e.g., both yes_price and no_price specified)
289//! - **HttpError**: Network or API errors
290//! - **InternalError**: Unexpected conditions
291//!
292//! # Best Practices
293//!
294//! 1. **Check balance before trading**: Use `get_balance()` to ensure sufficient funds
295//! 2. **Use limit orders**: For better price control (market orders may have unfavorable execution)
296//! 3. **Monitor fills**: Subscribe to fill notifications via WebSocket for real-time updates
297//! 4. **Use batch operations**: When placing/canceling multiple orders for better performance
298//! 5. **Set expiration times**: For orders that should only be active during specific periods
299//!
300//! # See Also
301//!
302//! - [`create_order`](crate::Kalshi::create_order) - Place a new order
303//! - [`get_orders`](crate::Kalshi::get_orders) - Query your orders
304//! - [`cancel_order`](crate::Kalshi::cancel_order) - Cancel an order
305//! - [`get_positions`](crate::Kalshi::get_positions) - View your positions
306//! - [`get_fills`](crate::Kalshi::get_fills) - Track your fills
307
308use super::Kalshi;
309use crate::kalshi_error::*;
310use std::fmt;
311use uuid::Uuid;
312
313use serde::{Deserialize, Deserializer, Serialize};
314
315const PORTFOLIO_PATH: &str = "/portfolio";
316
317impl Kalshi {
318 /// Retrieves the current balance of the authenticated user from the Kalshi exchange.
319 ///
320 /// This method fetches the user's balance, requiring a valid authentication token.
321 /// If the user is not logged in or the token is missing, it returns an error.
322 ///
323 /// # Returns
324 ///
325 /// - `Ok(i64)`: The user's current balance on successful retrieval.
326 /// - `Err(KalshiError)`: An error if the user is not authenticated or if there is an issue with the request.
327 ///
328 /// # Example
329 ///
330 /// ```
331 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
332 /// let balance = kalshi_instance.get_balance().await.unwrap();
333 /// ```
334 ///
335 pub async fn get_balance(&self) -> Result<i64, KalshiError> {
336 let result: BalanceResponse = self
337 .signed_get(&format!("{}/balance", PORTFOLIO_PATH))
338 .await?;
339 Ok(result.balance)
340 }
341
342 /// Retrieves a list of orders from the Kalshi exchange based on specified criteria.
343 ///
344 /// This method fetches multiple orders, allowing for filtering by ticker, event ticker, time range,
345 /// status, and pagination. A valid authentication token is required to access this information.
346 /// If the user is not logged in or the token is missing, it returns an error.
347 ///
348 /// # Arguments
349 ///
350 /// * `ticker` - An optional string to filter orders by market ticker.
351 /// * `event_ticker` - An optional string to filter orders by event ticker.
352 /// * `min_ts` - An optional minimum timestamp for order creation time.
353 /// * `max_ts` - An optional maximum timestamp for order creation time.
354 /// * `status` - An optional string to filter orders by their status.
355 /// * `limit` - An optional integer to limit the number of orders returned.
356 /// * `cursor` - An optional string for pagination cursor.
357 ///
358 /// # Returns
359 ///
360 /// - `Ok((Option<String>, Vec<Order>))`: A tuple containing an optional pagination cursor
361 /// and a vector of `Order` objects on successful retrieval.
362 /// - `Err(KalshiError)`: An error if the user is not authenticated or if there is an issue with the request.
363 ///
364 /// # Example
365 /// Retrieves all possible orders (Will crash, need to limit for a successful request).
366 /// ```
367 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
368 /// let orders = kalshi_instance.get_orders(
369 /// Some("ticker_name"), None, None, None, None, None, None
370 /// ).await.unwrap();
371 /// ```
372 ///
373 #[allow(clippy::too_many_arguments)]
374 pub async fn get_orders(
375 &self,
376 ticker: Option<String>,
377 event_ticker: Option<String>,
378 min_ts: Option<i64>,
379 max_ts: Option<i64>,
380 status: Option<OrderStatus>,
381 limit: Option<i32>,
382 cursor: Option<String>,
383 ) -> Result<(Option<String>, Vec<Order>), KalshiError> {
384 let mut params: Vec<(&str, String)> = Vec::with_capacity(7);
385
386 add_param!(params, "ticker", ticker);
387 add_param!(params, "limit", limit);
388 add_param!(params, "cursor", cursor);
389 add_param!(params, "min_ts", min_ts);
390 add_param!(params, "max_ts", max_ts);
391 add_param!(params, "event_ticker", event_ticker);
392 add_param!(params, "status", status.map(|s| s.to_string()));
393
394 let path = if params.is_empty() {
395 format!("{}/orders", PORTFOLIO_PATH)
396 } else {
397 let query_string = params
398 .iter()
399 .map(|(k, v)| format!("{}={}", k, v))
400 .collect::<Vec<_>>()
401 .join("&");
402 format!("{}/orders?{}", PORTFOLIO_PATH, query_string)
403 };
404
405 let result: MultipleOrderResponse = self.signed_get(&path).await?;
406 Ok((result.cursor, result.orders))
407 }
408
409 /// Retrieves detailed information about a specific order from the Kalshi exchange.
410 ///
411 /// This method fetches data for a single order identified by its order ID. A valid authentication token
412 /// is required to access this information. If the user is not logged in or the token is missing, it returns an error.
413 ///
414 /// # Arguments
415 ///
416 /// * `order_id` - A reference to a string representing the order's unique identifier.
417 ///
418 /// # Returns
419 ///
420 /// - `Ok(Order)`: Detailed information about the specified order on successful retrieval.
421 /// - `Err(KalshiError)`: An error if the user is not authenticated or if there is an issue with the request.
422 ///
423 /// # Example
424 ///
425 /// ```
426 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
427 /// let order_id = "some_order_id";
428 /// let order = kalshi_instance.get_single_order(&order_id).await.unwrap();
429 /// ```
430 ///
431 pub async fn get_single_order(&self, order_id: &String) -> Result<Order, KalshiError> {
432 let path = format!("{}/orders/{}", PORTFOLIO_PATH, order_id);
433 let result: SingleOrderResponse = self.signed_get(&path).await?;
434 Ok(result.order)
435 }
436
437 /// Cancels an existing order on the Kalshi exchange.
438 ///
439 /// This method cancels an order specified by its ID. A valid authentication token is
440 /// required to perform this action. If the user is not logged in or the token is missing,
441 /// it returns an error.
442 ///
443 /// # Arguments
444 ///
445 /// * `order_id` - A string slice referencing the ID of the order to be canceled.
446 ///
447 /// # Returns
448 ///
449 /// - `Ok((Order, i32))`: A tuple containing the updated `Order` object after cancellation
450 /// and an integer indicating the amount by which the order was reduced on successful cancellation.
451 /// - `Err(KalshiError)`: An error if the user is not authenticated or if there is an issue with the request.
452 ///
453 /// # Example
454 ///
455 /// ```
456 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
457 /// let order_id = "some_order_id";
458 /// let (order, reduced_by) = kalshi_instance.cancel_order(order_id).await.unwrap();
459 /// ```
460 ///
461 pub async fn cancel_order(&self, order_id: &str) -> Result<(Order, i32), KalshiError> {
462 let path = format!("{}/orders/{}", PORTFOLIO_PATH, order_id);
463 let result: DeleteOrderResponse = self.signed_delete(&path).await?;
464 Ok((result.order, result.reduced_by))
465 }
466 /// Decreases the size of an existing order on the Kalshi exchange.
467 ///
468 /// **Endpoint:**
469 /// `POST /portfolio/orders/{order_id}/decrease` (v2)
470 ///
471 /// This method allows reducing the size of an order either by specifying the amount to reduce
472 /// (`reduce_by`) or setting a new target size (`reduce_to`). A valid authentication token is
473 /// required for this operation. It's important to provide either `reduce_by` or `reduce_to`,
474 /// but not both at the same time.
475 ///
476 /// # Arguments
477 ///
478 /// * `order_id` - A string slice referencing the ID of the order to be decreased.
479 /// * `reduce_by` - An optional integer specifying how much to reduce the order by.
480 /// * `reduce_to` - An optional integer specifying the new size of the order.
481 ///
482 /// # Returns
483 ///
484 /// - `Ok(Order)`: The updated `Order` object after decreasing the size.
485 /// - `Err(KalshiError)`: An error if the user is not authenticated, if both `reduce_by` and `reduce_to` are provided,
486 /// or if there is an issue with the request.
487 ///
488 /// # Example
489 ///
490 /// ```rust
491 /// // shrink order ABC123 by 5 contracts
492 /// let order = kalshi_instance
493 /// .decrease_order("ABC123", Some(5), None)
494 /// .await?;
495 /// ```
496 ///
497 pub async fn decrease_order(
498 &self,
499 order_id: &str,
500 reduce_by: Option<i32>,
501 reduce_to: Option<i32>,
502 ) -> Result<Order, KalshiError> {
503 match (reduce_by, reduce_to) {
504 (Some(_), Some(_)) => {
505 return Err(KalshiError::UserInputError(
506 "Can only provide reduce_by strict exclusive or reduce_to, can't provide both"
507 .to_string(),
508 ));
509 }
510 (None, None) => {
511 return Err(KalshiError::UserInputError(
512 "Must provide either reduce_by exclusive or reduce_to, can't provide neither"
513 .to_string(),
514 ));
515 }
516 _ => {}
517 }
518
519 let decrease_payload = DecreaseOrderPayload {
520 reduce_by,
521 reduce_to,
522 };
523
524 // v2 portfolio API: POST /orders/{order_id}/decrease
525 let path = format!("{}/orders/{}/decrease", PORTFOLIO_PATH, order_id);
526
527 // response is now { "order": { … }, "reduced_by": int }
528 let result: DecreaseOrderResponse = self.signed_post(&path, &decrease_payload).await?;
529 Ok(result.order)
530 }
531
532 /// Retrieves a list of fills from the Kalshi exchange based on specified criteria.
533 ///
534 /// This method fetches multiple fills, allowing for filtering by ticker, order ID, time range,
535 /// and pagination. A valid authentication token is required to access this information.
536 /// If the user is not logged in or the token is missing, it returns an error.
537 ///
538 /// # Arguments
539 ///
540 /// * `ticker` - An optional string to filter fills by market ticker.
541 /// * `order_id` - An optional string to filter fills by order ID.
542 /// * `min_ts` - An optional minimum timestamp for fill creation time.
543 /// * `max_ts` - An optional maximum timestamp for fill creation time.
544 /// * `limit` - An optional integer to limit the number of fills returned.
545 /// * `cursor` - An optional string for pagination cursor.
546 ///
547 /// # Returns
548 ///
549 /// - `Ok((Option<String>, Vec<Fill>))`: A tuple containing an optional pagination cursor
550 /// and a vector of `Fill` objects on successful retrieval.
551 /// - `Err(KalshiError)`: An error if the user is not authenticated or if there is an issue with the request.
552 ///
553 /// # Example
554 /// Retrieves all filled orders
555 /// ```
556 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
557 /// let fills = kalshi_instance.get_fills(
558 /// Some("ticker_name"), None, None, None, None, None
559 /// ).await.unwrap();
560 /// ```
561 ///
562 pub async fn get_fills(
563 &self,
564 ticker: Option<String>,
565 order_id: Option<String>,
566 min_ts: Option<i64>,
567 max_ts: Option<i64>,
568 limit: Option<i32>,
569 cursor: Option<String>,
570 ) -> Result<(Option<String>, Vec<Fill>), KalshiError> {
571 let mut params: Vec<(&str, String)> = Vec::with_capacity(7);
572
573 add_param!(params, "ticker", ticker);
574 add_param!(params, "limit", limit);
575 add_param!(params, "cursor", cursor);
576 add_param!(params, "min_ts", min_ts);
577 add_param!(params, "max_ts", max_ts);
578 add_param!(params, "order_id", order_id);
579
580 let path = if params.is_empty() {
581 format!("{}/fills", PORTFOLIO_PATH)
582 } else {
583 let query_string = params
584 .iter()
585 .map(|(k, v)| format!("{}={}", k, v))
586 .collect::<Vec<_>>()
587 .join("&");
588 format!("{}/fills?{}", PORTFOLIO_PATH, query_string)
589 };
590
591 let result: MultipleFillsResponse = self.signed_get(&path).await?;
592 Ok((result.cursor, result.fills))
593 }
594
595 /// Retrieves a list of portfolio settlements from the Kalshi exchange.
596 ///
597 /// This method fetches settlements in the user's portfolio, with options for filtering
598 /// by ticker, event ticker, timestamp range, and pagination using limit and cursor.
599 /// A valid authentication token is required to access this information.
600 /// If the user is not logged in or the token is missing, it returns an error.
601 ///
602 /// # Arguments
603 ///
604 /// * `limit` - An optional integer to limit the number of settlements returned.
605 /// * `cursor` - An optional string for pagination cursor.
606 /// * `ticker` - An optional string to filter settlements by market ticker.
607 /// * `event_ticker` - An optional string to filter settlements by event ticker.
608 /// * `min_ts` - An optional minimum timestamp for settlement time.
609 /// * `max_ts` - An optional maximum timestamp for settlement time.
610 ///
611 /// # Returns
612 ///
613 /// - `Ok((Option<String>, Vec<Settlement>))`: A tuple containing an optional pagination cursor
614 /// and a vector of `Settlement` objects on successful retrieval.
615 /// - `Err(KalshiError)`: An error if the user is not authenticated or if there is an issue with the request.
616 ///
617 /// # Example
618 ///
619 /// ```
620 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
621 /// let settlements = kalshi_instance.get_settlements(None, None, None, None, None, None).await.unwrap();
622 /// ```
623 pub async fn get_settlements(
624 &self,
625 limit: Option<i64>,
626 cursor: Option<String>,
627 ticker: Option<String>,
628 event_ticker: Option<String>,
629 min_ts: Option<i64>,
630 max_ts: Option<i64>,
631 ) -> Result<(Option<String>, Vec<Settlement>), KalshiError> {
632 let mut params: Vec<(&str, String)> = Vec::with_capacity(6);
633
634 add_param!(params, "limit", limit);
635 add_param!(params, "cursor", cursor);
636 add_param!(params, "ticker", ticker);
637 add_param!(params, "event_ticker", event_ticker);
638 add_param!(params, "min_ts", min_ts);
639 add_param!(params, "max_ts", max_ts);
640
641 let path = if params.is_empty() {
642 format!("{}/settlements", PORTFOLIO_PATH)
643 } else {
644 let query_string = params
645 .iter()
646 .map(|(k, v)| format!("{}={}", k, v))
647 .collect::<Vec<_>>()
648 .join("&");
649 format!("{}/settlements?{}", PORTFOLIO_PATH, query_string)
650 };
651
652 let result: PortfolioSettlementResponse = self.signed_get(&path).await?;
653 Ok((result.cursor, result.settlements))
654 }
655
656 /// Retrieves the user's positions in events and markets from the Kalshi exchange.
657 ///
658 /// This method fetches the user's positions, providing options for filtering by settlement status,
659 /// specific ticker, and event ticker, as well as pagination using limit and cursor. A valid
660 /// authentication token is required to access this information. If the user is not logged in
661 /// or the token is missing, it returns an error.
662 ///
663 /// # Arguments
664 ///
665 /// * `limit` - An optional integer to limit the number of positions returned.
666 /// * `cursor` - An optional string for pagination cursor.
667 /// * `settlement_status` - An optional string to filter positions by their settlement status.
668 /// * `ticker` - An optional string to filter positions by market ticker.
669 /// * `event_ticker` - An optional string to filter positions by event ticker.
670 /// * `count_filter` - An optional string to filter positions by count type ("position", "total_traded", or both comma-separated).
671 ///
672 /// # Returns
673 ///
674 /// - `Ok((Option<String>, Vec<EventPosition>, Vec<MarketPosition>))`: A tuple containing an optional pagination cursor,
675 /// a vector of `EventPosition` objects, and a vector of `MarketPosition` objects on successful retrieval.
676 /// - `Err(KalshiError)`: An error if the user is not authenticated or if there is an issue with the request.
677 ///
678 /// # Example
679 ///
680 /// ```
681 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
682 /// let positions = kalshi_instance.get_positions(None, None, None, None, None, None).await.unwrap();
683 /// ```
684 ///
685 pub async fn get_positions(
686 &self,
687 limit: Option<i64>,
688 cursor: Option<String>,
689 settlement_status: Option<String>,
690 ticker: Option<String>,
691 event_ticker: Option<String>,
692 count_filter: Option<String>,
693 ) -> Result<(Option<String>, Vec<EventPosition>, Vec<MarketPosition>), KalshiError> {
694 let mut params: Vec<(&str, String)> = Vec::with_capacity(7);
695
696 add_param!(params, "limit", limit);
697 add_param!(params, "cursor", cursor);
698 add_param!(params, "settlement_status", settlement_status);
699 add_param!(params, "ticker", ticker);
700 add_param!(params, "event_ticker", event_ticker);
701 add_param!(params, "count_filter", count_filter);
702
703 let path = if params.is_empty() {
704 format!("{}/positions", PORTFOLIO_PATH)
705 } else {
706 let query_string = params
707 .iter()
708 .map(|(k, v)| format!("{}={}", k, v))
709 .collect::<Vec<_>>()
710 .join("&");
711 format!("{}/positions?{}", PORTFOLIO_PATH, query_string)
712 };
713
714 let result: GetPositionsResponse = self.signed_get(&path).await?;
715
716 Ok((
717 result.cursor,
718 result.event_positions,
719 result.market_positions,
720 ))
721 }
722
723 /// Submits an order to the Kalshi exchange.
724 ///
725 /// This method allows placing an order in the market, requiring details such as action, count, side,
726 /// ticker, order type, and other optional parameters. A valid authentication token is
727 /// required for this operation. Note that for limit orders, either `no_price` or `yes_price` must be provided,
728 /// but not both.
729 ///
730 /// # Arguments
731 ///
732 /// * `action` - The action (buy/sell) of the order.
733 /// * `client_order_id` - An optional client-side identifier for the order.
734 /// * `count` - The number of shares or contracts to trade.
735 /// * `side` - The side (Yes/No) of the order.
736 /// * `ticker` - The market ticker the order is placed in.
737 /// * `input_type` - The type of the order (e.g., market, limit).
738 /// * `buy_max_cost` - The maximum cost for a buy order. Optional.
739 /// * `expiration_ts` - The expiration timestamp for the order. Optional.
740 /// * `no_price` - The price for the 'No' option in a limit order. Optional.
741 /// * `sell_position_floor` - The minimum position size to maintain after selling. Optional.
742 /// * `yes_price` - The price for the 'Yes' option in a limit order. Optional.
743 ///
744 /// # Returns
745 ///
746 /// - `Ok(Order)`: The created `Order` object on successful placement.
747 /// - `Err(KalshiError)`: An error if the user is not authenticated, if both `no_price` and `yes_price` are provided for limit orders,
748 /// or if there is an issue with the request.
749 ///
750 /// # Example
751 ///
752 /// ```
753 /// // Assuming `kalshi_instance` is an already authenticated instance of `Kalshi`
754 /// let action = Action::Buy;
755 /// let side = Side::Yes;
756 /// let order = kalshi_instance.create_order(
757 /// action,
758 /// None,
759 /// 10,
760 /// side,
761 /// "example_ticker",
762 /// OrderType::Limit,
763 /// None,
764 /// None,
765 /// None,
766 /// None,
767 /// Some(100)
768 /// ).await.unwrap();
769 /// ```
770 // TODO: rewrite using generics
771 #[allow(clippy::too_many_arguments)]
772 pub async fn create_order(
773 &self,
774 action: Action,
775 client_order_id: Option<String>,
776 count: i32,
777 side: Side,
778 ticker: String,
779 input_type: OrderType,
780 buy_max_cost: Option<i64>,
781 expiration_ts: Option<i64>,
782 yes_price: Option<i64>,
783 no_price: Option<i64>,
784 sell_position_floor: Option<i32>,
785 yes_price_dollars: Option<String>,
786 no_price_dollars: Option<String>,
787 // NEW PARAMETERS for API parity:
788 time_in_force: Option<TimeInForce>,
789 post_only: Option<bool>,
790 reduce_only: Option<bool>,
791 self_trade_prevention_type: Option<SelfTradePreventionType>,
792 order_group_id: Option<String>,
793 cancel_order_on_pause: Option<bool>,
794 ) -> Result<Order, KalshiError> {
795 if let OrderType::Limit = input_type {
796 // Check if user provided both cent and dollar prices for the same side
797 if yes_price.is_some() && yes_price_dollars.is_some() {
798 return Err(KalshiError::UserInputError(
799 "Cannot provide both yes_price and yes_price_dollars".to_string(),
800 ));
801 }
802 if no_price.is_some() && no_price_dollars.is_some() {
803 return Err(KalshiError::UserInputError(
804 "Cannot provide both no_price and no_price_dollars".to_string(),
805 ));
806 }
807
808 // Check if any price is provided
809 let has_price = yes_price.is_some()
810 || no_price.is_some()
811 || yes_price_dollars.is_some()
812 || no_price_dollars.is_some();
813
814 if !has_price {
815 return Err(KalshiError::UserInputError(
816 "Must provide a price (yes_price, no_price, yes_price_dollars, or no_price_dollars)".to_string(),
817 ));
818 }
819
820 // Check if both yes and no prices are provided
821 let has_yes = yes_price.is_some() || yes_price_dollars.is_some();
822 let has_no = no_price.is_some() || no_price_dollars.is_some();
823 if has_yes && has_no {
824 return Err(KalshiError::UserInputError(
825 "Can only provide yes price or no price, not both".to_string(),
826 ));
827 }
828 }
829
830 let unwrapped_id = match client_order_id {
831 Some(id) => id,
832 _ => String::from(Uuid::new_v4()),
833 };
834
835 let order_payload = CreateOrderPayload {
836 action,
837 client_order_id: unwrapped_id,
838 count,
839 side,
840 ticker,
841 r#type: input_type,
842 buy_max_cost,
843 expiration_ts,
844 yes_price,
845 no_price,
846 sell_position_floor,
847 yes_price_dollars,
848 no_price_dollars,
849 time_in_force,
850 post_only,
851 reduce_only,
852 self_trade_prevention_type,
853 order_group_id,
854 cancel_order_on_pause,
855 };
856
857 let path = format!("{}/orders", PORTFOLIO_PATH);
858 let result: SingleOrderResponse = self.signed_post(&path, &order_payload).await?;
859 Ok(result.order)
860 }
861
862 // -----------------------------------------------------------------
863 // BATCH-CREATE (POST /portfolio/orders/batched)
864 // -----------------------------------------------------------------
865 pub async fn batch_create_order(
866 &self,
867 batch: Vec<OrderCreationField>,
868 ) -> Result<Vec<Result<Order, KalshiError>>, KalshiError> {
869 if batch.is_empty() {
870 return Ok(Vec::new());
871 }
872 if batch.len() > 20 {
873 return Err(KalshiError::UserInputError(
874 "Batch size exceeds 20; split the request".into(),
875 ));
876 }
877
878 // Convert the user-supplied OrderCreationField into raw payloads
879 let orders: Vec<CreateOrderPayload> = batch
880 .into_iter()
881 .map(|field| field.into_payload())
882 .collect();
883
884 let path = format!("{}/orders/batched", PORTFOLIO_PATH);
885 let body = BatchCreateOrderPayload { orders };
886
887 // NB: signed_post already injects auth headers & error mapping
888 let response: BatchCreateOrdersResponse = self.signed_post(&path, &body).await?;
889
890 // Convert the wire format into Vec<Result<...>>
891 let mut out = Vec::with_capacity(response.orders.len());
892 for item in response.orders {
893 match (item.order, item.error) {
894 (Some(order), None) => out.push(Ok(order)),
895 (_, Some(err)) => out.push(Err(KalshiError::UserInputError(
896 err.message.unwrap_or_else(|| "unknown error".into()),
897 ))),
898 _ => out.push(Err(KalshiError::InternalError(
899 "malformed batch-create response".into(),
900 ))),
901 }
902 }
903 Ok(out)
904 }
905
906 // -----------------------------------------------------------------
907 // BATCH-CANCEL (DELETE /portfolio/orders/batched)
908 // -----------------------------------------------------------------
909 pub async fn batch_cancel_order(
910 &self,
911 ids: Vec<String>,
912 ) -> Result<Vec<Result<(Order, i32), KalshiError>>, KalshiError> {
913 if ids.is_empty() {
914 return Ok(Vec::new());
915 }
916 if ids.len() > 20 {
917 return Err(KalshiError::UserInputError(
918 "Batch size exceeds 20; split the request".into(),
919 ));
920 }
921
922 let path = format!("{}/orders/batched", PORTFOLIO_PATH);
923 let body = BatchCancelOrderPayload { ids };
924
925 let response: BatchCancelOrdersResponse =
926 self.signed_delete_with_body(&path, &body).await?;
927
928 let mut out = Vec::with_capacity(response.orders.len());
929 for item in response.orders {
930 match (item.order, item.reduced_by, item.error) {
931 (Some(order), Some(reduced_by), None) => out.push(Ok((order, reduced_by))),
932 (_, _, Some(err)) => out.push(Err(KalshiError::UserInputError(
933 err.message.unwrap_or_else(|| "unknown error".into()),
934 ))),
935 _ => out.push(Err(KalshiError::InternalError(
936 "malformed batch-cancel response".into(),
937 ))),
938 }
939 }
940 Ok(out)
941 }
942
943 /// Retrieves the total value of all resting orders for the authenticated user.
944 ///
945 /// This endpoint is primarily intended for use by FCM members.
946 ///
947 /// # Returns
948 ///
949 /// - `Ok(i64)`: The total resting order value in cents on successful retrieval.
950 /// - `Err(KalshiError)`: An error if there is an issue with the request.
951 ///
952 /// # Example
953 ///
954 /// ```
955 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
956 /// let total_value = kalshi_instance.get_total_resting_order_value().await.unwrap();
957 /// println!("Total resting order value: {} cents", total_value);
958 /// ```
959 ///
960 /// # Note
961 ///
962 /// If you're uncertain about this endpoint, it likely does not apply to you.
963 ///
964 pub async fn get_total_resting_order_value(&self) -> Result<i64, KalshiError> {
965 let path = "/portfolio/summary/total_resting_order_value";
966 let res: TotalRestingOrderValueResponse = self.signed_get(path).await?;
967 Ok(res.total_resting_order_value)
968 }
969
970 /// Retrieves all order groups for the authenticated user.
971 ///
972 /// Order groups allow you to manage multiple related orders together.
973 ///
974 /// # Returns
975 ///
976 /// - `Ok(Vec<OrderGroup>)`: A vector of order groups on successful retrieval.
977 /// - `Err(KalshiError)`: An error if there is an issue with the request.
978 ///
979 /// # Example
980 ///
981 /// ```
982 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
983 /// let order_groups = kalshi_instance.get_order_groups().await.unwrap();
984 /// ```
985 ///
986 pub async fn get_order_groups(&self) -> Result<Vec<OrderGroup>, KalshiError> {
987 let path = "/portfolio/order_groups";
988 let res: OrderGroupsResponse = self.signed_get(path).await?;
989 Ok(res.order_groups)
990 }
991
992 /// Creates a new order group.
993 ///
994 /// Order groups allow you to manage multiple related orders with shared limits.
995 ///
996 /// # Arguments
997 ///
998 /// * `contracts_limit` - The maximum number of contracts allowed across all orders in this group.
999 ///
1000 /// # Returns
1001 ///
1002 /// - `Ok(OrderGroup)`: The created order group on successful creation.
1003 /// - `Err(KalshiError)`: An error if there is an issue with the request.
1004 ///
1005 /// # Example
1006 ///
1007 /// ```
1008 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
1009 /// let order_group = kalshi_instance.create_order_group(100).await.unwrap();
1010 /// ```
1011 ///
1012 pub async fn create_order_group(
1013 &self,
1014 contracts_limit: i32,
1015 ) -> Result<OrderGroup, KalshiError> {
1016 let path = "/portfolio/order_groups/create";
1017 let body = CreateOrderGroupRequest { contracts_limit };
1018 self.signed_post(path, &body).await
1019 }
1020
1021 /// Retrieves a specific order group by ID.
1022 ///
1023 /// # Arguments
1024 ///
1025 /// * `order_group_id` - The UUID of the order group to retrieve.
1026 ///
1027 /// # Returns
1028 ///
1029 /// - `Ok(OrderGroup)`: The order group details on successful retrieval.
1030 /// - `Err(KalshiError)`: An error if there is an issue with the request.
1031 ///
1032 /// # Example
1033 ///
1034 /// ```
1035 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
1036 /// let order_group = kalshi_instance.get_order_group("group-uuid").await.unwrap();
1037 /// ```
1038 ///
1039 pub async fn get_order_group(&self, order_group_id: &str) -> Result<OrderGroup, KalshiError> {
1040 let path = format!("/portfolio/order_groups/{}", order_group_id);
1041 let res: OrderGroupResponse = self.signed_get(&path).await?;
1042 Ok(res.order_group)
1043 }
1044
1045 /// Deletes an order group.
1046 ///
1047 /// This will remove the order group but not cancel the orders within it.
1048 ///
1049 /// # Arguments
1050 ///
1051 /// * `order_group_id` - The UUID of the order group to delete.
1052 ///
1053 /// # Returns
1054 ///
1055 /// - `Ok(())`: Success confirmation.
1056 /// - `Err(KalshiError)`: An error if there is an issue with the request.
1057 ///
1058 /// # Example
1059 ///
1060 /// ```
1061 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
1062 /// kalshi_instance.delete_order_group("group-uuid").await.unwrap();
1063 /// ```
1064 ///
1065 pub async fn delete_order_group(&self, order_group_id: &str) -> Result<(), KalshiError> {
1066 let path = format!("/portfolio/order_groups/{}", order_group_id);
1067 let _res: DeleteOrderGroupResponse = self.signed_delete(&path).await?;
1068 Ok(())
1069 }
1070
1071 /// Resets an order group, canceling all orders within it.
1072 ///
1073 /// # Arguments
1074 ///
1075 /// * `order_group_id` - The UUID of the order group to reset.
1076 ///
1077 /// # Returns
1078 ///
1079 /// - `Ok(OrderGroup)`: The reset order group on successful reset.
1080 /// - `Err(KalshiError)`: An error if there is an issue with the request.
1081 ///
1082 /// # Example
1083 ///
1084 /// ```
1085 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
1086 /// let order_group = kalshi_instance.reset_order_group("group-uuid").await.unwrap();
1087 /// ```
1088 ///
1089 pub async fn reset_order_group(&self, order_group_id: &str) -> Result<OrderGroup, KalshiError> {
1090 let path = format!("/portfolio/order_groups/{}/reset", order_group_id);
1091 self.signed_put(&path, None::<&()>).await
1092 }
1093
1094 /// Retrieves queue positions for multiple orders.
1095 ///
1096 /// This method provides information about where your orders are positioned
1097 /// in the order book queue, helping you understand order priority.
1098 ///
1099 /// # Arguments
1100 ///
1101 /// * `order_ids` - A vector of order IDs to get queue positions for.
1102 ///
1103 /// # Returns
1104 ///
1105 /// - `Ok(Vec<OrderQueuePosition>)`: A vector of queue positions on successful retrieval.
1106 /// - `Err(KalshiError)`: An error if there is an issue with the request.
1107 ///
1108 /// # Example
1109 ///
1110 /// ```
1111 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
1112 /// let order_ids = vec!["order-1".to_string(), "order-2".to_string()];
1113 /// let positions = kalshi_instance.get_queue_positions(order_ids).await.unwrap();
1114 /// ```
1115 ///
1116 pub async fn get_queue_positions(
1117 &self,
1118 order_ids: Vec<String>,
1119 ) -> Result<Vec<OrderQueuePosition>, KalshiError> {
1120 let path = "/portfolio/orders/queue_positions";
1121 let mut params = vec![];
1122
1123 // Add each order_id as a separate query parameter
1124 for id in order_ids {
1125 params.push(("order_ids".to_string(), id));
1126 }
1127
1128 let url = format!("{}{}", self.base_url, path);
1129 let final_url = reqwest::Url::parse_with_params(&url, ¶ms)?;
1130 let res: QueuePositionsResponse = self.client.get(final_url).send().await?.json().await?;
1131 Ok(res.queue_positions)
1132 }
1133
1134 /// Amends an existing order by modifying its price or quantity.
1135 ///
1136 /// This method allows updating an order's price and/or count. At most one price
1137 /// field (yes_price, no_price, yes_price_dollars, or no_price_dollars) can be provided.
1138 ///
1139 /// # Arguments
1140 ///
1141 /// * `order_id` - The order ID to amend.
1142 /// * `ticker` - Market ticker (required for validation).
1143 /// * `side` - Side of the order (yes/no).
1144 /// * `action` - Action of the order (buy/sell).
1145 /// * `client_order_id` - Original client order ID.
1146 /// * `updated_client_order_id` - New client order ID after amendment.
1147 /// * `yes_price` - Optional new yes price in cents.
1148 /// * `no_price` - Optional new no price in cents.
1149 /// * `yes_price_dollars` - Optional new yes price in dollars ("0.5600").
1150 /// * `no_price_dollars` - Optional new no price in dollars ("0.5600").
1151 /// * `count` - Optional new quantity of contracts.
1152 ///
1153 /// # Returns
1154 ///
1155 /// - `Ok(AmendOrderResponse)`: Contains both the old and new order on success.
1156 /// - `Err(KalshiError)`: An error if validation fails or there is an issue with the request.
1157 ///
1158 /// # Example
1159 ///
1160 /// ```
1161 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
1162 /// use kalshi::{Side, Action};
1163 ///
1164 /// let response = kalshi_instance.amend_order(
1165 /// "order-uuid",
1166 /// "TEST-MARKET",
1167 /// Side::Yes,
1168 /// Action::Buy,
1169 /// "original-client-id",
1170 /// "updated-client-id",
1171 /// Some(55), // yes_price
1172 /// None, // no_price
1173 /// None, // yes_price_dollars
1174 /// None, // no_price_dollars
1175 /// Some(10), // count
1176 /// ).await.unwrap();
1177 /// ```
1178 ///
1179 #[allow(clippy::too_many_arguments)]
1180 pub async fn amend_order(
1181 &self,
1182 order_id: &str,
1183 ticker: &str,
1184 side: Side,
1185 action: Action,
1186 client_order_id: &str,
1187 updated_client_order_id: &str,
1188 yes_price: Option<i32>,
1189 no_price: Option<i32>,
1190 yes_price_dollars: Option<String>,
1191 no_price_dollars: Option<String>,
1192 count: Option<i32>,
1193 ) -> Result<AmendOrderResponse, KalshiError> {
1194 // Validate: at most one price field can be provided
1195 let price_count = [
1196 yes_price.is_some(),
1197 no_price.is_some(),
1198 yes_price_dollars.is_some(),
1199 no_price_dollars.is_some(),
1200 ]
1201 .iter()
1202 .filter(|&&x| x)
1203 .count();
1204
1205 if price_count > 1 {
1206 return Err(KalshiError::UserInputError(
1207 "At most one of yes_price, no_price, yes_price_dollars, or no_price_dollars can be provided".to_string(),
1208 ));
1209 }
1210
1211 let path = format!("{}/orders/{}/amend", PORTFOLIO_PATH, order_id);
1212 let body = AmendOrderRequest {
1213 ticker: ticker.to_string(),
1214 side,
1215 action,
1216 client_order_id: client_order_id.to_string(),
1217 updated_client_order_id: updated_client_order_id.to_string(),
1218 yes_price,
1219 no_price,
1220 yes_price_dollars,
1221 no_price_dollars,
1222 count,
1223 };
1224 self.signed_post(&path, &body).await
1225 }
1226
1227 /// Retrieves the queue position for a single order.
1228 ///
1229 /// This method provides information about where a specific order is positioned
1230 /// in the order book queue.
1231 ///
1232 /// # Arguments
1233 ///
1234 /// * `order_id` - The order ID to get queue position for.
1235 ///
1236 /// # Returns
1237 ///
1238 /// - `Ok(OrderQueuePosition)`: The queue position on successful retrieval.
1239 /// - `Err(KalshiError)`: An error if there is an issue with the request.
1240 ///
1241 /// # Example
1242 ///
1243 /// ```
1244 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
1245 /// let position = kalshi_instance.get_order_queue_position("order-uuid").await.unwrap();
1246 /// println!("Order is at position {} in queue", position.queue_position);
1247 /// ```
1248 ///
1249 pub async fn get_order_queue_position(
1250 &self,
1251 order_id: &str,
1252 ) -> Result<OrderQueuePosition, KalshiError> {
1253 let path = format!("/portfolio/orders/{}/queue_position", order_id);
1254 self.signed_get(&path).await
1255 }
1256}
1257
1258// PRIVATE STRUCTS
1259// used in getbalance method
1260#[derive(Debug, Serialize, Deserialize)]
1261struct BalanceResponse {
1262 balance: i64,
1263}
1264
1265#[derive(Debug, Deserialize, Serialize)]
1266struct SingleOrderResponse {
1267 order: Order,
1268}
1269
1270#[derive(Debug, Deserialize, Serialize)]
1271struct MultipleOrderResponse {
1272 orders: Vec<Order>,
1273 #[serde(deserialize_with = "empty_string_is_none")]
1274 cursor: Option<String>,
1275}
1276
1277fn empty_string_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
1278where
1279 D: Deserializer<'de>,
1280{
1281 let s = String::deserialize(deserializer)?;
1282 if s.is_empty() {
1283 Ok(None)
1284 } else {
1285 Ok(Some(s))
1286 }
1287}
1288
1289#[derive(Debug, Deserialize, Serialize)]
1290struct DeleteOrderResponse {
1291 order: Order,
1292 reduced_by: i32,
1293}
1294
1295#[derive(Debug, Deserialize, Serialize)]
1296struct DecreaseOrderResponse {
1297 order: Order,
1298}
1299
1300#[derive(Debug, Deserialize, Serialize)]
1301struct DecreaseOrderPayload {
1302 reduce_by: Option<i32>,
1303 reduce_to: Option<i32>,
1304}
1305
1306#[derive(Debug, Deserialize, Serialize)]
1307struct MultipleFillsResponse {
1308 fills: Vec<Fill>,
1309 cursor: Option<String>,
1310}
1311
1312#[derive(Debug, Deserialize, Serialize)]
1313struct PortfolioSettlementResponse {
1314 cursor: Option<String>,
1315 settlements: Vec<Settlement>,
1316}
1317
1318#[derive(Debug, Deserialize, Serialize)]
1319struct GetPositionsResponse {
1320 cursor: Option<String>,
1321 event_positions: Vec<EventPosition>,
1322 market_positions: Vec<MarketPosition>,
1323}
1324
1325#[derive(Debug, Deserialize, Serialize)]
1326struct CreateOrderPayload {
1327 action: Action,
1328 client_order_id: String,
1329 count: i32,
1330 side: Side,
1331 ticker: String,
1332 r#type: OrderType,
1333 #[serde(skip_serializing_if = "Option::is_none")]
1334 buy_max_cost: Option<i64>,
1335 #[serde(skip_serializing_if = "Option::is_none")]
1336 expiration_ts: Option<i64>,
1337 #[serde(skip_serializing_if = "Option::is_none")]
1338 yes_price: Option<i64>,
1339 #[serde(skip_serializing_if = "Option::is_none")]
1340 no_price: Option<i64>,
1341 #[serde(skip_serializing_if = "Option::is_none")]
1342 sell_position_floor: Option<i32>,
1343 #[serde(skip_serializing_if = "Option::is_none")]
1344 yes_price_dollars: Option<String>,
1345 #[serde(skip_serializing_if = "Option::is_none")]
1346 no_price_dollars: Option<String>,
1347 // NEW FIELDS for API parity:
1348 #[serde(skip_serializing_if = "Option::is_none")]
1349 time_in_force: Option<TimeInForce>,
1350 #[serde(skip_serializing_if = "Option::is_none")]
1351 post_only: Option<bool>,
1352 #[serde(skip_serializing_if = "Option::is_none")]
1353 reduce_only: Option<bool>,
1354 #[serde(skip_serializing_if = "Option::is_none")]
1355 self_trade_prevention_type: Option<SelfTradePreventionType>,
1356 #[serde(skip_serializing_if = "Option::is_none")]
1357 order_group_id: Option<String>,
1358 #[serde(skip_serializing_if = "Option::is_none")]
1359 cancel_order_on_pause: Option<bool>,
1360}
1361
1362// PUBLIC STRUCTS
1363// -------------------------
1364
1365/// Represents an order in the Kalshi exchange.
1366///
1367/// This struct details an individual order, including its identification, status, prices, and various metrics related to its lifecycle.
1368///
1369#[derive(Debug, Deserialize, Serialize)]
1370pub struct Order {
1371 /// Unique identifier for the order.
1372 pub order_id: String,
1373 /// Identifier of the user who placed the order. Optional.
1374 #[serde(default)]
1375 pub user_id: Option<String>,
1376 /// Ticker of the market associated with the order.
1377 pub ticker: String,
1378 /// Current status of the order (e.g., resting, executed).
1379 pub status: OrderStatus,
1380 /// Price of the 'Yes' option in the order (cents). Optional for some responses.
1381 #[serde(default)]
1382 pub yes_price: Option<i32>,
1383 /// Price of the 'No' option in the order (cents). Optional for some responses.
1384 #[serde(default)]
1385 pub no_price: Option<i32>,
1386 /// Count of contracts in the order. Optional for some responses.
1387 #[serde(default)]
1388 pub count: Option<i32>,
1389
1390 /// Timestamp when the order was created. Optional.
1391 #[serde(default)]
1392 pub created_time: Option<String>,
1393 /// Last update time of the order. Optional.
1394 #[serde(default)]
1395 pub last_update_time: Option<String>,
1396 /// Expiration time of the order. Optional (often null).
1397 #[serde(default)]
1398 pub expiration_time: Option<String>,
1399
1400 // === Counts / queue ===
1401 /// Total fills (Kalshi now reports a single `fill_count`).
1402 #[serde(default)]
1403 pub fill_count: Option<i32>,
1404 /// Initial order size.
1405 #[serde(default)]
1406 pub initial_count: Option<i32>,
1407 /// Remaining count of the order. Optional.
1408 #[serde(default)]
1409 pub remaining_count: Option<i32>,
1410 /// Position of the order in the queue. Optional.
1411 #[serde(default)]
1412 pub queue_position: Option<i32>,
1413
1414 // === Legacy/optional counters kept for back-compat (often missing) ===
1415 #[serde(default)]
1416 pub taker_fill_count: Option<i32>,
1417 #[serde(default)]
1418 pub place_count: Option<i32>,
1419 #[serde(default)]
1420 pub decrease_count: Option<i32>,
1421 #[serde(default)]
1422 pub maker_fill_count: Option<i32>,
1423 #[serde(default)]
1424 pub fcc_cancel_count: Option<i32>,
1425 #[serde(default)]
1426 pub close_cancel_count: Option<i32>,
1427
1428 // === Fees / costs ===
1429 /// Fees incurred as a taker (cents).
1430 #[serde(default)]
1431 pub taker_fees: Option<i32>,
1432 /// Taker fees in dollars (string, sometimes null).
1433 #[serde(default)]
1434 pub taker_fees_dollars: Option<String>,
1435
1436 /// Total cost of taker fills (cents).
1437 #[serde(default)]
1438 pub taker_fill_cost: Option<i32>,
1439 /// Taker fill cost in dollars (string).
1440 #[serde(default)]
1441 pub taker_fill_cost_dollars: Option<String>,
1442
1443 /// Maker fees (cents).
1444 #[serde(default)]
1445 pub maker_fees: Option<i32>,
1446 /// Maker fees in dollars (string, sometimes null).
1447 #[serde(default)]
1448 pub maker_fees_dollars: Option<String>,
1449
1450 /// Total cost of maker fills (cents).
1451 #[serde(default)]
1452 pub maker_fill_cost: Option<i32>,
1453 /// Maker fill cost in dollars (string).
1454 #[serde(default)]
1455 pub maker_fill_cost_dollars: Option<String>,
1456
1457 // === Price (dollar string facades Kalshi now sends) ===
1458 #[serde(default)]
1459 pub yes_price_dollars: Option<String>,
1460 #[serde(default)]
1461 pub no_price_dollars: Option<String>,
1462
1463 // === Identifiers ===
1464 pub action: Action,
1465 pub side: Side,
1466 /// Type of the order (e.g., "limit").
1467 #[serde(rename = "type")]
1468 pub r#type: String,
1469 /// Client-side identifier for the order.
1470 pub client_order_id: String,
1471 /// Group identifier for the order (now nullable).
1472 #[serde(default)]
1473 pub order_group_id: Option<String>,
1474
1475 // === Misc newly-seen ===
1476 /// Self-trade prevention type (nullable).
1477 #[serde(default)]
1478 pub self_trade_prevention_type: Option<String>,
1479}
1480
1481/// A completed transaction (a 'fill') in the Kalshi exchange.
1482///
1483/// This struct details a single fill instance, including the action taken, the quantity,
1484/// the involved prices, and the identifiers of the order and trade.
1485///
1486#[derive(Debug, Deserialize, Serialize)]
1487pub struct Fill {
1488 /// The action (buy/sell) of the fill.
1489 pub action: Action,
1490 /// The number of contracts or shares involved in the fill.
1491 pub count: i32,
1492 /// The timestamp when the fill was created.
1493 pub created_time: String,
1494 /// Indicates if the fill was made by a taker.
1495 pub is_taker: bool,
1496 /// The price of the 'No' option in the fill.
1497 pub no_price: i64,
1498 /// The identifier of the associated order.
1499 pub order_id: String,
1500 /// The side (Yes/No) of the fill.
1501 pub side: Side,
1502 /// The ticker of the market in which the fill occurred.
1503 pub ticker: String,
1504 /// The unique identifier of the trade.
1505 pub trade_id: String,
1506 /// The price of the 'Yes' option in the fill.
1507 pub yes_price: i64,
1508}
1509
1510/// A settlement of a market position in the Kalshi exchange.
1511///
1512/// This struct provides details of a market settlement, including the result, quantities,
1513/// costs involved, and the timestamp of settlement.
1514///
1515#[derive(Debug, Deserialize, Serialize)]
1516pub struct Settlement {
1517 /// The result of the market settlement.
1518 pub market_result: String,
1519 /// The quantity involved in the 'No' position.
1520 pub no_count: i64,
1521 /// The total cost associated with the 'No' position.
1522 pub no_total_cost: i64,
1523 /// The revenue generated from the settlement, in cents.
1524 pub revenue: i64,
1525 /// The timestamp when the settlement occurred.
1526 pub settled_time: String,
1527 /// The ticker of the market that was settled.
1528 pub ticker: String,
1529 /// The quantity involved in the 'Yes' position.
1530 pub yes_count: i64,
1531 /// The total cost associated with the 'Yes' position, in cents.
1532 pub yes_total_cost: i64,
1533}
1534
1535/// A user's position in a specific event on the Kalshi exchange.
1536///
1537/// Details the user's exposure, costs, profits, and the number of resting orders related to a particular event.
1538///
1539#[derive(Debug, Deserialize, Serialize)]
1540pub struct EventPosition {
1541 /// The total exposure amount in the event.
1542 pub event_exposure: i64,
1543 /// The ticker of the event.
1544 pub event_ticker: String,
1545 /// The total fees paid in the event in cents.
1546 pub fees_paid: i64,
1547 /// The realized profit or loss in the event in cents.
1548 pub realized_pnl: i64,
1549 /// The count of resting (active but unfilled) orders in the event.
1550 #[serde(default)]
1551 pub resting_order_count: Option<i32>,
1552 /// The total cost incurred in the event in cents.
1553 pub total_cost: i64,
1554}
1555
1556/// A user's position in a specific market on the Kalshi exchange.
1557///
1558/// This struct includes details about the user's market position, including exposure, fees,
1559/// profits, and the number of resting orders.
1560///
1561#[derive(Debug, Deserialize, Serialize)]
1562pub struct MarketPosition {
1563 /// The total fees paid in the market in cents.
1564 pub fees_paid: i64,
1565 /// The total exposure amount in the market.
1566 pub market_exposure: i64,
1567 /// The current position of the user in the market.
1568 pub position: i32,
1569 /// The realized profit or loss in the market in cents.
1570 pub realized_pnl: i64,
1571 /// The count of resting orders in the market.
1572 #[serde(default)]
1573 pub resting_orders_count: Option<i32>,
1574 /// The ticker of the market.
1575 pub ticker: String,
1576 /// The total traded amount in the market.
1577 pub total_traded: i64,
1578}
1579
1580/// Represents the necessary fields for creating an order in the Kalshi exchange.
1581///
1582/// This struct is used to encapsulate all the data needed to create a new order. It includes details about the order type,
1583/// the action being taken (buy/sell), the market ticker, and various other optional parameters that can be specified
1584/// to fine-tune the order according to the user's needs.
1585#[derive(Debug, Deserialize, Serialize)]
1586pub struct OrderCreationField {
1587 /// The action (buy/sell) of the order.
1588 pub action: Action,
1589 /// Client-side identifier for the order. Optional.
1590 pub client_order_id: Option<String>,
1591 /// The number of contracts or shares involved in the order.
1592 pub count: i32,
1593 /// The side (Yes/No) of the order.
1594 pub side: Side,
1595 /// Ticker of the market associated with the order.
1596 pub ticker: String,
1597 /// Type of the order (e.g., market, limit).
1598 pub input_type: OrderType,
1599 /// The maximum cost the buyer is willing to incur for a 'buy' action. Optional.
1600 pub buy_max_cost: Option<i64>,
1601 /// Expiration time of the order. Optional.
1602 pub expiration_ts: Option<i64>,
1603 /// Price of the 'Yes' option in the order (in cents). Optional.
1604 pub yes_price: Option<i64>,
1605 /// Price of the 'No' option in the order (in cents). Optional.
1606 pub no_price: Option<i64>,
1607 /// The minimum position the seller is willing to hold after selling. Optional.
1608 pub sell_position_floor: Option<i32>,
1609 /// Price of the 'Yes' option in dollars (e.g., "0.5000"). Optional.
1610 pub yes_price_dollars: Option<String>,
1611 /// Price of the 'No' option in dollars (e.g., "0.5000"). Optional.
1612 pub no_price_dollars: Option<String>,
1613 // NEW FIELDS for API parity:
1614 /// The time-in-force behavior for the order. Optional.
1615 pub time_in_force: Option<TimeInForce>,
1616 /// If true, the order will only be placed if it can be added to the book (no immediate fills). Optional.
1617 pub post_only: Option<bool>,
1618 /// If true, the order can only reduce an existing position. Optional.
1619 pub reduce_only: Option<bool>,
1620 /// Specifies how self-trades should be prevented. Optional.
1621 pub self_trade_prevention_type: Option<SelfTradePreventionType>,
1622 /// The ID of the order group this order belongs to. Optional.
1623 pub order_group_id: Option<String>,
1624 /// If true, the order will be canceled if the market is paused. Optional.
1625 pub cancel_order_on_pause: Option<bool>,
1626}
1627
1628impl OrderCreationField {
1629 /// Converts the OrderCreationField into a CreateOrderPayload for batch operations.
1630 fn into_payload(self) -> CreateOrderPayload {
1631 CreateOrderPayload {
1632 action: self.action,
1633 client_order_id: self
1634 .client_order_id
1635 .unwrap_or_else(|| Uuid::new_v4().to_string()),
1636 count: self.count,
1637 side: self.side,
1638 ticker: self.ticker,
1639 r#type: self.input_type,
1640 buy_max_cost: self.buy_max_cost,
1641 expiration_ts: self.expiration_ts,
1642 yes_price: self.yes_price,
1643 no_price: self.no_price,
1644 sell_position_floor: self.sell_position_floor,
1645 yes_price_dollars: self.yes_price_dollars,
1646 no_price_dollars: self.no_price_dollars,
1647 time_in_force: self.time_in_force,
1648 post_only: self.post_only,
1649 reduce_only: self.reduce_only,
1650 self_trade_prevention_type: self.self_trade_prevention_type,
1651 order_group_id: self.order_group_id,
1652 cancel_order_on_pause: self.cancel_order_on_pause,
1653 }
1654 }
1655}
1656
1657/// The side of a market position in the Kalshi exchange.
1658///
1659/// This enum is used to indicate whether a market position, order, or trade is associated with the 'Yes' or 'No' outcome of a market event.
1660///
1661#[derive(Debug, Serialize, Deserialize)]
1662#[serde(rename_all = "lowercase")]
1663pub enum Side {
1664 /// Represents a position, order, or trade associated with the 'Yes' outcome of a market event.
1665 Yes,
1666 /// Represents a position, order, or trade associated with the 'No' outcome of a market event.
1667 No,
1668}
1669
1670/// This enum is used to specify the type of action a user wants to take in an order, either buying or selling.
1671///
1672#[derive(Debug, Serialize, Deserialize)]
1673#[serde(rename_all = "lowercase")]
1674pub enum Action {
1675 /// Represents a buy action.
1676 Buy,
1677 /// Represents a sell action.
1678 Sell,
1679}
1680
1681impl fmt::Display for Action {
1682 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1683 match self {
1684 Action::Buy => write!(f, "buy"),
1685 Action::Sell => write!(f, "sell"),
1686 }
1687 }
1688}
1689
1690/// The status of an order in the Kalshi exchange.
1691///
1692/// This enum categorizes an order's lifecycle state, from creation to completion or cancellation.
1693///
1694#[derive(Debug, Serialize, Deserialize)]
1695#[serde(rename_all = "lowercase")]
1696pub enum OrderStatus {
1697 /// The order is active but not yet filled or partially filled and still in the order book.
1698 Resting,
1699 /// The order has been canceled and is no longer active.
1700 Canceled,
1701 /// The order has been fully executed.
1702 Executed,
1703 /// The order has been created and is awaiting further processing.
1704 Pending,
1705}
1706
1707impl fmt::Display for OrderStatus {
1708 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1709 match self {
1710 OrderStatus::Resting => write!(f, "resting"),
1711 OrderStatus::Canceled => write!(f, "cancelled"),
1712 OrderStatus::Executed => write!(f, "executed"),
1713 OrderStatus::Pending => write!(f, "pending"),
1714 }
1715 }
1716}
1717
1718/// Defines the type of an order in the Kalshi exchange.
1719///
1720/// This enum is used to specify the nature of the order, particularly how it interacts with the market.
1721///
1722#[derive(Debug, Serialize, Deserialize)]
1723#[serde(rename_all = "lowercase")]
1724pub enum OrderType {
1725 /// A market order is executed immediately at the current market price.
1726 Market,
1727 /// A limit order is set to be executed at a specific price or better.
1728 Limit,
1729}
1730
1731/// Specifies the time-in-force behavior for an order.
1732///
1733/// This enum determines how long an order remains active before it is executed or canceled.
1734///
1735#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1736#[serde(rename_all = "snake_case")]
1737pub enum TimeInForce {
1738 /// Fill the entire order immediately or cancel it entirely.
1739 FillOrKill,
1740 /// Keep the order active until it is executed or manually canceled.
1741 GoodTillCanceled,
1742 /// Fill as much as possible immediately and cancel the rest.
1743 ImmediateOrCancel,
1744}
1745
1746/// Specifies the self-trade prevention behavior for an order.
1747///
1748/// This enum determines how the exchange handles situations where an order
1749/// would trade against another order from the same user.
1750///
1751#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1752#[serde(rename_all = "snake_case")]
1753pub enum SelfTradePreventionType {
1754 /// Cancel the incoming (taker) order when it would cross with a resting order from the same user.
1755 TakerAtCross,
1756 /// Cancel the resting (maker) order when it would cross with an incoming order from the same user.
1757 Maker,
1758}
1759
1760/// Payload for POST /portfolio/orders/batched
1761#[derive(Debug, Serialize, Deserialize)]
1762struct BatchCreateOrderPayload {
1763 orders: Vec<CreateOrderPayload>,
1764}
1765
1766/// Payload for DELETE /portfolio/orders/batched
1767#[derive(Debug, Serialize, Deserialize)]
1768struct BatchCancelOrderPayload {
1769 ids: Vec<String>,
1770}
1771
1772/// One element in the `orders` array that the batch-create endpoint returns.
1773#[derive(Debug, Serialize, Deserialize)]
1774struct ApiError {
1775 message: Option<String>,
1776}
1777
1778#[derive(Debug, Serialize, Deserialize)]
1779struct BatchCreateOrderResponseItem {
1780 order: Option<Order>,
1781 error: Option<ApiError>,
1782}
1783
1784/// One element in the `orders` array that the batch-cancel endpoint returns.
1785#[derive(Debug, Serialize, Deserialize)]
1786struct BatchCancelOrderResponseItem {
1787 order: Option<Order>,
1788 reduced_by: Option<i32>,
1789 error: Option<ApiError>,
1790}
1791
1792#[derive(Debug, Serialize, Deserialize)]
1793struct BatchCreateOrdersResponse {
1794 orders: Vec<BatchCreateOrderResponseItem>,
1795}
1796
1797#[derive(Debug, Serialize, Deserialize)]
1798struct BatchCancelOrdersResponse {
1799 orders: Vec<BatchCancelOrderResponseItem>,
1800}
1801
1802// -------- New Portfolio Endpoints Structs --------
1803
1804#[derive(Debug, Deserialize)]
1805struct TotalRestingOrderValueResponse {
1806 total_resting_order_value: i64,
1807}
1808
1809#[derive(Debug, Serialize)]
1810struct CreateOrderGroupRequest {
1811 contracts_limit: i32,
1812}
1813
1814#[derive(Debug, Deserialize)]
1815struct OrderGroupsResponse {
1816 order_groups: Vec<OrderGroup>,
1817}
1818
1819#[derive(Debug, Deserialize)]
1820struct OrderGroupResponse {
1821 order_group: OrderGroup,
1822}
1823
1824#[derive(Debug, Deserialize)]
1825struct DeleteOrderGroupResponse {}
1826
1827#[derive(Debug, Deserialize, Serialize)]
1828pub struct OrderGroup {
1829 pub id: String,
1830 pub contracts_limit: i32,
1831 pub total_contracts: Option<i32>,
1832 pub order_ids: Vec<String>,
1833 pub created_time: String,
1834}
1835
1836#[derive(Debug, Deserialize)]
1837struct QueuePositionsResponse {
1838 queue_positions: Vec<OrderQueuePosition>,
1839}
1840
1841#[derive(Debug, Deserialize, Serialize)]
1842pub struct OrderQueuePosition {
1843 pub order_id: String,
1844 pub queue_position: Option<i64>,
1845 pub total_queue_depth: Option<i64>,
1846}
1847
1848#[derive(Debug, Serialize)]
1849struct AmendOrderRequest {
1850 ticker: String,
1851 side: Side,
1852 action: Action,
1853 client_order_id: String,
1854 updated_client_order_id: String,
1855 #[serde(skip_serializing_if = "Option::is_none")]
1856 yes_price: Option<i32>,
1857 #[serde(skip_serializing_if = "Option::is_none")]
1858 no_price: Option<i32>,
1859 #[serde(skip_serializing_if = "Option::is_none")]
1860 yes_price_dollars: Option<String>,
1861 #[serde(skip_serializing_if = "Option::is_none")]
1862 no_price_dollars: Option<String>,
1863 #[serde(skip_serializing_if = "Option::is_none")]
1864 count: Option<i32>,
1865}
1866
1867/// Response from the amend_order endpoint, containing both the old and new order.
1868#[derive(Debug, Deserialize, Serialize)]
1869pub struct AmendOrderResponse {
1870 /// The original order before amendment.
1871 pub old_order: Order,
1872 /// The new order after amendment.
1873 pub order: Order,
1874}
1875
1876#[cfg(test)]
1877mod test {
1878 use crate::portfolio::MultipleOrderResponse;
1879
1880 #[test]
1881 fn test_serialize_multiple_order_response() -> serde_json::Result<()> {
1882 let json = r#"{"orders":[],"cursor":""}"#;
1883 let result = serde_json::from_str::<MultipleOrderResponse>(json)?;
1884 assert!(result.orders.is_empty());
1885 assert!(result.cursor.is_none());
1886 Ok(())
1887 }
1888}