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;
#[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 historical_data = client
.historical_data(&contract, BarSize::Sec)
.trading_hours(TradingHours::Extended)
.between(chunk_start - time::Duration::SECOND, 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 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;
use clap::Parser;
#[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");
}
}