Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ib_async-dx

ib_async-dx

ib_async-dx: ib_async, without the gateway ib_async-dx: ib_async, without the gateway

ib_async without the gateway. The program you already wrote against ib_async, with no IB Gateway, no Trader Workstation and no JVM between it and the venue.

Get started What drop-in means Notebooks GitHub

Two lines change

A program written against ib_async talks to IB Gateway or Trader Workstation over a socket on localhost, and that process talks to the venue. ib_async-dx puts the ib-dx engine where the gateway was: it logs in, holds the trading, market-data, historical and security-definition connections open, and answers every call ib_async makes.

- from ib_async import IB, Stock
+ from ib_async_dx import IB, Stock

  ib = IB()
- ib.connect("127.0.0.1", 4001, clientId=1)    # a gateway on localhost
+ ib.connect(username="...", password="...")   # a paper session; paper=False for live

That is the whole change. ib_async_dx is ib_async: the same names, the same objects and the same submodules, run by ib_async’s own code, installed as a dependency. Two classes differ: IB, a subclass of ib_async’s whose connect takes a login instead of a gateway’s address, and IBC, which has no gateway to start. The rest of the program — its events, its *Async calls, its Ticker and Trade, its util — runs unmodified, with the engine in place of the one layer that knew there was a socket.

Tip

Leave the credentials out of connect and it reads IB_USERNAME and IB_PASSWORD from the environment. Where those are set, the connect line can stay exactly as it was for a paper account: the host and port are not used, and the client id is carried into the login. The port does not choose paper or live; add paper=False for a live account.

What goes away

The gateway

Nothing to install, launch, log into on a schedule, or restart. No window either: it runs in a container, over ssh, on a machine with no display.

The JVM

No heap to size, and no JVM garbage collector pausing the process your ticks pass through.

The localhost socket

The engine runs inside your Python process. Nothing listens on a port, and ticks are delivered in-process.

IBC and the watchdog

No login window to script and no gateway to restart when it wedges. The engine logs in itself, and rebuilds a dropped connection on the session it already holds. ib_async's own Watchdog still runs, as a reconnect loop, with the login its IBC names.

What you get

ib_async, exactly

ib_async_dx exports ib_async's 103 names, and every one but IB and IBC is ib_async's own object, its __version__ among them. A test holds the two packages to that on every run.

Every call it makes, carried

ib_async's IB makes 67 calls on its transport at 2.1.0, and every one lands on the engine. A test reads that list out of ib_async's own source on every run.

Nothing in between

The engine is compiled Rust, running in your process through PyO3. There is no second process that can be wedged while your program looks healthy.

More than a gateway forwards

What the venue states that no documented call asks for — the order types the account may use, its algorithms, the features it has enabled, the whole option model — is a method on IB. Beyond ib_async lists them, with the four ib_async bugs fixed here.

Honest about its limits

A field the engine cannot carry is refused, as a gateway refuses a message it cannot read, instead of going out as something other than what was asked. The few things taken and not applied, and everything else that differs from ib_async over a gateway, are written down with the reason for each.

Where to go next

Status

Under active development. The Python package is here with its suite: 318 tests, 315 of which run offline on every push to main and every pull request, on Python 3.11, 3.13 and free-threaded 3.14t, 2 that need a live login, and 1 a Python version condition excludes. ib_async’s own suite is written to run against the engine too, and has not been run against the venue at this revision; see Evidence.

A Rust client — ib_async’s model in Rust spelling, on the engine’s public API — is coming. It is not in this repository yet.

Neither ib_async-dx nor the engine is published to PyPI or crates.io yet; both install from their repositories: see Getting started.

Note

An independent project. It is not affiliated with ib_async or its maintainers, nor with Interactive Brokers. ib_async-dx runs ib_async’s own code as a dependency and does not copy it. ib_async is BSD-2-Clause, and ib_async-dx is AGPL-3.0.

Getting started

What you need

  • An Interactive Brokers account, paper or live. No IB software: no IB Gateway, no Trader Workstation, and not the ibapi package either.
  • Python 3.11 or newer; for a notebook, 3.11 to 3.13 (Notebooks).
  • A Rust toolchain, 1.89 or newer. The engine installs from its repository, and installing it compiles it.

ib_async 2.1, ib_async’s current release, is a dependency of this package, and pip installs it with it. Nothing is on PyPI yet: ib_async-dx and the engine both install from their repositories.

Install

pip install "git+https://github.com/userFRM/ib-dx@c18523506144df7b9f81fdbb82b68405f24f13ea"
pip install "ib_async-dx @ git+https://github.com/userFRM/ib_async-dx"

The engine goes first, at the commit this package is tested against: it has no release yet. ib_async-dx names it as a dependency, and with nothing on PyPI, pip has to find it already installed.

Credentials

The credentials go to the session, through connect. There is no configuration file, and no process holding a login on your behalf.

from ib_async_dx import IB

ib = IB()
ib.connect(username="your_user", password="your_pass")

Left out, they are read from IB_USERNAME and IB_PASSWORD, and the scripts under scripts/ read the same two:

export IB_USERNAME="your_username"
export IB_PASSWORD="your_password"

There is no host to name. connect still takes a host, a port and a client id, because ib_async’s does. The host and port are not used: the engine finds the server the account lives on by itself. The client id is carried into the login, where it keys this session’s orders.

paper is True unless the program says otherwise; the port does not choose. A live session is asked for with paper=False, and that enters the venue’s second-factor approval, which waits on a device: connect returns once it has been answered. A paper session presents no second factor. Use a paper account while you are writing something; a live account is a live account.

timeout bounds each request ib_async makes as the session opens, as it does against a gateway. It does not bound the login, which a gateway also makes before a program connects, nor the engine’s wait of up to three seconds, inside the login, for the venue to name the working orders: a paper login presents no second factor, and a live one waits on it for as long as the engine allows. The login runs off ib_async’s event loop, so the loop keeps turning while it waits, and a connect cancelled, interrupted, or overtaken by disconnect(), ends at once; the engine drops the session that login opens. The login itself runs on until the engine returns, on a thread that does not hold the program open. A login that fails raises ConnectionError.

Important

One program per login. Each program is its own session on the login, and a second program on the same login, or a gateway, takes the session from the first; the venue says which host took it. Programs that share one gateway login under their own client ids each need a login of their own here.

Caution

Credentials are the account. Never commit them, never paste them into an issue, and never put them in a file the repository tracks.

A first program

An ib_async program, as ib_async writes one, with the import and the connect line changed:

from ib_async_dx import IB, Stock

ib = IB()
ib.connect()                                 # credentials from the environment, paper

spy = Stock("SPY", "SMART", "USD")
ib.qualifyContracts(spy)
bars = ib.reqHistoricalData(spy, "", "2 D", "1 hour", "TRADES", useRTH=True)
print(bars[-1])

ib.pendingTickersEvent += lambda tickers: print(len(tickers), "updates")
ib.reqMktData(spy)
ib.sleep(5)
ib.disconnect()

ib.sleep(), never time.sleep(). ib_async runs its event loop on the calling thread, so a plain sleep stops it: quotes stop arriving, and every stream reads as dead when it is only unattended.

What connect takes

ib_async’s parameters, with its defaults, and four keyword-only ones after them, username, password, paper and sessionFile. Every argument given at its default:

from ib_async_dx import IB, StartupFetchALL

ib = IB()
ib.connect(
    host="127.0.0.1", port=7497, clientId=1,    # host and port: accepted, not used
    timeout=4, readonly=False, account="",
    raiseSyncErrors=False, fetchFields=StartupFetchALL,
    username="", password="",                   # left empty: see below
    paper=True,                                 # False asks for a live session
    sessionFile=None,                           # where the session is kept between runs
)

A username and password left empty are an IBC’s login where one was started in the same context (see Limits), and otherwise IB_USERNAME and IB_PASSWORD.

connectAsync takes the same. readonly=True makes a read-only session, which refuses to send anything that places, changes or withdraws an order, as a gateway set to read-only does.

The session is kept between runs. With sessionFile=None it is kept in ~/.ib_dx/session-<user>-<paper|live>: readable by its owner only, sealed with the password, and refused if it names another account. A start that names a session the venue still holds is answered with a challenge rather than a full login. The venue lets a session go soon after the process holding it ends, so this covers a quick restart; a program started again later logs in afresh, and on a live account that login waits on the second factor. A path moves the file, and sessionFile=False keeps nothing.

An ib_async.IB you already hold

Code that builds ib_async.IB() itself can keep doing so, and hand the instance to attach:

import ib_async
import ib_async_dx

ib = ib_async_dx.attach(ib_async.IB(), username="your_user", password="your_pass")
ib.connect()                      # names no host: there is no gateway

