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}