finance_query/tickers/core/
quotes.rs1use super::{BatchQuotesResponse, Tickers};
2use crate::error::{FinanceError, Result};
3use crate::format::Both;
4use crate::models::format::Format;
5use crate::models::quote::{Quote, QuoteSummaryResponse};
6use crate::providers::Capability;
7use futures::stream::{self, StreamExt};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11impl Tickers {
12 pub async fn quotes(&self) -> Result<BatchQuotesResponse> {
20 {
22 let cache = self.quote_cache.read().await;
23 if self.all_cached(&cache, self.symbols.iter().cloned()) {
24 let mut response = BatchQuotesResponse::with_capacity(self.symbols.len());
25 for symbol in &self.symbols {
26 if let Some(entry) = cache.get(symbol) {
27 response
28 .quotes
29 .insert(symbol.to_string(), entry.value.clone());
30 }
31 }
32 return Ok(response);
33 }
34 }
35
36 let _fetch_guard = self.quotes_fetch.lock().await;
37
38 {
40 let cache = self.quote_cache.read().await;
41 if self.all_cached(&cache, self.symbols.iter().cloned()) {
42 let mut response = BatchQuotesResponse::with_capacity(self.symbols.len());
43 for symbol in &self.symbols {
44 if let Some(entry) = cache.get(symbol) {
45 response
46 .quotes
47 .insert(symbol.to_string(), entry.value.clone());
48 }
49 }
50 return Ok(response);
51 }
52 }
53
54 let symbol_strings: Vec<String> = self.symbols.iter().map(|s| s.to_string()).collect();
55 let mut response = BatchQuotesResponse::with_capacity(self.symbols.len());
56
57 let (quote_data, logos) = if self.include_logo {
58 let providers_logo = Arc::clone(&self.providers);
60 let syms_logo = symbol_strings.clone();
61 let logo_future = async move {
62 if let Ok(client) = providers_logo.first_yahoo() {
63 let syms_ref: Vec<&str> = syms_logo.iter().map(String::as_str).collect();
64 crate::adapters::yahoo::quote::quotes::fetch_with_fields(
65 &client,
66 &syms_ref,
67 Some(&["logoUrl", "companyLogoUrl"]),
68 true,
69 true,
70 )
71 .await
72 .ok()
73 } else {
74 None
75 }
76 };
77
78 let providers_quote = Arc::clone(&self.providers);
79 let syms_quote = symbol_strings.clone();
80 let quote_future = async move {
81 providers_quote
82 .fetch(Capability::QUOTE, |p| {
83 let syms = syms_quote.clone();
84 let p = p.clone();
85 async move {
86 let syms_ref: Vec<&str> = syms.iter().map(String::as_str).collect();
87 p.as_quote()
88 .ok_or_else(|| {
89 p.not_supported(crate::providers::Operation::QuotesBatch)
90 })?
91 .fetch_quotes_batch(&syms_ref)
92 .await
93 }
94 })
95 .await
96 };
97
98 let (batch_result, logo_result) = tokio::join!(quote_future, logo_future);
99 let quote_data = match batch_result {
100 Ok(data) => data,
101 Err(_) => {
102 self.fetch_quotes_per_symbol(&symbol_strings, &mut response)
103 .await
104 }
105 };
106 (quote_data, logo_result)
107 } else {
108 let providers = Arc::clone(&self.providers);
109 let syms = symbol_strings.clone();
110 let batch_result = providers
111 .fetch(Capability::QUOTE, |p| {
112 let syms = syms.clone();
113 let p = p.clone();
114 async move {
115 let syms_ref: Vec<&str> = syms.iter().map(String::as_str).collect();
116 p.as_quote()
117 .ok_or_else(|| {
118 p.not_supported(crate::providers::Operation::QuotesBatch)
119 })?
120 .fetch_quotes_batch(&syms_ref)
121 .await
122 }
123 })
124 .await;
125 let data = match batch_result {
126 Ok(data) => data,
127 Err(_) => {
128 self.fetch_quotes_per_symbol(&symbol_strings, &mut response)
129 .await
130 }
131 };
132 (data, None)
133 };
134
135 let logo_map: HashMap<String, (Option<String>, Option<String>)> = logos
136 .and_then(|l| l.get("quoteResponse")?.get("result")?.as_array().cloned())
137 .map(|results| {
138 results
139 .iter()
140 .filter_map(|r| {
141 let symbol = r.get("symbol")?.as_str()?.to_string();
142 let logo_url = r.get("logoUrl").and_then(|v| v.as_str()).map(String::from);
143 let company_logo_url = r
144 .get("companyLogoUrl")
145 .and_then(|v| v.as_str())
146 .map(String::from);
147 Some((symbol, (logo_url, company_logo_url)))
148 })
149 .collect()
150 })
151 .unwrap_or_default();
152
153 let mut parsed_quotes: Vec<(String, Quote)> = Vec::new();
154
155 for (symbol, summary) in quote_data {
156 let logo_url = logo_map.get(&symbol).and_then(|(l, _)| l.clone());
157 let company_logo_url = logo_map.get(&symbol).and_then(|(_, c)| c.clone());
158 let quote = Quote::from_response(&summary, logo_url, company_logo_url);
159 parsed_quotes.push((symbol, quote));
160 }
161
162 for (symbol, quote) in parsed_quotes {
163 response.quotes.insert(symbol, quote);
164 }
165
166 #[cfg(feature = "translation")]
169 self.translate_response(&mut response).await?;
170
171 if self.cache_mode.enabled() {
172 let mut cache = self.quote_cache.write().await;
173 for (symbol, quote) in &response.quotes {
174 self.cache_insert(&mut cache, symbol.as_str().into(), quote.clone());
175 }
176 }
177
178 for symbol in &self.symbols {
180 let s = &**symbol;
181 if !response.quotes.contains_key(s) && !response.errors.contains_key(s) {
182 response.errors.insert(
183 symbol.to_string(),
184 "Symbol not found in response".to_string(),
185 );
186 }
187 }
188
189 Ok(response)
190 }
191
192 async fn fetch_quotes_per_symbol(
195 &self,
196 symbols: &[String],
197 response: &mut BatchQuotesResponse,
198 ) -> Vec<(String, QuoteSummaryResponse)> {
199 let futures: Vec<_> = symbols
200 .iter()
201 .map(|sym| {
202 let providers = Arc::clone(&self.providers);
203 let sym = sym.clone();
204 async move {
205 let result = providers
206 .fetch(Capability::QUOTE, |p| {
207 let sym = sym.clone();
208 let p = p.clone();
209 async move {
210 p.as_quote()
211 .ok_or_else(|| {
212 p.not_supported(crate::providers::Operation::Quote)
213 })?
214 .fetch_quote(&sym)
215 .await
216 }
217 })
218 .await;
219 (sym, result)
220 }
221 })
222 .collect();
223
224 let results: Vec<_> = stream::iter(futures)
225 .buffer_unordered(self.max_concurrency)
226 .collect()
227 .await;
228
229 let mut successes = Vec::new();
230 for (sym, result) in results {
231 match result {
232 Ok(resp) => successes.push((sym, resp)),
233 Err(e) => {
234 response.errors.insert(sym, e.to_string());
235 }
236 }
237 }
238 successes
239 }
240
241 pub async fn quote<F>(&self, symbol: &str) -> Result<Quote<F>>
243 where
244 F: Format,
245 Quote<Both>: Into<Quote<F>>,
246 {
247 {
248 let cache = self.quote_cache.read().await;
249 if let Some(entry) = cache.get(symbol)
250 && self.is_cache_fresh(Some(entry))
251 {
252 return Ok(entry.value.clone().into());
253 }
254 }
255
256 let response = self.quotes().await?;
257
258 response
259 .quotes
260 .get(symbol)
261 .cloned()
262 .map(Into::into)
263 .ok_or_else(|| FinanceError::SymbolNotFound {
264 symbol: Some(symbol.to_string()),
265 context: response
266 .errors
267 .get(symbol)
268 .cloned()
269 .unwrap_or_else(|| "Symbol not found".to_string()),
270 })
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[tokio::test]
279 #[ignore = "requires network access"]
280 async fn test_tickers_quotes() {
281 let tickers = Tickers::new(["AAPL", "MSFT", "GOOGL"]).await.unwrap();
282 let result = tickers.quotes().await.unwrap();
283
284 assert!(result.success_count() > 0);
285 }
286}