# Building a Rust trading app: the architecture behind Scalpel

> How we built Scalpel, a Rust trading app for Interactive Brokers: the crates, threads, event bus, GPU interface, money types, tests and honest trade-offs.

Source: https://scalpeltrade.com/blog/rust-trading-app
By Scalpel Team. Published 2026-07-09, last changed 2026-10-22. 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.

## Why we wrote a Rust trading app

**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](https://www.microsoft.com/en-us/msrc/blog/2019/07/a-proactive-approach-to-more-secure-code). Google reported on September 25, 2024 that memory safety issues fell from [76% of Android vulnerabilities in 2019 to 24% in 2024](https://security.googleblog.com/2024/09/eliminating-memory-safety-vulnerabilities-Android.html) 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](https://interactivebrokers.github.io/tws-api/top_data.html). 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](https://scalpeltrade.com/blog/tws-api-scalping).

## Two processes and one socket

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](https://scalpeltrade.com/guides/enable-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.

*Video: [What is the TWS API?](https://www.youtube.com/watch?v=i_5R8gkVAw0) (Interactive Brokers, 3:54)*

One TWS session accepts [up to 32 client applications at once](https://interactivebrokers.github.io/tws-api/connection.html), 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](https://scalpeltrade.com/blog/tws-api-scalping), and if you are choosing which IBKR program to run underneath, see [IB Gateway vs TWS](https://scalpeltrade.com/blog/ib-gateway-vs-tws).

## The crates: building a trading platform in pieces

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.

## Threads, not actors: the event bus

Our first architecture notes describe async actors on the Tokio runtime. What ships is simpler: [rust-ibapi](https://github.com/wboayue/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:

- **The execution thread** waits on trader commands with a 5 ms timeout, then drains the bus: new books, quotes, prints and candles. It sizes orders at the moment you press a key, from the prices it already holds, and checks every stop.
- **The UI** turns the bus into an Iced subscription and redraws from it.
- **The recorder**, when you switch it on, is just one more subscriber writing to disk.

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.

![Diagram of Scalpel's architecture: TWS or IB Gateway feeds one thread per connection, which publish onto one event bus read by the execution thread, the user interface, the recorder and the scanner.](https://scalpeltrade.com/images/blog/rust-trading-app/architecture.webp)

*Each connection to TWS runs on its own thread and publishes onto one event bus. The execution thread and the interface are subscribers; only the execution thread sends orders, through the orders connection.*

## Render the latest, not every delta

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](https://github.com/iced-rs/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](https://github.com/tradingview/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.

## One order path, two backends

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](https://scalpeltrade.com/guides/paper-trading-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](https://scalpeltrade.com/blog/app-monitored-stops).

## How we test it

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`.

- **The pure crates are easy.** `core`, `indicators`, `sim` and `features` have no I/O and no clock, so a test feeds numbers in and checks numbers out.
- **The LIVE backend is tested without a broker.** Its tests stand in for the order thread with plain channels, then play back the broker's status messages: a fill, a cancel lost in a disconnect, an exit the market runs away from.
- **Recordings, for the reads.** With recording on, a session is written as gzip JSON lines at the event-bus boundary. The replay tool feeds those files through the same `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](https://scalpeltrade.com/guides/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.

## Shipping one binary

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](https://scalpeltrade.com/guides/updates)).

## The trade-offs, plainly

| 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](https://scalpeltrade.com/blog/ibkr-api-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.

## Where Scalpel fits

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](https://scalpeltrade.com/guides/cockpit-tour) walks through the screen, and [the Scalpel philosophy](https://scalpeltrade.com/guides/scalpel-philosophy) explains what we built it for and what we left out. This short video shows the desks the architecture above draws.

*Video: [Desks and layout | Scalpel guide 2 of 15](https://www.youtube.com/watch?v=ZDxSCm2tadM) (Scalpel Trade, 1:58)*

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.*

## Questions

**Is Rust a good language for a trading app?**

It is a strong choice for a long-running desktop app that handles orders: it prevents memory errors and data races at compile time and has no garbage collector. It does not make broker data faster, and its GUI libraries are younger than those of older languages.

**What GUI library does Scalpel use?**

Iced, a Rust GUI library inspired by Elm, built with its GPU renderer. On Windows the desk charts are TradingView's open-source Lightweight Charts in a WebView2 window; elsewhere a native canvas chart draws instead.

**How does a Rust app connect to Interactive Brokers?**

Through the TWS API socket served by TWS or IB Gateway on the same machine. Scalpel uses rust-ibapi, an open-source, unofficial Rust client, with one connection per job.

**Does Scalpel use floating point for prices?**

Not in the order path. Prices, quantities and dollar amounts there are exact decimals in their own types. Display data such as chart points uses floating point.

**Should I build my own trading platform in Rust?**

Usually not as a first step. Prototype in Python with IBKR's client, learn the API's limits, and move to Rust if you need a dependable, long-running app that other people will use.
