Skip to main content

finance_query/tickers/core/
membership.rs

1use super::Tickers;
2use std::sync::Arc;
3
4impl Tickers {
5    // ========================================================================
6    // Dynamic Symbol Management
7    // ========================================================================
8
9    /// Add symbols to the watch list
10    ///
11    /// Adds new symbols to track without affecting existing cached data.
12    ///
13    /// # Example
14    ///
15    /// ```no_run
16    /// use finance_query::Tickers;
17    ///
18    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19    /// let mut tickers = Tickers::new(["AAPL"]).await?;
20    /// tickers.add_symbols(["MSFT", "GOOGL"]);
21    /// assert_eq!(tickers.len(), 3);
22    /// # Ok(())
23    /// # }
24    /// ```
25    pub fn add_symbols<S, I>(&mut self, symbols: I)
26    where
27        S: Into<String>,
28        I: IntoIterator<Item = S>,
29    {
30        // Use HashSet for O(n+m) deduplication instead of O(n*m) linear search
31        use std::collections::HashSet;
32
33        let existing: HashSet<&str> = self.symbols.iter().map(|s| &**s).collect();
34        let to_add: Vec<Arc<str>> = symbols
35            .into_iter()
36            .map(Into::into)
37            .filter(|s| !existing.contains(s.as_str()))
38            .map(|s| s.into())
39            .collect();
40
41        self.symbols.extend(to_add);
42    }
43
44    /// Remove symbols from the watch list
45    ///
46    /// Removes symbols and clears their cached data to free memory.
47    ///
48    /// # Example
49    ///
50    /// ```no_run
51    /// use finance_query::Tickers;
52    ///
53    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
54    /// let mut tickers = Tickers::new(["AAPL", "MSFT", "GOOGL"]).await?;
55    /// tickers.remove_symbols(["MSFT"]);
56    /// assert_eq!(tickers.len(), 2);
57    /// # Ok(())
58    /// # }
59    /// ```
60    pub async fn remove_symbols<S, I>(&mut self, symbols: I)
61    where
62        S: Into<String>,
63        I: IntoIterator<Item = S>,
64    {
65        use std::collections::HashSet;
66        let owned: Vec<String> = symbols.into_iter().map(Into::into).collect();
67        let to_remove: HashSet<&str> = owned.iter().map(|s| s.as_str()).collect();
68
69        // Remove from symbol list — O(1) lookup per element
70        self.symbols.retain(|s| !to_remove.contains(&**s));
71
72        // Acquire all independent write locks in parallel
73        let (
74            mut quote_cache,
75            mut chart_cache,
76            mut events_cache,
77            mut financials_cache,
78            mut news_cache,
79            mut recommendations_cache,
80            mut options_cache,
81            mut spark_cache,
82        ) = tokio::join!(
83            self.quote_cache.write(),
84            self.chart_cache.write(),
85            self.events_cache.write(),
86            self.financials_cache.write(),
87            self.news_cache.write(),
88            self.recommendations_cache.write(),
89            self.options_cache.write(),
90            self.spark_cache.write(),
91        );
92
93        // Simple key caches — O(1) per removal
94        for symbol in &to_remove {
95            let key: Arc<str> = (*symbol).into();
96            quote_cache.remove(&key);
97            events_cache.remove(&key);
98            news_cache.remove(&key);
99        }
100
101        // Composite key caches — O(n) retain but O(1) contains check
102        chart_cache.retain(|(sym, _, _), _| !to_remove.contains(&**sym));
103        financials_cache.retain(|(sym, _, _), _| !to_remove.contains(&**sym));
104        recommendations_cache.retain(|(sym, _), _| !to_remove.contains(&**sym));
105        options_cache.retain(|(sym, _), _| !to_remove.contains(&**sym));
106        spark_cache.retain(|(sym, _, _), _| !to_remove.contains(&**sym));
107
108        // Drop all guards before cfg-gated lock
109        drop((
110            quote_cache,
111            chart_cache,
112            events_cache,
113            financials_cache,
114            news_cache,
115            recommendations_cache,
116            options_cache,
117            spark_cache,
118        ));
119
120        #[cfg(feature = "indicators")]
121        self.indicators_cache
122            .write()
123            .await
124            .retain(|(sym, _, _), _| !to_remove.contains(&**sym));
125    }
126
127    /// Clear all cached data and fetch guards, forcing fresh fetches on next access.
128    ///
129    /// Use this when you need up-to-date data from a long-lived `Tickers` instance.
130    /// Also clears fetch guard maps to prevent unbounded growth.
131    pub async fn clear_cache(&self) {
132        tokio::join!(
133            // Data caches
134            async { self.quote_cache.write().await.clear() },
135            async { self.chart_cache.write().await.clear() },
136            async { self.events_cache.write().await.clear() },
137            async { self.financials_cache.write().await.clear() },
138            async { self.news_cache.write().await.clear() },
139            async { self.recommendations_cache.write().await.clear() },
140            async { self.options_cache.write().await.clear() },
141            async { self.spark_cache.write().await.clear() },
142            async {
143                #[cfg(feature = "indicators")]
144                self.indicators_cache.write().await.clear();
145            },
146            // Fetch guard maps (prevent unbounded growth)
147            async { self.charts_fetch.write().await.clear() },
148            async { self.financials_fetch.write().await.clear() },
149            async { self.recommendations_fetch.write().await.clear() },
150            async { self.options_fetch.write().await.clear() },
151            async { self.spark_fetch.write().await.clear() },
152            async {
153                #[cfg(feature = "indicators")]
154                self.indicators_fetch.write().await.clear();
155            },
156        );
157    }
158
159    /// Clear only the cached quote data.
160    ///
161    /// The next call to `quotes()` or `quote()` will re-fetch from the API.
162    pub async fn clear_quote_cache(&self) {
163        self.quote_cache.write().await.clear();
164    }
165
166    /// Clear only the cached chart, spark, and events data.
167    ///
168    /// The next call to `charts()`, `spark()`, `dividends()`, `splits()`,
169    /// or `capital_gains()` will re-fetch from the API.
170    pub async fn clear_chart_cache(&self) {
171        tokio::join!(
172            async { self.chart_cache.write().await.clear() },
173            async { self.events_cache.write().await.clear() },
174            async { self.spark_cache.write().await.clear() },
175            async {
176                #[cfg(feature = "indicators")]
177                self.indicators_cache.write().await.clear();
178            },
179        );
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[tokio::test]
188    async fn test_tickers_add_symbols() {
189        let mut tickers = Tickers::new(["AAPL"]).await.unwrap();
190        assert_eq!(tickers.len(), 1);
191        assert_eq!(tickers.symbols(), &["AAPL"]);
192
193        tickers.add_symbols(["MSFT", "GOOGL"]);
194        assert_eq!(tickers.len(), 3);
195        assert!(tickers.symbols().contains(&"AAPL"));
196        assert!(tickers.symbols().contains(&"MSFT"));
197        assert!(tickers.symbols().contains(&"GOOGL"));
198
199        // Adding duplicate shouldn't increase count
200        tickers.add_symbols(["AAPL"]);
201        assert_eq!(tickers.len(), 3);
202    }
203
204    #[tokio::test]
205    #[ignore = "requires network access"]
206    async fn test_tickers_remove_symbols() {
207        let mut tickers = Tickers::new(["AAPL", "MSFT", "GOOGL"]).await.unwrap();
208        assert_eq!(tickers.len(), 3);
209
210        // Fetch some data to populate caches
211        let _ = tickers.quotes().await;
212
213        // Remove one symbol
214        tickers.remove_symbols(["MSFT"]).await;
215        assert_eq!(tickers.len(), 2);
216        assert!(tickers.symbols().contains(&"AAPL"));
217        assert!(!tickers.symbols().contains(&"MSFT"));
218        assert!(tickers.symbols().contains(&"GOOGL"));
219
220        // Verify cache was cleared
221        let quotes = tickers.quotes().await.unwrap();
222        assert!(!quotes.quotes.contains_key("MSFT"));
223        assert_eq!(quotes.quotes.len(), 2);
224    }
225}