Historical Bars
Fetch one trading day of 5-minute SPY bars and print the first and last. Use this whenever you need a finished series rather than a live feed.
What this shows
- Building a
Contractthat carriescon_id, so the venue is not asked to guess which listing you meant. req_historical_data, which takes every argument the request carries:req_id, contract,end_date_time,duration,bar_size,what_to_show,use_rth,format_date,keep_up_to_date.- Pumping the callbacks with
process_msgsuntilhistorical_data_endlands, or the request's refusal does. - Reading OHLCV out of
BarData.
An empty end_date_time means now. use_rth: true keeps the series inside
regular hours. format_date: 1 asks for the date written out rather than as
seconds. keep_up_to_date: false asks for closed bars only.
What comes back
One historical_data callback per bar, in time order, then one
historical_data_end carrying the first and last timestamps of the range that
was served. A request whose bars are refused once it has been handed over is
told why on error, and nothing follows it: a gateway states no end after a
refusal, so a program waiting for the end stops on the error too. A request
kept up to date whose five-second stream the venue refuses is told nothing, as
a gateway tells it nothing: a history still to come arrives with its end, and
no update follows. When the historical connection drops, a request kept up to
date ends on 10182, and one still arriving is told 165 and asked again once the
connection is back, as a gateway does. A 165 is a notice and ends nothing: the
call that waits for the bars waits through it.
With keep_up_to_date: true the bar still forming goes on from the history's
last bar, as a gateway's does: the live five-second bars inside it are folded
into the open, high, low and volume the venue stated for it. Five-second bars
are sent as the stream sends them. One that arrives before the history is in
is held, as a gateway holds it, and folded into the history's last bar with the
next one after it; one from before the bar in hand opened is passed over. So
nothing of the bar still forming is heard before historical_data_end, and
every update is heard on historical_data_update, whatever else the request's
number has done: a live bar stream under the same number is heard on
real_time_bar, and a second request under the number, refused while this
one answers, leaves this one's dates as it asked for them. A daily session supplied with the history
keeps its opening and end across UTC midnight. Its updates use the session
end's date on the series' timezone, as the history does, under both
date-format settings. Timed daily history is returned as yyyyMMdd; explicit
weekly and monthly date strings are preserved.
When that session ends, the next five-second bar opens the next bar at midnight UTC of its own day, as a gateway opens it. That bar ends at the next midnight UTC, or at the close the history stated where that falls between the two, and is dated by where it ends on the series' timezone; a bar that has ended is opened again by the next five-second bar. So a session that opens before midnight UTC, as a future's evening session does, is dated by the day the history closed until midnight UTC, and by the next one after it. On a timezone east of UTC the next midnight UTC falls on the following day there, so the bar a session there rolls over to is dated by the day after that session, as a gateway dates it. Intraday bars open on whole multiples of their length from the epoch; a week opens on Monday and a month on its first day at midnight UTC. Updates to these calendar bars remain dates under both date-format settings.
The shorter form
Two blocking calls do the same request without a wrapper:
let bars = client.historical_data(&spy, "", "1 D", "5 mins", "TRADES", true)?;
let same = client.bars(&spy, "1 D", "5 mins")?;
bars fills in the arguments most callers state the same way: trades, regular
hours, ending now. It asks for MIDPOINT instead of TRADES on instruments that
are quoted rather than traded (CASH, CFD, CMDTY), which answer a TRADES
request with no history at all.
Cancelling historical data also withdraws the corporate-actions query held by that request. A separate corporate-actions request keeps running, even when it uses the same caller request id.
Limits
bar_size and what_to_show are checked before anything is sent, so a
misspelling is refused here rather than answered with a different series. With
keep_up_to_date: true the size must be one this client can form from
the five-second bars the venue keeps sending: five seconds up to a day in
whole multiples of five seconds, a week, or a month. A second is shorter than
what arrives and is refused. A request kept up to date is refused, as a gateway
refuses it, with an end date, on a combination, or on a series other than
TRADES, MIDPOINT, BID, ASK and the two option open-interest series — an empty
series name included.
How far back a series goes, and which bar sizes pair with which durations, are
the venue's rules. A request outside them comes back as a stated refusal on the
error callback rather than as empty bars.
Run it
IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_bar_data
Source
//! Recipe: fetch 1 day of 5-minute SPY bars, print first/last bar.
//!
//! Usage: IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_bar_data
use std::env;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use ib_dx::api::client::{Contract, EClient, EClientConfig};
use ib_dx::api::types::BarData;
use ib_dx::api::wrapper::Wrapper;
#[derive(Default)]
struct State {
bars: Vec<BarData>,
done: bool,
}
struct BarsWrapper {
state: Arc<Mutex<State>>,
}
impl Wrapper for BarsWrapper {
fn historical_data(&mut self, _req_id: i64, bar: &BarData) {
self.state.lock().unwrap().bars.push(bar.clone());
}
fn historical_data_end(&mut self, _req_id: i64, _start: &str, _end: &str) {
self.state.lock().unwrap().done = true;
}
fn error(&mut self, req_id: i64, _error_time: i64, code: i64, msg: &str, _adv: &str) {
eprintln!("[error] req_id={req_id} code={code} msg={msg}");
// A refused request is told the error alone: no end follows it.
if req_id == 1 {
self.state.lock().unwrap().done = true;
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = EClient::connect(&EClientConfig {
username: env::var("IB_USERNAME")?,
password: env::var("IB_PASSWORD")?,
paper: true,
..Default::default()
})?;
let state = Arc::new(Mutex::new(State::default()));
let mut wrapper = BarsWrapper { state: state.clone() };
let spy = Contract {
con_id: 756733,
symbol: "SPY".into(),
sec_type: "STK".into(),
exchange: "SMART".into(),
currency: "USD".into(),
..Default::default()
};
client.req_historical_data(1, &spy, "", "1 D", "5 mins", "TRADES", true, 1, false);
let deadline = Instant::now() + Duration::from_secs(30);
while Instant::now() < deadline {
client.process_msgs(&mut wrapper);
if state.lock().unwrap().done { break; }
std::thread::sleep(Duration::from_millis(20));
}
let s = state.lock().unwrap();
println!("bars: {}", s.bars.len());
if let (Some(first), Some(last)) = (s.bars.first(), s.bars.last()) {
println!(" first: {} O={} H={} L={} C={} V={}",
first.date, first.open, first.high, first.low, first.close, first.volume);
println!(" last : {} O={} H={} L={} C={} V={}",
last.date, last.open, last.high, last.low, last.close, last.volume);
}
drop(s);
client.disconnect();
Ok(())
}