Skip to main content

tvdata_rs/equity/
mod.rs

1mod columns;
2mod decode;
3mod history;
4#[cfg(test)]
5mod tests;
6mod types;
7
8use futures_util::stream::{self, StreamExt as FuturesStreamExt, TryStreamExt};
9
10use self::columns::{
11    analyst_columns, analyst_forecast_columns, analyst_fx_rate_columns,
12    analyst_price_target_columns, analyst_recommendation_columns, earnings_calendar_columns,
13    equity_identity_columns, equity_quote_columns, equity_technical_columns, fundamentals_columns,
14    overview_columns,
15};
16use self::decode::{
17    decode_analyst, decode_analyst_forecasts, decode_analyst_fx_rates,
18    decode_analyst_price_targets, decode_analyst_recommendations, decode_earnings_calendar,
19    decode_fundamentals, decode_overview,
20};
21use self::history::{
22    decode_estimate_history, decode_point_in_time_fundamentals, estimate_history_fields,
23    fundamentals_history_fields,
24};
25use crate::client::TradingViewClient;
26use crate::error::Result;
27use crate::market_data::{
28    InstrumentIdentity, QuoteSnapshot, RowDecoder, SnapshotLoader, TechnicalSummary, decode_quote,
29    decode_technical,
30};
31use crate::scanner::fields::price;
32use crate::scanner::{Column, Market, ScanQuery, SortOrder, Ticker};
33use crate::transport::quote_session::QuoteSessionClient;
34
35pub use history::{
36    EarningsMetrics, EstimateHistory, EstimateMetrics, EstimateObservation, FundamentalMetrics,
37    FundamentalObservation, PointInTimeFundamentals,
38};
39pub use types::{
40    AnalystForecasts, AnalystFxRates, AnalystPriceTargets, AnalystRecommendations, AnalystSummary,
41    EarningsCalendar, EquityOverview, FundamentalsSnapshot,
42};
43
44const HISTORY_BATCH_CONCURRENCY: usize = 4;
45
46/// High-level equity data facade built on top of TradingView screener and quote sessions.
47///
48/// This facade exposes typed products for common stock workflows such as:
49///
50/// - quotes and market movers
51/// - fundamentals snapshots
52/// - analyst summaries, targets, forecasts, and earnings metadata
53/// - native estimate history and point-in-time fundamentals
54///
55/// # Examples
56///
57/// ```no_run
58/// use tvdata_rs::{Result, TradingViewClient};
59///
60/// #[tokio::main]
61/// async fn main() -> Result<()> {
62///     let client = TradingViewClient::builder().build()?;
63///     let equity = client.equity();
64///
65///     let quote = equity.quote("NASDAQ:AAPL").await?;
66///     let analyst = equity.analyst_summary("NASDAQ:AAPL").await?;
67///
68///     println!("{:?} {:?}", quote.close, analyst.price_targets.average);
69///     Ok(())
70/// }
71/// ```
72#[derive(Debug, Clone, Copy)]
73pub struct EquityClient<'a> {
74    client: &'a TradingViewClient,
75}
76
77impl<'a> EquityClient<'a> {
78    pub const fn new(client: &'a TradingViewClient) -> Self {
79        Self { client }
80    }
81
82    pub fn client(&self) -> &'a TradingViewClient {
83        self.client
84    }
85
86    /// Fetches a typed equity quote snapshot for a single symbol.
87    pub async fn quote(&self, symbol: impl Into<Ticker>) -> Result<QuoteSnapshot> {
88        let columns = equity_quote_columns();
89        let decoder = RowDecoder::new(&columns);
90        let row = self.loader().fetch_one(symbol, columns).await?;
91        Ok(decode_quote(&decoder, &row))
92    }
93
94    /// Fetches typed quote snapshots for multiple symbols while preserving the requested order.
95    pub async fn quotes<I, T>(&self, symbols: I) -> Result<Vec<QuoteSnapshot>>
96    where
97        I: IntoIterator<Item = T>,
98        T: Into<Ticker>,
99    {
100        let columns = equity_quote_columns();
101        let decoder = RowDecoder::new(&columns);
102        let rows = self.loader().fetch_many(symbols, columns).await?;
103
104        Ok(rows
105            .iter()
106            .map(|row| decode_quote(&decoder, row))
107            .collect::<Vec<_>>())
108    }
109
110    /// Fetches a fundamentals snapshot for a single equity symbol.
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// use tvdata_rs::{Result, TradingViewClient};
116    ///
117    /// #[tokio::main]
118    /// async fn main() -> Result<()> {
119    ///     let client = TradingViewClient::builder().build()?;
120    ///     let fundamentals = client.equity().fundamentals("NASDAQ:AAPL").await?;
121    ///
122    ///     println!("{:?}", fundamentals.market_cap);
123    ///     Ok(())
124    /// }
125    /// ```
126    pub async fn fundamentals(&self, symbol: impl Into<Ticker>) -> Result<FundamentalsSnapshot> {
127        let columns = fundamentals_columns();
128        let decoder = RowDecoder::new(&columns);
129        let row = self.loader().fetch_one(symbol, columns).await?;
130        Ok(decode_fundamentals(&decoder, &row))
131    }
132
133    pub async fn fundamentals_history(
134        &self,
135        symbol: impl Into<Ticker>,
136    ) -> Result<PointInTimeFundamentals> {
137        self.fundamentals_point_in_time(symbol).await
138    }
139
140    /// Fetches native point-in-time fundamentals history from TradingView quote sessions.
141    pub async fn fundamentals_point_in_time(
142        &self,
143        symbol: impl Into<Ticker>,
144    ) -> Result<PointInTimeFundamentals> {
145        let symbol = symbol.into();
146        let instrument = self.fetch_identity(&symbol).await?;
147        let values = self
148            .quote_session()
149            .fetch_fields(&symbol, &fundamentals_history_fields())
150            .await?;
151
152        Ok(decode_point_in_time_fundamentals(instrument, &values))
153    }
154
155    pub async fn fundamentals_histories<I, T>(
156        &self,
157        symbols: I,
158    ) -> Result<Vec<PointInTimeFundamentals>>
159    where
160        I: IntoIterator<Item = T>,
161        T: Into<Ticker>,
162    {
163        self.fetch_many_history_products(symbols, |symbol| async move {
164            self.fundamentals_point_in_time(symbol).await
165        })
166        .await
167    }
168
169    pub async fn fundamentals_point_in_time_batch<I, T>(
170        &self,
171        symbols: I,
172    ) -> Result<Vec<PointInTimeFundamentals>>
173    where
174        I: IntoIterator<Item = T>,
175        T: Into<Ticker>,
176    {
177        self.fundamentals_histories(symbols).await
178    }
179
180    pub async fn fundamentals_batch<I, T>(&self, symbols: I) -> Result<Vec<FundamentalsSnapshot>>
181    where
182        I: IntoIterator<Item = T>,
183        T: Into<Ticker>,
184    {
185        let columns = fundamentals_columns();
186        let decoder = RowDecoder::new(&columns);
187        let rows = self.loader().fetch_many(symbols, columns).await?;
188
189        Ok(rows
190            .iter()
191            .map(|row| decode_fundamentals(&decoder, row))
192            .collect::<Vec<_>>())
193    }
194
195    /// Fetches a rich analyst snapshot combining recommendations, targets, forecasts, earnings,
196    /// and FX conversion metadata.
197    ///
198    /// # Examples
199    ///
200    /// ```no_run
201    /// use tvdata_rs::{Result, TradingViewClient};
202    ///
203    /// #[tokio::main]
204    /// async fn main() -> Result<()> {
205    ///     let client = TradingViewClient::builder().build()?;
206    ///     let analyst = client.equity().analyst_summary("NASDAQ:AAPL").await?;
207    ///
208    ///     println!("{:?}", analyst.recommendations.rating);
209    ///     Ok(())
210    /// }
211    /// ```
212    pub async fn analyst_summary(&self, symbol: impl Into<Ticker>) -> Result<AnalystSummary> {
213        let columns = analyst_columns();
214        let decoder = RowDecoder::new(&columns);
215        let row = self.loader().fetch_one(symbol, columns).await?;
216        Ok(decode_analyst(&decoder, &row))
217    }
218
219    /// Fetches native analyst estimate history from TradingView quote sessions.
220    ///
221    /// The returned model is point-in-time aware and distinguishes annual and quarterly
222    /// observations.
223    ///
224    /// # Examples
225    ///
226    /// ```no_run
227    /// use tvdata_rs::{Result, TradingViewClient};
228    ///
229    /// #[tokio::main]
230    /// async fn main() -> Result<()> {
231    ///     let client = TradingViewClient::builder().build()?;
232    ///     let history = client.equity().estimate_history("NASDAQ:AAPL").await?;
233    ///
234    ///     println!(
235    ///         "quarterly observations: {}",
236    ///         history.quarterly.len()
237    ///     );
238    ///     Ok(())
239    /// }
240    /// ```
241    pub async fn estimate_history(&self, symbol: impl Into<Ticker>) -> Result<EstimateHistory> {
242        let symbol = symbol.into();
243        let instrument = self.fetch_identity(&symbol).await?;
244        let values = self
245            .quote_session()
246            .fetch_fields(&symbol, &estimate_history_fields())
247            .await?;
248
249        Ok(decode_estimate_history(instrument, &values))
250    }
251
252    pub async fn earnings_history(&self, symbol: impl Into<Ticker>) -> Result<EstimateHistory> {
253        self.estimate_history(symbol).await
254    }
255
256    pub async fn estimate_histories<I, T>(&self, symbols: I) -> Result<Vec<EstimateHistory>>
257    where
258        I: IntoIterator<Item = T>,
259        T: Into<Ticker>,
260    {
261        self.fetch_many_history_products(symbols, |symbol| async move {
262            self.estimate_history(symbol).await
263        })
264        .await
265    }
266
267    pub async fn earnings_histories<I, T>(&self, symbols: I) -> Result<Vec<EstimateHistory>>
268    where
269        I: IntoIterator<Item = T>,
270        T: Into<Ticker>,
271    {
272        self.estimate_histories(symbols).await
273    }
274
275    pub async fn analyst_recommendations(
276        &self,
277        symbol: impl Into<Ticker>,
278    ) -> Result<AnalystRecommendations> {
279        let columns = analyst_recommendation_columns();
280        self.fetch_analyst_section(symbol, columns, decode_analyst_recommendations)
281            .await
282    }
283
284    pub async fn price_targets(&self, symbol: impl Into<Ticker>) -> Result<AnalystPriceTargets> {
285        let columns = analyst_price_target_columns();
286        self.fetch_analyst_section(symbol, columns, decode_analyst_price_targets)
287            .await
288    }
289
290    pub async fn analyst_forecasts(&self, symbol: impl Into<Ticker>) -> Result<AnalystForecasts> {
291        let columns = analyst_forecast_columns();
292        self.fetch_analyst_section(symbol, columns, decode_analyst_forecasts)
293            .await
294    }
295
296    pub async fn earnings_calendar(&self, symbol: impl Into<Ticker>) -> Result<EarningsCalendar> {
297        let columns = earnings_calendar_columns();
298        self.fetch_analyst_section(symbol, columns, decode_earnings_calendar)
299            .await
300    }
301
302    pub async fn earnings_events(&self, symbol: impl Into<Ticker>) -> Result<EarningsCalendar> {
303        self.earnings_calendar(symbol).await
304    }
305
306    pub async fn analyst_fx_rates(&self, symbol: impl Into<Ticker>) -> Result<AnalystFxRates> {
307        let columns = analyst_fx_rate_columns();
308        self.fetch_analyst_section(symbol, columns, decode_analyst_fx_rates)
309            .await
310    }
311
312    pub async fn analyst_summaries<I, T>(&self, symbols: I) -> Result<Vec<AnalystSummary>>
313    where
314        I: IntoIterator<Item = T>,
315        T: Into<Ticker>,
316    {
317        let columns = analyst_columns();
318        let decoder = RowDecoder::new(&columns);
319        let rows = self.loader().fetch_many(symbols, columns).await?;
320
321        Ok(rows
322            .iter()
323            .map(|row| decode_analyst(&decoder, row))
324            .collect::<Vec<_>>())
325    }
326
327    pub async fn technical_summary(&self, symbol: impl Into<Ticker>) -> Result<TechnicalSummary> {
328        let columns = equity_technical_columns();
329        let decoder = RowDecoder::new(&columns);
330        let row = self.loader().fetch_one(symbol, columns).await?;
331        Ok(decode_technical(&decoder, &row))
332    }
333
334    pub async fn technical_summaries<I, T>(&self, symbols: I) -> Result<Vec<TechnicalSummary>>
335    where
336        I: IntoIterator<Item = T>,
337        T: Into<Ticker>,
338    {
339        let columns = equity_technical_columns();
340        let decoder = RowDecoder::new(&columns);
341        let rows = self.loader().fetch_many(symbols, columns).await?;
342
343        Ok(rows
344            .iter()
345            .map(|row| decode_technical(&decoder, row))
346            .collect::<Vec<_>>())
347    }
348
349    pub async fn overview(&self, symbol: impl Into<Ticker>) -> Result<EquityOverview> {
350        let columns = overview_columns();
351        let decoder = RowDecoder::new(&columns);
352        let row = self.loader().fetch_one(symbol, columns).await?;
353        Ok(decode_overview(&decoder, &row))
354    }
355
356    pub async fn overviews<I, T>(&self, symbols: I) -> Result<Vec<EquityOverview>>
357    where
358        I: IntoIterator<Item = T>,
359        T: Into<Ticker>,
360    {
361        let columns = overview_columns();
362        let decoder = RowDecoder::new(&columns);
363        let rows = self.loader().fetch_many(symbols, columns).await?;
364
365        Ok(rows
366            .iter()
367            .map(|row| decode_overview(&decoder, row))
368            .collect::<Vec<_>>())
369    }
370
371    /// Fetches the strongest equity movers in a market by percentage change.
372    pub async fn top_gainers(
373        &self,
374        market: impl Into<Market>,
375        limit: usize,
376    ) -> Result<Vec<QuoteSnapshot>> {
377        self.loader()
378            .fetch_market_quotes(market, limit, price::CHANGE_PERCENT.sort(SortOrder::Desc))
379            .await
380    }
381
382    pub async fn top_losers(
383        &self,
384        market: impl Into<Market>,
385        limit: usize,
386    ) -> Result<Vec<QuoteSnapshot>> {
387        self.loader()
388            .fetch_market_quotes(market, limit, price::CHANGE_PERCENT.sort(SortOrder::Asc))
389            .await
390    }
391
392    pub async fn most_active(
393        &self,
394        market: impl Into<Market>,
395        limit: usize,
396    ) -> Result<Vec<QuoteSnapshot>> {
397        self.loader()
398            .fetch_market_active_quotes(market, limit, price::VOLUME.sort(SortOrder::Desc))
399            .await
400    }
401
402    fn loader(&self) -> SnapshotLoader<'_> {
403        SnapshotLoader::new(self.client, ScanQuery::new())
404    }
405
406    fn quote_session(&self) -> QuoteSessionClient<'_> {
407        QuoteSessionClient::new(self.client)
408    }
409
410    async fn fetch_analyst_section<T, F>(
411        &self,
412        symbol: impl Into<Ticker>,
413        columns: Vec<Column>,
414        decode: F,
415    ) -> Result<T>
416    where
417        F: FnOnce(&RowDecoder, &crate::scanner::ScanRow) -> T,
418    {
419        let decoder = RowDecoder::new(&columns);
420        let row = self.loader().fetch_one(symbol, columns).await?;
421        Ok(decode(&decoder, &row))
422    }
423
424    async fn fetch_identity(&self, symbol: &Ticker) -> Result<InstrumentIdentity> {
425        let columns = equity_identity_columns();
426        let decoder = RowDecoder::new(&columns);
427        let row = self.loader().fetch_one(symbol.clone(), columns).await?;
428        Ok(decoder.identity(&row))
429    }
430
431    async fn fetch_many_history_products<I, T, O, F, Fut>(
432        &self,
433        symbols: I,
434        fetcher: F,
435    ) -> Result<Vec<O>>
436    where
437        I: IntoIterator<Item = T>,
438        T: Into<Ticker>,
439        F: Fn(Ticker) -> Fut + Copy,
440        Fut: std::future::Future<Output = Result<O>>,
441    {
442        let mut products =
443            stream::iter(symbols.into_iter().map(Into::into).enumerate())
444                .map(|(index, symbol)| async move {
445                    fetcher(symbol).await.map(|product| (index, product))
446                })
447                .buffered(HISTORY_BATCH_CONCURRENCY)
448                .try_collect::<Vec<_>>()
449                .await?;
450        products.sort_by_key(|(index, _)| *index);
451        Ok(products.into_iter().map(|(_, product)| product).collect())
452    }
453}
454
455impl TradingViewClient {
456    /// Returns the high-level equity facade.
457    ///
458    /// # Examples
459    ///
460    /// ```no_run
461    /// use tvdata_rs::{Result, TradingViewClient};
462    ///
463    /// #[tokio::main]
464    /// async fn main() -> Result<()> {
465    ///     let client = TradingViewClient::builder().build()?;
466    ///     let movers = client.equity().top_gainers("america", 5).await?;
467    ///
468    ///     println!("movers: {}", movers.len());
469    ///     Ok(())
470    /// }
471    /// ```
472    pub fn equity(&self) -> EquityClient<'_> {
473        EquityClient::new(self)
474    }
475}