By Scalpel Team. , updated . 10 min read.

In short
Scalpel is a Rust trading app: one native binary for Interactive Brokers, split into 15 library crates, that talks to TWS or IB Gateway through the open-source rust-ibapi client on dedicated threads. Every feed publishes onto one event bus; an execution thread owns orders, positions and stops; and an Iced user interface, drawn on the GPU, only renders what the engine sends it. Rust gave us memory safety without a garbage collector, threads the compiler checks, and exact decimal money. It cost us a younger GUI ecosystem and two chart paths to maintain, and it does not make IBKR's data any faster.
We build Scalpel, a desktop trading cockpit for Interactive Brokers, and we wrote all of it as a Rust trading app. This post is the architecture as it ships, taken from our repository: the crates, the threads, how market data reaches the screen, how an order reaches IBKR, how we test it, and the trade-offs we would point out to anyone building a trading platform of their own. There are no benchmark numbers in it: we have not published any and will not invent them. Scalpel is independent and not affiliated with or endorsed by Interactive Brokers.
Memory safety without a garbage collector. A trading cockpit is a long-running, multi-threaded program that holds your orders. The bug classes we feared most were the ones C and C++ make easy: use-after-free, buffer overruns and data races. The industry numbers are blunt. Microsoft's security response center wrote in 2019 that about 70% of the vulnerabilities it assigns a CVE each year are memory safety issues. Google reported on September 25, 2024 that memory safety issues fell from 76% of Android vulnerabilities in 2019 to 24% in 2024 as new code moved to memory-safe languages, Rust among them. Those are security figures, not trading ones, but the lesson carries: the compiler removes a whole category of crash before it can happen at 09:31.
Threads the compiler checks. Scalpel runs a dozen or more threads. In Rust, data shared between threads must be wrapped in a type that makes the sharing safe, or the program does not compile, which made one thread per broker connection practical.
Types for money. Prices, quantities and dollar amounts in the order path are exact decimals wrapped in their own types (Price, Qty, Money), never raw floating point. A float is fine for drawing a chart; it is not fine for a ledger, where 0.1 plus 0.2 must equal 0.3.
What Rust did not buy us is speed at the broker. Every quote comes through TWS or IB Gateway, and IBKR's own API documentation says its streaming Level 1 quote for stocks is an aggregate snapshot taken every 250 ms. No language choice on our side changes that. If you are deciding between Python and Rust for an IBKR tool, the honest answer is that the broker's limits shape your app more than your language does, which is the subject of our pillar post on scalping through the IBKR API.
The app is always two programs on the same machine: Scalpel and IBKR's TWS or IB Gateway, logged in, with the API switched on (enable the TWS API). There is no cloud route from a retail desktop app into an IBKR account with live Level 2 and the tape; the API socket on your own computer is it. IBKR's short introduction to the API explains the model well if you have never used it.

One TWS session accepts up to 32 client applications at once, each with its own client id. We use that headroom deliberately: an account connection, a separate order connection, one market-data connection per desk, and connections for the scanner, its quotes and the news feed. Heavy Level 2 traffic on a desk's socket can never queue in front of a sell order on the order socket. The full table of client ids and why is in the pillar post, and if you are choosing which IBKR program to run underneath, see IB Gateway vs TWS.
The workspace has 15 library crates, the binary, and an offline replay tool: about 47,500 lines of Rust, tests included. The rule that holds it together is about who may know what.
| Crate | Job | Knows IBKR? | Knows the UI? |
|---|---|---|---|
core | The shared vocabulary: prices, orders, events. No I/O | No | No |
config | Settings, presets, hotkeys, layout, saved as TOML | No | No |
ib | The rust-ibapi wrapper: feeds, scanner, news, the order wire | Yes, the only one | No |
sim | The paper fill simulator, ledger and stop manager | No | No |
engine | Owns the threads, the event bus, sizing, risk and execution | Through ib | No |
indicators | EMA, MACD, ATR, VWAP, candle math | No | No |
scanner | The momentum scans over the IBKR scanner's list | No | No |
intel | Borrow, dilution and catalyst reads from public sources | No | No |
features | The chart, tape and book reads, as pure functions | No | No |
recorder, capture | Optional session recording | No | No |
audio | Voice callouts and tape sound | No | No |
ui | The Iced views, charts, settings | No | Yes |
licence, update | Licence checks and signed updates | No | No |
Two boundaries matter most. Only ib touches the broker library, so a change in the IBKR client lands in one place. And ui does not depend on engine or ib at all: it only sees core types arriving over a channel. The interface has no access to the broker or to the execution code: it draws what the engine sends and sends the trader's intents back.
Our first architecture notes describe async actors on the Tokio runtime. What ships is simpler: rust-ibapi offers a blocking API as well as an async one, and we use the blocking one. Each connection runs on its own named thread, reads its subscriptions in a loop, and reconnects on its own with capped backoff and jitter when TWS drops. A desk losing its feed does not touch the order connection.
Every feed publishes onto one broadcast channel of engine events, sized at 8,192 slots. Everything that cares subscribes:
Sound has its own thread too: all decoding and audio device work happens there, never on the interface's update loop, so a sound cannot hold up a keypress.
A broadcast channel has one property you must design for: a subscriber that falls behind does not slow the producers, it skips. A lagging execution thread logs that market data was skipped and carries on with the newest prices, while the trader's commands travel on a separate queue that never drops. For a trading app, where stale prices are worse than missing ones, we think that is the right way round.

