finance_query/tickers/core/
charts.rs1use super::{BatchChartsResponse, BatchSparksResponse, Tickers};
2use crate::constants::{Interval, TimeRange};
3use crate::error::{FinanceError, Result};
4use crate::models::chart::Chart;
5use crate::providers::Capability;
6use futures::stream::{self, StreamExt};
7use std::sync::Arc;
8
9impl Tickers {
10 pub async fn charts(
15 &self,
16 interval: Interval,
17 range: TimeRange,
18 ) -> Result<BatchChartsResponse> {
19 {
21 let cache = self.chart_cache.read().await;
22 if self.all_cached(
23 &cache,
24 self.symbols.iter().map(|s| (s.clone(), interval, range)),
25 ) {
26 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
27 for symbol in &self.symbols {
28 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
29 response
30 .charts
31 .insert(symbol.to_string(), entry.value.clone());
32 }
33 }
34 return Ok(response);
35 }
36 }
37
38 let fetch_guard = Self::get_fetch_guard(&self.charts_fetch, (interval, range)).await;
40 let _guard = fetch_guard.lock().await;
41
42 {
44 let cache = self.chart_cache.read().await;
45 if self.all_cached(
46 &cache,
47 self.symbols.iter().map(|s| (s.clone(), interval, range)),
48 ) {
49 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
50 for symbol in &self.symbols {
51 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
52 response
53 .charts
54 .insert(symbol.to_string(), entry.value.clone());
55 }
56 }
57 return Ok(response);
58 }
59 }
60
61 let futures: Vec<_> = self
63 .symbols
64 .iter()
65 .map(|symbol| {
66 let providers = Arc::clone(&self.providers);
67 let symbol = Arc::clone(symbol);
68 async move {
69 let sym = symbol.to_string();
70 let result = providers
71 .fetch(Capability::CHART, |p| {
72 let sym = sym.clone();
73 let p = p.clone();
74 async move {
75 p.as_chart()
76 .ok_or_else(|| {
77 p.not_supported(crate::providers::Operation::Chart)
78 })?
79 .fetch_chart(&sym, interval, range)
80 .await
81 }
82 })
83 .await;
84 (symbol, result)
85 }
86 })
87 .collect();
88
89 let results: Vec<_> = stream::iter(futures)
90 .buffer_unordered(self.max_concurrency)
91 .collect()
92 .await;
93
94 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
95 let mut parsed_charts: Vec<(Arc<str>, Chart)> = Vec::new();
96
97 for (symbol, result) in results {
98 match result {
99 Ok(data) => {
100 let chart = data;
101 parsed_charts.push((symbol, chart));
102 }
103 Err(e) => {
104 response.errors.insert(symbol.to_string(), e.to_string());
105 }
106 }
107 }
108
109 if self.cache_mode.enabled() {
111 let mut cache = self.chart_cache.write().await;
112 let cache_keys: Vec<_> = parsed_charts
113 .into_iter()
114 .map(|(symbol, chart)| {
115 self.cache_insert(&mut cache, (symbol.clone(), interval, range), chart);
116 symbol
117 })
118 .collect();
119 for symbol in cache_keys {
120 if let Some(cached) = cache.get(&(symbol.clone(), interval, range)) {
121 response
122 .charts
123 .insert(symbol.to_string(), cached.value.clone());
124 }
125 }
126 } else {
127 for (symbol, chart) in parsed_charts {
128 response.charts.insert(symbol.to_string(), chart);
129 }
130 }
131
132 Ok(response)
133 }
134
135 pub async fn chart(&self, symbol: &str, interval: Interval, range: TimeRange) -> Result<Chart> {
137 {
138 let cache = self.chart_cache.read().await;
139 let key: Arc<str> = symbol.into();
140 if let Some(entry) = cache.get(&(key, interval, range))
141 && self.is_cache_fresh(Some(entry))
142 {
143 return Ok(entry.value.clone());
144 }
145 }
146
147 let response = self.charts(interval, range).await?;
148
149 response
150 .charts
151 .get(symbol)
152 .cloned()
153 .ok_or_else(|| FinanceError::SymbolNotFound {
154 symbol: Some(symbol.to_string()),
155 context: response
156 .errors
157 .get(symbol)
158 .cloned()
159 .unwrap_or_else(|| "Symbol not found".to_string()),
160 })
161 }
162
163 pub async fn charts_range(
175 &self,
176 interval: Interval,
177 start: i64,
178 end: i64,
179 ) -> Result<BatchChartsResponse> {
180 let futures: Vec<_> = self
181 .symbols
182 .iter()
183 .map(|symbol| {
184 let providers = Arc::clone(&self.providers);
185 let symbol = Arc::clone(symbol);
186 async move {
187 let sym = symbol.to_string();
188 let result = providers
189 .fetch(Capability::CHART, |p| {
190 let sym = sym.clone();
191 let p = p.clone();
192 async move {
193 p.as_chart()
194 .ok_or_else(|| {
195 p.not_supported(crate::providers::Operation::ChartRange)
196 })?
197 .fetch_chart_range(&sym, interval, start, end)
198 .await
199 }
200 })
201 .await;
202 (symbol, result)
203 }
204 })
205 .collect();
206
207 let results: Vec<_> = stream::iter(futures)
208 .buffer_unordered(self.max_concurrency)
209 .collect()
210 .await;
211
212 let mut response = BatchChartsResponse::with_capacity(self.symbols.len());
213
214 for (symbol, result) in results {
215 match result {
216 Ok(data) => {
217 let chart = data;
218 response.charts.insert(symbol.to_string(), chart);
219 }
220 Err(e) => {
221 response.errors.insert(symbol.to_string(), e.to_string());
222 }
223 }
224 }
225
226 Ok(response)
227 }
228
229 pub async fn spark(&self, interval: Interval, range: TimeRange) -> Result<BatchSparksResponse> {
258 {
260 let cache = self.spark_cache.read().await;
261 if self.all_cached(
262 &cache,
263 self.symbols.iter().map(|s| (s.clone(), interval, range)),
264 ) {
265 let mut response = BatchSparksResponse::with_capacity(self.symbols.len());
266 for symbol in &self.symbols {
267 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
268 response
269 .sparks
270 .insert(symbol.to_string(), entry.value.clone());
271 }
272 }
273 return Ok(response);
274 }
275 }
276
277 let fetch_guard = Self::get_fetch_guard(&self.spark_fetch, (interval, range)).await;
279 let _guard = fetch_guard.lock().await;
280
281 {
283 let cache = self.spark_cache.read().await;
284 if self.all_cached(
285 &cache,
286 self.symbols.iter().map(|s| (s.clone(), interval, range)),
287 ) {
288 let mut response = BatchSparksResponse::with_capacity(self.symbols.len());
289 for symbol in &self.symbols {
290 if let Some(entry) = cache.get(&(symbol.clone(), interval, range)) {
291 response
292 .sparks
293 .insert(symbol.to_string(), entry.value.clone());
294 }
295 }
296 return Ok(response);
297 }
298 }
299
300 let providers = Arc::clone(&self.providers);
303 let syms: Vec<String> = self.symbols.iter().map(|s| s.to_string()).collect();
304 let spark_result = providers
305 .fetch(Capability::CHART, |p| {
306 let syms = syms.clone();
307 let p = p.clone();
308 async move {
309 let syms_ref: Vec<&str> = syms.iter().map(String::as_str).collect();
310 p.as_chart()
311 .ok_or_else(|| p.not_supported(crate::providers::Operation::Spark))?
312 .fetch_spark(&syms_ref, interval, range)
313 .await
314 }
315 })
316 .await;
317
318 let mut response = BatchSparksResponse::with_capacity(self.symbols.len());
319
320 match spark_result {
321 Ok(parsed_sparks) => {
322 if self.cache_mode.enabled() {
324 let mut cache = self.spark_cache.write().await;
325 for (symbol, spark) in &parsed_sparks {
326 let key: Arc<str> = symbol.as_str().into();
327 self.cache_insert(&mut cache, (key, interval, range), spark.clone());
328 }
329 }
330
331 for (symbol, spark) in parsed_sparks {
333 response.sparks.insert(symbol, spark);
334 }
335
336 for symbol in &self.symbols {
338 let symbol_str = &**symbol;
339 if !response.sparks.contains_key(symbol_str)
340 && !response.errors.contains_key(symbol_str)
341 {
342 response.errors.insert(
343 symbol.to_string(),
344 "Symbol not found in response".to_string(),
345 );
346 }
347 }
348 }
349 Err(e) => {
350 for symbol in &self.symbols {
351 response.errors.insert(symbol.to_string(), e.to_string());
352 }
353 }
354 }
355
356 Ok(response)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[tokio::test]
365 #[ignore = "requires network access"]
366 async fn test_tickers_charts() {
367 let tickers = Tickers::new(["AAPL", "MSFT"]).await.unwrap();
368 let result = tickers
369 .charts(Interval::OneDay, TimeRange::FiveDays)
370 .await
371 .unwrap();
372
373 assert!(result.success_count() > 0);
374 }
375
376 #[tokio::test]
377 #[ignore = "requires network access"]
378 async fn test_tickers_spark() {
379 let tickers = Tickers::new(["AAPL", "MSFT", "GOOGL"]).await.unwrap();
380 let result = tickers
381 .spark(Interval::FiveMinutes, TimeRange::OneDay)
382 .await
383 .unwrap();
384
385 assert!(result.success_count() > 0);
386
387 if let Some(spark) = result.sparks.get("AAPL") {
389 assert!(!spark.closes.is_empty());
390 assert_eq!(spark.symbol, "AAPL");
391 assert!(spark.percent_change().is_some());
393 }
394 }
395}