attach puts the engine under that one instance and hands it back; nothing of ib_async’s classes or modules is patched. It takes the same login as connect, spelled username, password, paper, session_file and readonly; the client id is the one connect names. A session the instance holds is ended first, as its disconnect() ends one. It is what ib_async_dx.IB does on every connect.

Next steps

  • What drop-in means — the promise, and how it is proven
  • Running ib_async itself — what the engine changes, what it carries, and how ib_async’s own tests run here
  • Beyond ib_async — the four bugs fixed, and the calls ib_async has no name for
  • Notebooks — all eight of ib_async’s notebook subjects, with no gateway
  • Limits — read this before you depend on a call

What drop-in means

The promise is the one an ib_async user would ask for: an existing program changes its import and its connect line, and nothing else. The connect line gains credentials, or, for a paper account, stays as it was where IB_USERNAME and IB_PASSWORD are set, because nothing local is addressed: then only the import changes, as a gateway keeps its login in its own settings. A live account adds paper=False.

- from ib_async import IB, Stock
+ from ib_async_dx import IB, Stock

  ib = IB()
- ib.connect("127.0.0.1", 4001, clientId=1)    # a gateway on localhost
+ ib.connect(username="...", password="...")   # a paper session; paper=False for live

What the import gives

ib_async_dx is ib_async. It exports ib_async’s __all__, all 103 names, and every one of them is ib_async’s own object — the same class, the same function, the same enum, the same __version__ — except two:

  • IB, a subclass of ib_async.IB. Its connect and connectAsync put the engine in place of ib_async’s Client and then run ib_async’s own connect. Everything else on it is ib_async’s, apart from the four bug fixes and the added calls.
  • IBC, a subclass of ib_async.IBC with no gateway to launch: starting it holds the login it names for the connects made in the same context, and terminating it ends every session that login opened. ib_async’s own Watchdog, handed one, connects its IB with that login and connects it again when the session ends (Limits has the detail).

Client is ib_async’s own socket client, re-exported unchanged like every other name: a program that builds one itself still addresses a gateway. IB is what runs on the engine.

__version__ and __version_info__ are ib_async’s, so a program that checks the API level it runs on reads what it always did. This package’s own version is __ib_async_dx_version__.

ib_async’s submodules resolve here as themselves, so both import forms work: import ib_async_dx.order and from ib_async_dx.util import df. ib_async_dx.ib is ib_async’s ib module with IB replaced, and ib_async_dx.ibcontroller its ibcontroller module with IB and IBC replaced. Because the objects are ib_async’s own, isinstance checks, pickles and a module that still imports Stock from ib_async all agree with the rest of the program.

The connect line

connect is ib_async’s signature, with its parameters and defaults, and four keyword-only parameters after them: username, password, paper and sessionFile. Getting started writes it out.

  • host and port are accepted and not used. The port does not choose paper or live: the session is paper unless paper=False.
  • clientId is carried into the login, where it keys this session’s orders. One program per login: a second program on the same login takes the session from the first.
  • username and password, left empty, are an IBC’s login where one was started in the same context, and otherwise IB_USERNAME and IB_PASSWORD.
  • readonly=True makes a read-only session, which refuses to send anything that places, changes or withdraws an order, as a gateway set to read-only does.
  • timeout bounds each request ib_async makes as the session opens, as it does against a gateway. It does not bound the login, which a gateway also makes before a program connects, nor the engine’s wait of up to three seconds, inside the login, for the venue to name the working orders: a live login waits on its second factor for as long as the engine allows.

The rest is ib_async’s own connect: as it does against a gateway, it asks for the account’s values, its positions, its open and completed orders and its executions, as fetchFields says.

How it is proven

The package against ib_async, on every run. test_the_package_is_ib_async.py checks that ib_async_dx.__all__ equals ib_async’s and that every name in it is ib_async’s own object but IB and IBC, __version__ among them; that all 13 of ib_async’s submodules resolve under both import forms; that IB and IBC are subclasses of ib_async’s; and that connect and connectAsync are ib_async’s parameters, defaults and kinds with the four keyword-only ones after them. That is the parity check: a name ib_async adds to __all__, or a change to its connect, fails here before it reaches a program.

Against ib_async’s own source, on every run. test_every_call_their_library_makes_is_carried reads every call ib_async’s IB makes on its client out of ib_async’s installed source — 67 at 2.1.0 — and fails if any one of them does not land on the engine. It is not a list kept by hand, so a call ib_async adds is a failing test here rather than a program that stops. And test_a_raw_message_is_the_request_it_names.py runs each of the 80 requests ib_async’s client writes as a message, through its own send, and checks that each reads back into the message it was and reaches the engine exactly as the same request made by name.

Against ib_async’s own tests. tests/ib_async_upstream/conftest.py runs ib_async’s test suite against the engine instead of against a gateway: their shared ib fixture is an ib_async_dx.IB, connected with the login in the environment, and so is the ib_async.IB() a test builds for itself. The suite is theirs, fetched from their repository rather than copied into this one. It has not been run against the venue at this revision; what each of its three tests asks, and how to run them, is in Running ib_async itself.

Against the venue. Two tests take a live login, each with ib_async’s own IB attached. One runs an unmodified ib_async program — connect, qualify, bars, quotes through pendingTickersEvent. The other takes an order through its whole life in ib_async’s objects: priced as a what-if, placed, changed and withdrawn. Both are skipped where IB_USERNAME and IB_PASSWORD are not set, and neither has been run against the venue at this revision. The scripts under scripts/ do the same through ib_async_dx.IB against a paper account, and the notebooks do it a cell at a time. Neither has been run against the venue through ib_async_dx.IB yet, so Evidence rates both paths ✅ Offline.

What crosses, offline. The other 316 tests need no session: that a combination keeps its legs on every request path, that a fill’s cost reaches ib_async as its own CommissionReport, that a refused order reaches its Trade, that connect takes its login from the environment or an IBC and is paper unless told otherwise, that a session opens and closes once, on the program’s loop, however a connect is cancelled, overtaken or failed, and that each bug fix answers what ib_async 2.1 does not. Evidence lists what they cover.

In Rust

Coming: ib_async’s model and names, in Rust spelling, on the engine’s public API. It is not in this repository yet.

Running ib_async itself

ib_async is layered. IB, Wrapper, Ticker, Trade and everything above them are transport-agnostic; only its Client and Connection know there is a socket to a gateway on localhost. Replacing that one layer runs the rest of the library unchanged.

That is what ib_async_dx.IB does. Its connect gives the instance a client that answers from the engine, IbkrDxClient, in place of ib_async’s, and then runs ib_async’s own connect. Nothing of ib_async is copied or modified: it is installed as ib_async publishes it, and its classes and modules are left as they are.

from ib_async_dx import IB, Stock

ib = IB()
ib.connect(username="your_user", password="your_pass")

spy = Stock("SPY", "SMART", "USD")
ib.qualifyContracts(spy)
bars = ib.reqHistoricalData(spy, "", "2 D", "1 hour", "TRADES", useRTH=True)

ib.pendingTickersEvent += lambda tickers: print(len(tickers), "updates")
ib.reqMktData(spy)
ib.sleep(5)
ib.disconnect()

Their IB, their Wrapper, their events, their types — this engine underneath, and no gateway process.

An ib_async.IB built elsewhere gets the same client from ib_async_dx.attach(ib, username=..., password=...), which hands the instance back ready to connect. A session the instance holds is ended first, as its disconnect() ends one, and the client it replaces is no longer tied to it. ib_async_dx.IB calls it on every connect. An attached ib_async.IB is ib_async’s own, so the bugs fixed on ib_async_dx.IB are still there.

