use crate::DEFAULT_SYMBOL;
use clap::Args as ClapArgs;
use ibapi::{
Client,
contracts::Contract,
market_data::{TradingHours, historical::BarSize},
};
use std::error::Error;
use time::{self, OffsetDateTime, format_description::well_known::Iso8601};
const HISTORICAL_CHUNK_SECONDS: i64 = 1_800;
const HISTORICAL_MINIMUM_SECONDS: i64 = 60;
#[derive(ClapArgs)]
pub struct Args {
#[arg(long, value_parser = parse_datetime)]
start: OffsetDateTime,
#[arg(long, value_parser = parse_datetime)]
end: OffsetDateTime,
#[arg(long, default_value = DEFAULT_SYMBOL)]
symbol: String,
}
pub async fn run(address: &str, client_id: i32, args: &Args) -> Result<(), Box<dyn Error>> {
let client = Client::connect(address, client_id).await?;
fetch_historical_data(&client, args).await
}
async fn fetch_historical_data(client: &Client, args: &Args) -> Result<(), Box<dyn Error>> {
if args.end <= args.start {
return Err("end datetime must be after start datetime".into());
}
let contract = Contract::stock(&args.symbol).build();
println!("date,open,high,low,close,volume,wap,count");
let mut chunk_start = args.start;
while chunk_start < args.end {
let chunk_end =
(chunk_start + time::Duration::seconds(HISTORICAL_CHUNK_SECONDS)).min(args.end);
let request_start = historical_request_start(chunk_start, chunk_end);
let historical_data = client
.historical_data(&contract, BarSize::Sec)
.trading_hours(TradingHours::Extended)
.between(request_start, chunk_end)
.fetch()
.await?;
for bar in historical_data
.bars
.into_iter()
.filter(|bar| bar.date >= chunk_start.into() && bar.date < chunk_end.into())
{
println!(
"{},{},{},{},{},{},{},{}",
bar.date,
bar.open,
bar.high,
bar.low,
bar.close,
bar.volume,
bar.wap,
bar.count,
);
}
chunk_start = chunk_end;
}
Ok(())
}
fn historical_request_start(
chunk_start: OffsetDateTime,
chunk_end: OffsetDateTime,
) -> OffsetDateTime {
(chunk_start - time::Duration::SECOND)
.min(chunk_end - time::Duration::seconds(HISTORICAL_MINIMUM_SECONDS))
}
fn parse_datetime(value: &str) -> Result<OffsetDateTime, String> {
OffsetDateTime::parse(value, &Iso8601::DEFAULT).map_err(|error| error.to_string())
}
#[cfg(test)]
mod tests {
use super::{Args, historical_request_start};
use clap::Parser;
use time::OffsetDateTime;
#[derive(Parser)]
struct TestCli {
#[command(flatten)]
args: Args,
}
#[test]
fn parse_range() {
let cli = TestCli::try_parse_from([
"historical",
"--start",
"2026-07-31T09:30:00-04:00",
"--end",
"2026-07-31T16:00:00-04:00",
])
.unwrap();
assert_eq!((cli.args.end - cli.args.start).whole_seconds(), 23_400);
assert_eq!(cli.args.symbol, "SOXL");
}
#[test]
fn pad_short_request() {
let chunk_start = OffsetDateTime::from_unix_timestamp(1_785_527_995).unwrap();
let chunk_end = OffsetDateTime::from_unix_timestamp(1_785_528_005).unwrap();
let request_start = historical_request_start(chunk_start, chunk_end);
assert_eq!((chunk_end - request_start).whole_seconds(), 60);
}
#[test]
fn overlap_full_request() {
let chunk_start = OffsetDateTime::from_unix_timestamp(1_785_504_600).unwrap();
let chunk_end = OffsetDateTime::from_unix_timestamp(1_785_506_400).unwrap();
let request_start = historical_request_start(chunk_start, chunk_end);
assert_eq!((chunk_start - request_start).whole_seconds(), 1);
}
}