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