The session’s life

  • Callbacks run on the loop, a pass at a time. A pass hands their wrapper what the engine holds, as one batch: tcpDataArrived before its first message, tcpDataProcessed after its last, as their transport marks a packet. The loop makes each pass itself, 10 ms after the last, so a program away from its loop — in time.sleep, say — queues nothing, and one whose loop ends without a disconnect() leaves no pass running. A refusal stated inside a request call waits for the next pass, as a gateway’s comes back on the socket once the call has returned. A handler that ends the session ends the pass: nothing more of it reaches their wrapper, as their client drops what it had read once its socket is closed.
  • A handler can wait on the session. What the engine says inside a pass or a close reaches their wrapper once the engine has returned, and the next pass is due before one is made. So under util.startLoop() a blocking request made in a handler is answered, and a connect made on hearing the session end logs in, on an attached ib_async.IB as on ib_async_dx.IB.
  • The login runs off the loop, which keeps turning while it waits. What the engine announces from inside it — the connect acknowledgement, the accounts and the next id — reaches nothing there, on the login’s thread; the same three are said on the loop once it returns. Their wrapper is called on the loop’s thread and no other.
  • timeout bounds each request of ib_async’s startup sync, as it does against a gateway. The login is the engine’s to bound, the engine’s wait of up to three seconds for the venue to name the working orders among it: a paper login presents no second factor, and a live one waits on it for as long as the engine allows, as a gateway’s login is made before a program connects.
  • A connect ends at once when it is cancelled — by wait_for, a task cancel or an interrupt — or overtaken by disconnect() or by another connect. The engine is told, and drops the session its login opens instead of keeping it. disconnect() returns at once too: where the login holds the engine while it announces the session, the engine is told on a thread of its own. The login itself runs on inside the engine until the engine returns — on a live account, until the second factor is answered or its wait runs out — on a thread that does not hold the program open. An overtaken connect raises ConnectionError. Of two connects on one ib_async_dx.IB, the later one’s session is the one left open, whenever the later one is made. An attached ib_async.IB runs ib_async’s own connect, which does not stand down for another: there a connect overtaken during ib_async’s startup sync, once the engine has logged in, can still report success on the later session, or end it.
  • A login that fails raises ConnectionError, apiError says why, and the client reads disconnected, as their client fails a connect.
  • A session the engine ends as it opens fails the connect, with nothing said on disconnectedEvent: their client says nothing of a socket that closed before the API was ready either.
  • A session the engine ends once open is handled as their client handles a dropped socket, once: every waiting request fails, disconnectedEvent fires, and delivery stops. A pass that raises ends the session the same way, as their transport closes a socket whose data it could not handle.
  • A connect on an IB that is connected ends that session first, as disconnect() ends one, and opens the next, as their client closes its socket before it opens another.

What is carried

Their IB calls 67 methods on the transport layer, counted from their own source at 2.1.0. All of them are carried here. A test reads that list out of their source on every run rather than checking a list kept by hand, so a name they add is a failure here rather than a program that stops.

Their types are theirs. A callback carrying an object hands over one of this engine’s, and their wrapper reads one of theirs, so every argument is rebuilt on the way through — by its own type name, from their dataclass. A field they have and this engine does not keeps its default, and neither side needs editing when the other gains one. A fill’s cost reaches them as their CommissionReport, a histogram as their HistogramData, a historical tick as their own record.

Their requests are carried whole. A contract or an order going the other way is rebuilt as the engine’s, field by field, whatever the field: a combination keeps its legs, an algo its parameters, an order its soft-dollar tier and its conditions, joined by and or by or. A field is carried as their client sends it, one at their own default among them; what their client sends as an empty field (None, an empty string or list, their unset number) is left to the engine’s own default. A value its field cannot take — text where a number goes — is refused with 320 on errorEvent, under the request’s number, as a gateway refuses a message it cannot read, rather than the request going out on terms nobody stated.

An order is what their client writes. Their placeOrder writes the order as its message, and the message is read back as a gateway reads it, so what reaches the engine is what a gateway would be sent: a field their client does not write is not carried, and volatility, which their client clears on any order but a volatility order, is cleared. Their client also writes eTradeOnly, firmQuoteOnly and nbboPriceCap, which the venue no longer takes. They are carried to the engine, which answers an order stating one as a gateway answers it: where the venue has retired them for the account, refused with 10268, 10269 or 10270 under the order’s number; otherwise placed without it, with the notice 2168, 2169 or 2170.

Orders and requests are numbered from one counter, as their client numbers them, so an order never takes the number of a request still waiting. The counter starts past every id the account has used that a request can carry, which the venue names at every connect, and past the next order id the engine saved for the account and client id, and is kept past every one the venue names after: a new order never takes an id a fill has already spent, and an order placed elsewhere under an id wider than a request can carry leaves every request numberable. On such an account no request can be numbered past the account’s order ids, so a new order, a bracket and a what-if take the engine’s next order id, which the engine accepts, and requests go on from the counter. The engine keeps that saved id in a file of its own, and its documentation says where and how to move it.

Their client’s own messages are the requests they name. send is their client’s own, writing the fields as one message, and sendMsg reads a message back, as a gateway reads one, into the request their client writes it for, and answers it as a gateway does. A message naming no request their client writes is logged on ib_async_dx.bridge, and nothing answers it. One that does not read as the request it names is refused with 320 on errorEvent, under that request’s number where it was read before the field that failed, and under -1 before it. Either way the session carries on.

A few details are worth knowing:

  • A bar’s date is handed over in the spelling their own parser reads. Their parseIBDatetime decides the shape from the string — eight digits is a day, a date and a time and a zone separated by single spaces is an aware moment — and the frame example in their own tests calls tz_convert on the date column, which refuses a naive datetime.
  • A price and the size that goes with it reach them together, as their priceSizeTick, and a size that changed on its own as their tickSize, as a gateway sends them.
  • A refusal reaches their error in the four-argument shape their wrapper declares, as a gateway’s does, and after the call that caused it has returned. So a new order refused before it is sent reaches the Trade placeOrder handed back, which their wrapper marks as it marks one a gateway refused: Cancelled for an error, and, on ib_async_dx.IB, for 321, which ib_async counts as a warning (why).
  • The engine checks an option list as a gateway checks one: the one key taken is manual, valued 0 or 1, none is taken on the two option computations, and an entry with no key or no value, another key or another value is refused with 320, 10337 or 10338, under the request’s number, with nothing sent. reqFundamentalData reads no list. See Limits.
  • An order’s status names the client that placed it, as a gateway’s does, so their wrapper finds the Trade an order another client placed is kept under.
  • A record their wrapper builds whole arrives whole: a routing component, a family code. A bar’s average price arrives as its average, and a condition says how it joins the next. A callback that cannot be rebuilt, or that their wrapper raises on, is logged on ib_async_dx.bridge and passed over, as their decoder treats a message it cannot handle, and the session carries on.
  • Every account the login holds is listed by managedAccounts(), not only the first.
  • isConnected() answers as their client does. An outage the engine is still mending (1100 until 1102) leaves the session connected.
  • disconnect() does not call their connectionClosed, as their own client’s does not. Their wrapper treats that as a session that went away underneath them: it fails every request still waiting and raises on their global error event. That is right for a socket that dropped and wrong for a caller who asked to stop.
  • An IB connects again after it disconnected. Each session and each connect is counted, and nothing kept for an earlier one — a pass, a login, a held refusal — reaches the next.
  • A request made while not connected raises ConnectionError("Not connected"), as their client’s does. A request reached by name on ib.client takes its arguments as their client’s method does, by position or by keyword; connect and run on it are their client’s own, and reset ends the session. What leaves for the engine is paced as their client paces what it writes to a gateway: at most MaxRequests per RequestsInterval on the loop’s clock, throttleStart and throttleEnd said around a wait, and 0 disables the pacing.
  • Request ids stop at the widest a request can carry: the rest of the range is the engine’s own. An account whose ids reach it has getReqId() raise OverflowError rather than number a request the engine refuses.
  • updateEvent fires, and their wrapper’s lastTime moves, only on a pass of the engine that delivered something, as they do over a socket only when data arrives. A session with nothing arriving stays quiet, so setTimeout fires timeoutEvent.

Their own test suite

The strongest available statement about whether their library runs here is their own tests. They are not copied here; point a run at a checkout of theirs:

git clone https://github.com/ib-api-reloaded/ib_async /tmp/ib_async
git -C /tmp/ib_async checkout ab629f34c1    # 2.1.0
cp tests/ib_async_upstream/conftest.py /tmp/ib_async/tests/
IB_USERNAME=… IB_PASSWORD=… pytest /tmp/ib_async/tests \
    -o asyncio_mode=auto \
    -o asyncio_default_fixture_loop_scope=session \
    -o asyncio_default_test_loop_scope=session

Both loop scopes are needed: their session-scoped connection fixture and their tests must share one event loop, or callbacks land on a loop that is not running while the test waits on them. pandas has to be installed too: their test_contract.py imports it, and without it the run stops at collection.

The conftest replaces their shared ib fixture with an ib_async_dx.IB, connected with the login in the environment, and makes ib_async.IB this package’s, so every test runs on the engine. At 2.1.0 their suite is three tests, taken from their commit ab629f34c1; none has been run against the venue at this revision:

TestWhat it asks, and what holds offline
test_account_summaryThe account summary, through their ib fixture
test_request_error_raisedA RequestError carrying 321 from a refused what-if. ib_async 2.1 counts 321 as a warning, which never ends a request, so against any server that answers 321 its own IB waits for good; ib_async_dx.IB ends the request with the refusal, and test_a_what_if_refused_with_321_ends_with_the_refusal holds that offline
test_contract_format_data_pdIt builds its own ib_async.IB() and connects it to 127.0.0.1:4001 rather than taking the fixture. The conftest makes ib_async.IB this package’s before their tests are collected, in that run only, so that IB connects to the engine whatever host and port it names

