use time::OffsetDateTime;
use crate::contracts::Contract;
use crate::market_data::historical::{BarSize, BarTimestamp, Duration, HistoricalBarUpdate, HistoricalData, WhatToShow};
use crate::market_data::TradingHours;
use crate::Error;
#[cfg(test)]
#[path = "data_tests.rs"]
mod tests;
#[must_use = "HistoricalDataBuilder does nothing until you call .fetch() or .stream()"]
pub struct HistoricalDataBuilder<'a, C> {
client: &'a C,
contract: &'a Contract,
bar_size: BarSize,
what_to_show: WhatToShow,
trading_hours: TradingHours,
duration: Option<Duration>,
ending: Option<OffsetDateTime>,
between: Option<(OffsetDateTime, OffsetDateTime)>,
}
impl<'a, C> HistoricalDataBuilder<'a, C> {
pub(crate) fn new(client: &'a C, contract: &'a Contract, bar_size: BarSize) -> Self {
Self {
client,
contract,
bar_size,
what_to_show: WhatToShow::Trades,
trading_hours: TradingHours::Regular,
duration: None,
ending: None,
between: None,
}
}
pub fn what_to_show(mut self, what_to_show: WhatToShow) -> Self {
self.what_to_show = what_to_show;
self
}
pub fn trading_hours(mut self, trading_hours: TradingHours) -> Self {
self.trading_hours = trading_hours;
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn ending(mut self, end_date: OffsetDateTime) -> Self {
self.ending = Some(end_date);
self
}
pub fn between(mut self, start: OffsetDateTime, end: OffsetDateTime) -> Self {
self.between = Some((start, end));
self
}
fn resolve_date_spec(&self) -> Result<(Option<OffsetDateTime>, Duration), Error> {
match (self.between, self.duration, self.ending) {
(Some(_), Some(_), _) | (Some(_), _, Some(_)) => Err(Error::InvalidArgument(
"historical_data: cannot mix .between(...) with .duration()/.ending()".to_owned(),
)),
(Some((start, end)), None, None) => {
if end <= start {
return Err(Error::InvalidArgument(
"historical_data: .between(start, end) requires end > start".to_owned(),
));
}
Ok((Some(end), covering_duration(end - start)))
}
(None, Some(duration), ending) => Ok((ending, duration)),
(None, None, _) => Err(Error::InvalidArgument(
"historical_data: must set .duration() or .between(...)".to_owned(),
)),
}
}
fn resolve_for_stream(&self) -> Result<Duration, Error> {
if self.ending.is_some() || self.between.is_some() {
return Err(Error::InvalidArgument(
"historical_data().stream(): IBKR requires end_date = None for streaming; drop .ending() / .between()".to_owned(),
));
}
self.duration
.ok_or_else(|| Error::InvalidArgument("historical_data().stream(): must set .duration()".to_owned()))
}
}
const SECONDS_PER_DAY: u64 = 86_400;
const MAX_DAYS: u64 = 365;
fn covering_duration(span: time::Duration) -> Duration {
let seconds = span.whole_seconds().unsigned_abs() + u64::from(span.subsec_nanoseconds() != 0);
if seconds <= SECONDS_PER_DAY {
return Duration::seconds(seconds as i32);
}
let days = seconds.div_ceil(SECONDS_PER_DAY) + 1;
if days <= MAX_DAYS {
return Duration::days(days as i32);
}
Duration::years(seconds.div_ceil(MAX_DAYS * SECONDS_PER_DAY) as i32)
}
fn trim_to_range(mut data: HistoricalData, range: Option<(OffsetDateTime, OffsetDateTime)>) -> HistoricalData {
let Some((start, end)) = range else {
return data;
};
let (first, last) = (BarTimestamp::from(start), BarTimestamp::from(end));
data.bars.retain(|bar| bar.date >= first && bar.date < last);
data.start = start;
data.end = end;
data
}
#[cfg(feature = "sync")]
impl<'a> HistoricalDataBuilder<'a, crate::client::sync::Client> {
pub fn fetch(self) -> Result<HistoricalData, Error> {
let (end_date, duration) = self.resolve_date_spec()?;
let data = crate::market_data::historical::sync::historical_data(
self.client,
self.contract,
end_date,
duration,
self.bar_size,
self.what_to_show,
self.trading_hours,
)?;
Ok(trim_to_range(data, self.between))
}
pub fn stream(self) -> Result<crate::subscriptions::sync::Subscription<HistoricalBarUpdate>, Error> {
let duration = self.resolve_for_stream()?;
crate::market_data::historical::sync::historical_data_stream(
self.client,
self.contract,
duration,
self.bar_size,
self.what_to_show,
self.trading_hours,
)
}
}
#[cfg(feature = "async")]
impl<'a> HistoricalDataBuilder<'a, crate::client::r#async::Client> {
pub async fn fetch(self) -> Result<HistoricalData, Error> {
let (end_date, duration) = self.resolve_date_spec()?;
let data = crate::market_data::historical::r#async::historical_data(
self.client,
self.contract,
end_date,
duration,
self.bar_size,
self.what_to_show,
self.trading_hours,
)
.await?;
Ok(trim_to_range(data, self.between))
}
pub async fn stream(self) -> Result<crate::subscriptions::Subscription<HistoricalBarUpdate>, Error> {
let duration = self.resolve_for_stream()?;
crate::market_data::historical::r#async::historical_data_stream(
self.client,
self.contract,
duration,
self.bar_size,
self.what_to_show,
self.trading_hours,
)
.await
}
}