Skip to main content

tvdata_rs/history/
request.rs

1use std::collections::BTreeMap;
2
3use bon::Builder;
4use time::{Date, OffsetDateTime};
5
6use crate::metadata::DataLineage;
7use crate::scanner::{InstrumentRef, Ticker};
8
9pub(crate) fn default_history_batch_concurrency() -> usize {
10    4
11}
12
13pub(crate) fn default_history_max_chunk_bars() -> u32 {
14    5_000
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Builder)]
18pub struct HistoryRequest {
19    #[builder(into)]
20    pub symbol: Ticker,
21    pub interval: Interval,
22    pub bars: u32,
23    #[builder(default)]
24    pub fetch_all: bool,
25    #[builder(default)]
26    pub session: TradingSession,
27    #[builder(default)]
28    pub adjustment: Adjustment,
29}
30
31impl HistoryRequest {
32    pub fn new(symbol: impl Into<Ticker>, interval: Interval, bars: u32) -> Self {
33        Self::builder()
34            .symbol(symbol)
35            .interval(interval)
36            .bars(bars)
37            .build()
38    }
39
40    pub fn max(symbol: impl Into<Ticker>, interval: Interval) -> Self {
41        Self::builder()
42            .symbol(symbol)
43            .interval(interval)
44            .bars(default_history_max_chunk_bars())
45            .fetch_all(true)
46            .build()
47    }
48
49    pub fn session(mut self, session: TradingSession) -> Self {
50        self.session = session;
51        self
52    }
53
54    pub fn adjustment(mut self, adjustment: Adjustment) -> Self {
55        self.adjustment = adjustment;
56        self
57    }
58
59    pub fn fetch_all(mut self) -> Self {
60        self.fetch_all = true;
61        self
62    }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Builder)]
66pub struct HistoryBatchRequest {
67    pub symbols: Vec<Ticker>,
68    pub interval: Interval,
69    pub bars: u32,
70    #[builder(default)]
71    pub fetch_all: bool,
72    #[builder(default)]
73    pub session: TradingSession,
74    #[builder(default)]
75    pub adjustment: Adjustment,
76    #[builder(default = default_history_batch_concurrency())]
77    pub concurrency: usize,
78}
79
80impl HistoryBatchRequest {
81    pub fn new<I, T>(symbols: I, interval: Interval, bars: u32) -> Self
82    where
83        I: IntoIterator<Item = T>,
84        T: Into<Ticker>,
85    {
86        Self {
87            symbols: symbols.into_iter().map(Into::into).collect(),
88            interval,
89            bars,
90            fetch_all: false,
91            session: TradingSession::Regular,
92            adjustment: Adjustment::Splits,
93            concurrency: default_history_batch_concurrency(),
94        }
95    }
96
97    pub fn max<I, T>(symbols: I, interval: Interval) -> Self
98    where
99        I: IntoIterator<Item = T>,
100        T: Into<Ticker>,
101    {
102        Self {
103            symbols: symbols.into_iter().map(Into::into).collect(),
104            interval,
105            bars: default_history_max_chunk_bars(),
106            fetch_all: true,
107            session: TradingSession::Regular,
108            adjustment: Adjustment::Splits,
109            concurrency: default_history_batch_concurrency(),
110        }
111    }
112
113    pub fn symbols<I, T>(mut self, symbols: I) -> Self
114    where
115        I: IntoIterator<Item = T>,
116        T: Into<Ticker>,
117    {
118        self.symbols = symbols.into_iter().map(Into::into).collect();
119        self
120    }
121
122    pub fn push_symbol(mut self, symbol: impl Into<Ticker>) -> Self {
123        self.symbols.push(symbol.into());
124        self
125    }
126
127    pub fn session(mut self, session: TradingSession) -> Self {
128        self.session = session;
129        self
130    }
131
132    pub fn adjustment(mut self, adjustment: Adjustment) -> Self {
133        self.adjustment = adjustment;
134        self
135    }
136
137    pub fn concurrency(mut self, concurrency: usize) -> Self {
138        self.concurrency = concurrency;
139        self
140    }
141
142    pub fn fetch_all(mut self) -> Self {
143        self.fetch_all = true;
144        self
145    }
146
147    pub(crate) fn to_requests(&self) -> Vec<HistoryRequest> {
148        self.symbols
149            .iter()
150            .cloned()
151            .map(|symbol| HistoryRequest {
152                symbol,
153                interval: self.interval,
154                bars: self.bars,
155                fetch_all: self.fetch_all,
156                session: self.session,
157                adjustment: self.adjustment,
158            })
159            .collect()
160    }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
164pub enum Interval {
165    Min1,
166    Min3,
167    Min5,
168    Min15,
169    Min30,
170    Min45,
171    Hour1,
172    Hour2,
173    Hour3,
174    Hour4,
175    Day1,
176    Week1,
177    Month1,
178    Custom(&'static str),
179}
180
181impl Interval {
182    pub fn as_code(self) -> &'static str {
183        match self {
184            Self::Min1 => "1",
185            Self::Min3 => "3",
186            Self::Min5 => "5",
187            Self::Min15 => "15",
188            Self::Min30 => "30",
189            Self::Min45 => "45",
190            Self::Hour1 => "1H",
191            Self::Hour2 => "2H",
192            Self::Hour3 => "3H",
193            Self::Hour4 => "4H",
194            Self::Day1 => "1D",
195            Self::Week1 => "1W",
196            Self::Month1 => "1M",
197            Self::Custom(code) => code,
198        }
199    }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
203pub enum TradingSession {
204    #[default]
205    Regular,
206    Extended,
207}
208
209impl TradingSession {
210    pub(crate) fn as_code(self) -> &'static str {
211        match self {
212            Self::Regular => "regular",
213            Self::Extended => "extended",
214        }
215    }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
219pub enum Adjustment {
220    #[default]
221    Splits,
222    None,
223}
224
225impl Adjustment {
226    pub(crate) fn as_code(self) -> &'static str {
227        match self {
228            Self::Splits => "splits",
229            Self::None => "none",
230        }
231    }
232}
233
234#[derive(Debug, Clone, PartialEq)]
235pub struct Bar {
236    pub time: OffsetDateTime,
237    pub open: f64,
238    pub high: f64,
239    pub low: f64,
240    pub close: f64,
241    pub volume: Option<f64>,
242}
243
244#[derive(Debug, Clone, PartialEq)]
245pub struct HistorySeries {
246    pub symbol: Ticker,
247    pub interval: Interval,
248    pub bars: Vec<Bar>,
249    pub provenance: HistoryProvenance,
250}
251
252impl HistorySeries {
253    pub fn latest(&self) -> Option<&Bar> {
254        self.bars.last()
255    }
256
257    pub fn bar_on(&self, date: Date) -> Option<&Bar> {
258        self.bars.iter().find(|bar| bar.time.date() == date)
259    }
260
261    pub fn latest_on_or_before(&self, date: Date) -> Option<&Bar> {
262        self.bars.iter().rev().find(|bar| bar.time.date() <= date)
263    }
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
267pub enum BarSelectionPolicy {
268    ExactDate,
269    #[default]
270    LatestOnOrBefore,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Builder)]
274pub struct DailyBarRequest {
275    pub symbols: Vec<InstrumentRef>,
276    pub asof: Date,
277    #[builder(default)]
278    pub adjustment: Adjustment,
279    #[builder(default)]
280    pub session: TradingSession,
281    #[builder(default)]
282    pub selection: BarSelectionPolicy,
283    #[builder(default = default_history_batch_concurrency())]
284    pub concurrency: usize,
285}
286
287impl DailyBarRequest {
288    pub fn new<I>(symbols: I, asof: Date) -> Self
289    where
290        I: IntoIterator<Item = InstrumentRef>,
291    {
292        Self {
293            symbols: symbols.into_iter().collect(),
294            asof,
295            adjustment: Adjustment::default(),
296            session: TradingSession::default(),
297            selection: BarSelectionPolicy::default(),
298            concurrency: default_history_batch_concurrency(),
299        }
300    }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Builder)]
304pub struct DailyBarRangeRequest {
305    pub symbols: Vec<InstrumentRef>,
306    pub start: Date,
307    pub end: Date,
308    #[builder(default)]
309    pub adjustment: Adjustment,
310    #[builder(default)]
311    pub session: TradingSession,
312    #[builder(default = default_history_batch_concurrency())]
313    pub concurrency: usize,
314}
315
316impl DailyBarRangeRequest {
317    pub fn new<I>(symbols: I, start: Date, end: Date) -> Self
318    where
319        I: IntoIterator<Item = InstrumentRef>,
320    {
321        Self {
322            symbols: symbols.into_iter().collect(),
323            start,
324            end,
325            adjustment: Adjustment::default(),
326            session: TradingSession::default(),
327            concurrency: default_history_batch_concurrency(),
328        }
329    }
330}
331
332pub type HistorySeriesMap = BTreeMap<Ticker, HistorySeries>;
333
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct HistoryProvenance {
336    pub requested_symbol: Ticker,
337    pub resolved_symbol: Ticker,
338    pub exchange: Option<String>,
339    pub session: TradingSession,
340    pub adjustment: Adjustment,
341    pub authenticated: bool,
342    pub lineage: DataLineage,
343}
344
345#[cfg(test)]
346mod tests {
347    use time::macros::datetime;
348
349    use super::*;
350    use crate::metadata::{DataSourceKind, HistoryKind};
351
352    #[test]
353    fn selects_bars_by_date() {
354        let series = HistorySeries {
355            symbol: Ticker::from_static("NASDAQ:AAPL"),
356            interval: Interval::Day1,
357            bars: vec![
358                Bar {
359                    time: datetime!(2026-03-18 00:00 UTC),
360                    open: 1.0,
361                    high: 2.0,
362                    low: 0.5,
363                    close: 1.5,
364                    volume: Some(10.0),
365                },
366                Bar {
367                    time: datetime!(2026-03-20 00:00 UTC),
368                    open: 2.0,
369                    high: 3.0,
370                    low: 1.5,
371                    close: 2.5,
372                    volume: Some(12.0),
373                },
374            ],
375            provenance: HistoryProvenance {
376                requested_symbol: Ticker::from_static("NASDAQ:AAPL"),
377                resolved_symbol: Ticker::from_static("NASDAQ:AAPL"),
378                exchange: Some("NASDAQ".to_owned()),
379                session: TradingSession::Regular,
380                adjustment: Adjustment::Splits,
381                authenticated: false,
382                lineage: DataLineage::new(
383                    DataSourceKind::HistoryWebSocket,
384                    HistoryKind::Native,
385                    datetime!(2026-03-22 00:00 UTC),
386                    Some(datetime!(2026-03-20 00:00 UTC)),
387                ),
388            },
389        };
390
391        assert_eq!(
392            series
393                .bar_on(datetime!(2026-03-18 00:00 UTC).date())
394                .unwrap()
395                .close,
396            1.5
397        );
398        assert_eq!(
399            series
400                .latest_on_or_before(datetime!(2026-03-19 00:00 UTC).date())
401                .unwrap()
402                .close,
403            1.5
404        );
405        assert_eq!(series.latest().unwrap().close, 2.5);
406    }
407}