kalshi_rust/exchange/
mod.rs

1use super::Kalshi;
2use crate::kalshi_error::*;
3use serde::{Deserialize, Serialize};
4
5impl Kalshi {
6    /// Retrieves the current status of the Kalshi exchange and trading engine.
7    ///
8    /// This method checks whether both the exchange platform and the trading engine
9    /// are currently operational and accepting orders.
10    ///
11    /// # Returns
12    ///
13    /// - `Ok(ExchangeStatus)`: The current status of the exchange and trading engine on successful retrieval.
14    /// - `Err(KalshiError)`: An error if there is an issue with the request.
15    ///
16    /// # Example
17    ///
18    /// ```
19    /// // Assuming `kalshi_instance` is an instance of `Kalshi`
20    /// let status = kalshi_instance.get_exchange_status().await.unwrap();
21    /// if status.trading_active && status.exchange_active {
22    ///     println!("Exchange is live and accepting trades");
23    /// }
24    /// ```
25    ///
26    pub async fn get_exchange_status(&self) -> Result<ExchangeStatus, KalshiError> {
27        let url = format!("{}/exchange/status", self.base_url);
28        Ok(self.client.get(&url).send().await?.json().await?)
29    }
30
31    /// Retrieves the exchange schedule including trading hours and maintenance windows.
32    ///
33    /// This method fetches the standard trading hours for each day of the week
34    /// and any scheduled maintenance windows when the exchange may be unavailable.
35    ///
36    /// # Returns
37    ///
38    /// - `Ok(ExchangeSchedule)`: The exchange schedule including trading hours and maintenance windows on successful retrieval.
39    /// - `Err(KalshiError)`: An error if there is an issue with the request.
40    ///
41    /// # Example
42    ///
43    /// ```
44    /// // Assuming `kalshi_instance` is an instance of `Kalshi`
45    /// let schedule = kalshi_instance.get_exchange_schedule().await.unwrap();
46    /// println!("Standard hours: {:?}", schedule.standard_hours);
47    /// println!("Maintenance windows: {:?}", schedule.maintenance_windows);
48    /// ```
49    ///
50    pub async fn get_exchange_schedule(&self) -> Result<ExchangeSchedule, KalshiError> {
51        let url = format!("{}/exchange/schedule", self.base_url);
52        let res: ExchangeScheduleResponse = self.client.get(&url).send().await?.json().await?;
53        Ok(res.schedule)
54    }
55
56    /// Retrieves exchange announcements including active and historical messages.
57    ///
58    /// This method fetches all exchange-wide announcements, both active and expired,
59    /// allowing users to stay informed about important updates, maintenance schedules,
60    /// and other exchange-related information.
61    ///
62    /// # Arguments
63    ///
64    /// * `limit` - An optional integer to limit the number of announcements returned.
65    /// * `cursor` - An optional string for pagination cursor.
66    ///
67    /// # Returns
68    ///
69    /// - `Ok((Option<String>, Vec<ExchangeAnnouncement>))`: A tuple containing an optional pagination cursor
70    ///   and a vector of `ExchangeAnnouncement` objects on successful retrieval.
71    /// - `Err(KalshiError)`: An error if there is an issue with the request.
72    ///
73    /// # Example
74    ///
75    /// ```
76    /// // Assuming `kalshi_instance` is an instance of `Kalshi`
77    /// let (cursor, announcements) = kalshi_instance.get_exchange_announcements(
78    ///     Some(10), None
79    /// ).await.unwrap();
80    /// for announcement in announcements {
81    ///     println!("{}: {}", announcement.ts, announcement.message);
82    /// }
83    /// ```
84    ///
85    pub async fn get_exchange_announcements(
86        &self,
87        limit: Option<i64>,
88        cursor: Option<String>,
89    ) -> Result<(Option<String>, Vec<ExchangeAnnouncement>), KalshiError> {
90        let url = format!("{}/exchange/announcements", self.base_url);
91
92        let mut params = vec![];
93        add_param!(params, "limit", limit);
94        add_param!(params, "cursor", cursor);
95
96        let final_url = reqwest::Url::parse_with_params(&url, &params)?;
97        let res: ExchangeAnnouncementsResponse =
98            self.client.get(final_url).send().await?.json().await?;
99        Ok((res.cursor, res.announcements))
100    }
101
102    /// Retrieves the timestamp indicating when portfolio data was last refreshed.
103    ///
104    /// This method provides the timestamp of the last update to portfolio-related
105    /// endpoints, allowing users to determine the freshness of their account data.
106    ///
107    /// # Returns
108    ///
109    /// - `Ok(UserDataTimestamp)`: The timestamp of the last portfolio data refresh on successful retrieval.
110    /// - `Err(KalshiError)`: An error if there is an issue with the request.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// // Assuming `kalshi_instance` is an instance of `Kalshi`
116    /// let timestamp = kalshi_instance.get_user_data_timestamp().await.unwrap();
117    /// println!("Portfolio data last updated: {}", timestamp.last_validated_ts);
118    /// ```
119    ///
120    pub async fn get_user_data_timestamp(&self) -> Result<UserDataTimestamp, KalshiError> {
121        let url = format!("{}/exchange/user_data_timestamp", self.base_url);
122        Ok(self.client.get(&url).send().await?.json().await?)
123    }
124
125    /// Checks if the exchange is active with exponential backoff retry logic.
126    ///
127    /// This method attempts to verify that the exchange is operational and trading is active.
128    /// If the exchange is not active, it will retry with exponential backoff delays.
129    /// After all attempts are exhausted, the program will exit with a non-zero status code.
130    ///
131    /// # Arguments
132    ///
133    /// * `max_attempts` - Maximum number of attempts to check exchange status (default: 5)
134    /// * `base_delay_secs` - Base delay in seconds for exponential backoff (default: 30.0)
135    /// * `max_delay_secs` - Maximum delay in seconds to cap exponential growth (default: 300.0)
136    ///
137    /// # Returns
138    ///
139    /// - `Ok(())`: If the exchange becomes active within the retry attempts
140    /// - `Err(KalshiError)`: If there's an unrecoverable error during the process
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// // Assuming `kalshi_instance` is an instance of `Kalshi`
146    /// // This will exit the program if exchange doesn't become active after retries
147    /// kalshi_instance.check_exchange_active_with_backoff().await.unwrap();
148    ///
149    /// // Custom configuration
150    /// kalshi_instance.check_exchange_active_with_backoff(3, 60.0, 600.0).await.unwrap();
151    /// ```
152    ///
153    pub async fn check_exchange_active_with_backoff(
154        &self,
155        max_attempts: u32,
156        base_delay_secs: f64,
157        max_delay_secs: f64,
158    ) -> Result<(), KalshiError> {
159        use std::process;
160        use tokio::time::{sleep, Duration};
161
162        for attempt in 1..=max_attempts {
163            match self.get_exchange_status().await {
164                Ok(status) => {
165                    if status.trading_active && status.exchange_active {
166                        println!("Exchange is active (attempt {}/{}", attempt, max_attempts);
167                        return Ok(());
168                    }
169
170                    if attempt < max_attempts {
171                        let delay_secs = (base_delay_secs * (2.0_f64.powi((attempt - 1) as i32)))
172                            .min(max_delay_secs);
173                        println!(
174                            "Exchange not active (attempt {}/{}). Waiting {:.1} seconds before retry...",
175                            attempt, max_attempts, delay_secs
176                        );
177                        sleep(Duration::from_secs_f64(delay_secs)).await;
178                    } else {
179                        println!("Exchange not active after {} attempts", max_attempts);
180                    }
181                }
182                Err(e) => {
183                    if attempt < max_attempts {
184                        let delay_secs = (base_delay_secs * (2.0_f64.powi((attempt - 1) as i32)))
185                            .min(max_delay_secs);
186                        println!(
187                            "Error checking exchange status (attempt {}/{}): {}. Waiting {:.1} seconds before retry...",
188                            attempt, max_attempts, e, delay_secs
189                        );
190                        sleep(Duration::from_secs_f64(delay_secs)).await;
191                    } else {
192                        println!(
193                            "Failed to check exchange status after {} attempts: {}",
194                            max_attempts, e
195                        );
196                    }
197                }
198            }
199        }
200
201        println!("Exiting as exchange is not active after all retry attempts");
202        process::exit(1);
203    }
204
205    /// Convenience method to check exchange status with default backoff settings.
206    ///
207    /// This method calls `check_exchange_active_with_backoff` with default parameters:
208    /// - max_attempts: 5
209    /// - base_delay_secs: 30.0
210    /// - max_delay_secs: 300.0
211    ///
212    /// # Returns
213    ///
214    /// - `Ok(())`: If the exchange becomes active within the retry attempts
215    /// - `Err(KalshiError)`: If there's an unrecoverable error during the process
216    ///
217    /// # Example
218    ///
219    /// ```
220    /// // Assuming `kalshi_instance` is an instance of `Kalshi`
221    /// kalshi_instance.check_exchange_active().await.unwrap();
222    /// ```
223    ///
224    pub async fn check_exchange_active(&self) -> Result<(), KalshiError> {
225        self.check_exchange_active_with_backoff(5, 30.0, 300.0)
226            .await
227    }
228
229    /// Retrieves series fee changes from the exchange.
230    ///
231    /// This method fetches information about fee changes for specific series,
232    /// including historical and upcoming fee adjustments.
233    ///
234    /// # Arguments
235    ///
236    /// * `series_ticker` - Optional series ticker to filter fee changes.
237    ///
238    /// # Returns
239    ///
240    /// - `Ok(Vec<SeriesFeeChange>)`: A vector of fee change information on successful retrieval.
241    /// - `Err(KalshiError)`: An error if there is an issue with the request.
242    ///
243    /// # Example
244    ///
245    /// ```
246    /// // Assuming `kalshi_instance` is an instance of `Kalshi`
247    /// let fee_changes = kalshi_instance.get_series_fee_changes(None).await.unwrap();
248    /// for change in fee_changes {
249    ///     println!("Series: {}, New Fee: {}", change.series_ticker, change.new_fee);
250    /// }
251    /// ```
252    ///
253    pub async fn get_series_fee_changes(
254        &self,
255        series_ticker: Option<String>,
256    ) -> Result<Vec<SeriesFeeChange>, KalshiError> {
257        let path = "/series/fee_changes";
258        let mut params = vec![];
259        add_param!(params, "series_ticker", series_ticker);
260
261        let url = format!("{}{}", self.base_url, path);
262        let final_url = reqwest::Url::parse_with_params(&url, &params)?;
263        let res: SeriesFeeChangesResponse = self.client.get(final_url).send().await?.json().await?;
264        Ok(res.fee_changes)
265    }
266}
267
268// -------- public models --------
269
270/// Represents the operational status of the Kalshi exchange.
271///
272/// This struct provides simple boolean flags indicating whether the exchange
273/// platform and trading engine are currently active and operational.
274#[derive(Debug, Deserialize, Serialize)]
275pub struct ExchangeStatus {
276    /// Indicates whether the trading engine is currently active and accepting orders.
277    pub trading_active: bool,
278    /// Indicates whether the exchange platform is currently operational.
279    pub exchange_active: bool,
280}
281
282/// Represents the trading schedule and maintenance windows for the Kalshi exchange.
283///
284/// This struct contains the standard trading hours for each day of the week
285/// and any scheduled maintenance windows when the exchange may be unavailable.
286#[derive(Debug, Deserialize, Serialize)]
287pub struct ExchangeSchedule {
288    /// The standard trading hours for each day of the week.
289    pub standard_hours: Vec<StandardHours>,
290    /// Scheduled maintenance windows when the exchange may be unavailable.
291    pub maintenance_windows: Vec<MaintenanceWindow>,
292}
293
294/// Represents an exchange-wide announcement from Kalshi.
295///
296/// Announcements provide important information about exchange updates,
297/// maintenance schedules, new features, or other relevant information
298/// that users need to be aware of.
299#[derive(Debug, Deserialize, Serialize)]
300pub struct ExchangeAnnouncement {
301    /// The announcement message content.
302    pub message: String,
303    /// The timestamp when the announcement was created (seconds since epoch, ISO string, or RFC3339).
304    pub ts: String,
305    /// The current status of the announcement (e.g., "active" or "expired").
306    pub status: String,
307}
308
309/// Represents the timestamp of the last portfolio data refresh.
310///
311/// This struct provides information about when user portfolio data
312/// was last updated, allowing users to determine data freshness.
313#[derive(Debug, Deserialize, Serialize)]
314pub struct UserDataTimestamp {
315    /// The timestamp of the last portfolio data validation/refresh.
316    pub last_validated_ts: String,
317}
318
319/// Represents a scheduled maintenance window for the Kalshi exchange.
320///
321/// Maintenance windows indicate periods when the exchange may be unavailable
322/// for trading or other operations due to scheduled maintenance.
323#[derive(Debug, Deserialize, Serialize)]
324pub struct MaintenanceWindow {
325    /// The start datetime of the maintenance window.
326    pub start_datetime: String,
327    /// The end datetime of the maintenance window.
328    pub end_datetime: String,
329}
330
331/// Represents the trading schedule for a specific day.
332///
333/// This struct defines the opening and closing times for trading
334/// on a particular day of the week.
335#[derive(Debug, Deserialize, Serialize)]
336pub struct DaySchedule {
337    /// The time when trading opens for this day.
338    pub open_time: String,
339    /// The time when trading closes for this day.
340    pub close_time: String,
341}
342
343/// Represents the standard trading hours for the Kalshi exchange.
344///
345/// This struct defines the trading schedule for each day of the week,
346/// including multiple time slots per day if applicable.
347#[derive(Debug, Deserialize, Serialize)]
348pub struct StandardHours {
349    /// The start time for the trading period.
350    pub start_time: String,
351    /// The end time for the trading period.
352    pub end_time: String,
353    /// Trading schedule for Monday.
354    #[serde(default)]
355    pub monday: Vec<DaySchedule>,
356    /// Trading schedule for Tuesday.
357    #[serde(default)]
358    pub tuesday: Vec<DaySchedule>,
359    /// Trading schedule for Wednesday.
360    #[serde(default)]
361    pub wednesday: Vec<DaySchedule>,
362    /// Trading schedule for Thursday.
363    #[serde(default)]
364    pub thursday: Vec<DaySchedule>,
365    /// Trading schedule for Friday.
366    #[serde(default)]
367    pub friday: Vec<DaySchedule>,
368    /// Trading schedule for Saturday.
369    #[serde(default)]
370    pub saturday: Vec<DaySchedule>,
371    /// Trading schedule for Sunday.
372    #[serde(default)]
373    pub sunday: Vec<DaySchedule>,
374}
375
376// -------- response wrappers --------
377
378#[derive(Debug, Deserialize)]
379struct ExchangeScheduleResponse {
380    schedule: ExchangeSchedule,
381}
382
383#[derive(Debug, Deserialize)]
384struct ExchangeAnnouncementsResponse {
385    cursor: Option<String>,
386    announcements: Vec<ExchangeAnnouncement>,
387}
388
389#[derive(Debug, Deserialize)]
390struct SeriesFeeChangesResponse {
391    fee_changes: Vec<SeriesFeeChange>,
392}
393
394/// Represents a fee change for a series.
395#[derive(Debug, Deserialize, Serialize)]
396pub struct SeriesFeeChange {
397    /// The series ticker.
398    pub series_ticker: String,
399    /// The old fee (in cents or basis points).
400    pub old_fee: Option<f64>,
401    /// The new fee (in cents or basis points).
402    pub new_fee: f64,
403    /// The effective date of the fee change.
404    pub effective_date: String,
405}