What it does not carry

Some of what their IB reads off its client answers for a transport that has no socket. connectionStats() counts the messages each way, as their client does: a request is one sent, and what reaches their wrapper one received, over the session since it opened. Its byte counts are the engine’s count of the session’s protocol bytes with the venue. And their client’s conn, the socket connection, is not there.

Everything on their IB is routed. The rest of what differs is in Limits.

Where it answers as a gateway does

A program meeting one of these for the first time may read it as a difference. Each is what ib_async over a gateway does too.

  • readonly=True makes a read-only session, which refuses to send anything that places, changes or withdraws an order, as a gateway set to read-only does.
  • serverVersion() is 217 once connected, the level a current gateway states in its handshake and the engine states as its own, and 0 until then, as their client answers it.
  • reqExecutions() answers with the day’s executions: those this session has seen and those the venue restated when it opened, fills on orders already completed among them. ib_async 2.1’s request cannot ask for more. reqCompletedOrders() asks the venue.
  • numIds on reqIds changes nothing: ids are handed out one at a time.
  • The handshake a program makes with the gateway it connects to — verifyRequest and the three after it — is taken, and nothing answers it, as a gateway answers nothing. A name their client does not have is a missing attribute, as on theirs.
  • Callbacks that reach nothing in ib_async over a gateway reach nothing here. Their wrapper has no handler for displayGroupList, displayGroupUpdated, rerouteMktDataReq, rerouteMktDepthReq or replaceFAEnd, discards deltaNeutralValidation, and is never sent verifyMessageAPI, verifyCompleted, verifyAndAuthMessageAPI, verifyAndAuthCompleted or tickEFP, so a Ticker’s EFP fields keep their defaults. winError is no message at all. A request a gateway would reroute is itself answered differently: see Limits.
  • FlexReport fetches a report over the web with a token of its own and never touches a session, here or over a gateway.

Beyond ib_async

ib_async_dx.IB is ib_async’s IB with three kinds of addition: four of ib_async’s bugs fixed, the engine’s calls beyond the documented API, and a warning at connect. No field is added to an ib_async class, so ib_async’s objects, their reprs, util.df’s columns and equality stay ib_async’s.

ib_async’s bugs, fixed

Each fix is a small override on IB, with a test. The fixes are on ib_async_dx.IB: an ib_async.IB given to attach is ib_async’s own, and keeps its behaviour, bugs included.

In ib_async 2.1HereTest
reqUserInfo() returns []. Its wrapper’s userInfo ends the request without the White Branding ID it was answered withreqUserInfo() returns the White Branding ID, as its docstring saystest_reqUserInfo_answers_the_white_branding_id, beside ib_async’s own answer
connect asks for positions whatever fetchFields saysLeft out of fetchFields, StartupFetch.POSITIONS means the connect’s own request is not made, so positions() stays empty until reqPositions(), which asks as usual. Any other request, made while the connect runs among them, is the program’s owntest_fetchFields_without_positions_asks_for_no_positions, beside ib_async’s own answer; test_only_the_connects_own_startup_request_for_positions_is_skipped
Error 321 is a warning, which never ends what it is about. A what-if refused with it never resolves, and a new order refused with it stays ValidationError, open for good. ib_async’s own test expects a RequestError carrying 321, and its wrapper notes that a new order refused with 321 should probably be deletedib_async’s own rule for 110 applied to 321: a request waiting under the number ends with the refusal, raised as RequestError where RaiseRequestErrors is set, and a new order still PendingSubmit is cancelled. On an order already working, 321 stays a warning, since a refused change leaves the order livetest_a_what_if_refused_with_321_ends_with_the_refusal, test_a_refused_new_order_reaches_its_trade, test_321_on_an_order_already_working_stays_a_warning
disconnect() does nothing while a connect is under way, and the connect then opens the session the program asked to closeThe connect ends with ConnectionError, and the engine drops the session its login openstest_a_disconnect_during_the_login_ends_it

The tests are in tests/python/test_ib_runs_on_the_engine.py and tests/python/test_a_session_opens_and_closes_on_the_loop.py.

The engine’s calls beyond the documented API

The venue states more than the documented API has messages for. These methods read it from the engine, and return types of this package’s own: TickerExtras, OptionModel, OrderPreset, CompetingSession, CorporateAction, ScannedStrategy and PositionElsewhere, with the engine’s own SpreadScan for what a spread scan looks for. All are importable from ib_async_dx and none is in __all__. Called while not connected, before connect or after disconnect, each raises ConnectionError("Not connected"), as ib_async’s client does.

MethodReturnsWhat it answers
reqMktDataEx(..., marketDataType=None)TickerreqMktData’s arguments, and a market data type for this request only, numbered as reqMarketDataType numbers them: 1 live, 2 frozen, 3 delayed, 4 delayed frozen. None keeps the session’s. A contract holds one subscription: asked again while subscribed, it follows the one that is up, so cancel between two types
reqCurrentTimeInMillis(), reqCurrentTimeInMillisAsync()intThe time in milliseconds since the epoch: a call in the documented API that ib_async does not have. It is read off the session’s clock, the local clock set by the venue’s to within about a second, as a gateway answers it from its own clock
tickerExtras(ticker)TickerExtrasWhat the venue has stated for the ticker’s market data request beyond ib_async’s Ticker, read when called: sharesOutstanding, openAYearAgo, shortSaleRestricted (a short-sale circuit breaker is on, not whether the contract can be shorted), and statedFigures, statedRows, numberedFigures and pairedFigures, series by the venue’s own numbering. A series is asked for by its number in genericTickList. A figure not stated is nan
optionModel(ticker)OptionModel or NoneThe venue’s model of the ticker’s option: the eight figures of OptionComputation and the ten it has no field for — calDays, rate, rho, fugit, exerciseBoundary, forwardCoeff, modelYield, bridgeYield, timeValue and priceBasedVol. A figure not stated is None; priceBasedVol is always a bool, False when the venue did not state it
closingOptionModel(ticker)OptionModel or NoneThe same model as it stood at the close
companyData(contract)dict[int, list[tuple[str, str]]]What the venue states about the contract’s company or terms, as its own key and value pairs, by series. Asked for by number in genericTickList, and kept after the cancel. Empty means not entitled or nothing stated; the two cannot be told apart
enabledFeatures()list[str]The capability tokens the venue granted this account at logon
orderPermissions()dict[str, list[str]]The order types the venue permits, by security type
permittedOrderTypes(secType)list[str] or NoneThe order types permitted for one security type, or None if it is not permitted. An order the account may not place comes back Inactive with no text
algorithms()dict[str, list[str]]The algorithms the venue offers, keyed PROVIDER/SECTYPE
algorithmsFor(secType)list[str]The algorithms offered for one security type, across every provider
orderPresets()list[OrderPreset]The sets of order defaults the account holds, as key, version and lastChanged. Their values are not carried
competingSession()CompetingSession or NoneAnother session that held the account when this one connected: its origin, when it logged in (loggedInAt, in UTC), and readOnly, true when this session may read but not trade because the other holds the account
reqPing(), lastRtt()None, float or NonereqPing measures the round trip to the venue; lastRtt reads the latest, in milliseconds
reqCorporateActions(contract, startDate, endDate), reqCorporateActionsAsync(...)list[CorporateAction]A contract’s corporate actions over a range of days, YYYYMMDD, asked for by its conId. Each states its kind as the venue names it — CD a cash dividend, SD a dividend in shares, SS a split, SO a spin-off, RO a rights offer, FR a future rolling into the next month — with its date, value and currency, and the announceDate, recordDate, payDate, paymentType and distributionType the kind carries; one it does not carry is empty. A request given up before its answer, on a RequestTimeout among other ways, is withdrawn; one dropped by disconnect() raises ConnectionError
reqSpreadScan(contract, scan, timeout=10), reqSpreadScanAsync(...)list[ScannedStrategy]An underlying, by its conId, scanned by the venue for strategies worth putting on. scan is the engine’s SpreadScan, whose fields are the venue’s own words. As reqScannerData does, it subscribes, takes the first answer and cancels; the subscription’s quotes reach the underlying’s ticker meanwhile. Each strategy states its legs as contract ids and sizes, a leg sold negative, its kind and aggression as the venue numbers them, its thirteen figures in the venue’s order, its breakEvens and its lastFigure; a figure not stated is nan. Unanswered within timeout seconds of being asked, 0 for no limit, it found nothing: []; refused, it cancels nothing. The engine keeps a scan’s answer for as long as the underlying’s market data is subscribed, so a scan made while it still is — by the program’s own reqMktData, or by a scan just ended — can be answered with the strategies the last scan found. A scan dropped by disconnect() raises ConnectionError
positionsElsewhere()list[PositionElsewhere]Holdings the venue reports that this broker does not hold itself, as conId, symbol, secType, currency, position, avgCost and held: 'Away' for a position held at another broker, 'DisplayOnly' for a row shown but not held, 'Aside' for one reported apart without saying why. Kept out of positions(), so the account is not overstated
accountValuesElsewhere(held)list[AccountValue]The account figures for one of those sets, 'Away', 'DisplayOnly' or 'Aside', as ib_async’s own AccountValue with an empty modelCode. A figure stated in two currencies is two rows. Kept out of accountValues() and accountValueEvent

