finance_query_core/streaming/
mod.rs1use crate::client::error::YahooError;
8use crate::client::YahooFinanceClient;
9use crate::models::SimpleQuote;
10use crate::websocket::QuotesUpdate;
11use async_stream::stream;
12use chrono::Utc;
13use futures_util::Stream;
14use serde_json::Value;
15use std::pin::Pin;
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::time::interval;
19use tracing::{debug, error};
20
21pub struct QuoteStream;
23
24impl QuoteStream {
25 pub fn create(
49 client: Arc<YahooFinanceClient>,
50 symbols: Vec<String>,
51 poll_interval: Duration,
52 ) -> Pin<Box<dyn Stream<Item = Result<QuotesUpdate, YahooError>> + Send>> {
53 Box::pin(stream! {
54 let mut ticker = interval(poll_interval);
55
56 loop {
57 ticker.tick().await;
58 debug!("Fetching quotes for {:?}", symbols);
59
60 let symbol_refs: Vec<&str> = symbols.iter().map(|s| s.as_str()).collect();
61
62 match client.get_simple_quotes(&symbol_refs).await {
63 Ok(data) => {
64 let quotes = parse_simple_quotes(&data);
65 let update = QuotesUpdate::with_timestamp(quotes, Utc::now());
66 yield Ok(update);
67 }
68 Err(e) => {
69 error!("Failed to fetch quotes: {}", e);
70 yield Err(e);
71 }
72 }
73 }
74 })
75 }
76
77 pub fn with_default_interval(
79 client: Arc<YahooFinanceClient>,
80 symbols: Vec<String>,
81 ) -> Pin<Box<dyn Stream<Item = Result<QuotesUpdate, YahooError>> + Send>> {
82 Self::create(client, symbols, Duration::from_secs(5))
83 }
84}
85
86pub struct SingleQuoteStream;
88
89impl SingleQuoteStream {
90 pub fn create(
92 client: Arc<YahooFinanceClient>,
93 symbol: String,
94 poll_interval: Duration,
95 ) -> Pin<Box<dyn Stream<Item = Result<SimpleQuote, YahooError>> + Send>> {
96 Box::pin(stream! {
97 let mut ticker = interval(poll_interval);
98
99 loop {
100 ticker.tick().await;
101 debug!("Fetching quote for {}", symbol);
102
103 match client.get_simple_quotes(&[symbol.as_str()]).await {
104 Ok(data) => {
105 let quotes = parse_simple_quotes(&data);
106 if let Some(quote) = quotes.into_iter().next() {
107 yield Ok(quote);
108 }
109 }
110 Err(e) => {
111 error!("Failed to fetch quote for {}: {}", symbol, e);
112 yield Err(e);
113 }
114 }
115 }
116 })
117 }
118}
119
120pub struct IndexStream;
122
123impl IndexStream {
124 pub fn create(
153 client: Arc<YahooFinanceClient>,
154 index_symbols: Vec<String>,
155 poll_interval: Duration,
156 ) -> Pin<Box<dyn Stream<Item = Result<Vec<crate::models::MarketIndex>, YahooError>> + Send>> {
157 Box::pin(stream! {
158 let mut ticker = interval(poll_interval);
159
160 loop {
161 ticker.tick().await;
162 debug!("Fetching index data for {:?}", index_symbols);
163
164 let symbol_refs: Vec<&str> = index_symbols.iter().map(|s| s.as_str()).collect();
165
166 match client.get_simple_quotes(&symbol_refs).await {
167 Ok(data) => {
168 let indices = parse_market_indices(&data);
169 yield Ok(indices);
170 }
171 Err(e) => {
172 error!("Failed to fetch index data: {}", e);
173 yield Err(e);
174 }
175 }
176 }
177 })
178 }
179
180 pub fn with_default_interval(
182 client: Arc<YahooFinanceClient>,
183 index_symbols: Vec<String>,
184 ) -> Pin<Box<dyn Stream<Item = Result<Vec<crate::models::MarketIndex>, YahooError>> + Send>> {
185 Self::create(client, index_symbols, Duration::from_secs(5))
186 }
187
188 pub fn us_major_indices(
190 client: Arc<YahooFinanceClient>,
191 poll_interval: Duration,
192 ) -> Pin<Box<dyn Stream<Item = Result<Vec<crate::models::MarketIndex>, YahooError>> + Send>> {
193 let symbols = vec![
194 "^GSPC".to_string(), "^DJI".to_string(), "^IXIC".to_string(), ];
198 Self::create(client, symbols, poll_interval)
199 }
200}
201
202fn get_quote_results(data: &Value) -> Option<&Vec<Value>> {
204 data.get("quoteResponse")
205 .and_then(|qr| qr.get("result"))
206 .and_then(|r| r.as_array())
207}
208
209fn get_string_field(result: &Value, field: &str, fallback: &str) -> String {
211 result.get(field)
212 .and_then(|s| s.as_str())
213 .unwrap_or(fallback)
214 .to_string()
215}
216
217fn get_name_field(result: &Value) -> String {
219 result.get("longName")
220 .or_else(|| result.get("shortName"))
221 .and_then(|n| n.as_str())
222 .unwrap_or("")
223 .to_string()
224}
225
226fn parse_simple_quotes(data: &Value) -> Vec<SimpleQuote> {
228 let Some(results) = get_quote_results(data) else {
229 return Vec::new();
230 };
231
232 results.iter().map(|result| {
233 SimpleQuote {
234 symbol: get_string_field(result, "symbol", ""),
235 name: get_name_field(result),
236 price: result.get("regularMarketPrice")
237 .and_then(|p| p.as_f64())
238 .map(|p| format!("{:.2}", p))
239 .unwrap_or_else(|| "0.00".to_string()),
240 pre_market_price: result.get("preMarketPrice")
241 .and_then(|p| p.as_f64())
242 .map(|p| format!("{:.2}", p)),
243 after_hours_price: result.get("postMarketPrice")
244 .and_then(|p| p.as_f64())
245 .map(|p| format!("{:.2}", p)),
246 change: result.get("regularMarketChange")
247 .and_then(|c| c.as_f64())
248 .map(|c| format!("{:+.2}", c))
249 .unwrap_or_else(|| "0.00".to_string()),
250 percent_change: result.get("regularMarketChangePercent")
251 .and_then(|p| p.as_f64())
252 .map(|p| format!("{:+.2}%", p))
253 .unwrap_or_else(|| "0.00%".to_string()),
254 logo: None,
255 }
256 }).collect()
257}
258
259pub struct MoversStream;
261
262impl MoversStream {
263 pub fn create(
290 client: Arc<YahooFinanceClient>,
291 count: crate::models::MoverCount,
292 poll_interval: Duration,
293 ) -> Pin<Box<dyn Stream<Item = Result<crate::websocket::MoversUpdate, YahooError>> + Send>> {
294 Box::pin(stream! {
295 let mut ticker = interval(poll_interval);
296
297 loop {
298 ticker.tick().await;
299 debug!("Fetching market movers (count: {})", count.as_str());
300
301 match client.get_movers(count).await {
302 Ok((actives, gainers, losers)) => {
303 let update = crate::websocket::MoversUpdate::with_timestamp(
304 actives,
305 gainers,
306 losers,
307 Utc::now()
308 );
309 yield Ok(update);
310 }
311 Err(e) => {
312 error!("Failed to fetch movers: {}", e);
313 yield Err(e);
314 }
315 }
316 }
317 })
318 }
319
320 pub fn with_default_interval(
322 client: Arc<YahooFinanceClient>,
323 count: crate::models::MoverCount,
324 ) -> Pin<Box<dyn Stream<Item = Result<crate::websocket::MoversUpdate, YahooError>> + Send>> {
325 Self::create(client, count, Duration::from_secs(5))
326 }
327
328 pub fn with_defaults(
330 client: Arc<YahooFinanceClient>,
331 ) -> Pin<Box<dyn Stream<Item = Result<crate::websocket::MoversUpdate, YahooError>> + Send>> {
332 Self::create(client, crate::models::MoverCount::default(), Duration::from_secs(5))
333 }
334}
335
336fn parse_market_indices(data: &Value) -> Vec<crate::models::MarketIndex> {
338 let Some(results) = get_quote_results(data) else {
339 return Vec::new();
340 };
341
342 results.iter().map(|result| {
343 let value = result.get("regularMarketPrice")
344 .and_then(|p| p.as_f64())
345 .unwrap_or(0.0);
346
347 let change = result.get("regularMarketChange")
348 .and_then(|c| c.as_f64())
349 .map(|c| format!("{:+.2}", c))
350 .unwrap_or_else(|| "0.00".to_string());
351
352 let percent_change = result.get("regularMarketChangePercent")
353 .and_then(|p| p.as_f64())
354 .map(|p| format!("{:+.2}%", p))
355 .unwrap_or_else(|| "0.00%".to_string());
356
357 crate::models::MarketIndex {
360 name: get_name_field(result),
361 value,
362 change,
363 percent_change,
364 five_days_return: None,
365 one_month_return: None,
366 three_month_return: None,
367 six_month_return: None,
368 ytd_return: None,
369 year_return: None,
370 three_year_return: None,
371 five_year_return: None,
372 ten_year_return: None,
373 max_return: None,
374 }
375 }).collect()
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_parse_simple_quotes() {
384 let data = serde_json::json!({
385 "quoteResponse": {
386 "result": [
387 {
388 "symbol": "AAPL",
389 "longName": "Apple Inc.",
390 "regularMarketPrice": 175.50,
391 "regularMarketChange": 2.50,
392 "regularMarketChangePercent": 1.45
393 }
394 ]
395 }
396 });
397
398 let quotes = parse_simple_quotes(&data);
399 assert_eq!(quotes.len(), 1);
400 assert_eq!(quotes[0].symbol, "AAPL");
401 assert_eq!(quotes[0].name, "Apple Inc.");
402 assert_eq!(quotes[0].price, "175.50");
403 assert_eq!(quotes[0].change, "+2.50");
404 assert_eq!(quotes[0].percent_change, "+1.45%");
405 }
406
407 #[test]
408 fn test_movers_stream_creation() {
409 use crate::models::MoverCount;
412
413 let _count_25 = MoverCount::TwentyFive;
416 let _count_50 = MoverCount::Fifty;
417 let _count_100 = MoverCount::Hundred;
418
419 assert_eq!(_count_25.as_str(), "25");
420 assert_eq!(_count_50.as_str(), "50");
421 assert_eq!(_count_100.as_str(), "100");
422 }
423}