kalshi_rust/market/mod.rs
1//! Market data retrieval and analysis.
2//!
3//! This module provides comprehensive access to Kalshi market data, enabling you to
4//! query markets, retrieve orderbooks, track trades, analyze price history, and explore
5//! series and events. Market data is mostly public and does not require authentication
6//! (with some exceptions for authenticated user-specific queries).
7//!
8//! # Overview
9//!
10//! The market module encompasses several data categories:
11//!
12//! - **Markets**: Individual binary prediction markets (Yes/No outcomes)
13//! - **Series**: Collections of related events (e.g., "HIGHNY" for NYC temperature)
14//! - **Events**: Specific prediction events containing one or more markets
15//! - **Orderbooks**: Current bid/ask levels and market depth
16//! - **Trades**: Historical trade executions
17//! - **Candlesticks**: OHLC price data for technical analysis
18//!
19//! # Quick Start - Getting Market Data
20//!
21//! ```rust,ignore
22//! use kalshi::{Kalshi, TradingEnvironment};
23//!
24//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
25//! let kalshi = Kalshi::new(
26//! TradingEnvironment::DemoMode,
27//! "your-key-id",
28//! "path/to/private.pem"
29//! ).await?;
30//!
31//! // Get a specific market
32//! let market = kalshi.get_market("HIGHNY-24JAN15-T50").await?;
33//! println!("Market: {}", market.title);
34//! println!("Yes bid: {} | Yes ask: {}", market.yes_bid, market.yes_ask);
35//! println!("Volume: {} | Open interest: {}", market.volume, market.open_interest);
36//!
37//! // Get the current orderbook
38//! let orderbook = kalshi.get_orderbook("HIGHNY-24JAN15-T50", Some(5)).await?;
39//! println!("Orderbook depth: {:?}", orderbook.yes);
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! # Key Concepts
45//!
46//! ## Market Structure
47//!
48//! Each market represents a binary outcome (Yes/No) on a specific question:
49//! - **Ticker**: Unique identifier (e.g., "HIGHNY-24JAN15-T50")
50//! - **Event Ticker**: Parent event (e.g., "HIGHNY-24JAN15")
51//! - **Series Ticker**: Series family (e.g., "HIGHNY")
52//! - **Title**: Human-readable question
53//! - **Status**: open, closed, or settled
54//!
55//! ## Pricing
56//!
57//! Market prices are displayed in cents (0-100):
58//! - **yes_bid**: Highest price someone is willing to pay for YES
59//! - **yes_ask**: Lowest price someone is willing to sell YES for
60//! - **no_bid/no_ask**: Equivalent for NO contracts
61//! - **last_price**: Most recent trade execution price
62//!
63//! ## Orderbook
64//!
65//! The orderbook shows all active bids and asks at various price levels:
66//! - Each level shows [price, quantity]
67//! - **yes** side: Bids and asks for YES contracts
68//! - **no** side: Bids and asks for NO contracts
69//!
70//! # Common Workflows
71//!
72//! ## Finding Markets
73//!
74//! ```rust,ignore
75//! # use kalshi::Kalshi;
76//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
77//! // Search for open markets in a specific event
78//! let (cursor, markets) = kalshi.get_markets(
79//! Some(20), // limit
80//! None, // cursor
81//! Some("HIGHNY-24JAN15".to_string()), // event_ticker
82//! None, // series_ticker
83//! Some("open".to_string()), // status
84//! None, // tickers
85//! None, None, None, None, None, None, None, // timestamps & filters
86//! ).await?;
87//!
88//! for market in markets {
89//! println!("{}: {} (volume: {})", market.ticker, market.title, market.volume);
90//! }
91//! # Ok(())
92//! # }
93//! ```
94//!
95//! ## Analyzing the Orderbook
96//!
97//! ```rust,ignore
98//! # use kalshi::Kalshi;
99//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
100//! let orderbook = kalshi.get_orderbook("HIGHNY-24JAN15-T50", Some(10)).await?;
101//!
102//! // Check YES side liquidity
103//! if let Some(yes_levels) = &orderbook.yes {
104//! println!("YES orderbook:");
105//! for level in yes_levels {
106//! if level.len() >= 2 {
107//! println!(" Price: {} | Quantity: {}", level[0], level[1]);
108//! }
109//! }
110//! }
111//!
112//! // Dollar prices are also available
113//! for (price, qty) in &orderbook.yes_dollars {
114//! println!(" ${:.4} | {} contracts", price, qty);
115//! }
116//! # Ok(())
117//! # }
118//! ```
119//!
120//! ## Viewing Recent Trades
121//!
122//! ```rust,ignore
123//! # use kalshi::Kalshi;
124//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
125//! let (cursor, trades) = kalshi.get_trades(
126//! Some(50), // limit
127//! None, // cursor
128//! Some("HIGHNY-24JAN15-T50".to_string()), // ticker
129//! None, // min_ts
130//! None, // max_ts
131//! ).await?;
132//!
133//! for trade in trades {
134//! println!("Trade: {} contracts @ {} ({})",
135//! trade.count,
136//! trade.yes_price,
137//! trade.created_time
138//! );
139//! }
140//! # Ok(())
141//! # }
142//! ```
143//!
144//! ## Getting Historical Price Data
145//!
146//! ```rust,ignore
147//! # use kalshi::Kalshi;
148//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
149//! // Get 1-hour candlesticks for the past 24 hours
150//! let now = chrono::Utc::now().timestamp();
151//! let day_ago = now - 86400;
152//!
153//! let candlesticks = kalshi.get_market_candlesticks(
154//! "HIGHNY-24JAN15-T50",
155//! "HIGHNY",
156//! Some(day_ago),
157//! Some(now),
158//! Some(60), // 60-minute intervals
159//! ).await?;
160//!
161//! for candle in candlesticks {
162//! println!("{}: O:{} H:{} L:{} C:{} V:{}",
163//! candle.start_ts,
164//! candle.yes_open,
165//! candle.yes_high,
166//! candle.yes_low,
167//! candle.yes_close,
168//! candle.volume
169//! );
170//! }
171//! # Ok(())
172//! # }
173//! ```
174//!
175//! ## Exploring Series and Events
176//!
177//! ```rust,ignore
178//! # use kalshi::Kalshi;
179//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
180//! // Get a series and its events
181//! let series = kalshi.get_series("HIGHNY").await?;
182//! println!("Series: {}", series.title.as_ref().unwrap_or(&"Unknown".to_string()));
183//!
184//! // Get events in the series
185//! let (cursor, events) = kalshi.get_events(
186//! Some(10), // limit
187//! None, // cursor
188//! Some("open".to_string()), // status
189//! Some("HIGHNY".to_string()), // series_ticker
190//! Some(true), // with_nested_markets
191//! None, None, // with_milestones, min_close_ts
192//! ).await?;
193//!
194//! for event in events {
195//! println!("Event: {} - {}", event.event_ticker, event.title);
196//! if let Some(markets) = &event.markets {
197//! println!(" Contains {} markets", markets.len());
198//! }
199//! }
200//! # Ok(())
201//! # }
202//! ```
203//!
204//! # Advanced Features
205//!
206//! ## Batch Candlestick Retrieval
207//!
208//! Fetch candlestick data for multiple markets simultaneously for better performance:
209//!
210//! ```rust,ignore
211//! # use kalshi::Kalshi;
212//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
213//! let tickers = vec![
214//! "MARKET-1".to_string(),
215//! "MARKET-2".to_string(),
216//! "MARKET-3".to_string(),
217//! ];
218//!
219//! let now = chrono::Utc::now().timestamp();
220//! let hour_ago = now - 3600;
221//!
222//! let results = kalshi.batch_get_market_candlesticks(
223//! tickers,
224//! hour_ago,
225//! now,
226//! 60, // 60-minute interval
227//! Some(true), // include_latest_before_start
228//! ).await?;
229//!
230//! for market_candles in results {
231//! println!("Market {}: {} candlesticks",
232//! market_candles.ticker,
233//! market_candles.candlesticks.len()
234//! );
235//! }
236//! # Ok(())
237//! # }
238//! ```
239//!
240//! ## Filtering Markets with MVE
241//!
242//! Filter multivariate event (MVE) markets:
243//!
244//! ```rust,ignore
245//! # use kalshi::{Kalshi, MveFilter};
246//! # async fn example(kalshi: &Kalshi) -> Result<(), Box<dyn std::error::Error>> {
247//! // Only get MVE markets
248//! let (cursor, markets) = kalshi.get_markets(
249//! Some(50),
250//! None, None, None, None, None,
251//! None, None, None, None, None, None,
252//! Some(MveFilter::Only), // Only MVE markets
253//! ).await?;
254//! # Ok(())
255//! # }
256//! ```
257//!
258//! # Data Structures
259//!
260//! ## Market
261//!
262//! The [`Market`] struct contains comprehensive information about a prediction market:
263//! - Market identification (ticker, event_ticker, series_ticker)
264//! - Status and lifecycle (status, open_time, close_time, expiration_time)
265//! - Current prices (yes_bid, yes_ask, no_bid, no_ask, last_price)
266//! - Market activity (volume, volume_24h, open_interest, liquidity)
267//! - Settlement (result, settlement_value)
268//!
269//! ## Orderbook
270//!
271//! The [`Orderbook`] struct represents the current market depth:
272//! - `yes` / `no`: Price levels in cents as `Vec<Vec<i32>>`
273//! - `yes_dollars` / `no_dollars`: Price levels in dollars as `Vec<(f32, i32)>`
274//!
275//! ## Candle
276//!
277//! The [`Candle`] struct provides OHLC data for price analysis:
278//! - Time range (start_ts, end_ts)
279//! - YES prices (yes_open, yes_high, yes_low, yes_close)
280//! - NO prices (no_open, no_high, no_low, no_close)
281//! - Volume and open_interest for the period
282//!
283//! # Best Practices
284//!
285//! 1. **Use filters**: Narrow down market queries with status, event_ticker, or series_ticker
286//! 2. **Limit results**: Always specify a reasonable limit to avoid overwhelming responses
287//! 3. **Batch operations**: Use batch endpoints when fetching data for multiple markets
288//! 4. **WebSocket for real-time**: For live data, use WebSocket subscriptions instead of polling
289//! 5. **Candlestick intervals**: Choose appropriate intervals (1, 60, or 1440 minutes)
290//!
291//! # See Also
292//!
293//! - [`get_market`](crate::Kalshi::get_market) - Retrieve a specific market
294//! - [`get_markets`](crate::Kalshi::get_markets) - Query multiple markets
295//! - [`get_orderbook`](crate::Kalshi::get_orderbook) - Get current orderbook
296//! - [`get_trades`](crate::Kalshi::get_trades) - Retrieve trade history
297//! - [`get_market_candlesticks`](crate::Kalshi::get_market_candlesticks) - Historical price data
298
299use super::Kalshi;
300use crate::kalshi_error::*;
301use serde::{Deserialize, Deserializer, Serialize};
302use std::collections::HashMap;
303
304impl Kalshi {
305 /// Retrieves a list of markets from the Kalshi exchange based on specified criteria.
306 ///
307 /// This method fetches multiple markets, allowing for filtering by event ticker, series ticker,
308 /// status, tickers, time range, and pagination. Markets represent the individual trading
309 /// instruments within events.
310 ///
311 /// # Arguments
312 ///
313 /// * `limit` - An optional integer to limit the number of markets returned.
314 /// * `cursor` - An optional string for pagination cursor.
315 /// * `event_ticker` - An optional string to filter markets by event ticker.
316 /// * `series_ticker` - An optional string to filter markets by series ticker.
317 /// * `status` - An optional string to filter markets by their status.
318 /// * `tickers` - An optional string to filter markets by specific tickers.
319 /// * `min_close_ts` - An optional minimum timestamp for market close time.
320 /// * `max_close_ts` - An optional maximum timestamp for market close time.
321 /// * `min_created_ts` - An optional minimum timestamp for market creation time.
322 /// * `max_created_ts` - An optional maximum timestamp for market creation time.
323 /// * `min_settled_ts` - An optional minimum timestamp for market settlement time.
324 /// * `max_settled_ts` - An optional maximum timestamp for market settlement time.
325 /// * `mve_filter` - An optional filter for multivariate events (Only or Exclude).
326 ///
327 /// # Returns
328 ///
329 /// - `Ok((Option<String>, Vec<Market>))`: A tuple containing an optional pagination cursor
330 /// and a vector of `Market` objects on successful retrieval.
331 /// - `Err(KalshiError)`: An error if there is an issue with the request.
332 ///
333 /// # Example
334 ///
335 /// ```
336 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
337 /// let (cursor, markets) = kalshi_instance.get_markets(
338 /// Some(10), None, Some("SOME-EVENT".to_string()), None,
339 /// Some("open".to_string()), None, None, None, None, None, None, None, None
340 /// ).await.unwrap();
341 /// ```
342 ///
343 #[allow(clippy::too_many_arguments)]
344 pub async fn get_markets(
345 &self,
346 limit: Option<i64>,
347 cursor: Option<String>,
348 event_ticker: Option<String>,
349 series_ticker: Option<String>,
350 status: Option<String>,
351 tickers: Option<String>,
352 min_close_ts: Option<i64>,
353 max_close_ts: Option<i64>,
354 min_created_ts: Option<i64>,
355 max_created_ts: Option<i64>,
356 min_settled_ts: Option<i64>,
357 max_settled_ts: Option<i64>,
358 mve_filter: Option<MveFilter>,
359 ) -> Result<(Option<String>, Vec<Market>), KalshiError> {
360 let url = format!("{}/markets", self.base_url);
361 let mut p = vec![];
362 add_param!(p, "limit", limit);
363 add_param!(p, "cursor", cursor);
364 add_param!(p, "event_ticker", event_ticker);
365 add_param!(p, "series_ticker", series_ticker);
366 add_param!(p, "status", status);
367 add_param!(p, "tickers", tickers);
368 add_param!(p, "min_close_ts", min_close_ts);
369 add_param!(p, "max_close_ts", max_close_ts);
370 add_param!(p, "min_created_ts", min_created_ts);
371 add_param!(p, "max_created_ts", max_created_ts);
372 add_param!(p, "min_settled_ts", min_settled_ts);
373 add_param!(p, "max_settled_ts", max_settled_ts);
374 add_param!(p, "mve_filter", mve_filter);
375
376 let res: MarketListResponse = self
377 .client
378 .get(reqwest::Url::parse_with_params(&url, &p)?)
379 .send()
380 .await?
381 .json()
382 .await?;
383 Ok((res.cursor, res.markets))
384 }
385
386 /// Retrieves detailed information about a specific market from the Kalshi exchange.
387 ///
388 /// This method fetches data for a single market identified by its ticker.
389 /// The market represents a specific trading instrument within an event.
390 ///
391 /// # Arguments
392 ///
393 /// * `ticker` - A string slice referencing the market's unique ticker identifier.
394 ///
395 /// # Returns
396 ///
397 /// - `Ok(Market)`: Detailed information about the specified market on successful retrieval.
398 /// - `Err(KalshiError)`: An error if there is an issue with the request.
399 ///
400 /// # Example
401 ///
402 /// ```
403 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
404 /// let ticker = "SOME-MARKET-2024";
405 /// let market = kalshi_instance.get_market(ticker).await.unwrap();
406 /// ```
407 ///
408 pub async fn get_market(&self, ticker: &str) -> Result<Market, KalshiError> {
409 let url = format!("{}/markets/{}", self.base_url, ticker);
410 let res: SingleMarketResponse = self.client.get(url).send().await?.json().await?;
411 Ok(res.market)
412 }
413
414 /// Retrieves the orderbook for a specific market from the Kalshi exchange.
415 ///
416 /// This method fetches the current orderbook data for a market, showing the current
417 /// bid and ask orders for both Yes and No sides of the market.
418 ///
419 /// # Arguments
420 ///
421 /// * `ticker` - A string slice referencing the market's unique ticker identifier.
422 /// * `depth` - Optional depth parameter to limit the number of price levels returned.
423 ///
424 /// # Returns
425 ///
426 /// - `Ok(Orderbook)`: The current orderbook data for the specified market on successful retrieval.
427 /// - `Err(KalshiError)`: An error if there is an issue with the request.
428 ///
429 /// # Example
430 ///
431 /// ```
432 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
433 /// let ticker = "SOME-MARKET-2024";
434 /// let orderbook = kalshi_instance.get_orderbook(ticker, Some(10)).await.unwrap();
435 /// ```
436 ///
437 pub async fn get_orderbook(
438 &self,
439 ticker: &str,
440 depth: Option<i32>,
441 ) -> Result<Orderbook, KalshiError> {
442 let mut url = format!("{}/markets/{}/orderbook", self.base_url, ticker);
443
444 if let Some(d) = depth {
445 url.push_str(&format!("?depth={}", d));
446 }
447
448 let response = self.client.get(&url).send().await?;
449 let response_text = response.text().await?;
450
451 // Try to parse as JSON first to see what we're getting
452 let json_value: serde_json::Value = serde_json::from_str(&response_text).map_err(|e| {
453 eprintln!(
454 "ERROR: Failed to parse response as JSON for ticker {}: {}",
455 ticker, e
456 );
457 eprintln!("ERROR: Raw response: {}", response_text);
458 KalshiError::UserInputError(format!("Failed to parse JSON: {}", e))
459 })?;
460
461 // Check if the response has an "orderbook" field
462 if !json_value.is_object() || !json_value.as_object().unwrap().contains_key("orderbook") {
463 eprintln!(
464 "ERROR: Response does not contain 'orderbook' field for ticker: {}",
465 ticker
466 );
467 eprintln!(
468 "ERROR: Available keys: {:?}",
469 json_value
470 .as_object()
471 .map(|obj| obj.keys().collect::<Vec<_>>())
472 );
473 eprintln!(
474 "ERROR: Full response: {}",
475 serde_json::to_string_pretty(&json_value).unwrap()
476 );
477 return Err(KalshiError::UserInputError(
478 "missing field `orderbook`".to_string(),
479 ));
480 }
481
482 let res: OrderbookResponse = serde_json::from_value(json_value).map_err(|e| {
483 eprintln!(
484 "ERROR: Failed to deserialize OrderbookResponse for ticker {}: {}",
485 ticker, e
486 );
487 KalshiError::UserInputError(format!("Failed to deserialize: {}", e))
488 })?;
489
490 Ok(res.orderbook)
491 }
492
493 /// Retrieves the orderbook for a specific market from the Kalshi exchange (without depth limit).
494 ///
495 /// This is a convenience method that calls `get_orderbook(ticker, None)`.
496 ///
497 /// # Arguments
498 ///
499 /// * `ticker` - A string slice referencing the market's unique ticker identifier.
500 ///
501 /// # Returns
502 ///
503 /// - `Ok(Orderbook)`: The current orderbook data for the specified market on successful retrieval.
504 /// - `Err(KalshiError)`: An error if there is an issue with the request.
505 ///
506 /// # Example
507 ///
508 /// ```
509 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
510 /// let ticker = "SOME-MARKET-2024";
511 /// let orderbook = kalshi_instance.get_orderbook_full(ticker).await.unwrap();
512 /// ```
513 ///
514 pub async fn get_orderbook_full(&self, ticker: &str) -> Result<Orderbook, KalshiError> {
515 self.get_orderbook(ticker, None).await
516 }
517
518 /// Retrieves candlestick data for a specific market from the Kalshi exchange.
519 ///
520 /// This method fetches historical price data in candlestick format for a market,
521 /// allowing for analysis of price movements over time with various time intervals.
522 ///
523 /// # Arguments
524 ///
525 /// * `ticker` - A string slice referencing the market's unique ticker identifier.
526 /// * `series_ticker` - A string slice referencing the series ticker.
527 /// * `start_ts` - Optional timestamp for the start of the data range (restricts candlesticks to those ending on or after this timestamp).
528 /// * `end_ts` - Optional timestamp for the end of the data range (restricts candlesticks to those ending on or before this timestamp).
529 /// * `period_interval` - Optional integer specifying the length of each candlestick period in minutes (must be 1, 60, or 1440).
530 ///
531 /// # Returns
532 ///
533 /// - `Ok(Vec<Candle>)`: A vector of `Candle` objects on successful retrieval.
534 /// - `Err(KalshiError)`: An error if there is an issue with the request.
535 ///
536 /// # Example
537 ///
538 /// ```
539 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
540 /// let candlesticks = kalshi_instance.get_market_candlesticks(
541 /// "SOME-MARKET-2024", "SOME-SERIES", 1640995200, 1641081600, 60
542 /// ).await.unwrap();
543 /// ```
544 ///
545 pub async fn get_market_candlesticks(
546 &self,
547 ticker: &str,
548 series_ticker: &str,
549 start_ts: Option<i64>,
550 end_ts: Option<i64>,
551 period_interval: Option<i32>,
552 ) -> Result<Vec<Candle>, KalshiError> {
553 let url = format!(
554 "{}/series/{}/markets/{}/candlesticks",
555 self.base_url, series_ticker, ticker
556 );
557 let mut p = vec![];
558 add_param!(p, "start_ts", start_ts);
559 add_param!(p, "end_ts", end_ts);
560 add_param!(p, "period_interval", period_interval);
561
562 let res: CandlestickListResponse = self
563 .client
564 .get(reqwest::Url::parse_with_params(&url, &p)?)
565 .send()
566 .await?
567 .json()
568 .await?;
569 Ok(res.candlesticks)
570 }
571
572 /// Retrieves candlestick data for multiple markets in a single request.
573 ///
574 /// This method fetches historical price data in candlestick format for multiple markets
575 /// simultaneously, which is more efficient than making individual requests for each market.
576 ///
577 /// # Arguments
578 ///
579 /// * `market_tickers` - A vector of market ticker identifiers (max 100)
580 /// * `start_ts` - Start timestamp in Unix seconds
581 /// * `end_ts` - End timestamp in Unix seconds
582 /// * `period_interval` - Candlestick period in minutes (must be 1, 60, or 1440)
583 /// * `include_latest_before_start` - If true, prepends the latest candlestick before start_ts
584 ///
585 /// # Returns
586 ///
587 /// - `Ok(Vec<MarketCandlesticks>)`: A vector of `MarketCandlesticks` objects, one per market
588 /// - `Err(KalshiError)`: An error if there is an issue with the request
589 ///
590 /// # Example
591 ///
592 /// ```
593 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
594 /// let tickers = vec!["MARKET-1".to_string(), "MARKET-2".to_string()];
595 /// let now = chrono::Utc::now().timestamp();
596 /// let start = now - 86400; // 1 day ago
597 /// let candlesticks = kalshi_instance.batch_get_market_candlesticks(
598 /// tickers, start, now, 60, None
599 /// ).await.unwrap();
600 /// ```
601 ///
602 pub async fn batch_get_market_candlesticks(
603 &self,
604 market_tickers: Vec<String>,
605 start_ts: i64,
606 end_ts: i64,
607 period_interval: i32,
608 include_latest_before_start: Option<bool>,
609 ) -> Result<Vec<MarketCandlesticks>, KalshiError> {
610 if market_tickers.len() > 100 {
611 return Err(KalshiError::UserInputError(
612 "Maximum 100 market tickers allowed per batch request".to_string(),
613 ));
614 }
615
616 let url = format!("{}/markets/candlesticks/batch", self.base_url);
617
618 // Join tickers with commas as required by the API
619 let tickers_param = market_tickers.join(",");
620
621 let mut p = vec![
622 ("market_tickers", tickers_param),
623 ("start_ts", start_ts.to_string()),
624 ("end_ts", end_ts.to_string()),
625 ("period_interval", period_interval.to_string()),
626 ];
627
628 if let Some(include_latest) = include_latest_before_start {
629 p.push(("include_latest_before_start", include_latest.to_string()));
630 }
631
632 let res: BatchCandlestickResponse = self
633 .client
634 .get(reqwest::Url::parse_with_params(&url, &p)?)
635 .send()
636 .await?
637 .json()
638 .await?;
639 Ok(res.markets)
640 }
641
642 /// Retrieves a list of trades from the Kalshi exchange based on specified criteria.
643 ///
644 /// This method fetches multiple trades, allowing for filtering by ticker, time range,
645 /// and pagination. Trades represent executed orders between buyers and sellers.
646 ///
647 /// # Arguments
648 ///
649 /// * `limit` - An optional integer to limit the number of trades returned.
650 /// * `cursor` - An optional string for pagination cursor.
651 /// * `ticker` - An optional string to filter trades by market ticker.
652 /// * `min_ts` - An optional minimum timestamp for trade creation time.
653 /// * `max_ts` - An optional maximum timestamp for trade creation time.
654 ///
655 /// # Returns
656 ///
657 /// - `Ok((Option<String>, Vec<Trade>))`: A tuple containing an optional pagination cursor
658 /// and a vector of `Trade` objects on successful retrieval.
659 /// - `Err(KalshiError)`: An error if there is an issue with the request.
660 ///
661 /// # Example
662 ///
663 /// ```
664 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
665 /// let (cursor, trades) = kalshi_instance.get_trades(
666 /// Some(100), None, Some("SOME-MARKET-2024".to_string()),
667 /// Some(1640995200), Some(1641081600)
668 /// ).await.unwrap();
669 /// ```
670 ///
671 pub async fn get_trades(
672 &self,
673 limit: Option<i64>,
674 cursor: Option<String>,
675 ticker: Option<String>,
676 min_ts: Option<i64>,
677 max_ts: Option<i64>,
678 ) -> Result<(Option<String>, Vec<Trade>), KalshiError> {
679 let url = format!("{}/markets/trades", self.base_url);
680 let mut p = vec![];
681 add_param!(p, "limit", limit);
682 add_param!(p, "cursor", cursor);
683 add_param!(p, "ticker", ticker);
684 add_param!(p, "min_ts", min_ts);
685 add_param!(p, "max_ts", max_ts);
686
687 let res: TradeListResponse = self
688 .client
689 .get(reqwest::Url::parse_with_params(&url, &p)?)
690 .send()
691 .await?
692 .json()
693 .await?;
694 Ok((res.cursor, res.trades))
695 }
696
697 /// Retrieves a list of series from the Kalshi exchange based on specified criteria.
698 ///
699 /// This method fetches multiple series, allowing for filtering by category, tags,
700 /// and pagination. Series represent collections of related events and markets.
701 ///
702 /// # Arguments
703 ///
704 /// * `limit` - An optional integer to limit the number of series returned.
705 /// * `cursor` - An optional string for pagination cursor.
706 /// * `category` - An optional string to filter series by category.
707 /// * `tags` - An optional string to filter series by tags.
708 ///
709 /// # Returns
710 ///
711 /// - `Ok((Option<String>, Vec<Series>))`: A tuple containing an optional pagination cursor
712 /// and a vector of `Series` objects on successful retrieval.
713 /// - `Err(KalshiError)`: An error if there is an issue with the request.
714 ///
715 /// # Example
716 ///
717 /// ```
718 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
719 /// let (cursor, series) = kalshi_instance.get_series_list(
720 /// Some(20), None, Some("politics".to_string()), Some("election".to_string())
721 /// ).await.unwrap();
722 /// ```
723 ///
724 pub async fn get_series_list(
725 &self,
726 limit: Option<i64>,
727 cursor: Option<String>,
728 category: Option<String>,
729 tags: Option<String>,
730 ) -> Result<(Option<String>, Vec<Series>), KalshiError> {
731 // --- build query string ------------------------------------------------
732 let mut p = Vec::new();
733 add_param!(p, "limit", limit);
734 add_param!(p, "cursor", cursor);
735 add_param!(p, "category", category);
736 add_param!(p, "tags", tags);
737
738 let path = if p.is_empty() {
739 "/series".to_string()
740 } else {
741 format!("/series?{}", serde_urlencoded::to_string(&p)?)
742 };
743
744 // --- signed GET --------------------------------------------------------
745 #[derive(Debug, serde::Deserialize)]
746 struct SeriesListResponse {
747 cursor: Option<String>,
748 series: Option<Vec<Series>>, // ← tolerate `null`
749 }
750
751 let res: SeriesListResponse = self.signed_get(&path).await?;
752 Ok((res.cursor, res.series.unwrap_or_default()))
753 }
754
755 /// Retrieves detailed information about a specific series from the Kalshi exchange.
756 ///
757 /// This method fetches data for a single series identified by its series ticker.
758 /// The series represents a collection of related events and markets.
759 ///
760 /// # Arguments
761 ///
762 /// * `series_ticker` - A string slice referencing the series' unique ticker identifier.
763 ///
764 /// # Returns
765 ///
766 /// - `Ok(Series)`: Detailed information about the specified series on successful retrieval.
767 /// - `Err(KalshiError)`: An error if there is an issue with the request.
768 ///
769 /// # Example
770 ///
771 /// ```
772 /// // Assuming `kalshi_instance` is an instance of `Kalshi`
773 /// let series_ticker = "SOME-SERIES";
774 /// let series = kalshi_instance.get_series(series_ticker).await.unwrap();
775 /// ```
776 ///
777 pub async fn get_series(&self, series_ticker: &str) -> Result<Series, KalshiError> {
778 let url = format!("{}/series/{}", self.base_url, series_ticker);
779 let res: SingleSeriesResponse = self.client.get(url).send().await?.json().await?;
780 Ok(res.series)
781 }
782}
783
784/// When the API gives `"field": null` treat it as an empty Vec.
785fn null_to_empty_vec<'de, D, T>(d: D) -> Result<Vec<T>, D::Error>
786where
787 D: serde::Deserializer<'de>,
788 T: serde::Deserialize<'de>,
789{
790 let opt = Option::<Vec<T>>::deserialize(d)?;
791 Ok(opt.unwrap_or_default())
792}
793
794/// Deserializes dollar price levels from the API format [[string, number], ...]
795/// to Vec<(f32, i32)> where the string is converted to f32.
796fn deserialize_dollar_levels<'de, D>(d: D) -> Result<Vec<(f32, i32)>, D::Error>
797where
798 D: Deserializer<'de>,
799{
800 use serde::de::Error;
801
802 // First, deserialize as a Vec of generic JSON values
803 let opt = Option::<Vec<serde_json::Value>>::deserialize(d)?;
804
805 // If null or missing, return empty vec
806 let Some(arr) = opt else {
807 return Ok(Vec::new());
808 };
809
810 // Convert each [price_string, count] to (f32, i32)
811 let mut result = Vec::new();
812 for item in arr {
813 let level = item
814 .as_array()
815 .ok_or_else(|| Error::custom("Expected array for price level"))?;
816
817 if level.len() != 2 {
818 return Err(Error::custom("Expected array of length 2 for price level"));
819 }
820
821 // Parse price (can be string or number)
822 let price: f32 = match &level[0] {
823 serde_json::Value::String(s) => s
824 .parse()
825 .map_err(|_| Error::custom(format!("Failed to parse price string: {}", s)))?,
826 serde_json::Value::Number(n) => n
827 .as_f64()
828 .ok_or_else(|| Error::custom("Failed to convert price number to f64"))?
829 as f32,
830 _ => return Err(Error::custom("Price must be string or number")),
831 };
832
833 // Parse count (should be a number)
834 let count: i32 = level[1]
835 .as_i64()
836 .ok_or_else(|| Error::custom("Count must be a number"))?
837 as i32;
838
839 result.push((price, count));
840 }
841
842 Ok(result)
843}
844
845// -------- public models --------
846
847/// Represents an event on the Kalshi exchange.
848///
849/// An event is a prediction market that contains multiple markets for trading.
850/// Events can have various statuses and may include nested markets.
851#[derive(Debug, Deserialize, Serialize)]
852pub struct Event {
853 pub event_ticker: String,
854 pub series_ticker: String,
855 pub title: String,
856 pub sub_title: String,
857 pub mutually_exclusive: bool,
858 pub category: String,
859 pub strike_date: Option<String>,
860 pub strike_period: Option<String>,
861 pub markets: Option<Vec<Market>>,
862}
863
864/// Represents a market on the Kalshi exchange.
865///
866/// A market is a specific trading instrument within an event, representing
867/// a binary outcome that users can trade on (Yes/No).
868#[derive(Debug, Deserialize, Serialize)]
869pub struct Market {
870 pub ticker: String,
871 pub event_ticker: String,
872 pub market_type: String,
873 pub title: String,
874 pub subtitle: String,
875 pub yes_sub_title: String,
876 pub no_sub_title: String,
877 pub open_time: String,
878 pub close_time: String,
879 pub expected_expiration_time: Option<String>,
880 pub expiration_time: Option<String>,
881 pub latest_expiration_time: String,
882 pub settlement_timer_seconds: i64,
883 pub status: String,
884 pub response_price_units: String,
885 pub notional_value: i64,
886 pub tick_size: i64,
887 pub yes_bid: i64,
888 pub yes_ask: i64,
889 pub no_bid: i64,
890 pub no_ask: i64,
891 pub last_price: i64,
892 pub previous_yes_bid: i64,
893 pub previous_yes_ask: i64,
894 pub previous_price: i64,
895 pub volume: i64,
896 pub volume_24h: i64,
897 pub liquidity: i64,
898 pub open_interest: i64,
899 pub result: SettlementResult,
900 pub cap_strike: Option<f64>,
901 pub can_close_early: bool,
902 pub expiration_value: String,
903 pub category: String,
904 pub risk_limit_cents: i64,
905 pub strike_type: Option<String>,
906 pub floor_strike: Option<f64>,
907 pub rules_primary: String,
908 pub rules_secondary: String,
909 pub settlement_value: Option<String>,
910 pub functional_strike: Option<String>,
911}
912
913/// Represents a series on the Kalshi exchange.
914///
915/// A series is a collection of related events and markets, typically
916/// organized around a common theme or category.
917#[derive(Debug, Deserialize, Serialize)]
918pub struct Series {
919 #[serde(default)]
920 pub ticker: Option<String>,
921 #[serde(default)]
922 pub frequency: Option<String>,
923 #[serde(default)]
924 pub title: Option<String>,
925 #[serde(default)]
926 pub category: Option<String>,
927 #[serde(default, deserialize_with = "null_to_empty_vec")]
928 pub tags: Vec<String>,
929 #[serde(default, deserialize_with = "null_to_empty_vec")]
930 pub settlement_sources: Vec<SettlementSource>,
931 #[serde(default)]
932 pub contract_url: Option<String>,
933 #[serde(flatten)]
934 pub extra: HashMap<String, serde_json::Value>,
935}
936
937/// Represents a multivariate event collection on the Kalshi exchange.
938///
939/// A multivariate event collection contains multiple related markets
940/// that are analyzed together as a group.
941#[derive(Debug, Deserialize, Serialize)]
942pub struct MultivariateEventCollection {
943 pub collection_ticker: String,
944 pub title: String,
945 pub description: String,
946 pub category: String,
947 #[serde(default, deserialize_with = "null_to_empty_vec")]
948 pub tags: Vec<String>,
949 #[serde(default, deserialize_with = "null_to_empty_vec")]
950 pub markets: Vec<Market>,
951 pub created_time: String,
952 pub updated_time: String,
953}
954
955/// Represents a candlestick data point for market analysis.
956///
957/// Candlesticks provide historical price data including open, high, low, and close
958/// prices for both Yes and No sides of a market over a specific time period.
959#[derive(Debug, Deserialize, Serialize)]
960pub struct Candle {
961 pub start_ts: i64,
962 pub end_ts: i64,
963 pub yes_open: i32,
964 pub yes_high: i32,
965 pub yes_low: i32,
966 pub yes_close: i32,
967 pub no_open: i32,
968 pub no_high: i32,
969 pub no_low: i32,
970 pub no_close: i32,
971 pub volume: i64,
972 pub open_interest: i64,
973}
974
975/// Represents the orderbook for a market on the Kalshi exchange.
976///
977/// The orderbook contains current bid and ask orders for both Yes and No sides
978/// of a market, showing the current market depth and liquidity.
979#[derive(Debug, Clone, Deserialize, Serialize)]
980pub struct Orderbook {
981 /// Price levels in cents: [[price_cents, count], ...]
982 pub yes: Option<Vec<Vec<i32>>>,
983 /// Price levels in cents: [[price_cents, count], ...]
984 pub no: Option<Vec<Vec<i32>>>,
985 /// Price levels in dollars: [[price_dollars, count], ...]
986 /// The price_dollars string from API is converted to f32 (4 dp, range 0-1)
987 #[serde(default, deserialize_with = "deserialize_dollar_levels")]
988 pub yes_dollars: Vec<(f32, i32)>,
989 /// Price levels in dollars: [[price_dollars, count], ...]
990 /// The price_dollars string from API is converted to f32 (4 dp, range 0-1)
991 #[serde(default, deserialize_with = "deserialize_dollar_levels")]
992 pub no_dollars: Vec<(f32, i32)>,
993}
994
995/// Represents a market snapshot at a specific point in time.
996///
997/// A snapshot provides a summary of market activity including current prices,
998/// volume, and open interest at a specific timestamp.
999#[derive(Debug, Deserialize, Serialize)]
1000pub struct Snapshot {
1001 pub yes_price: i32,
1002 pub yes_bid: i32,
1003 pub yes_ask: i32,
1004 pub no_bid: i32,
1005 pub no_ask: i32,
1006 pub volume: i32,
1007 pub open_interest: i32,
1008 pub ts: i64,
1009}
1010
1011/// Represents a trade executed on the Kalshi exchange.
1012///
1013/// A trade represents a completed transaction between a buyer and seller,
1014/// including the price, quantity, and timing of the execution.
1015#[derive(Debug, Deserialize, Serialize)]
1016pub struct Trade {
1017 pub trade_id: String,
1018 pub taker_side: String,
1019 pub ticker: String,
1020 pub count: i32,
1021 pub yes_price: i32,
1022 pub no_price: i32,
1023 pub created_time: String,
1024}
1025
1026/// Represents the possible settlement results for a market.
1027///
1028/// Markets can settle in various ways depending on the outcome of the event
1029/// and the specific market rules.
1030#[derive(Debug, Serialize, Deserialize)]
1031#[serde(rename_all = "lowercase")]
1032pub enum SettlementResult {
1033 Yes,
1034 No,
1035 #[serde(rename = "")]
1036 Void,
1037 #[serde(rename = "all_no")]
1038 AllNo,
1039 #[serde(rename = "all_yes")]
1040 AllYes,
1041}
1042
1043/// Represents the possible statuses of a market.
1044///
1045/// Markets can be in various states throughout their lifecycle from creation to settlement.
1046#[derive(Debug, Serialize, Deserialize)]
1047#[serde(rename_all = "lowercase")]
1048pub enum MarketStatus {
1049 Open,
1050 Closed,
1051 Settled,
1052}
1053
1054/// Filter for multivariate events (MVE) in market queries.
1055///
1056/// This enum allows filtering markets based on whether they belong to
1057/// multivariate event collections.
1058#[derive(Debug, Clone, Serialize, Deserialize)]
1059#[serde(rename_all = "lowercase")]
1060pub enum MveFilter {
1061 /// Only include markets that are part of multivariate events
1062 Only,
1063 /// Exclude markets that are part of multivariate events
1064 Exclude,
1065}
1066
1067impl std::fmt::Display for MveFilter {
1068 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1069 match self {
1070 MveFilter::Only => write!(f, "only"),
1071 MveFilter::Exclude => write!(f, "exclude"),
1072 }
1073 }
1074}
1075
1076/// Represents candlestick data for a specific market in batch responses.
1077///
1078/// Contains the market ticker and its associated candlestick data.
1079#[derive(Debug, Deserialize, Serialize)]
1080pub struct MarketCandlesticks {
1081 /// The market ticker identifier
1082 pub ticker: String,
1083 /// The candlestick data for this market
1084 pub candlesticks: Vec<Candle>,
1085}
1086
1087/// Represents a settlement source for a series.
1088///
1089/// Settlement sources provide the data or methodology used to determine
1090/// the final outcome of markets in a series.
1091#[derive(Debug, Deserialize, Serialize)]
1092pub struct SettlementSource {
1093 #[serde(default)]
1094 pub url: Option<String>,
1095 #[serde(default)]
1096 pub name: Option<String>,
1097}
1098
1099// -------- response wrappers --------
1100
1101#[derive(Debug, Deserialize)]
1102struct MarketListResponse {
1103 cursor: Option<String>,
1104 markets: Vec<Market>,
1105}
1106
1107#[derive(Debug, Deserialize)]
1108#[allow(dead_code)] // Used by serde for deserialization
1109struct SeriesListResponse {
1110 cursor: Option<String>,
1111 #[serde(default)]
1112 series: Vec<Series>,
1113}
1114
1115#[derive(Debug, Deserialize)]
1116struct TradeListResponse {
1117 cursor: Option<String>,
1118 trades: Vec<Trade>,
1119}
1120
1121#[derive(Debug, Deserialize)]
1122#[allow(dead_code)] // cursor field reserved for future pagination support
1123struct CandlestickListResponse {
1124 cursor: Option<String>,
1125 candlesticks: Vec<Candle>,
1126}
1127
1128#[derive(Debug, Deserialize)]
1129struct SingleMarketResponse {
1130 market: Market,
1131}
1132
1133#[derive(Debug, Deserialize)]
1134struct SingleSeriesResponse {
1135 series: Series,
1136}
1137
1138#[derive(Debug, Deserialize)]
1139struct OrderbookResponse {
1140 orderbook: Orderbook,
1141}
1142
1143#[derive(Debug, Deserialize)]
1144struct BatchCandlestickResponse {
1145 markets: Vec<MarketCandlesticks>,
1146}