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 new(
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::new(client, symbols, Duration::from_secs(5))
83 }
84}
85
86pub struct SingleQuoteStream;
88
89impl SingleQuoteStream {
90 pub fn new(
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
120fn parse_simple_quotes(data: &Value) -> Vec<SimpleQuote> {
122 let mut quotes = Vec::new();
123
124 if let Some(results) = data
125 .get("quoteResponse")
126 .and_then(|qr| qr.get("result"))
127 .and_then(|r| r.as_array())
128 {
129 for result in results {
130 let quote = SimpleQuote {
131 symbol: result.get("symbol")
132 .and_then(|s| s.as_str())
133 .unwrap_or("")
134 .to_string(),
135 name: result.get("longName")
136 .or_else(|| result.get("shortName"))
137 .and_then(|n| n.as_str())
138 .unwrap_or("")
139 .to_string(),
140 price: result.get("regularMarketPrice")
141 .and_then(|p| p.as_f64())
142 .map(|p| format!("{:.2}", p))
143 .unwrap_or_else(|| "0.00".to_string()),
144 pre_market_price: result.get("preMarketPrice")
145 .and_then(|p| p.as_f64())
146 .map(|p| format!("{:.2}", p)),
147 after_hours_price: result.get("postMarketPrice")
148 .and_then(|p| p.as_f64())
149 .map(|p| format!("{:.2}", p)),
150 change: result.get("regularMarketChange")
151 .and_then(|c| c.as_f64())
152 .map(|c| format!("{:+.2}", c))
153 .unwrap_or_else(|| "0.00".to_string()),
154 percent_change: result.get("regularMarketChangePercent")
155 .and_then(|p| p.as_f64())
156 .map(|p| format!("{:+.2}%", p))
157 .unwrap_or_else(|| "0.00%".to_string()),
158 logo: None,
159 };
160 quotes.push(quote);
161 }
162 }
163
164 quotes
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_parse_simple_quotes() {
173 let data = serde_json::json!({
174 "quoteResponse": {
175 "result": [
176 {
177 "symbol": "AAPL",
178 "longName": "Apple Inc.",
179 "regularMarketPrice": 175.50,
180 "regularMarketChange": 2.50,
181 "regularMarketChangePercent": 1.45
182 }
183 ]
184 }
185 });
186
187 let quotes = parse_simple_quotes(&data);
188 assert_eq!(quotes.len(), 1);
189 assert_eq!(quotes[0].symbol, "AAPL");
190 assert_eq!(quotes[0].name, "Apple Inc.");
191 assert_eq!(quotes[0].price, "175.50");
192 assert_eq!(quotes[0].change, "+2.50");
193 assert_eq!(quotes[0].percent_change, "+1.45%");
194 }
195}