A warning at connect. When another session held the account as this one connected, connect logs it at WARNING on this package’s ib_async_dx.ib logger: where the other session connected from, when it logged in as the venue states it, and whether it holds the account. It is said only of a session still open, and never fails a connect that has completed. ib_async’s own loggers carry only what ib_async says.

Tip

These calls are one-way. A program that uses one cannot move back to a gateway, because a gateway has no message to carry it. Everything ib_async itself names moves both ways.

Notebooks

notebooks/ holds eight notebooks, one on each of ib_async’s own subjects, written against ib_async itself — its IB, its contracts, its events, its util.df — and run on the engine through ib_async_dx.IB. None of them starts a gateway, and every one opens a paper session.

Each begins the same way. It is the one cell that differs from a notebook written for a gateway: the import names ib_async_dx, and connect takes the login.

import os
from dotenv import load_dotenv
from ib_async_dx import IB, util

util.startLoop()
load_dotenv()

ib = IB()
ib.connect(
    username=os.environ["IB_USERNAME"],
    password=os.environ["IB_PASSWORD"],
    paper=True,
)

util.startLoop() is ib_async’s own: it applies nest_asyncio so that ib_async’s blocking calls can run inside the notebook’s loop, as it does over a gateway. A handler can make one too, a request on an error or a connect when the session ends. nest_asyncio cannot run asyncio’s timeouts on Python 3.14, so there ib_async’s own calls fail under it, over a gateway as here: run the notebooks on Python 3.11 to 3.13.

The eight

NotebookWhat it showsib_async calls
basicsWhat the account is worth, what it holds, and one quoteaccountSummary, positions, qualifyContracts, reqMktData, cancelMktData
contract_detailsWhat the venue knows about a contract, how it answers a description that matches more than one, and a search by namereqContractDetails, reqMatchingSymbols
bar_dataHow far back the venue holds a series, the bars themselves as a frame, and a series kept up to datereqHeadTimeStamp, reqHistoricalData, util.df, keepUpToDate=True, cancelHistoricalData
tick_dataTop of book on three contracts as it changes, and every print as it happensreqMktData, pendingTickersEvent, reqTickByTickData, ticker.tickByTicks
market_depthWhich venues will answer for a book, a ten-level book, and the book movingreqMktDepthExchanges, reqMktDepth, ticker.domBids, ticker.domAsks, ticker.updateEvent
scannersWhat the venue can scan for, one scan run, and its rows as a framereqScannerParameters, ScannerSubscription, reqScannerData, util.df
orderingAn order placed, watched, moved and withdrawn, and one previewed rather than sentplaceOrder, LimitOrder, trade.log, cancelOrder, openTrades, whatIfOrder
option_chainThe S&P 500 index’s option chains, the options within 20 points of it on the next three monthly expiries, and their quotes and model greeksreqMarketDataType, reqTickers, reqSecDefOptParams, qualifyContracts, ticker.modelGreeks

A few things they show along the way:

  • A quote wants a qualified contract. ib_async keys a Ticker by the contract, and a contract without its conId cannot be a key, so qualifyContracts comes first.
  • One Ticker per contract, amended in place. pendingTickersEvent fires with the tickers that changed, rather than on a timer; tick-by-tick is the trade stream itself rather than a summary of it.
  • A kept-up-to-date series amends its last bar as it forms, rather than repeating it.
  • A book asked for at a named venue is answered by that venue. Asked for at none, it is acknowledged and may produce nothing, which is what an account with no aggregate entitlement is told.
  • The venue states its scanner parameters as one XML document, and the notebook reads the scan codes out of it.
  • A Trade is amended as the venue reports on it, and its log is the whole history of the order. A modification is the same order under the same id, at a new price; whatIfOrder asks what an order would cost the account and sends nothing.
  • An option chain is every expiry against every strike. The notebook takes the options it wants from that matrix and qualifies them in one call.

Note

ordering places a real order on the paper account: a buy at half the last price, meant to rest unfilled until a later cell withdraws it. A price is not a guarantee; the trade’s status says what happened. Run the notebook through, or withdraw it yourself.

Running them

pip install "git+https://github.com/userFRM/ib-dx@c18523506144df7b9f81fdbb82b68405f24f13ea"
pip install "ib_async-dx @ git+https://github.com/userFRM/ib_async-dx" \
    jupyter python-dotenv pandas
git clone https://github.com/userFRM/ib_async-dx
jupyter lab ib_async-dx/notebooks

util.df needs pandas, which ib_async does not install on its own. The credentials come from a .env file at the repository root, read by python-dotenv, as IB_USERNAME and IB_PASSWORD. The repository ignores that file; keep it that way.

One thing to keep

ib.sleep(), never time.sleep(). The library runs its event loop on the calling thread, so a plain sleep stops it: quotes stop arriving, and every stream reads as dead when it is only unattended.

Limits

What a caller has to know before writing against ib_async-dx, sorted by what kind of thing it is: where ib_async on the engine behaves differently from ib_async over a gateway, what is taken and not applied, and what is the venue’s answer rather than this package’s.

ib_async runs as itself, so its calls, its arguments and its answers are its own. What differs is what its IB reads off a transport with no socket underneath, and what the engine does that a gateway does not. What a gateway refuses is refused as a gateway refuses it, on errorEvent. What is taken and not applied is named below, where it happens. Where the engine answers as a gateway does, and a program might not expect it to, Running ib_async itself says so.

Where it differs from a gateway

  • connect addresses nothing. host and port are accepted and not used, and the port does not choose paper or live: the session is paper unless connect is given paper=False. clientId is carried into the login.
  • One program per login. Each program is its own session on the login, and a second program on the same login, or a gateway, takes the session from the first; the venue says which host took it. Programs that share one gateway login under their own client ids each need a login of their own here.
  • Every session hears every order on the account, whatever its clientId: trades(), openTrades(), reqOpenOrders() and openOrderEvent include the account’s orders placed elsewhere — in TWS, on the phone, or by a program under another client id. Over a gateway, a program is told of its own orders only, unless it connects as client 0 or as the gateway’s master client, or asks with reqAllOpenOrders().
  • A request a gateway reroutes is refused. Asked for a quote or a book on a contract the venue serves under another, a contract for difference standing for a share among them, a gateway tells the program where to ask instead, and ib_async, which has no handler for that, hears nothing. Here the request is refused in the venue’s words, on errorEvent.
  • A pass’s ticks reach a ticker in the engine’s order. The engine states a quote’s prices before its sizes, and a price is handed over with the size that goes with it, so the bid, ask and last a pass states land in ticker.ticks after the other ticks of that pass. The ticker’s fields end the pass as they would over a gateway.
  • connectionStats() counts the messages each way, as ib_async’s client does: a request is one sent, and what reaches ib_async’s wrapper one received. Its byte counts are the session’s protocol bytes with the venue, as the engine counts them, where ib_async’s are the bytes on its socket to a gateway.
  • The engine checks each option list as a gateway checks it. On nine of the eleven requests a gateway reads a list on — reqMktData, placeOrder (the order’s orderMiscOptions), reqMktDepth, reqHistoricalData, reqScannerSubscription, reqRealTimeBars, reqNewsArticle, reqHistoricalNews and reqHistoricalTicks — the one key taken is manual, valued 0 or 1, and it changes nothing a gateway sends. The other two, calculateImpliedVolatility and calculateOptionPrice, take no key at all. An entry with no key or no value is refused with 320, a key the request does not take with 10337 and another value with 10338, on errorEvent under the request’s number, and the request is not sent. Where the venue lifts the checks for the account (NOAPIMISCVLD among its enabledFeatures()), no key or value is refused on those grounds, and a manual that does not read as the number 0 or 1 is still refused, with 321. reqFundamentalData reads no list, as a gateway reads none: fundamentalDataOptions is taken, and nothing in it is checked or applied.
  • An account named on an account request is checked as a gateway checks it. groupName on reqAccountSummary, the account on reqAccountUpdates, and the account on reqPnL and reqPnLSingle are refused where a gateway refuses them, in its words. On a login holding one account, the account named on reqAccountUpdates is ignored, as a gateway ignores it. On a login holding several, each request answers for the account it names, and All on reqAccountSummary for every account the login holds. modelCode, an advisor group and AllNonProp are taken and not applied, and so is All on reqPnL, reqPnLSingle, reqAccountUpdatesMulti and reqPositionsMulti, which answer for the session’s account; the engine logs a warning once per session saying so.
  • IBC launches nothing, and holds the login it names. ib_async_dx.IBC is ib_async’s with no gateway to launch: the engine logs in on connect, and rebuilds a dropped connection on the session it already holds. Starting it holds its userid and password, on a live session where tradingMode is 'live' and on paper otherwise, as the gateway it would launch holds a login; a connect in the same context that names no login of its own logs in with it, and an empty userid or password is read from IB_USERNAME or IB_PASSWORD. A login named on connect, or given to attach, comes first. Terminating it ends every session that login opened, as stopping a gateway ends the sessions connected to it, and from whichever context it is terminated, no connect logs in with it afterwards. ib_async’s own Watchdog, handed one, is a reconnect loop: it starts the IBC and connects its IB in a task of its own, so each Watchdog logs in with its own IBC’s login, and when the session ends it connects again. Its probes and timeouts are ib_async’s own. A session that ends because another program or a gateway logged in on the same login is one the Watchdog connects again, which takes it back. The paths, the Java settings and the FIX login are a gateway’s, and nothing reads them. Nor is IBC’s own config.ini: a login or a TradingMode kept there is not read, so userid and password are given to the IBC or left to the environment, and a tradingMode left empty is paper. Where the IBC’s login is used, its tradingMode, not connect’s paper, decides whether the session is live.

