finance_query/adapters/fmp/
prices.rs1use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::models::{FmpQuote, HistoricalPriceResponse, IntradayPrice};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct StockPriceChange {
18 pub symbol: Option<String>,
20 #[serde(rename = "1D")]
22 pub one_day: Option<f64>,
23 #[serde(rename = "5D")]
25 pub five_day: Option<f64>,
26 #[serde(rename = "1M")]
28 pub one_month: Option<f64>,
29 #[serde(rename = "3M")]
31 pub three_month: Option<f64>,
32 #[serde(rename = "6M")]
34 pub six_month: Option<f64>,
35 pub ytd: Option<f64>,
37 #[serde(rename = "1Y")]
39 pub one_year: Option<f64>,
40 #[serde(rename = "3Y")]
42 pub three_year: Option<f64>,
43 #[serde(rename = "5Y")]
45 pub five_year: Option<f64>,
46 #[serde(rename = "10Y")]
48 pub ten_year: Option<f64>,
49 pub max: Option<f64>,
51}
52
53#[derive(Debug, Clone, Default)]
55pub struct HistoricalPriceParams {
56 pub from: Option<String>,
58 pub to: Option<String>,
60}
61
62pub async fn quote(symbol: &str) -> Result<Vec<FmpQuote>> {
68 let client = super::build_client()?;
69 client
70 .get(
71 &format!("/api/v3/quote/{}", encode_path_segment(symbol)),
72 &[],
73 )
74 .await
75}
76
77pub async fn batch_quote(symbols: &[&str]) -> Result<Vec<FmpQuote>> {
79 let client = super::build_client()?;
80 let joined = symbols.join(",");
81 client
82 .get(
83 &format!("/api/v3/quote/{}", encode_path_segment(&joined)),
84 &[],
85 )
86 .await
87}
88
89pub async fn stock_price(symbol: &str) -> Result<Vec<StockPriceChange>> {
91 let client = super::build_client()?;
92 client
93 .get(
94 &format!("/api/v3/stock-price-change/{}", encode_path_segment(symbol)),
95 &[],
96 )
97 .await
98}
99
100pub async fn historical_price_daily(
102 symbol: &str,
103 params: Option<HistoricalPriceParams>,
104) -> Result<HistoricalPriceResponse> {
105 let client = super::build_client()?;
106 let p = params.unwrap_or_default();
107 let mut query_params: Vec<(&str, &str)> = Vec::new();
108 if let Some(ref from) = p.from {
109 query_params.push(("from", from));
110 }
111 if let Some(ref to) = p.to {
112 query_params.push(("to", to));
113 }
114 client
115 .get(
116 &format!(
117 "/api/v3/historical-price-full/{}",
118 encode_path_segment(symbol)
119 ),
120 &query_params,
121 )
122 .await
123}
124
125pub async fn historical_price_intraday(
129 symbol: &str,
130 interval: &str,
131 params: Option<HistoricalPriceParams>,
132) -> Result<Vec<IntradayPrice>> {
133 let client = super::build_client()?;
134 let p = params.unwrap_or_default();
135 let mut query_params: Vec<(&str, &str)> = Vec::new();
136 if let Some(ref from) = p.from {
137 query_params.push(("from", from));
138 }
139 if let Some(ref to) = p.to {
140 query_params.push(("to", to));
141 }
142 client
143 .get(
144 &format!(
145 "/api/v3/historical-chart/{}/{}",
146 encode_path_segment(interval),
147 encode_path_segment(symbol)
148 ),
149 &query_params,
150 )
151 .await
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[tokio::test]
159 async fn test_quote_mock() {
160 let mut server = mockito::Server::new_async().await;
161 let _mock = server
162 .mock("GET", "/api/v3/quote/AAPL")
163 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
164 "apikey".into(),
165 "test-key".into(),
166 )]))
167 .with_status(200)
168 .with_body(
169 serde_json::json!([{
170 "symbol": "AAPL",
171 "name": "Apple Inc.",
172 "price": 178.72,
173 "change": 2.15,
174 "changesPercentage": 1.22,
175 "dayLow": 176.21,
176 "dayHigh": 179.63,
177 "yearLow": 124.17,
178 "yearHigh": 199.62,
179 "marketCap": 2794000000000_f64,
180 "volume": 58405568,
181 "avgVolume": 54638267,
182 "open": 177.09,
183 "previousClose": 176.57,
184 "eps": 6.42,
185 "pe": 27.84,
186 "timestamp": 1701460800,
187 "exchange": "NASDAQ"
188 }])
189 .to_string(),
190 )
191 .create_async()
192 .await;
193
194 let client = super::super::build_test_client(&server.url()).unwrap();
195 let result: Vec<FmpQuote> = client.get("/api/v3/quote/AAPL", &[]).await.unwrap();
196
197 assert_eq!(result.len(), 1);
198 assert_eq!(result[0].symbol, "AAPL");
199 assert_eq!(result[0].name.as_deref(), Some("Apple Inc."));
200 assert_eq!(result[0].price, Some(178.72));
201 assert_eq!(result[0].pe, Some(27.84));
202 }
203
204 #[tokio::test]
205 async fn test_historical_price_daily_mock() {
206 let mut server = mockito::Server::new_async().await;
207 let _mock = server
208 .mock("GET", "/api/v3/historical-price-full/AAPL")
209 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
210 "apikey".into(),
211 "test-key".into(),
212 )]))
213 .with_status(200)
214 .with_body(
215 serde_json::json!({
216 "symbol": "AAPL",
217 "historical": [
218 {
219 "date": "2024-01-02",
220 "open": 187.15,
221 "high": 188.44,
222 "low": 183.89,
223 "close": 185.64,
224 "adjClose": 184.96,
225 "volume": 82488700,
226 "unadjustedVolume": 82488700,
227 "change": -1.51,
228 "changePercent": -0.8068,
229 "vwap": 185.99,
230 "label": "January 02, 2024",
231 "changeOverTime": -0.008068
232 },
233 {
234 "date": "2024-01-03",
235 "open": 184.22,
236 "high": 185.88,
237 "low": 183.43,
238 "close": 184.25,
239 "adjClose": 183.57,
240 "volume": 58414500,
241 "unadjustedVolume": 58414500,
242 "change": 0.03,
243 "changePercent": 0.0163,
244 "vwap": 184.52,
245 "label": "January 03, 2024",
246 "changeOverTime": 0.000163
247 }
248 ]
249 })
250 .to_string(),
251 )
252 .create_async()
253 .await;
254
255 let client = super::super::build_test_client(&server.url()).unwrap();
256 let result: HistoricalPriceResponse = client
257 .get("/api/v3/historical-price-full/AAPL", &[])
258 .await
259 .unwrap();
260
261 assert_eq!(result.symbol.as_deref(), Some("AAPL"));
262 assert_eq!(result.historical.len(), 2);
263 assert_eq!(result.historical[0].date.as_deref(), Some("2024-01-02"));
264 assert_eq!(result.historical[0].close, Some(185.64));
265 }
266
267 #[tokio::test]
268 async fn test_intraday_price_mock() {
269 let mut server = mockito::Server::new_async().await;
270 let _mock = server
271 .mock("GET", "/api/v3/historical-chart/5min/AAPL")
272 .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
273 "apikey".into(),
274 "test-key".into(),
275 )]))
276 .with_status(200)
277 .with_body(
278 serde_json::json!([
279 {
280 "date": "2024-01-02 09:30:00",
281 "open": 187.15,
282 "high": 187.44,
283 "low": 186.89,
284 "close": 187.20,
285 "volume": 1234567
286 },
287 {
288 "date": "2024-01-02 09:35:00",
289 "open": 187.20,
290 "high": 187.50,
291 "low": 187.10,
292 "close": 187.35,
293 "volume": 987654
294 }
295 ])
296 .to_string(),
297 )
298 .create_async()
299 .await;
300
301 let client = super::super::build_test_client(&server.url()).unwrap();
302 let result: Vec<IntradayPrice> = client
303 .get("/api/v3/historical-chart/5min/AAPL", &[])
304 .await
305 .unwrap();
306
307 assert_eq!(result.len(), 2);
308 assert_eq!(result[0].date.as_deref(), Some("2024-01-02 09:30:00"));
309 assert_eq!(result[0].close, Some(187.20));
310 }
311}