#![allow(clippy::uninlined_format_args)]
#![allow(clippy::format_in_format_args)]
use std::sync::Arc;
use clap::{Parser, ValueEnum};
use ibapi::market_data::historical::{BarTimestamp, HistoricalBarUpdate};
use ibapi::prelude::*;
use time::OffsetDateTime;
#[derive(Parser)]
#[command(name = "historical_data")]
#[command(about = "Fetch historical bar data from IB")]
struct Args {
#[arg(long, value_enum, default_value = "stock")]
asset: AssetType,
#[arg(long, short = 's')]
streaming_only: bool,
}
#[derive(Clone, Debug, ValueEnum)]
enum AssetType {
Stock,
Forex,
Futures,
}
fn format_time(ts: &BarTimestamp) -> String {
match ts {
BarTimestamp::Date(d) => format!("{:04}-{:02}-{:02}", d.year(), d.month() as u8, d.day()),
BarTimestamp::DateTime(dt) => format!("{:02}:{:02}:{:02}", dt.hour(), dt.minute(), dt.second()),
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let args = Args::parse();
let client = Arc::new(Client::connect("127.0.0.1:4002", 100).await?);
println!("Connected to IB Gateway");
let contract = match args.asset {
AssetType::Stock => Contract::stock("AAPL").build(),
AssetType::Forex => Contract::forex("EUR", "USD").build(),
AssetType::Futures => {
println!("Resolving front-month contract for ES...");
let query = Contract::futures("ES").front_month().build();
let details = client.contract_details(&query).await?;
let front = details.into_iter().next().ok_or("No front-month contract found for ES")?;
println!(
" Found front-month: local_symbol='{}', contract_month='{}'",
front.contract.local_symbol, front.contract.last_trade_date_or_contract_month
);
front.contract
}
};
println!("Requesting historical data for {} ({:?})", contract.symbol, args.asset);
println!(
" local_symbol: '{}', exchange: {}, contract_month: '{}'\n",
contract.local_symbol, contract.exchange, contract.last_trade_date_or_contract_month
);
if !args.streaming_only {
println!("=== Head Timestamp ===");
let head_timestamp = client
.head_timestamp(&contract, HistoricalWhatToShow::Trades, TradingHours::Regular)
.await?;
println!("Earliest available historical data: {head_timestamp:?}");
println!("\n=== Recent Intraday Data (5-min bars) ===");
let end_date = OffsetDateTime::now_utc();
let historical_data = client
.historical_data(&contract, HistoricalBarSize::Min5)
.what_to_show(HistoricalWhatToShow::Trades)
.duration(1.days())
.ending(end_date)
.fetch()
.await?;
println!("Period: {} to {}", historical_data.start, historical_data.end);
println!("Total bars: {}", historical_data.bars.len());
for (i, bar) in historical_data.bars.iter().take(5).enumerate() {
println!(
"Bar {}: {} - O: ${:.2}, H: ${:.2}, L: ${:.2}, C: ${:.2}, V: {:.0}",
i + 1,
format_time(&bar.date),
bar.open,
bar.high,
bar.low,
bar.close,
bar.volume
);
}
if historical_data.bars.len() > 10 {
println!("...");
let start_idx = historical_data.bars.len() - 5;
for (i, bar) in historical_data.bars.iter().skip(start_idx).enumerate() {
println!(
"Bar {}: {} - O: ${:.2}, H: ${:.2}, L: ${:.2}, C: ${:.2}, V: {:.0}",
start_idx + i + 1,
format_time(&bar.date),
bar.open,
bar.high,
bar.low,
bar.close,
bar.volume
);
}
}
println!("\n=== Daily Data (past month) ===");
let daily_data = client
.historical_data(&contract, HistoricalBarSize::Day)
.what_to_show(HistoricalWhatToShow::Trades)
.duration(1.months())
.ending(end_date)
.fetch()
.await?;
println!("Daily bars received: {}", daily_data.bars.len());
for bar in daily_data.bars.iter().take(5) {
println!(
"{}: O: ${:.2}, H: ${:.2}, L: ${:.2}, C: ${:.2}, V: {:.0}K",
format_time(&bar.date),
bar.open,
bar.high,
bar.low,
bar.close,
bar.volume / 1000.0
);
}
println!("\n=== Different Data Types ===");
let bid_data = client
.historical_data(&contract, HistoricalBarSize::Min)
.what_to_show(HistoricalWhatToShow::Bid)
.duration(1.days())
.ending(end_date)
.fetch()
.await?;
println!("Bid bars (1-min): {} bars", bid_data.bars.len());
if let Some(bar) = bid_data.bars.first() {
println!(" First bar: {} - Bid: ${:.2}", format_time(&bar.date), bar.close);
}
let ask_data = client
.historical_data(&contract, HistoricalBarSize::Min)
.what_to_show(HistoricalWhatToShow::Ask)
.duration(1.days())
.ending(end_date)
.fetch()
.await?;
println!("Ask bars (1-min): {} bars", ask_data.bars.len());
if let Some(bar) = ask_data.bars.first() {
println!(" First bar: {} - Ask: ${:.2}", format_time(&bar.date), bar.close);
}
println!("\n=== Histogram Data ===");
let histogram = client.histogram_data(&contract, TradingHours::Regular, HistoricalBarSize::Day).await?;
println!("Histogram entries: {}", histogram.len());
for entry in histogram.iter().take(5) {
println!(" Price: ${:.2}, Size: {}", entry.price, entry.size);
}
}
println!("\n=== Streaming Historical Data (keepUpToDate=true) ===");
println!("Press Ctrl+C to stop streaming...\n");
let what_to_show = match args.asset {
AssetType::Forex => HistoricalWhatToShow::MidPoint,
_ => HistoricalWhatToShow::Trades,
};
let subscription = client
.historical_data(&contract, HistoricalBarSize::Min)
.what_to_show(what_to_show)
.trading_hours(TradingHours::Extended)
.duration(1.days())
.stream()
.await?;
let mut subscription = subscription.filter_data();
while let Some(update) = subscription.next().await {
match update {
Ok(HistoricalBarUpdate::Historical(data)) => {
println!("Received {} initial historical bars", data.bars.len());
if let Some(bar) = data.bars.last() {
println!(
" Latest: {} - O: ${:.2}, H: ${:.2}, L: ${:.2}, C: ${:.2}",
format_time(&bar.date),
bar.open,
bar.high,
bar.low,
bar.close
);
}
println!("Now streaming updates...");
}
Ok(HistoricalBarUpdate::Update(bar)) => {
println!(
"UPDATE: {} - O: ${:.2}, H: ${:.2}, L: ${:.2}, C: ${:.2}, V: {:.0}",
format_time(&bar.date),
bar.open,
bar.high,
bar.low,
bar.close,
bar.volume
);
}
Ok(HistoricalBarUpdate::End { start, end }) => {
println!("Stream ended: {} - {}", start, end);
break;
}
Err(e) => {
eprintln!("Stream error: {e}");
break;
}
}
}
println!("Stream ended");
println!("\nHistorical data example completed!");
Ok(())
}