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 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 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 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 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 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 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 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 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;