A restart is a new login

The session lives in the program’s process and ends with it. A gateway held its login while a program restarted; here each start logs in, and on a live account that login waits on the second factor. The session is kept in ~/.ib_dx/session-<user>-<paper|live> unless connect is given another sessionFile, and a start naming a session the venue still holds is answered with a challenge rather than a full login. The venue lets a session go soon after its process ends, so that covers a quick restart only.

What ib_async itself does not have

  • reqCurrentTimeInMillis is in the documented API and not in ib_async. ib_async_dx.IB adds it, with its …Async twin; see Beyond ib_async.

The venue’s answer, not this package’s

These are the venue’s answers, as the engine’s sessions have met them, and none is something this package decides. They are written down because a program meeting one for the first time reads it as a fault here.

  • Market depth depends on the entitlement. A venue the account is not entitled to refuses by name. A book asked for on no particular venue is acknowledged and then produces nothing, which is what an account with no aggregate entitlement is told. It does not error.
  • News headlines and corporate events need subscriptions. Without a news subscription the providers list comes back and every query is empty; without a Wall Street Horizon subscription the calendar’s schema arrives and its events are empty.
  • The protocol is not published. The venue can change it without notice.

The engine’s own limits — what it measures, and what no session has settled yet — are set out in ib-dx’s documentation.

Evidence

What was measured, and on what. Status is assigned from a named artifact — a test, a script, or a recorded server response — not from reading the code.

StatusDefinition
✅ SupportedImplemented; exercised against IBKR production servers; the answer is delivered to the caller
✅ DocumentedA property of the venue rather than a call: established against IBKR production servers, with nothing for this package to implement
✅ OfflineImplemented, and held by the offline suite on every run; not yet exercised against IBKR production servers
— Not yetNot in this repository yet

The suites

SuiteCountNeeds a session
tests/python3182 of them. The other 315 run offline, and 1 a Python version condition excludes
tests/ib_async_upstreamib_async’s own suite, 3 tests at 2.1.0 (ab629f34c1), written to run on the engine; not run against the venue at this revisionYes
scripts/3 checks against a paper account; not run against the venue at this revisionYes

CI builds the engine from source with its test hooks, at the engine commit this package is tested against (c1852350), and runs tests/python on every push to main and every pull request, on Python 3.11, 3.13 and free-threaded 3.14t, against this package installed from the wheel and from the source distribution it builds, never from the checkout. The same runs fail when a test count the book or the README publishes differs from what the suite collects. It also installs the wheel beside the engine built as a release builds it, without its test hooks, and imports it. The workflow passes no credentials, so there the two live tests are skipped; they run against the venue locally, with IB_USERNAME and IB_PASSWORD set.

A release is the Python package and the Rust client together. Its workflow refuses a tag until the Rust client is in the repository and nothing on this page is still “Not yet”, then runs the suite again against the tag’s wheel and source distribution before attaching them to the release.

Surfaces

SurfaceStatusVerification
ib_async_dx is ib_async’s public API✅ Offline__all__ equal to ib_async’s, and every name ib_async’s own object except IB and IBC, __version__ among them; ib_async’s 13 submodules under both import forms: tests/python/test_the_package_is_ib_async.py
ib_async’s IB on the engine, through attach✅ OfflineIts events, *Async calls and types, with no gateway. All 67 transport calls their IB makes are carried, read out of their source on every run by test_every_call_their_library_makes_is_carried. Written for the venue and not run against it at this revision: test_an_unmodified_program_runs_on_this_engine and test_an_order_lives_its_whole_life_through_their_api, in tests/python/test_ib_async_transport.py
Their client’s send and sendMsg✅ OfflineEach of the 80 requests their client writes as a message, written by their own code through send, reads back into the message it was and reaches the engine exactly as the same request made by name; a message that does not read is refused with 320, and one naming no request is logged and unanswered: tests/python/test_a_raw_message_is_the_request_it_names.py
ib_async_dx.IB on the engine✅ OfflineThe same client, installed by its connect, with the login, the positions fix and the competing-session warning: tests/python/test_ib_runs_on_the_engine.py. Written for the venue and not yet run there: their own suite through tests/ib_async_upstream/conftest.py; scripts/sdk_sweep.py (every read, printing what came back; places nothing), scripts/sdk_lifecycle.py and scripts/order_round_trip.py (an order placed, changed and withdrawn on paper); the eight notebooks
The session’s life: on the loop, a pass a batch; a login cancelled, overtaken or refused; a session ended as it opens or once open✅ Offlinetests/python/test_a_session_opens_and_closes_on_the_loop.py
The four ib_async bug fixes✅ Offlinetests/python/test_ib_runs_on_the_engine.py and tests/python/test_a_session_opens_and_closes_on_the_loop.py; see Beyond ib_async
The engine’s calls beyond the documented API, on IB✅ Offlinetests/python/test_ib_runs_on_the_engine.py; see Beyond ib_async
reqCorporateActions, reqSpreadScan, positionsElsewhere, accountValuesElsewhere✅ Offlinetests/python/test_ib_runs_on_the_engine.py; see Beyond ib_async
TickerExtras.statedRows✅ Offlinetests/python/test_ib_runs_on_the_engine.py; see Beyond ib_async
IBC with ib_async’s own Watchdog✅ OfflineA reconnect loop, connecting with its IBC’s login and again when the session ends, each Watchdog with its own: tests/python/test_ib_runs_on_the_engine.py
Rust client— Not yetComing, on the engine’s public API

ib_async’s own suite. Its three tests at 2.1.0, from their commit ab629f34c1, have not been run against the venue at this revision, so none of them is a result here. test_request_error_raised asserts a RequestError carrying code 321 from a refused what-if, which their own IB never raises: it counts 321 as a warning, and a warning never ends the request it belongs to. ib_async_dx.IB ends it with the refusal, which test_a_what_if_refused_with_321_ends_with_the_refusal holds offline. test_contract_format_data_pd builds its own ib_async.IB(), which the runner’s conftest makes this package’s before their tests are collected, so it connects to the engine.

The package is ib_async

All offline, in tests/python/test_the_package_is_ib_async.py.

What holdsTest
__all__ is ib_async’s 103 names, and only IB and IBC are other objectstest_the_names_are_theirs_and_so_are_the_objects
__version__ and __version_info__ are ib_async’s, and this package’s version is __ib_async_dx_version__, outside __all__test_the_version_is_ib_asyncs_and_this_packages_is_apart
Every submodule of ib_async’s resolves here, as an attribute and as an importtest_every_submodule_of_theirs_resolves_here, test_from_a_submodule_import_works
ib_async_dx.ib is ib_async’s ib module with IB replaced, and a star import of it binds the names ib_async’s doestest_the_ib_module_is_theirs_with_ib_replaced
ib_async_dx.ibcontroller is ib_async’s ibcontroller module with IB and IBC replaced, its Watchdog ib_async’s owntest_the_ibcontroller_module_is_theirs_with_IB_and_IBC_replaced
IB is a subclass of ib_async’s, with ib_async’s own __init__, and attach is outside __all__test_ib_is_their_ib
connect and connectAsync are ib_async’s parameters, defaults and kinds, then four keyword-only onestest_connect_is_theirs_plus_the_login

