A Basic Algo Trading System In Rust: Part I

Paul Folbrecht
Rustaceans
Published in
9 min readJun 8, 2024

Introduction

Writing a personal algorithmic trading system is a fantastic way to make substantial amounts of money with minimal effort.

Ha! If only that were so. No, because of the efficiency of equity and other liquid markets, etc., easy money is not to be had.

However, writing such a system for fun and as a learning exercise (both technical and trading) is still worthwhile. This series will demonstrate the creation of a such a system, simple but fully-functional, in Rust.

A query is prompted: Why Rust? Python is, by far, the most popular programming language for algo traders. With Python & numpy, one can be off to the races in a big hurry.

But a language like Rust still has appeal:

  • It is blazingly fast, natively (not just with libs written in C). There are contexts where this matters.
  • It is far more flexible — in a sense, infinitely so, compared to programming with a glue language using canned libraries.
  • It’s much more robust, even than Python, and certainly compared to any other systems-level language

While the system presented here is ‘toy’ in the sense that the one simple strategy presented is unlikely to generate alpha, except by coincidence or with highly-educated choosing of names, the system is also a reasonably complete framework, with pluggable trading strategies. Thus, anything is possible.

The piece is more about Rust application architecture and idiomatic programming than it is about the domain.

Complete source code is available.

Functionality

I’m going to assume little-to-no understanding of algo trading. The basics functional requirements of such a system are as follows:

  • Gather live and historical market data
  • Feed that data to a trading strategy which can determine to buy or sell securities
  • [Optional: Run proposed trades through some sort of risk system. We’ll consider risk assessment out-of-scope here — something to be decided non-programmically.]
  • Place and track orders, and positions

In 2024, there are a number of very good commercial trading API offerings. I chose to use the Tradier system for the following reasons:

  • Good reputation for completeness of functionality, reliability, and customer service
  • It provides both live and historical data, and full order/position functionality
  • Reasonable pricing

Architectural Decision Record

Having had good success with so-called onion architecture (most recently specifically with the ZIO Service Pattern in Scala, incidentally), I sought to discover if this was an appropriate architecture for Rust applications. It turns out that it is, with certain caveats specific to this language.

In that vein, I created a workspace-based project, with several libraries with dependencies all pointing in the right direction: from services to domain to core. That is, the most critical part of the application, domain, where business logic lives, has no dependencies on I/O or technical implementation concerns.

Services

Starting from a clean rusty slate, as I set out to do, the simplest solution is bare functions. But we need to be able to do at least one other thing: Mock implementations for unit testing. With that in mind, using some mechanism to group related functions together and change the implementation at will seems like a good idea.

Thankfully, Rust is not an object-oriented language; rather than bundling the rather disparate elements of interface, encapsulation, and dispatch/polymorphism together, it keeps these concerns distinct and allows you to compose them appropriately.

The interface mechanism in Rust is the trait: Traits groups related sets of functions together which can be implemented in various ways.

With traits and struct implementations, we can implement a simple but entirely adequate mechanism of dependency injection, and thus make our code unit-testable.

There are essentially two ways to use traits to achieve pluggable implementations in Rust: Static (compile-time) binding — impl Trait — and dynamic binding — the dyn Trait mechanism. I chose the former, for reasons I won’t get in to here, but both are viable, and the slight performance hit of dynamic binding would not be an issue for service-level interaction.

Concurrency

It is advantageous for almost all nontrivial applications to make use of some sort of parallelism mechanism. Here, we know we are going to have several disparate concerns that should ideally run in parallel:

  • Responding to market events
  • Making trading decisions
  • Placing or editing orders

With that in mind, I made the following decisions:

  • A single process: There is no reason here to incur the overhead of both additional structural complexity and poorer performance that breaking the system into microservices would incur. Rather, we will use a clean logical dependency structure to keep application portions appropriately isolated
  • No async: This type of application will never be in the position of handling a large number of concurrent I/O events. Thus, we do not need to multiplex threads onto tasks, and going async has no real purpose here. (Async Rust is, in a sense, a beast unto itself, and best avoided unless you need what it buys you.)
  • Channels rather than ZeroMQ or another in process messaging mechanism. Rust’s built-in channels are an excellent mechanism for decoupling in-process components; there is no reason in Rust to use a third-party messaging library for this type of use case. Doing so we needlessly increase complexity And likely decrease performance.
  • Getting into the weeds, I chose tungstenite over ezsockets for websocket support, crossbeam over native channels because they add useful functionality at no real cost, and the ubiquitous reqwest library for HTTP, along with its pal serde for serialization.

Code

I am a relatively new Rust programmer. If you’re a Rust guru and you spot something that could be done better, please comment.

I will call out these known areas for improvement:

  • Use of a proper ADT error system rather than ‘String’
  • Clean shutdown

With that prelude, it’s time to get into the code — parts of it, that is. We won’t be covering here utilities (core), unit tests, and other less interesting bits.

We’ll start, naturally, with main.

