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 Contract that carries con_id, so the venue is not asked to guess which listing you meant.
  • req_historical_data(req_id, contract, end_date_time, duration_str, bar_size_setting, what_to_show, use_rth), with format_date, keep_up_to_date and chart_options left at their defaults.
  • Driving the callback loop on a daemon thread with EClient.run.
  • Reading OHLCV off the bar object.

An empty end_date_time means now. use_rth=1 keeps the series inside regular hours.

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

One blocking method does the same request and returns the list:

bars = c.historical_data(spy, "", "1 D", "5 mins", "TRADES", use_rth=1)

It sends, waits and returns, taking its answer off the queue by request id and releasing the interpreter lock while it waits. Do not run run() beside it on the same client: run() drains every queue rather than only its own, so the two compete for the answer. Pick the callbacks or pick this.

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_setting 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=... python examples/hello_bar_data.py

Source

"""Recipe: fetch 1 day of 5-minute SPY bars, print first/last bar.

Usage:
    IB_USERNAME=... IB_PASSWORD=... python examples/hello_bar_data.py
"""

import os
import threading

from ib_dx import EClient, EWrapper, Contract


class BarsWrapper(EWrapper):
    def __init__(self):
        self.bars = []
        self.done = threading.Event()

    def historical_data(self, req_id, bar):
        self.bars.append(bar)

    def historical_data_end(self, req_id, start, end):
        self.done.set()

    def error(self, req_id, error_time, code, msg, advanced=""):
        if code not in (2104, 2106, 2158):
            print(f"[error] {code}: {msg}")
        # A refused request is told the error alone: no end follows it.
        if req_id == 1:
            self.done.set()


w = BarsWrapper()
c = EClient(w)
c.connect(
    username=os.environ["IB_USERNAME"],
    password=os.environ["IB_PASSWORD"],
    paper=True,
)
threading.Thread(target=c.run, daemon=True).start()

spy = Contract()
spy.con_id = 756733
spy.symbol = "SPY"
spy.sec_type = "STK"
spy.exchange = "SMART"
spy.currency = "USD"

c.req_historical_data(
    1, spy,
    end_date_time="",
    duration_str="1 D",
    bar_size_setting="5 mins",
    what_to_show="TRADES",
    use_rth=1,
)

if not w.done.wait(timeout=30):
    raise RuntimeError("the bar request was not answered")

print(f"bars: {len(w.bars)}")
if w.bars:
    first, last = w.bars[0], w.bars[-1]
    print(f"  first: {first.date}  O={first.open} H={first.high} L={first.low} C={first.close} V={first.volume}")
    print(f"  last : {last.date}  O={last.open} H={last.high} L={last.low} C={last.close} V={last.volume}")

c.disconnect()