Getting started

What you need

  • An Interactive Brokers account, paper or live. No IB software, and not the ibapi package either.
  • Rust 1.89 or newer. Both install routes compile the engine.
  • Python 3.11 or newer, for the Python surface.

Install

Both routes build from the repository.

Rust

[dependencies]
ib-dx = { git = "https://github.com/userFRM/ib-dx" }

Python

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

The package imports as ib_dx. The build backend is maturin, which compiles the Rust core into the extension module, and pyproject.toml names the features it needs, so there is nothing to pass. It builds for CPython 3.11 to 3.14 and the free-threaded 3.14t; the free-threaded 3.13t is not supported.

Either route compiles the engine, so it needs Rust 1.89 or newer and, on Linux, the OpenSSL headers and pkg-config (libssl-dev on Debian and Ubuntu, openssl-devel on Fedora and RHEL).

From the source

Working on the client itself, build it in place:

git clone https://github.com/userFRM/ib-dx
cd ib-dx
uv venv .venv
source .venv/bin/activate          # .venv\Scripts\activate on Windows
uv pip install maturin
maturin develop

maturin develop replaces the installed module. A local pytest run tests whatever was built last, so rebuild before running one.

Feature flags

default = []. The Rust client needs nothing turned on.

FeatureWhat it adds
pythonThe PyO3 bindings
extension-moduleTells PyO3 not to link libpython. Right for the wheel, wrong for cargo test. maturin sets it; you do not
dev-toolsThe binaries under src/bin — the benchmarks, and the capture tools this repository is developed with. Most of them read credentials and open a session

The features not in that table exist for this repository's own test suites. They are off by default and should stay off in anything you install.

Credentials

The credentials go in the connect call. There is no configuration file and no process holding a login on your behalf.

client.connect(username="your_user", password="your_pass", paper=True)
let client = EClient::connect(&Config {
    username: "your_user".into(),
    password: "your_pass".into(),
    paper: true,
    ..Default::default()
})?;

Do not name a host. Left empty, the client knocks on one of the venue's regional doors and the venue answers by naming the server this account actually lives on; the session moves there. A host is worth stating only to knock at a particular region.

paper. true skips the live second-factor approval gate — a paper session presents no second factor. false enters that gate on connect, and that call blocks until it is answered.

Which factor an account presents is stated by the venue when the gate opens, and this client handles two: an authenticator code, which has nothing to fall back on and needs a code_provider, and an IBKey push, which does not. Nothing below has been exercised against a live account from here, so treat the timing and the shape of the prompt as the venue's to state rather than as described.

Use a paper account while you are writing something. A live account is a live account.

One program per login. Each program opens its own session, and a login holds one session at a time: a second program on the same login takes the first one's session, and the venue names the host that took it. Give each program that runs at the same time a login of its own.

One place reads the environment instead of the call: the programs under examples/ read IB_USERNAME and IB_PASSWORD:

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

Pick a surface

Every surface below drives the same engine. Pick by the program you have, not by capability.

LanguageSurfacePick it when
Pythonib_dx.EClient / EWrapperYour program is written against ibapi. Change ibapi to ib_dx in its imports, and its connect call to the one above.
RustEClient / WrapperYou are porting a TWS API program and want its callbacks.

For ib_async, use ib_async-dx, which runs it on this engine.

Hello, world

Both files below are the ones in the repository, included rather than copied, so this page cannot drift from what actually runs. The Rust one is compiled by CI along with the rest of examples/. Each subscribes to SPY for five seconds and prints the last quote it saw.

Rust

//! Recipe: stream SPY market data for 5 seconds, print latest bid/ask/last.
//!
//! Usage: IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_tick_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::TickAttrib;
use ib_dx::api::wrapper::Wrapper;

#[derive(Default)]
struct State {
    bid: f64,
    ask: f64,
    last: f64,
    ticks: u64,
}

struct TickWrapper {
    state: Arc<Mutex<State>>,
}

impl Wrapper for TickWrapper {
    fn tick_price(&mut self, _req_id: i64, tick_type: i32, price: f64, _: &TickAttrib) {
        let mut s = self.state.lock().unwrap();
        s.ticks += 1;
        match tick_type {
            1 => s.bid = price,
            2 => s.ask = price,
            4 => s.last = price,
            _ => {}
        }
    }
    fn error(&mut self, req_id: i64, _error_time: i64, code: i64, msg: &str, _adv: &str) {
        if !matches!(code, 2104 | 2106 | 2158) {
            eprintln!("[error] req_id={req_id} code={code} msg={msg}");
        }
    }
}

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 = TickWrapper { state: state.clone() };

    let spy = Contract {
        con_id: 756733,
        symbol: "SPY".into(),
        sec_type: "STK".into(),
        exchange: "SMART".into(),
        currency: "USD".into(),
        ..Default::default()
    };

    let req_id = 1;
    println!("streaming SPY for 5s…");
    client.req_mkt_data(req_id, &spy, "", false, false, &[]);

    let deadline = Instant::now() + Duration::from_secs(5);
    while Instant::now() < deadline {
        client.process_msgs(&mut wrapper);
        std::thread::sleep(Duration::from_millis(20));
    }

    client.cancel_mkt_data(req_id);

    let s = state.lock().unwrap();
    println!("ticks: {}  bid={:.2}  ask={:.2}  last={:.2}", s.ticks, s.bid, s.ask, s.last);

    drop(s);
    client.disconnect();
    Ok(())
}
IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_tick_data

Python

"""Recipe: stream SPY market data for 5 seconds, print latest bid/ask/last.

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

import os
import threading
import time

from ib_dx import EClient, EWrapper, Contract


class TickWrapper(EWrapper):
    def __init__(self):
        self.bid = 0.0
        self.ask = 0.0
        self.last = 0.0
        self.ticks = 0

    def tick_price(self, req_id, tick_type, price, attrib):
        self.ticks += 1
        if tick_type == 1:
            self.bid = price
        elif tick_type == 2:
            self.ask = price
        elif tick_type == 4:
            self.last = price

    def tick_size(self, req_id, tick_type, size):
        self.ticks += 1

    def error(self, req_id, error_time, code, msg, advanced=""):
        if code not in (2104, 2106, 2158):
            print(f"[error] {code}: {msg}")


w = TickWrapper()
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"

req_id = 1
print("streaming SPY for 5s…")
c.req_mkt_data(req_id, spy, "", False, False)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
    time.sleep(0.1)
c.cancel_mkt_data(req_id)

print(f"ticks: {w.ticks}  bid={w.bid:.2f}  ask={w.ask:.2f}  last={w.last:.2f}")
c.disconnect()
IB_USERNAME=... IB_PASSWORD=... python examples/hello_tick_data.py

Next steps