fn main loads the application’s config, which specifies the trading strategies to be instantiated along with their parameters, then instantiates them with the dependent services.

Note that a set of symbols to be monitored by market data is constructed from the parameters of all configured strategies. This is done for efficiency: We want to use a single websocket for all market data events.

The config, as noted, supports n trading strategies:

The system in its present state supports only mean-reversion as a strategy, but is set up to configure and create any number of any type of trading strategy implementation.

Market data sources are, in a sense, the heart of any trading system. No live data, no trading.

This gist demonstrates the pattern for services I followed throughout the app (some parts of which are unique, AFAIK):

  • A trait for the interface (because I’m not using dynamic binding (trait objects), I’m able to use generic types in these interfaces, though we don’t see that here)
  • An fn new() constructor that returns impl Trait wrapped by an Arc — the Arc is necessary to allow shared access (among threads) to the service
  • An implementation by struct hidden in a submodule; this is not necessary for visibility reasons (of course, non-pub members of the module are not visible outside it, except to submodules); I did this for organizational purposes only
  • Unit tests in a separate file using the #path macro (contrary to the Rust norm of putting tests in the same file as the production code — that’s just a bad norm)

Implementation elements:

  • subscribe() registers a subscriber by creating an unbounded channel.
  • init() connects a websocket to the Tradier market data streaming endpoint. The system collected symbols from all registered trading strategies (above) — this set is sent to the socket to register for events. Note also that we are filtering on “quote” events. See the Tradier API Docs for details.
  • init() then spawns a thread to listen to the websocket and send quotes to subscribers
  • The subscribers collection is guarded by a mutex. Unlike legacy languages, mutexes in Rust not only guard data but also own it, making it impossible to mutate without acquiring a lock.

Finally, note that you must acquire a Tradier API access token in order to use the system. This is passed as a command-line argument to the process. More on this later.

Historical data is also necessary for virtually every trading strategy.

At first blush, one might assume that live and historical data should be abstracted behind a common interface. But this is not accurate — the data provided is different, which is reflected by our structs, which mirror Tradier (and other) API responses:

So, “live” and “historical” quotes are fundamentally different.

(But, “live” doesn’t necessarily always mean literally “live”: When we get to backtesting, “live” quotes will actually be historical.)

Here’s the service code:

This is simpler than the MarketDataService, as it uses a simple HTTP Get request rather than a websocket (because this isn’t live data).

Now we’re onto the good stuff — TradingService:

With this gist, the Rust Elephant has entered the room: Lifetimes. We see one here: ‘market_data. Even though this piece isn’t a Rust tutorial per se, we have to spend some time on this.

I will share the insight that was the aha! moment for me regarding Rust lifetimes:

Lifetimes tie references together, indicating they point to the same piece of memory.

That is what they do. That is what they are for, and all they are for. Understand this, and the mysteries of the borrow checker will begin to unravel.

So, here we have a specified lifetime. The compiler will not allow it to be elided because it cannot be inferred — it is used in struct member references.

What we are specifying with this lifetime is that all such references are tied to the set of symbols. That’s it. That’s the data the lifetime refers to.

(This lifetime could have certainly been ‘static, incidentally. I wanted to illustrate usage of a non-static lifetime, and also the idiom of naming them appropriately.)

There are two main functions here:

  • load_history loads the last 20 days of price history (extremely boorish of me to have that figure hard-coded, I realize, but it’s kind of a standard for the Bollinger Band mean-reversion we’ll be getting to), calculates the basic statistics (mean and standard deviation) we’ll need for the algo, and stores that data.
  • run processes quotes from the market and passes them to the attached Strategy, which will make trading decisions.

Finally, we get to the thingy that does — or will — actually do some trading:

The algorithm on display here is a variant of mean-reversion inspired by Bollinger Bands. We see that the decision to enter long is based on this simple equation (in English): Is the (ask) price less than the mean minus two standard deviations?

Instructions for running, from the readme:

Finally, here is a screenshot of system startup (with my access token blurred):

Even with only two (liquid) names, quotes will fly by at blinding speed at all times during market hours.

And, hey, look at that: AAPL actually got reasonably close to entry range.

Next

As is clear from the above, our singular trading strategy isn’t doing anything other than making a buy decision. We have the following left:

  • Making sell decisions
  • Generating Tradier API orders
  • Storing orders & positions locally (mongodb)
  • Recing local data with Tradier on startup
  • Additional, and more sophisticated, trading strategies, starting with using volume-weighted history

The repository is here.

UPDATE: Part II published.

Hey Rustaceans!

Thanks for being an awesome part of the community. Before you head off, here are a few ways to stay connected and show your love:

  • Give us a clap — Your appreciation helps us keep creating valuable content.
  • Become a contributor ✍️ We’d love to hear your voice. Learn how to write for us.
  • Stay in the loop Subscribe to the Rust Bytes Newsletter for the latest news and insights.
  • Connect with us: X
  • Support our work Buy us a coffee.

--

--