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