IB, connected

All offline, on the engine’s test session, in tests/python/test_ib_runs_on_the_engine.py.

What holdsTest
The login comes from the environment when connect names none, and the session file is named for the accounttest_the_login_comes_from_the_environment_when_connect_names_none
A login named on connect is the one used, in ib_async’s positional form; the client id reaches the login; sessionFile=False keeps nothingtest_a_login_named_on_connect_is_the_one_used
A session is paper unless the program says livetest_a_session_is_paper_unless_the_program_says_live
An IB connects again after it disconnected, with ib_async’s own placeOrdertest_an_ib_connects_again_after_it_disconnected
A second connect closes the session the first openedtest_a_second_connect_closes_the_first_session
An IBC’s login is the one a connect naming none logs in with, paper unless its trading mode is live, and terminating it ends that sessiontest_an_ibc_carries_its_login_to_the_connect_and_terminates_its_session
Terminating an IBC from another context ends every session its login opened, and no later connect logs in with ittest_a_terminated_ibc_ends_every_session_of_its_login_and_lends_it_to_none
ib_async’s own Watchdog logs in with its IBC’s login, again after the session ends, and each Watchdog with its owntest_an_unmodified_watchdog_logs_in_with_its_ibcs_login
An order’s status names the client that placed it, so an order another client placed reaches its Tradetest_an_order_status_names_the_client_that_placed_the_order
A price reaches their ticker with the size that goes with it, and a size that changed alone as a size ticktest_a_price_carries_its_size_and_a_size_alone_reaches_tickSize
A refused new order reaches its Trade, and one refused with 321 is cancelledtest_a_refused_new_order_reaches_its_trade
An order placed once the engine has given the session up is refused after placeOrder has made its Trade, and the trade is cancelled before the closetest_an_order_placed_as_the_engine_gives_the_session_up_reaches_its_trade
A what-if refused with 321 ends with the refusal, as RequestError where RaiseRequestErrors is settest_a_what_if_refused_with_321_ends_with_the_refusal
321 on an order already working stays a warning, and the order is kepttest_321_on_an_order_already_working_stays_a_warning
The engine checks an option list as a gateway checks one: 10337 for another key, none taken on the two option computations, 10338 for another value, in the gateway’s words, under the request’s number, nothing senttest_an_option_list_is_checked_as_a_gateway_checks_one
An order stating an attribute the venue no longer takes is carried to the engine, which gives the notice a gateway givestest_an_order_stating_a_retired_attribute_reaches_the_engine_with_it
A handler asking again on every refusal is answered a refusal a pass, and does not hold the looptest_a_handler_that_asks_again_on_every_refusal_does_not_hold_the_loop
A wrapper that raises is logged, and the session carries ontest_a_wrapper_that_raises_is_logged_and_the_session_carries_on
An order id the venue names after the connect is not handed outtest_an_order_id_the_venue_names_after_the_connect_is_not_handed_out
On an account whose order ids have outgrown a request, a new order, a bracket and a what-if take the engine’s next order id and are taken, and requests go on from their own countertest_an_order_is_numbered_past_an_order_id_no_request_can_carry
Prices stated as the session ends reach the ticker before the closetest_prices_stated_as_the_session_ends_reach_the_ticker
A refusal held as the program disconnects does not reach the next sessiontest_a_refusal_held_as_the_program_disconnects_does_not_reach_the_next_session
Orders and requests are numbered from one counter, so an order’s refusal reaches the order and not a request still waitingtest_orders_and_requests_are_numbered_from_one_counter
updateEvent fires on what arrives, and timeoutEvent when nothing doestest_updateEvent_and_timeoutEvent_follow_what_arrives
fetchFields without POSITIONS asks for no positions at connect; ib_async 2.1 asks anywaytest_fetchFields_without_positions_asks_for_no_positions
reqUserInfo answers the White Branding ID; ib_async 2.1 answers []test_reqUserInfo_answers_the_white_branding_id
reqMktDataEx asks with the market data type named, with its option list for the engine to check, and None keeps the session’stest_reqMktDataEx_asks_with_the_market_data_type_named
reqCurrentTimeInMillis answers an int of milliseconds, within 2 s of the local clock on a session with no venuetest_reqCurrentTimeInMillis_is_the_venues_clock
The option model is read by the ticker’s request, and an unstated figure is Nonetest_the_option_model_is_read_by_the_tickers_request
What the engine states arrives in TickerExtras, OrderPreset and CompetingSessiontest_what_the_engine_states_is_carried_in_this_packages_types
The account’s grants are read from the enginetest_the_accounts_grants_are_read_from_the_engine
A ping reaches the enginetest_a_ping_reaches_the_engine
An extra called while not connected, before connect or after disconnect, raises ConnectionErrortest_an_extra_while_not_connected_raises
A record ib_async builds whole, a routing component, is answered from the engine and the session carries ontest_the_routing_components_arrive_as_their_records
connectionStats() counts the messages each way, carries the engine’s byte counts, and raises while not connectedtest_connectionStats_counts_the_messages_each_way
The competing-session warning is logged on ib_async_dx.ibtest_the_competing_session_warning_is_this_packages_own
A connect that completed is not failed by the warning after it: not by a stamp that does not parse, nor by a handler that disconnectedtest_a_completed_connect_is_not_failed_by_what_follows_it
priceBasedVol is a bool, False when the venue did not state ittest_priceBasedVol_is_a_bool_when_unstated
The size kept for a quote goes with its subscription, cancelled or a snapshot answeredtest_a_subscription_over_leaves_no_size_behind
ib_async’s own Watchdog, with this package’s IBC, connects and connects again when the session endstest_an_unmodified_watchdog_keeps_the_session_up
reqCorporateActions asks by the contract’s id, takes the answer, withdraws a query given up, and refuses a contract with no idtest_reqCorporateActions_asks_by_the_contracts_id_and_takes_the_answer, test_reqCorporateActions_given_up_withdraws_the_query, test_reqCorporateActions_needs_the_contracts_id
reqSpreadScan takes the first answer and cancels, its quotes go to the underlying’s ticker, unanswered in time it found nothing, and refused it cancels nothingtest_reqSpreadScan_takes_the_first_answer_and_cancels, test_reqSpreadScan_unanswered_in_time_found_nothing, test_reqSpreadScan_refused_cancels_nothing
A reqCorporateActions or reqSpreadScan the program disconnects under raises ConnectionError and withdraws nothingtest_a_request_the_program_disconnects_under_ends_with_the_session (2)
Holdings elsewhere and their figures are read apart from the account’s own, and a set is one of threetest_holdings_elsewhere_are_kept_apart, test_accountValuesElsewhere_names_one_of_three_sets

The session’s life

All offline, in tests/python/test_a_session_opens_and_closes_on_the_loop.py. Where a login has to be held open, the test engine keeps the engine’s own rule: a disconnect counted during a login drops the session it opens.

