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#[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 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 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 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 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 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 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 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 pub fn equity(&self) -> EquityClient<'_> {
473 EquityClient::new(self)
474 }
475}