A Basic Algo Trading System In Rust: Part II

Paul Folbrecht
Rustaceans
Published in
6 min readJun 27, 2024

We’re back for the next phase of this little algo-trading system — and by the end of this piece, we’ll be trading.

Functionality

At the end of Part I, we had a system that could stream live market data, obtain historical price data, and a framework for running arbitrary trading strategies.

With this stage, we’ll accomplish the following:

  • Full generation of signals from the mean-reversion strategy
  • Translation of signals into possible orders
  • Broker order creation (and execution) — only market orders, and no shorting (short-selling comes with unlimited theoretical risk and we don’t want to be one of those hobby traders who loses his life savings)
  • Persistence of orders & positions in a private database (MongoDB)
  • A “sandbox” mode for paper trading (necessary both for backtesting and so that so running unit tests doesn’t lead to bankruptcy)

Architectural Decision Record

Almost all of the true “architectural” decisions were made in Part I, but there were a couple things to consider in this expansion of functionality.

Most pertinent was which new areas should be synchronous (with trading threads) or asynchronous.

We decouple things asynchronously for essentially two reasons:

  • To isolate components for fault-tolerance — an async component can possibly die without bringing down the system
  • To improve performance/throughput

Always think about what should be decoupled — it adds complexity. Decoupling increases resilience, but if all pieces are required for the system to function, there’s no win there. Breaking things apart simply increases complexity and creates more failure points.

The question I faced here was whether or not to decouple the following two tasks from trading threads:

  • Order Creation
  • Local (Non-Broker) Persistence

Order Creation

Order creation has to be synchronous, because it is necessary to be certain an order has gone to the street before proceeding to possibly create further orders. The system cannot function correctly otherwise.

Persistence

Local persistence, on the other hand, is a non-critical activity. In commercial trading systems I’ve architected, I’ve always ensured that trading could proceed even in the case of the local persistence layer being unavailable. That should generally be the rule.

Code

I’m going to begin with sharing a part of core/utility code.

As I began to write new HTTP requests, I realized there were a few aspects I wanted to abstract over and encapsulate:

  • Headers
  • Retry with exponential backoff
  • Response Deserialization

This did the trick:

ExponentialBackoff is from the backoff crate. Again and again, using Rust, I am impressed by how much solid functionality exists in the library ecosystem, and how easy it is to use.

(This Scala programmer felt very much at home with this type of trait-based abstraction, incidentally.)

Next, we have the new OrderService. We’ll take this in pieces, as it’s a bit large already.

Above we see the definition of the service along with the code to initialize it.

The methods shown are what is required for a minimally-viable product: We need to be able to create orders, and trading logic needs access to open positions.

On startup the system reads positions from the broker, and, for now, simply overwrites the local store with them — the broker is the Source of Truth.

Note that there is a translation from the broker/HTTP-specific artifacts shown above and the domain representation, done in the idiomatic Rust way of implementing the From trait:

Because implementing From gives you Into as well, we can call into() on a TradierPosition, and the compiler figures out, from type signatures alone, what we want to convert to — cool.

(The serde artifacts here are for the BSON serialization in the persistence layer.)

The with_ mutators, which produce new copies of the structs, is something that comes from the Scala/FP world of immutable objects. I’m not entirely sure how Rust-idiomatic the pattern is, but I like it and will continue to use it for Clone-able structs.

Moving on, we see the implementing struct:

As before, we’re using generics to specify service trait bounds, wrapping services in Arc for safe access, and using Mutex to interiorly-guard mutable data.

The pattern of using an implemtation-level Mutex keeps &mut self receivers out of interfaces. Those can be problematic in Rust — &mut self gives exclusive ownership, meaning that you cannot, for example, call such a method (or a combination of them, or even a single &mut self along with &self methods) more than once in the same scope.

Both the HTTP calls we’ve seen in this module use the core/http.rs utility discussed above.

Other things to note:

  • A position is created/updated along with the order. That a market order for a relatively small quantity, for any reasonably liquid name, will be filled essentially instantaneously is a safe assumption. (The very complete Tradier API does offer an endpoint for monitoring active orders.)
  • As discussed above, the persistence layer is asynchronous — those persistence.write calls (for the order and position) return immediately.

On to persistence.rs:

You can see that we are using the same AtomicBool flag as a thread-shutdown mechanism as seen previously, and that the channel used to decouple callers from the database actions is hidden as an implementation detail.

Note also that the implementation is split into two structs. This makes sense, since the concerns of implementing the API methods are not the same as those of the writing, done asynchronously, but it’s worth noting that I didn’t have it that way originally — I was led in that direction by the borrow-checker. That experience made me realize that Rust’s obsession with memory safety can also lead to better modeling.

Another interesting aspect here is the mechanism used to abstract over Persistable objects:

The as_any cast to &dyn Any along with the use of downcast_ref is the way dynamic downcasting is done in Rust. I wanted to use a single channel for persistence, so this was the way to go.

This allowed me to write an upsert fn that is entirely agnostic to what is being written, as seen in the first gist above.

Finally, we’re to the good stuff — trading!

Trading strategies should be responsible for generating signals only — not placing orders.

The signals generated by this or any Strategy are handled by a TradingService instance:

The logic is as follows:

  • If the Strategy returns a Buy signal, we buy shares up to the capital allocated to the symbol minus the current position
  • If a Sell signal is generated, we unwind any existing position completely

Rust detail: I gave up on the ‘market_data lifetime. It became a matter of jumping through hoops, and I was already aware that ‘market_data was really the same as ‘static. Around this time I read somewhere that new Rust programmers have an unfounded aversion to using ‘static, and I realized that that was me. ‘static is not “bad” — it is what it is. If your data lives for the lifetime of the program, it’s ‘static — embrace it.

Also note that matching, even on primitives, is usually cleaner than an if-else.

Ok, let’s see a bit of the system in action:

A few interesting things happened here:

  • An AAPL Buy signal was generated. This was done by jury-rigging a fake quote of 100.0 (who wouldn’t load-up on Apple at that price?). The system calculated the shares to buy based on the name’s allocated capital ($100,000), the existing position’s market value ($0), and the (actual) ask of 212.89.
  • We see the logs from the asynchronous Mongo writes lower in the trace
  • A real Sell signal for AMZN was generated! Unfortunately, we had no position to unwind. If we did, we would have generated our first bit of alpha.

That’s it for now. The next installment will cover the critical area of backtesting, without which no trading algorithm should be allowed near real money.

--

--