A busy small cap can send hundreds of Level 2 updates a second, and nobody can read hundreds of frames a second. So each desk's market-data thread polls its subscriptions every 30 ms and publishes a fresh snapshot of the book, quote, tape or candles at most once every 60 ms per stream, about 16 times a second. The code comment says it in five words: render the latest, not every delta. Our original notes aimed at about 60 frames a second. The shipped feeds publish less often than that, and we think that is right: a book that repaints 16 times a second is already faster than anyone reads it, and it leaves the GPU and the event bus room for six desks.
The interface is Iced, a Rust GUI library inspired by Elm: a model, messages, an update function and a view. We build it with the GPU renderer only (the software renderer is compiled out), and the Level 2 montage and tape are drawn on Iced's canvas.
The chart is where Rust's younger GUI ecosystem showed. On Windows, each desk's chart is TradingView's open-source Lightweight Charts library running in a WebView2 child window, floated over the chart pane. That forced a renderer decision we did not expect: a Vulkan surface is composed on top of child windows and hides them, so on Windows Scalpel asks the GPU layer for DirectX 12 or OpenGL instead. Everywhere else, or if WebView2 cannot start, a native canvas chart draws in its place. Two chart paths is a real maintenance cost. Rust stays the source of truth either way: a stop you drag on the web chart is re-checked by the Rust rules before any order moves.
SIM and LIVE are two implementations of one ExecutionBackend trait, which is how you can switch between them without a restart (paper trading in SIM). The LIVE backend reuses the paper engine's ledger and stop manager outright, so position, average cost, P&L and stop math are the same code in both modes; only the fills differ. The order thread behind it holds no trading logic: it places and cancels limit orders, and never originates one. If the connection drops and comes back, any placement that waited more than 2 seconds is refused rather than sent at a price from before the gap.
Stops are the one place this design has a hard edge: they live in the app, not at the broker, so they only work while Scalpel runs with a live feed. Why we chose that, and what it costs, is its own post: why Scalpel's stops live in the app.
The workspace has about 370 unit tests, and the names read like the rules they guard: second_exit_replaces_the_first_and_never_stacks, sell_is_clamped_to_net_long, trailing_ratchets_up_and_never_down.
core, indicators, sim and features have no I/O and no clock, so a test feeds numbers in and checks numbers out.features code the app runs live, so a replayed read is exactly the live read. That is how we checked our chart reads against real sessions: the bull trap read was the one pattern that held up, and only for the next minute or so (read line and read bar).What we do not do is replay recordings through the LIVE order path. Order behavior is covered by the unit tests above and by SIM against the live feed, which is the everyday harness.
Fonts, sounds and voice clips are compiled into the executable. A panic hook is installed before anything else, and release builds let a panic unwind, so a crashed feed thread is logged instead of taking the cockpit down. Settings are written to a temporary file and renamed into place, so a crash mid-save cannot corrupt them. Updates are signed offline, and the app installs a file only if its signature and checksum match (updates).
| Rust gave us | Rust cost us |
|---|---|
| No use-after-free or data race in safe code, and no collector to pause | A younger GUI ecosystem: a web chart on Windows, a native one elsewhere |
| Exact decimal money as types the compiler enforces | More code than a Python prototype for the same feature |
| One self-contained binary per platform | Slower release builds: thin LTO and one codegen unit, by choice |
| Pure crates that are trivial to test | A vendored one-line patch to the IBKR client for a scanner message it ended streams on |
If you are building a trading platform for yourself, we would not tell you to start in Rust. Start with Python and IBKR's own client (Interactive Brokers API with Python), learn the API's limits, then decide. We chose Rust because Scalpel is a product other traders run all day, and that changes what a crash costs.
Scalpel is the result: a keyboard-first cockpit for your own IBKR account with up to six desks, each with a Level 2 montage, tape, chart and order panel, on Windows 10 and 11 and Macs with Apple silicon. The cockpit tour walks through the screen, and the Scalpel philosophy explains what we built it for and what we left out. This short video shows the desks the architecture above draws.

Scalpel is long only, never trades for you, and needs TWS or IB Gateway running on the same machine. Figures from Microsoft, Google and IBKR were checked on October 22, 2026.
Trading carries a significant risk of loss. Nothing here is financial advice.