Skip to main content

tvdata_rs/history/
mod.rs

1mod fetch;
2mod request;
3
4use request::HistorySeriesMap;
5use time::{Date, OffsetDateTime};
6#[cfg(feature = "tracing")]
7use tracing::debug;
8
9use crate::batch::BatchResult;
10use crate::client::{ClientEvent, HistoryBatchCompletedEvent, HistoryBatchMode, TradingViewClient};
11use crate::error::Result;
12use crate::scanner::{InstrumentRef, Ticker};
13
14pub use request::{
15    Adjustment, Bar, BarSelectionPolicy, DailyBarRangeRequest, DailyBarRequest,
16    HistoryBatchRequest, HistoryProvenance, HistoryRequest, HistorySeries, Interval,
17    TradingSession,
18};
19
20fn estimated_daily_bars_since(date: Date) -> u32 {
21    let today = OffsetDateTime::now_utc().date();
22    let days = if date <= today {
23        (today - date).whole_days().max(0) as u32
24    } else {
25        0
26    };
27
28    days.saturating_add(32).max(64)
29}
30
31fn daily_batch_request(
32    symbols: &[InstrumentRef],
33    start: Date,
34    session: TradingSession,
35    adjustment: Adjustment,
36    concurrency: usize,
37) -> HistoryBatchRequest {
38    HistoryBatchRequest::new(
39        symbols.iter().cloned().map(Into::<Ticker>::into),
40        Interval::Day1,
41        estimated_daily_bars_since(start),
42    )
43    .session(session)
44    .adjustment(adjustment)
45    .concurrency(concurrency)
46}
47
48impl TradingViewClient {
49    /// Downloads multiple OHLCV history series with bounded concurrency.
50    ///
51    /// # Examples
52    ///
53    /// ```no_run
54    /// use tvdata_rs::{HistoryBatchRequest, Interval, Result, TradingViewClient};
55    ///
56    /// #[tokio::main]
57    /// async fn main() -> Result<()> {
58    ///     let client = TradingViewClient::builder().build()?;
59    ///     let request = HistoryBatchRequest::new(["NASDAQ:AAPL", "NASDAQ:MSFT"], Interval::Day1, 30);
60    ///     let series = client.history_batch(&request).await?;
61    ///
62    ///     println!("series: {}", series.len());
63    ///     Ok(())
64    /// }
65    /// ```
66    pub async fn history_batch(&self, request: &HistoryBatchRequest) -> Result<Vec<HistorySeries>> {
67        #[cfg(feature = "tracing")]
68        debug!(
69            target: "tvdata_rs::history",
70            requested = request.symbols.len(),
71            interval = request.interval.as_code(),
72            bars = request.bars,
73            concurrency = request.concurrency,
74            "starting history batch",
75        );
76
77        let series = fetch::fetch_history_batch_with(
78            request.to_requests(),
79            request.concurrency,
80            |request| async move { self.history(&request).await },
81        )
82        .await?;
83
84        self.emit_event(ClientEvent::HistoryBatchCompleted(
85            HistoryBatchCompletedEvent {
86                requested: request.symbols.len(),
87                successes: series.len(),
88                missing: 0,
89                failures: 0,
90                concurrency: request.concurrency,
91                mode: HistoryBatchMode::Strict,
92            },
93        ));
94
95        Ok(series)
96    }
97
98    /// Downloads multiple OHLCV history series and returns successes, missing symbols, and
99    /// failures separately.
100    pub async fn history_batch_detailed(
101        &self,
102        request: &HistoryBatchRequest,
103    ) -> Result<BatchResult<HistorySeries>> {
104        #[cfg(feature = "tracing")]
105        debug!(
106            target: "tvdata_rs::history",
107            requested = request.symbols.len(),
108            interval = request.interval.as_code(),
109            bars = request.bars,
110            concurrency = request.concurrency,
111            "starting detailed history batch",
112        );
113
114        let batch = fetch::fetch_history_batch_detailed_with(
115            request.to_requests(),
116            request.concurrency,
117            |request| async move { self.history(&request).await },
118        )
119        .await?;
120
121        self.emit_event(ClientEvent::HistoryBatchCompleted(
122            HistoryBatchCompletedEvent {
123                requested: request.symbols.len(),
124                successes: batch.successes.len(),
125                missing: batch.missing.len(),
126                failures: batch.failures.len(),
127                concurrency: request.concurrency,
128                mode: HistoryBatchMode::Detailed,
129            },
130        ));
131
132        Ok(batch)
133    }
134
135    /// Downloads the maximum history currently available for multiple symbols.
136    ///
137    /// The crate keeps requesting older bars over the chart websocket until
138    /// TradingView stops returning new history.
139    ///
140    /// # Examples
141    ///
142    /// ```no_run
143    /// use tvdata_rs::{Interval, Result, TradingViewClient};
144    ///
145    /// #[tokio::main]
146    /// async fn main() -> Result<()> {
147    ///     let client = TradingViewClient::builder().build()?;
148    ///     let series = client
149    ///         .download_history_max(["NASDAQ:AAPL", "NASDAQ:MSFT"], Interval::Day1)
150    ///         .await?;
151    ///
152    ///     println!("series: {}", series.len());
153    ///     Ok(())
154    /// }
155    /// ```
156    pub async fn download_history_max<I, T>(
157        &self,
158        symbols: I,
159        interval: Interval,
160    ) -> Result<Vec<HistorySeries>>
161    where
162        I: IntoIterator<Item = T>,
163        T: Into<Ticker>,
164    {
165        let defaults = self.history_config();
166        let request = HistoryBatchRequest::max(symbols, interval)
167            .session(defaults.default_session)
168            .adjustment(defaults.default_adjustment)
169            .concurrency(defaults.default_batch_concurrency);
170        self.history_batch(&request).await
171    }
172
173    /// Convenience wrapper around [`TradingViewClient::history_batch`] for a list of symbols.
174    pub async fn download_history<I, T>(
175        &self,
176        symbols: I,
177        interval: Interval,
178        bars: u32,
179    ) -> Result<Vec<HistorySeries>>
180    where
181        I: IntoIterator<Item = T>,
182        T: Into<Ticker>,
183    {
184        let defaults = self.history_config();
185        let request = HistoryBatchRequest::new(symbols, interval, bars)
186            .session(defaults.default_session)
187            .adjustment(defaults.default_adjustment)
188            .concurrency(defaults.default_batch_concurrency);
189        self.history_batch(&request).await
190    }
191
192    /// Downloads multiple history series and returns them keyed by symbol.
193    pub async fn download_history_map<I, T>(
194        &self,
195        symbols: I,
196        interval: Interval,
197        bars: u32,
198    ) -> Result<HistorySeriesMap>
199    where
200        I: IntoIterator<Item = T>,
201        T: Into<Ticker>,
202    {
203        let series = self.download_history(symbols, interval, bars).await?;
204        Ok(series
205            .into_iter()
206            .map(|series| (series.symbol.clone(), series))
207            .collect())
208    }
209
210    /// Downloads the maximum history available and returns it keyed by symbol.
211    pub async fn download_history_map_max<I, T>(
212        &self,
213        symbols: I,
214        interval: Interval,
215    ) -> Result<HistorySeriesMap>
216    where
217        I: IntoIterator<Item = T>,
218        T: Into<Ticker>,
219    {
220        let series = self.download_history_max(symbols, interval).await?;
221        Ok(series
222            .into_iter()
223            .map(|series| (series.symbol.clone(), series))
224            .collect())
225    }
226
227    /// Downloads daily bars for a set of instruments and selects the best bar for the requested
228    /// trading date.
229    pub async fn daily_bars_on(&self, request: &DailyBarRequest) -> Result<BatchResult<Bar>> {
230        #[cfg(feature = "tracing")]
231        debug!(
232            target: "tvdata_rs::history",
233            symbols = request.symbols.len(),
234            asof = %request.asof,
235            selection = ?request.selection,
236            concurrency = request.concurrency,
237            "starting daily bar selection",
238        );
239
240        let history_request = daily_batch_request(
241            &request.symbols,
242            request.asof,
243            request.session,
244            request.adjustment,
245            request.concurrency,
246        );
247        let batch = self.history_batch_detailed(&history_request).await?;
248
249        let mut selected = BatchResult {
250            missing: batch.missing,
251            failures: batch.failures,
252            ..BatchResult::default()
253        };
254
255        for (ticker, series) in batch.successes {
256            let bar = match request.selection {
257                BarSelectionPolicy::ExactDate => series.bar_on(request.asof),
258                BarSelectionPolicy::LatestOnOrBefore => series.latest_on_or_before(request.asof),
259            };
260
261            match bar.cloned() {
262                Some(bar) => {
263                    selected.successes.insert(ticker, bar);
264                }
265                None => selected.missing.push(ticker),
266            }
267        }
268
269        #[cfg(feature = "tracing")]
270        debug!(
271            target: "tvdata_rs::history",
272            asof = %request.asof,
273            successes = selected.successes.len(),
274            missing = selected.missing.len(),
275            failures = selected.failures.len(),
276            "daily bar selection completed",
277        );
278
279        Ok(selected)
280    }
281
282    /// Downloads daily history and trims each successful series to the requested date window.
283    pub async fn daily_bars_range(
284        &self,
285        request: &DailyBarRangeRequest,
286    ) -> Result<BatchResult<HistorySeries>> {
287        if request.start > request.end {
288            return Ok(BatchResult::default());
289        }
290
291        #[cfg(feature = "tracing")]
292        debug!(
293            target: "tvdata_rs::history",
294            symbols = request.symbols.len(),
295            start = %request.start,
296            end = %request.end,
297            concurrency = request.concurrency,
298            "starting daily history range selection",
299        );
300
301        let history_request = daily_batch_request(
302            &request.symbols,
303            request.start,
304            request.session,
305            request.adjustment,
306            request.concurrency,
307        );
308        let batch = self.history_batch_detailed(&history_request).await?;
309
310        let mut selected = BatchResult {
311            missing: batch.missing,
312            failures: batch.failures,
313            ..BatchResult::default()
314        };
315
316        for (ticker, mut series) in batch.successes {
317            series
318                .bars
319                .retain(|bar| bar.time.date() >= request.start && bar.time.date() <= request.end);
320
321            if series.bars.is_empty() {
322                selected.missing.push(ticker);
323            } else {
324                selected.successes.insert(ticker, series);
325            }
326        }
327
328        #[cfg(feature = "tracing")]
329        debug!(
330            target: "tvdata_rs::history",
331            start = %request.start,
332            end = %request.end,
333            successes = selected.successes.len(),
334            missing = selected.missing.len(),
335            failures = selected.failures.len(),
336            "daily history range selection completed",
337        );
338
339        Ok(selected)
340    }
341}
342
343pub(crate) use fetch::fetch_history_with_timeout_for_client;