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_MINIMUM_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,
#[arg(long, default_value = "SEC")]
interval: BarSize,
}
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;
let chunk_seconds = historical_chunk_seconds(args.interval);
while chunk_start < args.end {
let chunk_end = (chunk_start + time::Duration::seconds(chunk_seconds)).min(args.end);
let request_start = historical_request_start(chunk_start, chunk_end, args.interval);
let historical_data = client
.historical_data(&contract, args.interval)
.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,
interval: BarSize,
) -> OffsetDateTime {
let minimum_seconds = HISTORICAL_MINIMUM_SECONDS.max(bar_size_seconds(interval));
(chunk_start - time::Duration::SECOND).min(chunk_end - time::Duration::seconds(minimum_seconds))
}
fn historical_chunk_seconds(interval: BarSize) -> i64 {
HISTORICAL_MINIMUM_CHUNK_SECONDS.max(bar_size_seconds(interval))
}
fn bar_size_seconds(interval: BarSize) -> i64 {
match interval {
BarSize::Sec => 1,
BarSize::Sec5 => 5,
BarSize::Sec10 => 10,
BarSize::Sec15 => 15,
BarSize::Sec30 => 30,
BarSize::Min => 60,
BarSize::Min2 => 120,
BarSize::Min3 => 180,
BarSize::Min4 => 240,
BarSize::Min5 => 300,
BarSize::Min10 => 600,
BarSize::Min15 => 900,
BarSize::Min20 => 1_200,
BarSize::Min30 => 1_800,
BarSize::Hour => 3_600,
BarSize::Hour2 => 7_200,
BarSize::Hour3 => 10_800,
BarSize::Hour4 => 14_400,
BarSize::Hour8 => 28_800,
BarSize::Day => 86_400,
BarSize::Week => 604_800,
BarSize::Month => 2_678_400,
}
}
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_chunk_seconds, historical_request_start};
use clap::Parser;
use ibapi::market_data::historical::BarSize;
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");
assert_eq!(cli.args.interval, BarSize::Sec);
}
#[test]
fn parse_interval() {
let cli = TestCli::try_parse_from([
"historical",
"--start",
"2026-07-31T09:30:00-04:00",
"--end",
"2026-07-31T16:00:00-04:00",
"--interval",
"MIN5",
])
.unwrap();
assert_eq!(cli.args.interval, BarSize::Min5);
}
#[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, BarSize::Sec);
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, BarSize::Sec);
assert_eq!((chunk_start - request_start).whole_seconds(), 1);
}
#[test]
fn expand_chunks_for_large_intervals() {
assert_eq!(historical_chunk_seconds(BarSize::Hour), 3_600);
assert_eq!(historical_chunk_seconds(BarSize::Day), 86_400);
}
}