fmp_rs/endpoints/
charts.rs

1//! Historical price chart endpoints
2
3use crate::client::FmpClient;
4use crate::error::Result;
5use crate::models::charts::{HistoricalDividend, HistoricalPrice, IntradayPrice, StockSplit};
6use crate::models::common::Timeframe;
7use serde::Serialize;
8
9/// Historical price chart API endpoints
10pub struct Charts {
11    client: FmpClient,
12}
13
14impl Charts {
15    pub(crate) fn new(client: FmpClient) -> Self {
16        Self { client }
17    }
18
19    /// Get historical daily prices (EOD - End of Day)
20    pub async fn get_historical_prices(
21        &self,
22        symbol: &str,
23        from: Option<&str>,
24        to: Option<&str>,
25    ) -> Result<Vec<HistoricalPrice>> {
26        #[derive(Serialize)]
27        struct Query<'a> {
28            symbol: &'a str,
29            #[serde(skip_serializing_if = "Option::is_none")]
30            from: Option<&'a str>,
31            #[serde(skip_serializing_if = "Option::is_none")]
32            to: Option<&'a str>,
33            apikey: &'a str,
34        }
35
36        let url = self.client.build_url("/historical-price-eod/full");
37        self.client
38            .get_with_query(
39                &url,
40                &Query {
41                    symbol,
42                    from,
43                    to,
44                    apikey: self.client.api_key(),
45                },
46            )
47            .await
48    }
49
50    /// Get intraday prices
51    pub async fn get_intraday_prices(
52        &self,
53        symbol: &str,
54        timeframe: Timeframe,
55        from: Option<&str>,
56        to: Option<&str>,
57    ) -> Result<Vec<IntradayPrice>> {
58        #[derive(Serialize)]
59        struct Query<'a> {
60            symbol: &'a str,
61            #[serde(skip_serializing_if = "Option::is_none")]
62            from: Option<&'a str>,
63            #[serde(skip_serializing_if = "Option::is_none")]
64            to: Option<&'a str>,
65            apikey: &'a str,
66        }
67
68        let url = self
69            .client
70            .build_url(&format!("/historical-chart/{}", timeframe));
71        self.client
72            .get_with_query(
73                &url,
74                &Query {
75                    symbol,
76                    from,
77                    to,
78                    apikey: self.client.api_key(),
79                },
80            )
81            .await
82    }
83
84    /// Get 1-minute intraday prices
85    pub async fn get_1min_prices(
86        &self,
87        symbol: &str,
88        from: Option<&str>,
89        to: Option<&str>,
90    ) -> Result<Vec<IntradayPrice>> {
91        self.get_intraday_prices(symbol, Timeframe::OneMinute, from, to)
92            .await
93    }
94
95    /// Get 5-minute intraday prices
96    pub async fn get_5min_prices(
97        &self,
98        symbol: &str,
99        from: Option<&str>,
100        to: Option<&str>,
101    ) -> Result<Vec<IntradayPrice>> {
102        self.get_intraday_prices(symbol, Timeframe::FiveMinutes, from, to)
103            .await
104    }
105
106    /// Get 15-minute intraday prices
107    pub async fn get_15min_prices(
108        &self,
109        symbol: &str,
110        from: Option<&str>,
111        to: Option<&str>,
112    ) -> Result<Vec<IntradayPrice>> {
113        self.get_intraday_prices(symbol, Timeframe::FifteenMinutes, from, to)
114            .await
115    }
116
117    /// Get 30-minute intraday prices
118    pub async fn get_30min_prices(
119        &self,
120        symbol: &str,
121        from: Option<&str>,
122        to: Option<&str>,
123    ) -> Result<Vec<IntradayPrice>> {
124        self.get_intraday_prices(symbol, Timeframe::ThirtyMinutes, from, to)
125            .await
126    }
127
128    /// Get 1-hour intraday prices
129    pub async fn get_1hour_prices(
130        &self,
131        symbol: &str,
132        from: Option<&str>,
133        to: Option<&str>,
134    ) -> Result<Vec<IntradayPrice>> {
135        self.get_intraday_prices(symbol, Timeframe::OneHour, from, to)
136            .await
137    }
138
139    /// Get 4-hour intraday prices
140    pub async fn get_4hour_prices(
141        &self,
142        symbol: &str,
143        from: Option<&str>,
144        to: Option<&str>,
145    ) -> Result<Vec<IntradayPrice>> {
146        self.get_intraday_prices(symbol, Timeframe::FourHours, from, to)
147            .await
148    }
149
150    /// Get historical dividend data
151    ///
152    /// # Arguments
153    /// * `symbol` - Stock symbol (e.g., "AAPL")
154    ///
155    /// # Example
156    /// ```no_run
157    /// # use fmp_rs::FmpClient;
158    /// # #[tokio::main]
159    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
160    /// let client = FmpClient::new()?;
161    /// let dividends = client.charts().get_historical_dividends("AAPL").await?;
162    /// for div in dividends.iter().take(5) {
163    ///     println!("{}: ${:.2} (Adjusted: ${:.2})",
164    ///         div.date, div.dividend, div.adj_dividend);
165    /// }
166    /// # Ok(())
167    /// # }
168    /// ```
169    pub async fn get_historical_dividends(&self, symbol: &str) -> Result<Vec<HistoricalDividend>> {
170        self.client
171            .get_with_query(
172                &format!("v3/historical-price-full/stock_dividend/{}", symbol),
173                &(),
174            )
175            .await
176    }
177
178    /// Get stock split history
179    ///
180    /// # Arguments
181    /// * `symbol` - Stock symbol (e.g., "AAPL")
182    ///
183    /// # Example
184    /// ```no_run
185    /// # use fmp_rs::FmpClient;
186    /// # #[tokio::main]
187    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
188    /// let client = FmpClient::new()?;
189    /// let splits = client.charts().get_stock_splits("AAPL").await?;
190    /// for split in splits {
191    ///     println!("{}: {}:{} split", split.date, split.numerator, split.denominator);
192    /// }
193    /// # Ok(())
194    /// # }
195    /// ```
196    pub async fn get_stock_splits(&self, symbol: &str) -> Result<Vec<StockSplit>> {
197        self.client
198            .get_with_query(
199                &format!("v3/historical-price-full/stock_split/{}", symbol),
200                &(),
201            )
202            .await
203    }
204
205    /// Get survivor bias free EOD prices
206    ///
207    /// This includes data for delisted companies, providing a more complete
208    /// picture of historical market data without survivor bias.
209    ///
210    /// # Arguments
211    /// * `date` - Date in YYYY-MM-DD format
212    ///
213    /// # Example
214    /// ```no_run
215    /// # use fmp_rs::FmpClient;
216    /// # #[tokio::main]
217    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
218    /// let client = FmpClient::new()?;
219    /// let prices = client.charts().get_survivor_bias_free_eod("2024-01-01").await?;
220    /// println!("Total symbols on 2024-01-01: {}", prices.len());
221    /// # Ok(())
222    /// # }
223    /// ```
224    pub async fn get_survivor_bias_free_eod(&self, date: &str) -> Result<Vec<HistoricalPrice>> {
225        self.client
226            .get_with_query(
227                &format!("v4/batch-request-end-of-day-prices?date={}", date),
228                &(),
229            )
230            .await
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_new() {
240        let client = FmpClient::builder().api_key("test_key").build().unwrap();
241        let _ = Charts::new(client);
242    }
243
244    // Golden path tests
245    #[tokio::test]
246    #[ignore = "requires FMP API key"]
247    async fn test_get_historical_prices() {
248        let client = FmpClient::new().unwrap();
249        let result = client
250            .charts()
251            .get_historical_prices("AAPL", None, None)
252            .await;
253        assert!(result.is_ok());
254        let prices = result.unwrap();
255        assert!(!prices.is_empty());
256    }
257
258    #[tokio::test]
259    #[ignore = "requires FMP API key"]
260    async fn test_get_historical_prices_with_date_range() {
261        let client = FmpClient::new().unwrap();
262        let result = client
263            .charts()
264            .get_historical_prices("AAPL", Some("2024-01-01"), Some("2024-12-31"))
265            .await;
266        assert!(result.is_ok());
267        let prices = result.unwrap();
268        assert!(!prices.is_empty());
269    }
270
271    #[tokio::test]
272    #[ignore = "requires FMP API key"]
273    async fn test_get_1min_prices() {
274        let client = FmpClient::new().unwrap();
275        let result = client.charts().get_1min_prices("AAPL", None, None).await;
276        assert!(result.is_ok());
277    }
278
279    #[tokio::test]
280    #[ignore = "requires FMP API key"]
281    async fn test_get_5min_prices() {
282        let client = FmpClient::new().unwrap();
283        let result = client.charts().get_5min_prices("AAPL", None, None).await;
284        assert!(result.is_ok());
285    }
286
287    #[tokio::test]
288    #[ignore = "requires FMP API key"]
289    async fn test_get_15min_prices() {
290        let client = FmpClient::new().unwrap();
291        let result = client.charts().get_15min_prices("AAPL", None, None).await;
292        assert!(result.is_ok());
293    }
294
295    #[tokio::test]
296    #[ignore = "requires FMP API key"]
297    async fn test_get_30min_prices() {
298        let client = FmpClient::new().unwrap();
299        let result = client.charts().get_30min_prices("AAPL", None, None).await;
300        assert!(result.is_ok());
301    }
302
303    #[tokio::test]
304    #[ignore = "requires FMP API key"]
305    async fn test_get_1hour_prices() {
306        let client = FmpClient::new().unwrap();
307        let result = client.charts().get_1hour_prices("AAPL", None, None).await;
308        assert!(result.is_ok());
309    }
310
311    #[tokio::test]
312    #[ignore = "requires FMP API key"]
313    async fn test_get_4hour_prices() {
314        let client = FmpClient::new().unwrap();
315        let result = client.charts().get_4hour_prices("AAPL", None, None).await;
316        assert!(result.is_ok());
317    }
318
319    #[tokio::test]
320    #[ignore = "requires FMP API key"]
321    async fn test_get_historical_dividends() {
322        let client = FmpClient::new().unwrap();
323        let result = client.charts().get_historical_dividends("AAPL").await;
324        assert!(result.is_ok());
325        let dividends = result.unwrap();
326        assert!(!dividends.is_empty());
327    }
328
329    #[tokio::test]
330    #[ignore = "requires FMP API key"]
331    async fn test_get_stock_splits() {
332        let client = FmpClient::new().unwrap();
333        let result = client.charts().get_stock_splits("AAPL").await;
334        assert!(result.is_ok());
335        // AAPL has had splits in the past
336    }
337
338    #[tokio::test]
339    #[ignore = "requires FMP API key"]
340    async fn test_get_survivor_bias_free_eod() {
341        let client = FmpClient::new().unwrap();
342        let result = client
343            .charts()
344            .get_survivor_bias_free_eod("2024-01-01")
345            .await;
346        assert!(result.is_ok());
347        let prices = result.unwrap();
348        assert!(!prices.is_empty());
349    }
350
351    // Edge case tests
352    #[tokio::test]
353    #[ignore = "requires FMP API key"]
354    async fn test_get_historical_prices_invalid_symbol() {
355        let client = FmpClient::new().unwrap();
356        let result = client
357            .charts()
358            .get_historical_prices("INVALID_SYMBOL_XYZ123", None, None)
359            .await;
360        // Should handle gracefully
361        if let Ok(prices) = result {
362            assert!(prices.is_empty());
363        }
364    }
365
366    #[tokio::test]
367    #[ignore = "requires FMP API key"]
368    async fn test_get_historical_dividends_no_dividends() {
369        let client = FmpClient::new().unwrap();
370        // Some symbols may not have dividends
371        let result = client.charts().get_historical_dividends("TSLA").await;
372        assert!(result.is_ok());
373        // May be empty if no dividends
374    }
375
376    #[tokio::test]
377    #[ignore = "requires FMP API key"]
378    async fn test_get_stock_splits_no_splits() {
379        let client = FmpClient::new().unwrap();
380        // Some symbols may not have had splits
381        let result = client.charts().get_stock_splits("MSFT").await;
382        assert!(result.is_ok());
383        // May be empty if no splits
384    }
385
386    #[tokio::test]
387    #[ignore = "requires FMP API key"]
388    async fn test_get_intraday_prices_with_date_range() {
389        let client = FmpClient::new().unwrap();
390        let result = client
391            .charts()
392            .get_intraday_prices(
393                "AAPL",
394                Timeframe::FiveMinutes,
395                Some("2024-01-01"),
396                Some("2024-01-02"),
397            )
398            .await;
399        assert!(result.is_ok());
400    }
401
402    // Error handling tests
403    #[tokio::test]
404    async fn test_invalid_api_key() {
405        let client = FmpClient::builder()
406            .api_key("invalid_key_12345")
407            .build()
408            .unwrap();
409        let result = client
410            .charts()
411            .get_historical_prices("AAPL", None, None)
412            .await;
413        assert!(result.is_err());
414    }
415
416    #[tokio::test]
417    async fn test_empty_symbol() {
418        let client = FmpClient::builder().api_key("test_key").build().unwrap();
419        let result = client.charts().get_historical_prices("", None, None).await;
420        // Should handle gracefully
421        assert!(result.is_err() || result.unwrap().is_empty());
422    }
423}