What holdsTest
A connect cancelled during its login tells the engine, and leaves no sessiontest_a_connect_cancelled_during_its_login_leaves_no_session
A disconnect() during the login ends the connect with ConnectionError, leaves no session, and returns at once while the login holds the enginetest_a_disconnect_during_the_login_ends_it
Of two connects on one IB, the later one’s session is left open and the earlier ends at once, on ib_async_dx.IB and on an attached ib_async.IBtest_a_later_connect_retires_an_earlier_one_still_logging_in (2)
Only the connect’s own startup request for positions is skippedtest_only_the_connects_own_startup_request_for_positions_is_skipped
A connect overtaken once logged in, still waiting on the venue, fails and leaves the later session open, on ib_async_dx.IB and on an attached ib_async.IBtest_a_connect_overtaken_after_its_login_leaves_the_later_session_open (2)
A connect overtaken in ib_async’s startup sync fails, and leaves the later session open with connectedEvent said once, whatever raiseSyncErrors saystest_a_connect_overtaken_in_its_startup_sync_fails_and_leaves_the_later_open (2)
Of three connects made at once, the last is left opentest_of_three_connects_at_once_the_last_is_left_open
A disconnect() from a handler the connect runs ends the connecttest_a_disconnect_from_a_handler_during_the_connect_ends_it
A login given up on does not hold the program opentest_a_login_given_up_on_does_not_hold_the_program_open
Under util.startLoop() a handler can wait on the session: a blocking request made in an errorEvent handler is answered, and a connect made in a disconnectedEvent handler of an attached ib_async.IB logs in, whether the engine ended the session or its IBC was terminated. Skipped on Python 3.14, where nest_asyncio cannot run asyncio’s timeoutstest_a_handler_can_wait_on_the_session_under_startLoop
A login given up on reaches nothing and keeps nothing, when the engine installs its session after the disconnect, and its engine’s close does not end the later sessiontest_a_login_given_up_on_reaches_nothing_and_keeps_nothing (2)
An interrupted connect() opens nothing latertest_an_interrupted_connect_opens_nothing_later
A handler that ends the session ends the pass: no held price and no held refusal reaches the cleared wrappertest_a_handler_that_ends_the_session_mid_pass_ends_the_pass, test_a_handler_that_ends_the_session_leaves_the_refusals_behind_it
Their wrapper is called on the loop’s thread only, what the engine announces from inside the login among it, and hears connectAck, managedAccounts and nextValidId theretest_their_wrapper_is_called_on_the_loops_thread_only
A program away from its loop queues no passes, and passes go on while the loop runstest_a_program_that_leaves_the_loop_queues_no_passes
A loop closed without disconnect() leaves no pass runningtest_a_loop_closed_without_disconnect_leaves_nothing_running
A session that ends as it opens fails the connect, says why on apiError, and nothing more is done for ittest_a_session_that_ends_as_it_opens_fails_the_connect_and_stops
A refused login raises ConnectionError, says why on apiError, and leaves the client disconnectedtest_a_login_refused_raises_ConnectionError_and_says_so
timeout does not bound the login; were the engine to wait for the next id after it, timeout would bound that (a guard: the engine answers at once)test_timeout_bounds_the_wait_after_the_login_not_the_login
Connecting an attached IB that is connected ends that session and opens a new onetest_connecting_an_attached_ib_that_is_connected_opens_a_new_session
serverVersion() is 0 until connectedtest_serverVersion_is_nought_until_connected
attach to a connected IB ends its session first, and unties the old clienttest_attach_to_a_connected_ib_ends_its_session_first
attach names no client idtest_attach_names_no_client_id
connect, run, reset, MaxRequests, RequestsInterval and events on the client mean what they mean on theirstest_the_client_level_names_mean_what_they_mean_in_ib_async
What leaves for the engine is paced: at most MaxRequests per RequestsInterval, the overflow deferred on the loop’s clock, throttleStart and throttleEnd said around the wait, 0 letting the whole burst leave at once, and a session ended mid-throttle leaving nothing queued for the nexttest_requests_past_the_rate_leave_an_interval_later
A request reached by name on the client takes keywordstest_a_request_reached_by_name_takes_keywords
A pass that raises ends the session oncetest_a_pass_that_raises_ends_the_session_once
What one pass delivers is one batch, on the looptest_what_one_pass_delivers_is_one_batch

What crosses to the engine

All offline, in tests/python.

What holdsTest
attach replaces the transport and nothing else: their IB, their wrappertest_ib_async_transport.py::test_attach_replaces_only_the_transport
A combination keeps its legs on every request path — details, quotes, barstest_ib_async_bridge_carries_everything.py::test_a_combination_keeps_its_legs_on_every_request_path
A contract named by more than its symbol keeps the rest: an ISIN, includeExpiredtest_ib_async_bridge_carries_everything.py::test_a_contract_named_by_more_than_its_symbol_keeps_the_rest
A contract named by its id states nothing else beside ittest_ib_async_transport.py::test_a_contract_named_by_id_states_nothing_else
An algo’s parameters, a combination’s routing and a soft-dollar tier reach the ordertest_ib_async_transport.py::test_what_tunes_an_algo_reaches_the_order
A field set to something the engine cannot carry is refused, by nametest_ib_async_transport.py::test_a_field_that_cannot_be_carried_is_refused
A field at ib_async’s default is carried as ib_async sends it; what it sends empty is left to the enginetest_ib_async_transport.py::test_a_field_at_their_default_is_carried
An order condition joined by or is carriedtest_ib_async_transport.py::test_a_condition_joined_by_or_is_carried
A fill’s cost reaches them as their own CommissionReporttest_ib_async_bridge_carries_everything.py::test_a_fills_cost_arrives_as_the_record_their_wrapper_reads, test_ib_async_depth.py::test_a_commission_report_reaches_them_as_their_own_type
A histogram reaches them as their HistogramDatatest_a_histogram_crosses_the_bridge_in_their_type.py (2)
A historical tick reaches them as their own recordtest_ib_async_depth.py::test_a_historical_tick_is_handed_over_as_their_own_record
A record tuple is rebuilt field by field, and a record named as theirs and of another type is rebuilt as theirstest_ib_async_bridge_carries_everything.py::test_a_record_tuple_is_rebuilt_field_by_field, ::test_a_record_named_as_theirs_and_not_theirs_is_rebuilt_as_theirs
A book level reaches their ticker, on both sides and below the toptest_ib_async_depth.py::test_a_book_level_reaches_their_ticker, ::test_the_other_side_and_a_deeper_level
A size with no price beside it is a size ticktest_ib_async_bridge_carries_everything.py::test_a_size_with_no_price_beside_it_is_a_size_tick
A bar carries its average price, a condition how it joins the next, and a record ib_async builds whole arrives wholetest_ib_async_bridge_carries_everything.py::test_a_bar_carries_its_average_price, ::test_a_condition_arrives_saying_how_it_joins_the_next, ::test_a_record_theirs_builds_whole_arrives_whole
A callback that cannot be rebuilt is logged and passed overtest_ib_async_bridge_carries_everything.py::test_a_callback_that_cannot_be_rebuilt_is_logged_and_passed_over
A refusal reaches their error in the four-argument shape it declarestest_a_refusal_reaches_their_wrapper.py (3)
Every account the login holds crosses overtest_ib_async_bridge_carries_everything.py::test_every_account_the_login_holds_crosses_over
A wide order id placed elsewhere leaves requests numberable, and no id past the widest a request carries is handed outtest_an_order_id_wider_than_a_request_leaves_requests_numberable.py (2), test_ib_async_depth.py::test_a_seeded_order_id_is_the_next_one_their_client_issues
readonly reaches the sessiontest_ib_async_transport.py::test_readonly_reaches_the_session_through_the_adapter
An outage leaves the session connected; a session the engine ends fires disconnectedEvent once, stops delivery and refuses requeststest_ib_async_transport.py::test_an_outage_leaves_the_session_open_and_an_end_closes_it
Ending a session is not reported as a session that went awaytest_ib_async_transport.py::test_ending_a_session_is_not_a_session_that_went_away
The handshake their client can send, verifyRequest and the three after it, is taken and answered by nothing, as over a gatewaytest_ib_async_transport.py::test_the_handshake_goes_unanswered_as_over_a_gateway
Their suite’s own ib_async.IB() is this package’s in the runnertest_ib_async_transport.py::test_their_suite_builds_this_packages_IB_where_it_builds_its_own
Every request their client writes as a message reads back into the message it was, an order field for field, and reaches the engine as the request it names, an order whole whatever parts of the message it writes; an order carries what their client writes and nothing else; a message that does not read is refused with 320, and one naming no request is logged and unanswered; nothing is sent while not connectedtest_a_raw_message_is_the_request_it_names.py (171)
Each of ib_async’s eight notebook subjects has a notebook here, and each is Python that connects through ib_async_dx on a paper sessiontest_the_notebooks_are_ib_asyncs_subjects.py (2)

Measured on the engine

These were measured on the engine this package runs on, with the client this package uses, and are recorded with the engine in ib-dx’s evidence, as it stood when they were taken.

CapabilityStatusWhat was seen
Market depth through ib_async✅ SupportedInserts, updates and deletes delivered as the venue states them, ten levels a side, through ib_async’s own ticker. Which venues answer is the account’s entitlement
Real-time bars through ib_async✅ SupportedFive-second bars during regular hours, each with open, high, low, close and volume, alongside a book on the same session, through ib_async’s own reqRealTimeBars
A session survives losing its connection✅ SupportedA dropped connection is rebuilt on the session already open, with no second factor: five forced drops recovered in 2–8 s, and an eight-hour session rode through its losses unattended
A session does not survive its process✅ DocumentedEnded without logging out, the session was gone forty seconds later, and a later start was answered with a full login rather than a challenge. A restart is therefore a new login. What that costs an account with a second factor has not been measured; a paper session presents none
A long session✅ SupportedOne session held for 175 minutes across a market open: 106,053 quotes, 180,433 trades, 95,985 book rows and 4,148 bars, with no unrequested disconnect