ruvector_data_framework/
finance_clients.rs

1//! Finance & Economics API integrations for market data and economic indicators
2//!
3//! This module provides async clients for fetching financial market data, cryptocurrency prices,
4//! exchange rates, and labor statistics, converting responses to SemanticVector format for RuVector discovery.
5
6use 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
19/// Rate limiting configuration
20const FINNHUB_RATE_LIMIT_MS: u64 = 1000; // 60/min = 1/sec for free tier
21const TWELVEDATA_RATE_LIMIT_MS: u64 = 120; // ~500/min conservative
22const COINGECKO_RATE_LIMIT_MS: u64 = 1200; // 50/min for free tier
23const ECB_RATE_LIMIT_MS: u64 = 100; // No strict limit, be polite
24const BLS_RATE_LIMIT_MS: u64 = 600; // ~100/min conservative
25const MAX_RETRIES: u32 = 3;
26const RETRY_DELAY_MS: u64 = 1000;
27
28// ============================================================================
29// Finnhub Stock Market Client
30// ============================================================================
31
32/// Finnhub quote response
33#[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/// Finnhub symbol search result
50#[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/// Finnhub company news
67#[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/// Finnhub crypto symbols
79#[derive(Debug, Deserialize)]
80struct FinnhubCryptoSymbol {
81    description: String,
82    #[serde(rename = "displaySymbol")]
83    display_symbol: String,
84    symbol: String,
85}
86
87/// Client for Finnhub Stock Market API
88///
89/// Provides access to real-time stock quotes, company news, and cryptocurrency data.
90/// Free tier: 60 API calls/minute
91///
92/// # Example
93/// ```rust,ignore
94/// use ruvector_data_framework::FinnhubClient;
95///
96/// let client = FinnhubClient::new(Some("YOUR_API_KEY".to_string()))?;
97/// let quote = client.get_quote("AAPL").await?;
98/// let news = client.get_company_news("TSLA", "2024-01-01", "2024-01-31").await?;
99/// ```
100pub 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    /// Create a new Finnhub client
110    ///
111    /// # Arguments
112    /// * `api_key` - Optional Finnhub API key (get from https://finnhub.io/)
113    ///               Falls back to mock data if not provided
114    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    /// Get real-time stock quote
130    ///
131    /// # Arguments
132    /// * `symbol` - Stock ticker symbol (e.g., "AAPL", "TSLA", "MSFT")
133    pub async fn get_quote(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
134        // Return mock data if no API key
135        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    /// Search for stock symbols
178    ///
179    /// # Arguments
180    /// * `query` - Search query (company name or ticker)
181    pub async fn search_symbols(&self, query: &str) -> Result<Vec<SemanticVector>> {
182        // Return mock data if no API key
183        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    /// Get company news
226    ///
227    /// # Arguments
228    /// * `symbol` - Stock ticker symbol
229    /// * `from` - Start date (YYYY-MM-DD)
230    /// * `to` - End date (YYYY-MM-DD)
231    pub async fn get_company_news(
232        &self,
233        symbol: &str,
234        from: &str,
235        to: &str,
236    ) -> Result<Vec<SemanticVector>> {
237        // Return mock data if no API key
238        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    /// Get cryptocurrency symbols
283    pub async fn get_crypto_symbols(&self) -> Result<Vec<SemanticVector>> {
284        // Return mock data if no API key
285        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    // Mock data methods for when API key is not available
323
324    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    /// Fetch with retry logic
423    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// ============================================================================
446// Twelve Data Client (OHLCV Time Series)
447// ============================================================================
448
449/// Twelve Data time series response
450#[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/// Twelve Data quote response
477#[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
495/// Client for Twelve Data API
496///
497/// Provides OHLCV time series data, real-time quotes, and cryptocurrency prices.
498/// Free tier: 800 API calls/day
499///
500/// # Example
501/// ```rust,ignore
502/// use ruvector_data_framework::TwelveDataClient;
503///
504/// let client = TwelveDataClient::new(Some("YOUR_API_KEY".to_string()))?;
505/// let series = client.get_time_series("AAPL", "1day", Some(30)).await?;
506/// ```
507pub 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    /// Create a new Twelve Data client
517    ///
518    /// # Arguments
519    /// * `api_key` - Optional Twelve Data API key (get from https://twelvedata.com/)
520    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    /// Get OHLCV time series data
536    ///
537    /// # Arguments
538    /// * `symbol` - Stock ticker symbol
539    /// * `interval` - Time interval (1min, 5min, 1day, 1week, 1month)
540    /// * `limit` - Number of data points (max 5000)
541    pub async fn get_time_series(
542        &self,
543        symbol: &str,
544        interval: &str,
545        limit: Option<usize>,
546    ) -> Result<Vec<SemanticVector>> {
547        // Return mock data if no API key
548        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            // Parse datetime
591            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    /// Get real-time quote
610    ///
611    /// # Arguments
612    /// * `symbol` - Stock ticker symbol
613    pub async fn get_quote(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
614        // Return mock data if no API key
615        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    /// Get cryptocurrency price
654    ///
655    /// # Arguments
656    /// * `symbol` - Crypto symbol (e.g., "BTC/USD", "ETH/USD")
657    pub async fn get_crypto(&self, symbol: &str) -> Result<Vec<SemanticVector>> {
658        self.get_quote(symbol).await
659    }
660
661    // Mock data methods
662
663    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    /// Fetch with retry logic
718    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// ============================================================================
741// CoinGecko Cryptocurrency Client
742// ============================================================================
743
744/// CoinGecko simple price response
745#[derive(Debug, Deserialize)]
746struct CoinGeckoPrice {
747    #[serde(flatten)]
748    prices: HashMap<String, HashMap<String, f64>>,
749}
750
751/// CoinGecko coin details
752#[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/// CoinGecko market chart response
777#[derive(Debug, Deserialize)]
778struct CoinGeckoMarketChart {
779    prices: Vec<Vec<f64>>, // [timestamp_ms, price]
780    #[serde(default)]
781    market_caps: Vec<Vec<f64>>,
782    #[serde(default)]
783    total_volumes: Vec<Vec<f64>>,
784}
785
786/// CoinGecko search result
787#[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
801/// Client for CoinGecko Cryptocurrency API
802///
803/// Provides cryptocurrency prices, market data, and historical charts.
804/// No authentication required for basic usage (50 calls/minute).
805///
806/// # Example
807/// ```rust,ignore
808/// use ruvector_data_framework::CoinGeckoClient;
809///
810/// let client = CoinGeckoClient::new()?;
811/// let prices = client.get_price(&["bitcoin", "ethereum"], &["usd", "eur"]).await?;
812/// let coin = client.get_coin("bitcoin").await?;
813/// ```
814pub struct CoinGeckoClient {
815    client: Client,
816    base_url: String,
817    rate_limit_delay: Duration,
818    embedder: Arc<SimpleEmbedder>,
819}
820
821impl CoinGeckoClient {
822    /// Create a new CoinGecko client
823    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    /// Get simple price for cryptocurrencies
838    ///
839    /// # Arguments
840    /// * `ids` - Coin IDs (e.g., ["bitcoin", "ethereum"])
841    /// * `vs_currencies` - Target currencies (e.g., ["usd", "eur"])
842    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    /// Get detailed coin information
884    ///
885    /// # Arguments
886    /// * `id` - Coin ID (e.g., "bitcoin", "ethereum")
887    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    /// Get historical market chart data
928    ///
929    /// # Arguments
930    /// * `id` - Coin ID
931    /// * `days` - Number of days (1, 7, 14, 30, 90, 180, 365, max)
932    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    /// Search for coins
975    ///
976    /// # Arguments
977    /// * `query` - Search query
978    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    /// Fetch with retry logic
1016    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// ============================================================================
1045// ECB (European Central Bank) Client
1046// ============================================================================
1047
1048/// ECB exchange rate data
1049#[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
1089/// Client for European Central Bank Statistical Data Warehouse
1090///
1091/// Provides access to EUR exchange rates and economic series.
1092/// No authentication required.
1093///
1094/// # Example
1095/// ```rust,ignore
1096/// use ruvector_data_framework::EcbClient;
1097///
1098/// let client = EcbClient::new()?;
1099/// let rates = client.get_exchange_rates("USD").await?;
1100/// ```
1101pub struct EcbClient {
1102    client: Client,
1103    base_url: String,
1104    rate_limit_delay: Duration,
1105    embedder: Arc<SimpleEmbedder>,
1106}
1107
1108impl EcbClient {
1109    /// Create a new ECB client
1110    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    /// Get EUR exchange rates
1125    ///
1126    /// # Arguments
1127    /// * `currency` - Target currency code (e.g., "USD", "GBP", "JPY")
1128    pub async fn get_exchange_rates(&self, currency: &str) -> Result<Vec<SemanticVector>> {
1129        // ECB API endpoint for daily EUR exchange rates
1130        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        // For demo, return mock data as ECB API can be complex
1138        self.get_mock_exchange_rates(currency)
1139    }
1140
1141    /// Get economic series data
1142    ///
1143    /// # Arguments
1144    /// * `series_key` - ECB series key (e.g., "EXR.D.USD.EUR.SP00.A")
1145    pub async fn get_series(&self, series_key: &str) -> Result<Vec<SemanticVector>> {
1146        // For production use, uncomment this to use real ECB API:
1147        // let _url = format!("{}/series_key?format=jsondata", self.base_url);
1148        // For now, return mock data
1149        self.get_mock_series(series_key)
1150    }
1151
1152    // Mock data methods
1153
1154    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// ============================================================================
1219// BLS (Bureau of Labor Statistics) Client
1220// ============================================================================
1221
1222/// BLS API response
1223#[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
1259/// Client for Bureau of Labor Statistics API
1260///
1261/// Provides access to US labor market data including employment, unemployment,
1262/// wages, and price indices.
1263///
1264/// # Example
1265/// ```rust,ignore
1266/// use ruvector_data_framework::BlsClient;
1267///
1268/// let client = BlsClient::new(None)?;
1269/// let data = client.get_series(&["LNS14000000"], Some(2023), Some(2024)).await?;
1270/// ```
1271pub 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    /// Create a new BLS client
1281    ///
1282    /// # Arguments
1283    /// * `api_key` - Optional BLS API key (increases rate limits)
1284    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    /// Get labor statistics series
1300    ///
1301    /// # Arguments
1302    /// * `series_ids` - BLS series IDs (e.g., ["LNS14000000"] for unemployment rate)
1303    /// * `start_year` - Start year
1304    /// * `end_year` - End year
1305    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        // Return mock data for demo
1312        self.get_mock_series(series_ids, start_year, end_year)
1313    }
1314
1315    // Mock data method
1316
1317    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// ============================================================================
1362// Tests
1363// ============================================================================
1364
1365#[cfg(test)]
1366mod tests {
1367    use super::*;
1368
1369    // Finnhub Tests
1370
1371    #[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    // Twelve Data Tests
1422
1423    #[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    // CoinGecko Tests
1449
1450    #[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    // ECB Tests
1463
1464    #[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    // BLS Tests
1481
1482    #[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); // 12 months
1494        assert_eq!(series[0].domain, Domain::Economic);
1495        assert!(series[0].id.starts_with("BLS:"));
1496    }
1497
1498    // Rate Limiting Tests
1499
1500    #[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}