Getting started
What you need
- An Interactive Brokers account, paper or live. No IB software, and not the
ibapipackage 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.
| Feature | What it adds |
|---|---|
python | The PyO3 bindings |
extension-module | Tells PyO3 not to link libpython. Right for the wheel, wrong for cargo test. maturin sets it; you do not |
dev-tools | The 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.
| Language | Surface | Pick it when |
|---|---|---|
| Python | ib_dx.EClient / EWrapper | Your program is written against ibapi. Change ibapi to ib_dx in its imports, and its connect call to the one above. |
| Rust | EClient / Wrapper | You 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
- Login — connect, take the first order id, disconnect
- Streaming ticks · L2 depth
- Order lifecycle — place, modify, cancel, fill
- Limits — read this before you depend on a call
- Requests, delivery and shutdown — request signatures, ordered callbacks, error origins and the shutdown result