1use crate::client::FmpClient;
4use crate::error::Result;
5use crate::models::charts::{HistoricalDividend, HistoricalPrice, IntradayPrice, StockSplit};
6use crate::models::common::Timeframe;
7use serde::Serialize;
8
9pub struct Charts {
11 client: FmpClient,
12}
13
14impl Charts {
15 pub(crate) fn new(client: FmpClient) -> Self {
16 Self { client }
17 }
18
19 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 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 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 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 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 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 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 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 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 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 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 #[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 }
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 #[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 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 let result = client.charts().get_historical_dividends("TSLA").await;
372 assert!(result.is_ok());
373 }
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 let result = client.charts().get_stock_splits("MSFT").await;
382 assert!(result.is_ok());
383 }
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 #[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 assert!(result.is_err() || result.unwrap().is_empty());
422 }
423}