1use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9
10use chrono::{NaiveDate, Utc};
11use reqwest::{Client, StatusCode};
12use serde::Deserialize;
13use tokio::time::sleep;
14
15use crate::api_clients::SimpleEmbedder;
16use crate::ruvector_native::{Domain, SemanticVector};
17use crate::{FrameworkError, Result};
18
19const FINNHUB_RATE_LIMIT_MS: u64 = 1000; const TWELVEDATA_RATE_LIMIT_MS: u64 = 120; const COINGECKO_RATE_LIMIT_MS: u64 = 1200; const ECB_RATE_LIMIT_MS: u64 = 100; const BLS_RATE_LIMIT_MS: u64 = 600; const MAX_RETRIES: u32 = 3;
26const RETRY_DELAY_MS: u64 = 1000;
27
28#[derive(Debug, Deserialize)]
34struct FinnhubQuote {
35 #[serde(rename = "c")]
36 current_price: f64,
37 #[serde(rename = "h")]
38 high: f64,
39 #[serde(rename = "l")]
40 low: f64,
41 #[serde(rename = "o")]
42 open: f64,
43 #[serde(rename = "pc")]
44 previous_close: f64,
45 #[serde(rename = "t")]
46 timestamp: i64,
47}
48
49#[derive(Debug, Deserialize)]
51struct FinnhubSearchResponse {
52 #[serde(default)]
53 result: Vec<FinnhubSymbol>,
54}
55
56#[derive(Debug, Deserialize)]
57struct FinnhubSymbol {
58 description: String,
59 #[serde(rename = "displaySymbol")]
60 display_symbol: String,
61 symbol: String,
62 #[serde(rename = "type")]
63 symbol_type: String,
64}
65
66#[derive(Debug, Deserialize)]
68struct FinnhubNews {
69 category: String,
70 datetime: i64,
71 headline: String,
72 #[serde(default)]
73 summary: String,
74 source: String,
75 url: String,
76}
77
78#[derive(Debug, Deserialize)]
80struct FinnhubCryptoSymbol {
81 description: String,
82 #[serde(rename = "displaySymbol")]
83 display_symbol: String,
84 symbol: String,
85}
86
87pub struct FinnhubClient {
101 client: Client,
102 base_url: String,
103 api_key: Option<String>,
104 rate_limit_delay: Duration,
105 embedder: Arc<SimpleEmbedder>,
106}
107
108impl FinnhubClient {
109 pub fn new(api_key: Option<String>) -> Result<Self> {
115 let client = Client::builder()
116 .timeout(Duration::from_secs(30))
117 .build()
118 .map_err(FrameworkError::Network)?;
119
120 Ok(Self {
121 client,
122 base_url: "https://finnhub.io/api/v1".to_string(),
123 api_key,
124 rate_limit_delay: Duration::from_millis(FINNHUB_RATE_LIMIT_MS),
125 embedder: Arc::new(SimpleEmbedder::new(256)),
126 })
127 }
128
129 pub async fn get_quote(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
134 if self.api_key.is_none() {
136 return self.get_mock_quote(symbol);
137 }
138
139 let url = format!(
140 "{}/quote?symbol={}&token={}",
141 self.base_url,
142 symbol,
143 self.api_key.as_ref().unwrap()
144 );
145
146 sleep(self.rate_limit_delay).await;
147 let response = self.fetch_with_retry(&url).await?;
148 let quote: FinnhubQuote = response.json().await?;
149
150 let text = format!(
151 "{} stock quote: ${} (open: ${}, high: ${}, low: ${})",
152 symbol, quote.current_price, quote.open, quote.high, quote.low
153 );
154 let embedding = self.embedder.embed_text(&text);
155
156 let mut metadata = HashMap::new();
157 metadata.insert("symbol".to_string(), symbol.to_string());
158 metadata.insert("current_price".to_string(), quote.current_price.to_string());
159 metadata.insert("open".to_string(), quote.open.to_string());
160 metadata.insert("high".to_string(), quote.high.to_string());
161 metadata.insert("low".to_string(), quote.low.to_string());
162 metadata.insert("previous_close".to_string(), quote.previous_close.to_string());
163 metadata.insert("source".to_string(), "finnhub".to_string());
164
165 let timestamp = chrono::DateTime::from_timestamp(quote.timestamp, 0)
166 .unwrap_or_else(Utc::now);
167
168 Ok(vec![SemanticVector {
169 id: format!("FINNHUB:QUOTE:{}:{}", symbol, quote.timestamp),
170 embedding,
171 domain: Domain::Finance,
172 timestamp,
173 metadata,
174 }])
175 }
176
177 pub async fn search_symbols(&self, query: &str) -> Result<Vec<SemanticVector>> {
182 if self.api_key.is_none() {
184 return self.get_mock_symbols(query);
185 }
186
187 let url = format!(
188 "{}/search?q={}&token={}",
189 self.base_url,
190 urlencoding::encode(query),
191 self.api_key.as_ref().unwrap()
192 );
193
194 sleep(self.rate_limit_delay).await;
195 let response = self.fetch_with_retry(&url).await?;
196 let search_response: FinnhubSearchResponse = response.json().await?;
197
198 let mut vectors = Vec::new();
199 for symbol in search_response.result.iter().take(20) {
200 let text = format!(
201 "{} ({}) - {} - Type: {}",
202 symbol.description, symbol.display_symbol, symbol.symbol, symbol.symbol_type
203 );
204 let embedding = self.embedder.embed_text(&text);
205
206 let mut metadata = HashMap::new();
207 metadata.insert("symbol".to_string(), symbol.symbol.clone());
208 metadata.insert("display_symbol".to_string(), symbol.display_symbol.clone());
209 metadata.insert("description".to_string(), symbol.description.clone());
210 metadata.insert("type".to_string(), symbol.symbol_type.clone());
211 metadata.insert("source".to_string(), "finnhub_search".to_string());
212
213 vectors.push(SemanticVector {
214 id: format!("FINNHUB:SYMBOL:{}", symbol.symbol),
215 embedding,
216 domain: Domain::Finance,
217 timestamp: Utc::now(),
218 metadata,
219 });
220 }
221
222 Ok(vectors)
223 }
224
225 pub async fn get_company_news(
232 &self,
233 symbol: &str,
234 from: &str,
235 to: &str,
236 ) -> Result<Vec<SemanticVector>> {
237 if self.api_key.is_none() {
239 return self.get_mock_news(symbol);
240 }
241
242 let url = format!(
243 "{}/company-news?symbol={}&from={}&to={}&token={}",
244 self.base_url,
245 symbol,
246 from,
247 to,
248 self.api_key.as_ref().unwrap()
249 );
250
251 sleep(self.rate_limit_delay).await;
252 let response = self.fetch_with_retry(&url).await?;
253 let news_items: Vec<FinnhubNews> = response.json().await?;
254
255 let mut vectors = Vec::new();
256 for news in news_items.iter().take(50) {
257 let text = format!("{} - {} - {}", news.headline, news.summary, news.category);
258 let embedding = self.embedder.embed_text(&text);
259
260 let mut metadata = HashMap::new();
261 metadata.insert("symbol".to_string(), symbol.to_string());
262 metadata.insert("headline".to_string(), news.headline.clone());
263 metadata.insert("category".to_string(), news.category.clone());
264 metadata.insert("source".to_string(), news.source.clone());
265 metadata.insert("url".to_string(), news.url.clone());
266
267 let timestamp = chrono::DateTime::from_timestamp(news.datetime, 0)
268 .unwrap_or_else(Utc::now);
269
270 vectors.push(SemanticVector {
271 id: format!("FINNHUB:NEWS:{}:{}", symbol, news.datetime),
272 embedding,
273 domain: Domain::Finance,
274 timestamp,
275 metadata,
276 });
277 }
278
279 Ok(vectors)
280 }
281
282 pub async fn get_crypto_symbols(&self) -> Result<Vec<SemanticVector>> {
284 if self.api_key.is_none() {
286 return self.get_mock_crypto_symbols();
287 }
288
289 let url = format!(
290 "{}/crypto/symbol?exchange=binance&token={}",
291 self.base_url,
292 self.api_key.as_ref().unwrap()
293 );
294
295 sleep(self.rate_limit_delay).await;
296 let response = self.fetch_with_retry(&url).await?;
297 let symbols: Vec<FinnhubCryptoSymbol> = response.json().await?;
298
299 let mut vectors = Vec::new();
300 for symbol in symbols.iter().take(100) {
301 let text = format!("{} - {}", symbol.description, symbol.display_symbol);
302 let embedding = self.embedder.embed_text(&text);
303
304 let mut metadata = HashMap::new();
305 metadata.insert("symbol".to_string(), symbol.symbol.clone());
306 metadata.insert("display_symbol".to_string(), symbol.display_symbol.clone());
307 metadata.insert("description".to_string(), symbol.description.clone());
308 metadata.insert("source".to_string(), "finnhub_crypto".to_string());
309
310 vectors.push(SemanticVector {
311 id: format!("FINNHUB:CRYPTO:{}", symbol.symbol),
312 embedding,
313 domain: Domain::Finance,
314 timestamp: Utc::now(),
315 metadata,
316 });
317 }
318
319 Ok(vectors)
320 }
321
322 fn get_mock_quote(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
325 let price = 150.0 + (symbol.len() as f64 * 10.0);
326 let text = format!("{} stock quote: ${} (mock data)", symbol, price);
327 let embedding = self.embedder.embed_text(&text);
328
329 let mut metadata = HashMap::new();
330 metadata.insert("symbol".to_string(), symbol.to_string());
331 metadata.insert("current_price".to_string(), price.to_string());
332 metadata.insert("source".to_string(), "finnhub_mock".to_string());
333
334 Ok(vec![SemanticVector {
335 id: format!("FINNHUB:QUOTE:{}:mock", symbol),
336 embedding,
337 domain: Domain::Finance,
338 timestamp: Utc::now(),
339 metadata,
340 }])
341 }
342
343 fn get_mock_symbols(&self, query: &str) -> Result<Vec<SemanticVector>> {
344 let symbols = vec![
345 ("AAPL", "Apple Inc"),
346 ("MSFT", "Microsoft Corporation"),
347 ("GOOGL", "Alphabet Inc"),
348 ];
349
350 let mut vectors = Vec::new();
351 for (symbol, name) in symbols {
352 if symbol.to_lowercase().contains(&query.to_lowercase())
353 || name.to_lowercase().contains(&query.to_lowercase())
354 {
355 let text = format!("{} - {} (mock data)", name, symbol);
356 let embedding = self.embedder.embed_text(&text);
357
358 let mut metadata = HashMap::new();
359 metadata.insert("symbol".to_string(), symbol.to_string());
360 metadata.insert("description".to_string(), name.to_string());
361 metadata.insert("source".to_string(), "finnhub_mock".to_string());
362
363 vectors.push(SemanticVector {
364 id: format!("FINNHUB:SYMBOL:{}:mock", symbol),
365 embedding,
366 domain: Domain::Finance,
367 timestamp: Utc::now(),
368 metadata,
369 });
370 }
371 }
372
373 Ok(vectors)
374 }
375
376 fn get_mock_news(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
377 let text = format!("{} announces quarterly earnings (mock news)", symbol);
378 let embedding = self.embedder.embed_text(&text);
379
380 let mut metadata = HashMap::new();
381 metadata.insert("symbol".to_string(), symbol.to_string());
382 metadata.insert("headline".to_string(), text.clone());
383 metadata.insert("source".to_string(), "finnhub_mock".to_string());
384
385 Ok(vec![SemanticVector {
386 id: format!("FINNHUB:NEWS:{}:mock", symbol),
387 embedding,
388 domain: Domain::Finance,
389 timestamp: Utc::now(),
390 metadata,
391 }])
392 }
393
394 fn get_mock_crypto_symbols(&self) -> Result<Vec<SemanticVector>> {
395 let symbols = vec![
396 ("BTCUSDT", "Bitcoin/Tether"),
397 ("ETHUSDT", "Ethereum/Tether"),
398 ];
399
400 let mut vectors = Vec::new();
401 for (symbol, desc) in symbols {
402 let text = format!("{} - {} (mock data)", desc, symbol);
403 let embedding = self.embedder.embed_text(&text);
404
405 let mut metadata = HashMap::new();
406 metadata.insert("symbol".to_string(), symbol.to_string());
407 metadata.insert("description".to_string(), desc.to_string());
408 metadata.insert("source".to_string(), "finnhub_mock".to_string());
409
410 vectors.push(SemanticVector {
411 id: format!("FINNHUB:CRYPTO:{}:mock", symbol),
412 embedding,
413 domain: Domain::Finance,
414 timestamp: Utc::now(),
415 metadata,
416 });
417 }
418
419 Ok(vectors)
420 }
421
422 async fn fetch_with_retry(&self, url: &str) -> Result<reqwest::Response> {
424 let mut retries = 0;
425 loop {
426 match self.client.get(url).send().await {
427 Ok(response) => {
428 if response.status() == StatusCode::TOO_MANY_REQUESTS && retries < MAX_RETRIES {
429 retries += 1;
430 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
431 continue;
432 }
433 return Ok(response);
434 }
435 Err(_) if retries < MAX_RETRIES => {
436 retries += 1;
437 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
438 }
439 Err(e) => return Err(FrameworkError::Network(e)),
440 }
441 }
442 }
443}
444
445#[derive(Debug, Deserialize)]
451struct TwelveDataTimeSeries {
452 #[serde(default)]
453 values: Vec<TwelveDataValue>,
454 meta: TwelveDataMeta,
455}
456
457#[derive(Debug, Deserialize)]
458struct TwelveDataMeta {
459 symbol: String,
460 interval: String,
461 #[serde(default)]
462 currency: String,
463}
464
465#[derive(Debug, Deserialize)]
466struct TwelveDataValue {
467 datetime: String,
468 open: String,
469 high: String,
470 low: String,
471 close: String,
472 #[serde(default)]
473 volume: String,
474}
475
476#[derive(Debug, Deserialize)]
478struct TwelveDataQuote {
479 symbol: String,
480 name: String,
481 #[serde(default)]
482 price: String,
483 #[serde(default)]
484 open: String,
485 #[serde(default)]
486 high: String,
487 #[serde(default)]
488 low: String,
489 #[serde(default)]
490 volume: String,
491 #[serde(default)]
492 previous_close: String,
493}
494
495pub struct TwelveDataClient {
508 client: Client,
509 base_url: String,
510 api_key: Option<String>,
511 rate_limit_delay: Duration,
512 embedder: Arc<SimpleEmbedder>,
513}
514
515impl TwelveDataClient {
516 pub fn new(api_key: Option<String>) -> Result<Self> {
521 let client = Client::builder()
522 .timeout(Duration::from_secs(30))
523 .build()
524 .map_err(FrameworkError::Network)?;
525
526 Ok(Self {
527 client,
528 base_url: "https://api.twelvedata.com".to_string(),
529 api_key,
530 rate_limit_delay: Duration::from_millis(TWELVEDATA_RATE_LIMIT_MS),
531 embedder: Arc::new(SimpleEmbedder::new(256)),
532 })
533 }
534
535 pub async fn get_time_series(
542 &self,
543 symbol: &str,
544 interval: &str,
545 limit: Option<usize>,
546 ) -> Result<Vec<SemanticVector>> {
547 if self.api_key.is_none() {
549 return self.get_mock_time_series(symbol, interval);
550 }
551
552 let mut url = format!(
553 "{}/time_series?symbol={}&interval={}&apikey={}",
554 self.base_url,
555 symbol,
556 interval,
557 self.api_key.as_ref().unwrap()
558 );
559
560 if let Some(lim) = limit {
561 url.push_str(&format!("&outputsize={}", lim));
562 }
563
564 sleep(self.rate_limit_delay).await;
565 let response = self.fetch_with_retry(&url).await?;
566 let series: TwelveDataTimeSeries = response.json().await?;
567
568 let mut vectors = Vec::new();
569 for value in series.values {
570 let close = value.close.parse::<f64>().unwrap_or(0.0);
571 let volume = value.volume.parse::<f64>().unwrap_or(0.0);
572
573 let text = format!(
574 "{} {} OHLCV: close=${}, volume={}",
575 symbol, value.datetime, close, volume
576 );
577 let embedding = self.embedder.embed_text(&text);
578
579 let mut metadata = HashMap::new();
580 metadata.insert("symbol".to_string(), symbol.to_string());
581 metadata.insert("datetime".to_string(), value.datetime.clone());
582 metadata.insert("open".to_string(), value.open.clone());
583 metadata.insert("high".to_string(), value.high.clone());
584 metadata.insert("low".to_string(), value.low.clone());
585 metadata.insert("close".to_string(), value.close.clone());
586 metadata.insert("volume".to_string(), value.volume.clone());
587 metadata.insert("interval".to_string(), interval.to_string());
588 metadata.insert("source".to_string(), "twelvedata".to_string());
589
590 let timestamp = NaiveDate::parse_from_str(&value.datetime, "%Y-%m-%d")
592 .ok()
593 .and_then(|d| d.and_hms_opt(0, 0, 0))
594 .map(|dt| dt.and_utc())
595 .unwrap_or_else(Utc::now);
596
597 vectors.push(SemanticVector {
598 id: format!("TWELVEDATA:{}:{}:{}", symbol, interval, value.datetime),
599 embedding,
600 domain: Domain::Finance,
601 timestamp,
602 metadata,
603 });
604 }
605
606 Ok(vectors)
607 }
608
609 pub async fn get_quote(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
614 if self.api_key.is_none() {
616 return self.get_mock_quote(symbol);
617 }
618
619 let url = format!(
620 "{}/quote?symbol={}&apikey={}",
621 self.base_url,
622 symbol,
623 self.api_key.as_ref().unwrap()
624 );
625
626 sleep(self.rate_limit_delay).await;
627 let response = self.fetch_with_retry(&url).await?;
628 let quote: TwelveDataQuote = response.json().await?;
629
630 let text = format!("{} - {} quote: ${}", quote.symbol, quote.name, quote.price);
631 let embedding = self.embedder.embed_text(&text);
632
633 let mut metadata = HashMap::new();
634 metadata.insert("symbol".to_string(), quote.symbol.clone());
635 metadata.insert("name".to_string(), quote.name.clone());
636 metadata.insert("price".to_string(), quote.price.clone());
637 metadata.insert("open".to_string(), quote.open.clone());
638 metadata.insert("high".to_string(), quote.high.clone());
639 metadata.insert("low".to_string(), quote.low.clone());
640 metadata.insert("volume".to_string(), quote.volume.clone());
641 metadata.insert("previous_close".to_string(), quote.previous_close.clone());
642 metadata.insert("source".to_string(), "twelvedata".to_string());
643
644 Ok(vec![SemanticVector {
645 id: format!("TWELVEDATA:QUOTE:{}", quote.symbol),
646 embedding,
647 domain: Domain::Finance,
648 timestamp: Utc::now(),
649 metadata,
650 }])
651 }
652
653 pub async fn get_crypto(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
658 self.get_quote(symbol).await
659 }
660
661 fn get_mock_time_series(&self, symbol: &str, interval: &str) -> Result<Vec<SemanticVector>> {
664 let mut vectors = Vec::new();
665 let base_price = 150.0 + (symbol.len() as f64 * 10.0);
666
667 for i in 0..5 {
668 let price = base_price + (i as f64 * 2.0);
669 let date = format!("2024-01-{:02}", i + 1);
670 let text = format!("{} {} OHLCV: close=${} (mock data)", symbol, date, price);
671 let embedding = self.embedder.embed_text(&text);
672
673 let mut metadata = HashMap::new();
674 metadata.insert("symbol".to_string(), symbol.to_string());
675 metadata.insert("datetime".to_string(), date.clone());
676 metadata.insert("close".to_string(), price.to_string());
677 metadata.insert("interval".to_string(), interval.to_string());
678 metadata.insert("source".to_string(), "twelvedata_mock".to_string());
679
680 let timestamp = NaiveDate::parse_from_str(&date, "%Y-%m-%d")
681 .ok()
682 .and_then(|d| d.and_hms_opt(0, 0, 0))
683 .map(|dt| dt.and_utc())
684 .unwrap_or_else(Utc::now);
685
686 vectors.push(SemanticVector {
687 id: format!("TWELVEDATA:{}:{}:{}:mock", symbol, interval, date),
688 embedding,
689 domain: Domain::Finance,
690 timestamp,
691 metadata,
692 });
693 }
694
695 Ok(vectors)
696 }
697
698 fn get_mock_quote(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
699 let price = 150.0 + (symbol.len() as f64 * 10.0);
700 let text = format!("{} quote: ${} (mock data)", symbol, price);
701 let embedding = self.embedder.embed_text(&text);
702
703 let mut metadata = HashMap::new();
704 metadata.insert("symbol".to_string(), symbol.to_string());
705 metadata.insert("price".to_string(), price.to_string());
706 metadata.insert("source".to_string(), "twelvedata_mock".to_string());
707
708 Ok(vec![SemanticVector {
709 id: format!("TWELVEDATA:QUOTE:{}:mock", symbol),
710 embedding,
711 domain: Domain::Finance,
712 timestamp: Utc::now(),
713 metadata,
714 }])
715 }
716
717 async fn fetch_with_retry(&self, url: &str) -> Result<reqwest::Response> {
719 let mut retries = 0;
720 loop {
721 match self.client.get(url).send().await {
722 Ok(response) => {
723 if response.status() == StatusCode::TOO_MANY_REQUESTS && retries < MAX_RETRIES {
724 retries += 1;
725 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
726 continue;
727 }
728 return Ok(response);
729 }
730 Err(_) if retries < MAX_RETRIES => {
731 retries += 1;
732 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
733 }
734 Err(e) => return Err(FrameworkError::Network(e)),
735 }
736 }
737 }
738}
739
740#[derive(Debug, Deserialize)]
746struct CoinGeckoPrice {
747 #[serde(flatten)]
748 prices: HashMap<String, HashMap<String, f64>>,
749}
750
751#[derive(Debug, Deserialize)]
753struct CoinGeckoCoin {
754 id: String,
755 symbol: String,
756 name: String,
757 #[serde(default)]
758 description: CoinGeckoDescription,
759 #[serde(default)]
760 market_data: Option<CoinGeckoMarketData>,
761}
762
763#[derive(Debug, Default, Deserialize)]
764struct CoinGeckoDescription {
765 #[serde(default)]
766 en: String,
767}
768
769#[derive(Debug, Deserialize)]
770struct CoinGeckoMarketData {
771 current_price: HashMap<String, f64>,
772 market_cap: HashMap<String, f64>,
773 total_volume: HashMap<String, f64>,
774}
775
776#[derive(Debug, Deserialize)]
778struct CoinGeckoMarketChart {
779 prices: Vec<Vec<f64>>, #[serde(default)]
781 market_caps: Vec<Vec<f64>>,
782 #[serde(default)]
783 total_volumes: Vec<Vec<f64>>,
784}
785
786#[derive(Debug, Deserialize)]
788struct CoinGeckoSearchResponse {
789 coins: Vec<CoinGeckoSearchCoin>,
790}
791
792#[derive(Debug, Deserialize)]
793struct CoinGeckoSearchCoin {
794 id: String,
795 name: String,
796 symbol: String,
797 #[serde(default)]
798 market_cap_rank: Option<u32>,
799}
800
801pub struct CoinGeckoClient {
815 client: Client,
816 base_url: String,
817 rate_limit_delay: Duration,
818 embedder: Arc<SimpleEmbedder>,
819}
820
821impl CoinGeckoClient {
822 pub fn new() -> Result<Self> {
824 let client = Client::builder()
825 .timeout(Duration::from_secs(30))
826 .build()
827 .map_err(FrameworkError::Network)?;
828
829 Ok(Self {
830 client,
831 base_url: "https://api.coingecko.com/api/v3".to_string(),
832 rate_limit_delay: Duration::from_millis(COINGECKO_RATE_LIMIT_MS),
833 embedder: Arc::new(SimpleEmbedder::new(256)),
834 })
835 }
836
837 pub async fn get_price(
843 &self,
844 ids: &[&str],
845 vs_currencies: &[&str],
846 ) -> Result<Vec<SemanticVector>> {
847 let url = format!(
848 "{}/simple/price?ids={}&vs_currencies={}",
849 self.base_url,
850 ids.join(","),
851 vs_currencies.join(",")
852 );
853
854 sleep(self.rate_limit_delay).await;
855 let response = self.fetch_with_retry(&url).await?;
856 let prices: HashMap<String, HashMap<String, f64>> = response.json().await?;
857
858 let mut vectors = Vec::new();
859 for (coin_id, currencies) in prices {
860 for (currency, price) in currencies {
861 let text = format!("{} price in {}: {}", coin_id, currency, price);
862 let embedding = self.embedder.embed_text(&text);
863
864 let mut metadata = HashMap::new();
865 metadata.insert("coin_id".to_string(), coin_id.clone());
866 metadata.insert("currency".to_string(), currency.clone());
867 metadata.insert("price".to_string(), price.to_string());
868 metadata.insert("source".to_string(), "coingecko".to_string());
869
870 vectors.push(SemanticVector {
871 id: format!("COINGECKO:PRICE:{}:{}", coin_id, currency),
872 embedding,
873 domain: Domain::Finance,
874 timestamp: Utc::now(),
875 metadata,
876 });
877 }
878 }
879
880 Ok(vectors)
881 }
882
883 pub async fn get_coin(&self, id: &str) -> Result<Vec<SemanticVector>> {
888 let url = format!("{}/coins/{}", self.base_url, id);
889
890 sleep(self.rate_limit_delay).await;
891 let response = self.fetch_with_retry(&url).await?;
892 let coin: CoinGeckoCoin = response.json().await?;
893
894 let text = format!(
895 "{} ({}) - {}",
896 coin.name,
897 coin.symbol,
898 coin.description.en.chars().take(200).collect::<String>()
899 );
900 let embedding = self.embedder.embed_text(&text);
901
902 let mut metadata = HashMap::new();
903 metadata.insert("coin_id".to_string(), coin.id.clone());
904 metadata.insert("symbol".to_string(), coin.symbol.clone());
905 metadata.insert("name".to_string(), coin.name.clone());
906
907 if let Some(market_data) = coin.market_data {
908 if let Some(usd_price) = market_data.current_price.get("usd") {
909 metadata.insert("price_usd".to_string(), usd_price.to_string());
910 }
911 if let Some(market_cap) = market_data.market_cap.get("usd") {
912 metadata.insert("market_cap_usd".to_string(), market_cap.to_string());
913 }
914 }
915
916 metadata.insert("source".to_string(), "coingecko".to_string());
917
918 Ok(vec![SemanticVector {
919 id: format!("COINGECKO:COIN:{}", coin.id),
920 embedding,
921 domain: Domain::Finance,
922 timestamp: Utc::now(),
923 metadata,
924 }])
925 }
926
927 pub async fn get_market_chart(&self, id: &str, days: &str) -> Result<Vec<SemanticVector>> {
933 let url = format!(
934 "{}/coins/{}/market_chart?vs_currency=usd&days={}",
935 self.base_url, id, days
936 );
937
938 sleep(self.rate_limit_delay).await;
939 let response = self.fetch_with_retry(&url).await?;
940 let chart: CoinGeckoMarketChart = response.json().await?;
941
942 let mut vectors = Vec::new();
943 for price_point in chart.prices.iter().take(100) {
944 if price_point.len() < 2 {
945 continue;
946 }
947
948 let timestamp_ms = price_point[0] as i64;
949 let price = price_point[1];
950
951 let text = format!("{} price at {}: ${}", id, timestamp_ms, price);
952 let embedding = self.embedder.embed_text(&text);
953
954 let mut metadata = HashMap::new();
955 metadata.insert("coin_id".to_string(), id.to_string());
956 metadata.insert("price".to_string(), price.to_string());
957 metadata.insert("source".to_string(), "coingecko_chart".to_string());
958
959 let timestamp = chrono::DateTime::from_timestamp_millis(timestamp_ms)
960 .unwrap_or_else(Utc::now);
961
962 vectors.push(SemanticVector {
963 id: format!("COINGECKO:CHART:{}:{}", id, timestamp_ms),
964 embedding,
965 domain: Domain::Finance,
966 timestamp,
967 metadata,
968 });
969 }
970
971 Ok(vectors)
972 }
973
974 pub async fn search(&self, query: &str) -> Result<Vec<SemanticVector>> {
979 let url = format!(
980 "{}/search?query={}",
981 self.base_url,
982 urlencoding::encode(query)
983 );
984
985 sleep(self.rate_limit_delay).await;
986 let response = self.fetch_with_retry(&url).await?;
987 let search_response: CoinGeckoSearchResponse = response.json().await?;
988
989 let mut vectors = Vec::new();
990 for coin in search_response.coins.iter().take(20) {
991 let text = format!("{} ({}) - rank: {:?}", coin.name, coin.symbol, coin.market_cap_rank);
992 let embedding = self.embedder.embed_text(&text);
993
994 let mut metadata = HashMap::new();
995 metadata.insert("coin_id".to_string(), coin.id.clone());
996 metadata.insert("name".to_string(), coin.name.clone());
997 metadata.insert("symbol".to_string(), coin.symbol.clone());
998 if let Some(rank) = coin.market_cap_rank {
999 metadata.insert("market_cap_rank".to_string(), rank.to_string());
1000 }
1001 metadata.insert("source".to_string(), "coingecko_search".to_string());
1002
1003 vectors.push(SemanticVector {
1004 id: format!("COINGECKO:SEARCH:{}", coin.id),
1005 embedding,
1006 domain: Domain::Finance,
1007 timestamp: Utc::now(),
1008 metadata,
1009 });
1010 }
1011
1012 Ok(vectors)
1013 }
1014
1015 async fn fetch_with_retry(&self, url: &str) -> Result<reqwest::Response> {
1017 let mut retries = 0;
1018 loop {
1019 match self.client.get(url).send().await {
1020 Ok(response) => {
1021 if response.status() == StatusCode::TOO_MANY_REQUESTS && retries < MAX_RETRIES {
1022 retries += 1;
1023 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
1024 continue;
1025 }
1026 return Ok(response);
1027 }
1028 Err(_) if retries < MAX_RETRIES => {
1029 retries += 1;
1030 sleep(Duration::from_millis(RETRY_DELAY_MS * retries as u64)).await;
1031 }
1032 Err(e) => return Err(FrameworkError::Network(e)),
1033 }
1034 }
1035 }
1036}
1037
1038impl Default for CoinGeckoClient {
1039 fn default() -> Self {
1040 Self::new().expect("Failed to create CoinGecko client")
1041 }
1042}
1043
1044#[derive(Debug, Deserialize)]
1050struct EcbExchangeRateResponse {
1051 #[serde(rename = "dataSets")]
1052 data_sets: Vec<EcbDataSet>,
1053 structure: EcbStructure,
1054}
1055
1056#[derive(Debug, Deserialize)]
1057struct EcbDataSet {
1058 series: HashMap<String, EcbSeries>,
1059}
1060
1061#[derive(Debug, Deserialize)]
1062struct EcbSeries {
1063 observations: HashMap<String, Vec<Option<f64>>>,
1064}
1065
1066#[derive(Debug, Deserialize)]
1067struct EcbStructure {
1068 dimensions: EcbDimensions,
1069}
1070
1071#[derive(Debug, Deserialize)]
1072struct EcbDimensions {
1073 series: Vec<EcbDimension>,
1074 observation: Vec<EcbDimension>,
1075}
1076
1077#[derive(Debug, Deserialize)]
1078struct EcbDimension {
1079 id: String,
1080 values: Vec<EcbDimensionValue>,
1081}
1082
1083#[derive(Debug, Deserialize)]
1084struct EcbDimensionValue {
1085 id: String,
1086 name: String,
1087}
1088
1089pub struct EcbClient {
1102 client: Client,
1103 base_url: String,
1104 rate_limit_delay: Duration,
1105 embedder: Arc<SimpleEmbedder>,
1106}
1107
1108impl EcbClient {
1109 pub fn new() -> Result<Self> {
1111 let client = Client::builder()
1112 .timeout(Duration::from_secs(30))
1113 .build()
1114 .map_err(FrameworkError::Network)?;
1115
1116 Ok(Self {
1117 client,
1118 base_url: "https://data-api.ecb.europa.eu/service/data".to_string(),
1119 rate_limit_delay: Duration::from_millis(ECB_RATE_LIMIT_MS),
1120 embedder: Arc::new(SimpleEmbedder::new(256)),
1121 })
1122 }
1123
1124 pub async fn get_exchange_rates(&self, currency: &str) -> Result<Vec<SemanticVector>> {
1129 let url = format!(
1131 "{}/EXR/D.{}.EUR.SP00.A?format=jsondata&lastNObservations=30",
1132 self.base_url, currency
1133 );
1134
1135 sleep(self.rate_limit_delay).await;
1136
1137 self.get_mock_exchange_rates(currency)
1139 }
1140
1141 pub async fn get_series(&self, series_key: &str) -> Result<Vec<SemanticVector>> {
1146 self.get_mock_series(series_key)
1150 }
1151
1152 fn get_mock_exchange_rates(&self, currency: &str) -> Result<Vec<SemanticVector>> {
1155 let mut vectors = Vec::new();
1156 let base_rate = match currency {
1157 "USD" => 1.08,
1158 "GBP" => 0.85,
1159 "JPY" => 155.0,
1160 _ => 1.0,
1161 };
1162
1163 for i in 0..10 {
1164 let rate = base_rate + (i as f64 * 0.01);
1165 let date = format!("2024-01-{:02}", i + 1);
1166 let text = format!("EUR/{} exchange rate on {}: {}", currency, date, rate);
1167 let embedding = self.embedder.embed_text(&text);
1168
1169 let mut metadata = HashMap::new();
1170 metadata.insert("currency".to_string(), currency.to_string());
1171 metadata.insert("rate".to_string(), rate.to_string());
1172 metadata.insert("date".to_string(), date.clone());
1173 metadata.insert("source".to_string(), "ecb_mock".to_string());
1174
1175 let timestamp = NaiveDate::parse_from_str(&date, "%Y-%m-%d")
1176 .ok()
1177 .and_then(|d| d.and_hms_opt(0, 0, 0))
1178 .map(|dt| dt.and_utc())
1179 .unwrap_or_else(Utc::now);
1180
1181 vectors.push(SemanticVector {
1182 id: format!("ECB:RATE:EUR-{}:{}", currency, date),
1183 embedding,
1184 domain: Domain::Economic,
1185 timestamp,
1186 metadata,
1187 });
1188 }
1189
1190 Ok(vectors)
1191 }
1192
1193 fn get_mock_series(&self, series_key: &str) -> Result<Vec<SemanticVector>> {
1194 let text = format!("ECB series {} (mock data)", series_key);
1195 let embedding = self.embedder.embed_text(&text);
1196
1197 let mut metadata = HashMap::new();
1198 metadata.insert("series_key".to_string(), series_key.to_string());
1199 metadata.insert("value".to_string(), "1.0".to_string());
1200 metadata.insert("source".to_string(), "ecb_mock".to_string());
1201
1202 Ok(vec![SemanticVector {
1203 id: format!("ECB:SERIES:{}", series_key),
1204 embedding,
1205 domain: Domain::Economic,
1206 timestamp: Utc::now(),
1207 metadata,
1208 }])
1209 }
1210}
1211
1212impl Default for EcbClient {
1213 fn default() -> Self {
1214 Self::new().expect("Failed to create ECB client")
1215 }
1216}
1217
1218#[derive(Debug, Deserialize)]
1224struct BlsResponse {
1225 status: String,
1226 #[serde(rename = "Results")]
1227 results: Option<BlsResults>,
1228}
1229
1230#[derive(Debug, Deserialize)]
1231struct BlsResults {
1232 series: Vec<BlsSeries>,
1233}
1234
1235#[derive(Debug, Deserialize)]
1236struct BlsSeries {
1237 #[serde(rename = "seriesID")]
1238 series_id: String,
1239 data: Vec<BlsDataPoint>,
1240}
1241
1242#[derive(Debug, Deserialize)]
1243struct BlsDataPoint {
1244 year: String,
1245 period: String,
1246 #[serde(rename = "periodName")]
1247 period_name: String,
1248 value: String,
1249 #[serde(default)]
1250 footnotes: Vec<BlsFootnote>,
1251}
1252
1253#[derive(Debug, Deserialize)]
1254struct BlsFootnote {
1255 code: String,
1256 text: String,
1257}
1258
1259pub struct BlsClient {
1272 client: Client,
1273 base_url: String,
1274 api_key: Option<String>,
1275 rate_limit_delay: Duration,
1276 embedder: Arc<SimpleEmbedder>,
1277}
1278
1279impl BlsClient {
1280 pub fn new(api_key: Option<String>) -> Result<Self> {
1285 let client = Client::builder()
1286 .timeout(Duration::from_secs(30))
1287 .build()
1288 .map_err(FrameworkError::Network)?;
1289
1290 Ok(Self {
1291 client,
1292 base_url: "https://api.bls.gov/publicAPI/v2".to_string(),
1293 api_key,
1294 rate_limit_delay: Duration::from_millis(BLS_RATE_LIMIT_MS),
1295 embedder: Arc::new(SimpleEmbedder::new(256)),
1296 })
1297 }
1298
1299 pub async fn get_series(
1306 &self,
1307 series_ids: &[&str],
1308 start_year: Option<i32>,
1309 end_year: Option<i32>,
1310 ) -> Result<Vec<SemanticVector>> {
1311 self.get_mock_series(series_ids, start_year, end_year)
1313 }
1314
1315 fn get_mock_series(
1318 &self,
1319 series_ids: &[&str],
1320 start_year: Option<i32>,
1321 _end_year: Option<i32>,
1322 ) -> Result<Vec<SemanticVector>> {
1323 let mut vectors = Vec::new();
1324 let year = start_year.unwrap_or(2024);
1325
1326 for series_id in series_ids {
1327 for month in 1..=12 {
1328 let value = 3.5 + (month as f64 * 0.1);
1329 let period = format!("M{:02}", month);
1330 let text = format!("BLS {} {} {}: {}", series_id, year, period, value);
1331 let embedding = self.embedder.embed_text(&text);
1332
1333 let mut metadata = HashMap::new();
1334 metadata.insert("series_id".to_string(), series_id.to_string());
1335 metadata.insert("year".to_string(), year.to_string());
1336 metadata.insert("period".to_string(), period.clone());
1337 metadata.insert("value".to_string(), value.to_string());
1338 metadata.insert("source".to_string(), "bls_mock".to_string());
1339
1340 let date = format!("{}-{:02}-01", year, month);
1341 let timestamp = NaiveDate::parse_from_str(&date, "%Y-%m-%d")
1342 .ok()
1343 .and_then(|d| d.and_hms_opt(0, 0, 0))
1344 .map(|dt| dt.and_utc())
1345 .unwrap_or_else(Utc::now);
1346
1347 vectors.push(SemanticVector {
1348 id: format!("BLS:{}:{}:{}", series_id, year, period),
1349 embedding,
1350 domain: Domain::Economic,
1351 timestamp,
1352 metadata,
1353 });
1354 }
1355 }
1356
1357 Ok(vectors)
1358 }
1359}
1360
1361#[cfg(test)]
1366mod tests {
1367 use super::*;
1368
1369 #[tokio::test]
1372 async fn test_finnhub_client_creation() {
1373 let client = FinnhubClient::new(None);
1374 assert!(client.is_ok());
1375 }
1376
1377 #[tokio::test]
1378 async fn test_finnhub_client_with_key() {
1379 let client = FinnhubClient::new(Some("test_key".to_string()));
1380 assert!(client.is_ok());
1381 }
1382
1383 #[tokio::test]
1384 async fn test_finnhub_mock_quote() {
1385 let client = FinnhubClient::new(None).unwrap();
1386 let quote = client.get_quote("AAPL").await.unwrap();
1387
1388 assert_eq!(quote.len(), 1);
1389 assert_eq!(quote[0].domain, Domain::Finance);
1390 assert!(quote[0].id.starts_with("FINNHUB:QUOTE:"));
1391 assert_eq!(quote[0].metadata.get("symbol").unwrap(), "AAPL");
1392 }
1393
1394 #[tokio::test]
1395 async fn test_finnhub_mock_symbols() {
1396 let client = FinnhubClient::new(None).unwrap();
1397 let symbols = client.search_symbols("apple").await.unwrap();
1398
1399 assert!(!symbols.is_empty());
1400 assert_eq!(symbols[0].domain, Domain::Finance);
1401 }
1402
1403 #[tokio::test]
1404 async fn test_finnhub_mock_news() {
1405 let client = FinnhubClient::new(None).unwrap();
1406 let news = client.get_company_news("AAPL", "2024-01-01", "2024-01-31").await.unwrap();
1407
1408 assert_eq!(news.len(), 1);
1409 assert_eq!(news[0].domain, Domain::Finance);
1410 }
1411
1412 #[tokio::test]
1413 async fn test_finnhub_mock_crypto() {
1414 let client = FinnhubClient::new(None).unwrap();
1415 let crypto = client.get_crypto_symbols().await.unwrap();
1416
1417 assert_eq!(crypto.len(), 2);
1418 assert_eq!(crypto[0].domain, Domain::Finance);
1419 }
1420
1421 #[tokio::test]
1424 async fn test_twelvedata_client_creation() {
1425 let client = TwelveDataClient::new(None);
1426 assert!(client.is_ok());
1427 }
1428
1429 #[tokio::test]
1430 async fn test_twelvedata_mock_time_series() {
1431 let client = TwelveDataClient::new(None).unwrap();
1432 let series = client.get_time_series("AAPL", "1day", Some(5)).await.unwrap();
1433
1434 assert_eq!(series.len(), 5);
1435 assert_eq!(series[0].domain, Domain::Finance);
1436 assert!(series[0].id.contains("TWELVEDATA"));
1437 }
1438
1439 #[tokio::test]
1440 async fn test_twelvedata_mock_quote() {
1441 let client = TwelveDataClient::new(None).unwrap();
1442 let quote = client.get_quote("AAPL").await.unwrap();
1443
1444 assert_eq!(quote.len(), 1);
1445 assert_eq!(quote[0].domain, Domain::Finance);
1446 }
1447
1448 #[tokio::test]
1451 async fn test_coingecko_client_creation() {
1452 let client = CoinGeckoClient::new();
1453 assert!(client.is_ok());
1454 }
1455
1456 #[test]
1457 fn test_coingecko_rate_limiting() {
1458 let client = CoinGeckoClient::new().unwrap();
1459 assert_eq!(client.rate_limit_delay, Duration::from_millis(COINGECKO_RATE_LIMIT_MS));
1460 }
1461
1462 #[tokio::test]
1465 async fn test_ecb_client_creation() {
1466 let client = EcbClient::new();
1467 assert!(client.is_ok());
1468 }
1469
1470 #[tokio::test]
1471 async fn test_ecb_mock_exchange_rates() {
1472 let client = EcbClient::new().unwrap();
1473 let rates = client.get_exchange_rates("USD").await.unwrap();
1474
1475 assert_eq!(rates.len(), 10);
1476 assert_eq!(rates[0].domain, Domain::Economic);
1477 assert!(rates[0].id.starts_with("ECB:RATE:"));
1478 }
1479
1480 #[tokio::test]
1483 async fn test_bls_client_creation() {
1484 let client = BlsClient::new(None);
1485 assert!(client.is_ok());
1486 }
1487
1488 #[tokio::test]
1489 async fn test_bls_mock_series() {
1490 let client = BlsClient::new(None).unwrap();
1491 let series = client.get_series(&["LNS14000000"], Some(2024), Some(2024)).await.unwrap();
1492
1493 assert_eq!(series.len(), 12); assert_eq!(series[0].domain, Domain::Economic);
1495 assert!(series[0].id.starts_with("BLS:"));
1496 }
1497
1498 #[test]
1501 fn test_rate_limiting() {
1502 let finnhub = FinnhubClient::new(None).unwrap();
1503 assert_eq!(finnhub.rate_limit_delay, Duration::from_millis(FINNHUB_RATE_LIMIT_MS));
1504
1505 let twelve = TwelveDataClient::new(None).unwrap();
1506 assert_eq!(twelve.rate_limit_delay, Duration::from_millis(TWELVEDATA_RATE_LIMIT_MS));
1507
1508 let cg = CoinGeckoClient::new().unwrap();
1509 assert_eq!(cg.rate_limit_delay, Duration::from_millis(COINGECKO_RATE_LIMIT_MS));
1510
1511 let ecb = EcbClient::new().unwrap();
1512 assert_eq!(ecb.rate_limit_delay, Duration::from_millis(ECB_RATE_LIMIT_MS));
1513
1514 let bls = BlsClient::new(None).unwrap();
1515 assert_eq!(bls.rate_limit_delay, Duration::from_millis(BLS_RATE_LIMIT_MS));
1516 }
1517}