Exploring Design Patterns in Rust with Algorithmic Trading Examples

·

24 min read

Exploring Design Patterns in Rust with Algorithmic Trading Examples

Algorithmic trading system

At the heart of an algorithmic trading system are several essential components, including Order Management Systems (OMS), Execution Management Systems (EMS), and backtesting frameworks. Before exploring the design patterns, let's take a moment to appreciate and discuss these components briefly to gain a better understanding of the design pattern examples.

Order management systems (OMS)

Order management systems (OMS) play a crucial role in monitoring and executing trades for clients, such as portfolio managers and traders. For instance, when a portfolio manager decides to purchase 5,000 shares of NVDA at a limit price of $130 per share, they enter this order into the OMS. The OMS checks whether the order violates fund's concentration limit and if it does then alerts the PM. If all okay then analyzes current market conditions and routes the order to an exchange where the likelihood of execution at the desired price is highest. After execution, OMS sends trade details to the back-office system.

It's impressive how OMS can manage such high volumes of orders from various sources, control the flow, validate them, and ensure that trades are completed both efficiently and accurately.

Execution Management Systems (EMS)

To be added:

The SOLID Design Principles

Let's review the SOLID design principles since we'll mention them while discussing design patterns. It's helpful to remember these principles when designing maintainable and scalable object-oriented systems.

  1. Single Responsibility Principle (SRP): A class should have only one reason to exist, meaning it should have only one job or responsibility. You should not overload your objects with two many responsibility, just create a new class for it.

  2. Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code.

  3. Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. This ensures that a subclass can stand in for its superclass.

  4. Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. This means creating smaller, more specific interfaces rather than a large, general-purpose one.

  5. Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details. Details should depend on abstractions. This principle helps in reducing the coupling between different parts of a system.

Strategy Pattern

The Strategy design pattern is a behavioral design pattern that enables selecting an algorithm's behavior at runtime. This pattern is based on composition. Lets see the definition from the “Head First Design Pattern”:

The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets algorithm vary independently from clients that use it.

It is naturally useful in trading systems where algorithms need to switch between different execution methods. In this example, we'll implement a simplified trading system in Rust that executes orders using different strategies like TWAP (Time-Weighted Average Price), VWAP (Volume-Weighted Average Price), and POV (Percentage of Volume). These are your execution strategies. You can choose one of the strategies based on certain factors or constraints.

Structure

  1. The Strategy interface is common to all concrete strategies. It declares a method the context uses to execute a strategy. We define a common interface for all execution strategies, the method execute_order that all execution strategies must implement.
trait ExecutionStrategy {
    fn execute_order(&self, order_id: u32, quantity: u32);
}
  1. Concrete Strategies implement different variations of an algorithm the context uses. In this example, we write implementations of TWAP, VWAP, and POV strategies.

     struct TwapStrategy;
    
     impl ExecutionStrategy for TwapStrategy {
         fn execute_order(&self, order_id: u32, quantity: u32) {
             println!("Executing order {} using TWAP for {} units.", order_id, quantity);
             // Implement TWAP logic here
         }
     }
     struct VwapStrategy;
    
     impl ExecutionStrategy for VwapStrategy {
         fn execute_order(&self, order_id: u32, quantity: u32) {
             println!("Executing order {} using VWAP for {} units.", order_id, quantity);
             // Implement VWAP logic here
         }
     }
     struct PovStrategy {
         participation_rate: f64,
     }
    
     impl ExecutionStrategy for PovStrategy {
         fn execute_order(&self, order_id: u32, quantity: u32) {
             println!(
                 "Executing order {} using POV at {}% participation for {} units.",
                 order_id, self.participation_rate * 100.0, quantity
             );
             // Implement POV logic here
         }
     }
    
  2. The Context maintains a reference to one of the concrete strategies and communicates with this object only via the strategy interface. Here we write the OrderExecutor that holds a reference to a strategy and uses it to execute orders. The context calls the execution method on the linked strategy object each time it needs to run the algorithm. The context doesn’t know what type of strategy it works with or how the algorithm is executed.

     struct OrderExecutor {
         strategy: Box<dyn ExecutionStrategy>,
     }
    
     impl OrderExecutor {
         fn new(strategy: Box<dyn ExecutionStrategy>) -> Self {
             OrderExecutor { strategy }
         }
         fn set_strategy(&mut self, strategy: Box<dyn ExecutionStrategy>) {
             self.strategy = strategy;
         }
         fn execute(&self, order_id: u32, quantity: u32) {
             self.strategy.execute_order(order_id, quantity);
         }
     }
    
  3. The Client creates a specific strategy object and passes it to the context. The context exposes a setter which lets clients replace the strategy associated with the context at runtime.

     fn main() {
         let order_id = 101;
         let quantity = 1000;
    
         // Using TWAP Strategy
         let twap_strategy = Box::new(TwapStrategy);
         let mut executor = OrderExecutor::new(twap_strategy);
         executor.execute(order_id, quantity);
    
         // Switching to VWAP Strategy
         let vwap_strategy = Box::new(VwapStrategy);
         executor.set_strategy(vwap_strategy);
         executor.execute(order_id+1, quantity);
    
         // Switching to POV Strategy
         let pov_strategy = Box::new(PovStrategy {
             participation_rate: 0.1,
         });
         executor.set_strategy(pov_strategy);
         executor.execute(order_id+2, quantity);
     }
    

Strategy pattern Class Diagram

+-----------------------------------------------------------------+
| <<interface>>                                                   |
| ExecutionStrategy                                               |
|-----------------------------------------------------------------|
| + execute_order(...)                                            |
+-----------------------------------------------------------------+

     /|\                  /|\                   /|\
      |                    |                     |
      |                    |                     |
+---------------+     +--------------+       +--------------+
| TwapStrategy  |     | VwapStrategy |       | PovStrategy  |
|---------------|     |--------------|       |--------------|
|               |     |              |       | - participation_rate: f64 |
|---------------|     |--------------|       |--------------|
| + execute_order(...)|+ execute_order(...)  |+ execute_order(...)|
+---------------+     +--------------+       +--------------+

+-----------------------------+
| OrderExecutor               |
|-----------------------------|
| - strategy: ExecutionStrategy |
|-----------------------------|
| + new(strategy)             |
| + set_strategy(strategy)    |
| + execute(order_id, quantity) |
+-----------------------------+

Observer Pattern

The Observer design pattern is a behavioral design pattern that enables an object, known as the Subject, to maintain a list of its dependents, called Observers, and automatically notify them of any state changes, typically by calling one of their methods. This pattern is particularly useful in Event-Driven Systems, when you need to notify multiple objects about events without tightly coupling them. The definition from the “Head First Design Pattern”:

The observer pattern defines a one to many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically.

In the context of algorithmic trading , the Observer pattern can be applied to scenarios such as:

  • Market Data Feeds: Notifying trading strategies when new market data arrives.

  • Order Execution Updates: Informing interested parties when an order is executed or its status changes.

  • Price Alerts: Triggering alerts when certain price thresholds are crossed.

Structure

  • Subject (Observable or Publisher): The Subject issues events of interest to other objects. These events occur when the subject (publisher) changes its state or executes some behaviors. Subjects maintain a subscription infrastructure, list of observers and provides methods to attach, detach, and notify them.

  • Observer (Subscriber): The Observer interface declares the notification interface. In most cases, it consists of a single update method.

  • Concrete Subject: Implements the subject interface and holds the state of interest.

  • Concrete Observer: Concrete Observer perform some actions in response to notifications issued by the subject. All of these classes must implement the same interface so the subject isn’t coupled to concrete classes.

Usually, subscribers need some contextual information to handle the update correctly. For this reason, publishers often pass some context data as arguments of the notification method. The publisher can pass itself as an argument, letting subscriber fetch any required data directly.

Implementation Example

We will implement a simplified version of trading system using Rust where we have:

  • Market Data Feed (Subject): Provides live price updates for various financial instruments.

  • Trading Strategies (Observers): React to market data updates to make trading decisions.

Let look at the code implementation.

Observer Trait

The Observer trait declares the update method, which observers must implement.

use std::rc::Rc;
use std::cell::RefCell;

trait Observer {
    fn update(&self, instrument_id: &str, price: f64);
}

Implement Concrete Observers (Trading Strategies)

struct MomentumStrategy {
    name: String,
    threshold: f64,
}

impl Observer for MomentumStrategy {
    fn update(&self, instrument_id: &str, price: f64) {
        if price > self.threshold {
            println!(
                "{}: [{}] Price crossed above threshold! Price: {}",
                self.name, instrument_id, price
            );
            // Implement buy logic
        } else {
            println!(
                "{}: [{}] Price below threshold. Price: {}",
                self.name, instrument_id, price
            );
            // Implement hold or sell logic
        }
    }
}

struct MeanReversionStrategy {
    name: String,
    average_price: RefCell<f64>,
}

impl Observer for MeanReversionStrategy {
    fn update(&self, instrument_id: &str, price: f64) {
        let mut avg = self.average_price.borrow_mut();
        *avg = (*avg * 0.9) + (price * 0.1); // Update moving average
        if price < *avg {
            println!(
                "{}: [{}] Price below average! Price: {}, Average: {:.2}",
                self.name, instrument_id, price, *avg
            );
            // Implement buy logic
        } else {
            println!(
                "{}: [{}] Price above average. Price: {}, Average: {:.2}",
                self.name, instrument_id, price, *avg
            );
            // Implement sell logic
        }
    }
}

Define the Subject Trait

trait Subject {
    fn attach(&mut self, observer: Rc<dyn Observer>);
    fn detach(&mut self, observer: &Rc<dyn Observer>);
    fn notify(&self);
}

Implement the Concrete Subject (Market Data Feed)

struct MarketDataFeed {
    instrument_id: String,
    observers: RefCell<Vec<Rc<dyn Observer>>>,
    price: RefCell<f64>,
}

impl MarketDataFeed {
    fn new(instrument_id: &str) -> Self {
        MarketDataFeed {
            instrument_id: instrument_id.to_string(),
            observers: RefCell::new(Vec::new()),
            price: RefCell::new(0.0),
        }
    }
    fn set_price(&self, new_price: f64) {
        *self.price.borrow_mut() = new_price;
        self.notify();
    }
}
impl Subject for MarketDataFeed {
    fn attach(&mut self, observer: Rc<dyn Observer>) {
        self.observers.borrow_mut().push(observer);
    }
    fn detach(&mut self, observer: &Rc<dyn Observer>) {
        let mut observers = self.observers.borrow_mut();
        if let Some(pos) = observers.iter().position(|x| Rc::ptr_eq(x, observer)) {
            observers.remove(pos);
        }
    }
    fn notify(&self) {
        let price = *self.price.borrow();
        let instrument_id = &self.instrument_id;
        for observer in self.observers.borrow().iter() {
            observer.update(instrument_id, price);
        }
    }
}

Client Code

fn main() {
    // Create market data feed for AAPL
    let mut market_data_feed = MarketDataFeed::new("AAPL");

    // Create observers
    let momentum_strategy: Rc<dyn Observer> = Rc::new(MomentumStrategy {
        name: String::from("MomentumStrategy"),
        threshold: 150.0,
    });

    let mean_reversion_strategy: Rc<dyn Observer> = Rc::new(MeanReversionStrategy {
        name: String::from("MeanReversionStrategy"),
        average_price: RefCell::new(145.0),
    });

    // Attach observers
    market_data_feed.attach(momentum_strategy.clone());
    market_data_feed.attach(mean_reversion_strategy.clone());

    // Simulate market data updates
    let price_updates = vec![148.0, 151.0, 149.5, 152.5, 147.0];

    for price in price_updates {
        println!("\nMarketDataFeed [{}]: New price is {}", "AAPL", price);
        market_data_feed.set_price(price);
    }

    // Detach momentum strategy
    market_data_feed.detach(&momentum_strategy);

    // More updates
    let more_price_updates = vec![153.0, 146.5];

    for price in more_price_updates {
        println!("\nMarketDataFeed [{}]: New price is {}", "AAPL", price);
        market_data_feed.set_price(price);
    }
}

Notes on Rust Implementation

  • Reference Counting (Rc): Used to allow multiple ownership of observers by the subject.

  • Interior Mutability (RefCell): Allows us to mutate data even when it is wrapped in an immutable reference, which is necessary when observers need to update their internal state upon receiving updates.

  • Trait Objects (dyn Trait): Trait objects like dyn Observer allow for dynamic dispatch in Rust. They allow the subject to hold a collection of different types that implement the same trait.

In Rust, comparing trait objects (dyn Observer) directly is not straightforward because trait objects do not implement PartialEq by default. We use Rc::ptr_eq to compare the pointers of the Rc smart pointers, which checks if they point to the same allocation.

In the line, if we don’t explicitly say the type Rc<dyn Observer> then momentum_strategy will be of type Rc<MomentumStrategy> and it will be fine for attach method however not work with detach method.

let momentum_strategy: Rc<dyn Observer> = Rc::new(MomentumStrategy{...});

In Rust, even though MomentumStrategy implements the Observer trait, Rc<MomentumStrategy> and Rc<dyn Observer> are different types and are not directly interchangeable.

Note: The Rc::ptr_eq function requires that both Rc pointers have the same type parameter. By ensuring that both Rc pointers are of type Rc<dyn Observer>, we satisfy this requirement.

Rust can automatically coerce a reference to a concrete type into a reference to a trait object if the type implements the trait. This is what happens when you assign Rc::new(MomentumStrategy { /* fields */ }) to a variable of type Rc<dyn Observer>.

Decorator Pattern

The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects dynamically without affecting the behavior of other objects from the same class. The definition from the “Head First Design Pattern”:

The decorator pattern attaches additional responsibilities to an object dynamically. Decorators provides a flexible alternative to subclassing for extending functionality.

Structure

  • Component Interface: Defines the interface for objects that can have responsibilities added to them dynamically.

  • Concrete Component: The original object to which additional responsibilities are added.

  • Decorator: Abstract class that implements the component interface and contains a reference to a component object.

  • Concrete Decorators: Extend the functionality of the component by overriding methods and adding additional behaviors.

UML Diagram

+------------------+
|    Component     |<---------------------------+
+------------------+                            |
| + operation()    |                            |
+------------------+                            |
          ^                                     |
          |                                     |
+------------------+          +------------------+
| ConcreteComponent|          |    Decorator     |
+------------------+          +------------------+
| + operation()    |          | - component      |
+------------------+          | + operation()    |
                              +------------------+
                                       ^
                                       |
                      +----------------+----------------+
                      |                                 |
            +------------------+               +------------------+
            | ConcreteDecoratorA|               | ConcreteDecoratorB|
            +------------------+               +------------------+
            | + operation()    |               | + operation()    |
            +------------------+               +------------------+

Example: Enhancing Order Execution with Decorators

We have an OrderExecutor component responsible for executing trades. We want to add additional behaviors:

  • LoggingDecorator: Logs the details of each order execution.

  • ValidationDecorator: Validates orders before execution.

Component Trait

#[derive(Debug)]
struct Order {
    symbol: String,
    quantity: i32,
    price: f64,
}
trait OrderExecutor {
    fn execute_order(&self, order: &Order) -> Result<(), String>;
}

Concrete Component

struct BasicOrderExecutor;

impl OrderExecutor for BasicOrderExecutor {
    fn execute_order(&self, order: &Order) -> Result<(), String> {
        // Simulate order execution logic
        println!("Executing order: {:?}", order);
        Ok(())
    }
}

Concrete Decorators

struct LoggingDecorator<T: OrderExecutor> {
    executor: T,
}

impl<T: OrderExecutor> LoggingDecorator<T> {
    fn new(executor: T) -> Self {
        LoggingDecorator { executor }
    }
}

impl<T: OrderExecutor> OrderExecutor for LoggingDecorator<T> {
    fn execute_order(&self, order: &Order) -> Result<(), String> {
        println!("LoggingDecorator: Order received: {:?}", order);
        let result = self.executor.execute_order(order);
        println!("LoggingDecorator: Order execution result: {:?}", result);
        result
    }
}
struct ValidationDecorator<T: OrderExecutor> {
    executor: T,
}

impl<T: OrderExecutor> ValidationDecorator<T> {
    fn new(executor: T) -> Self {
        ValidationDecorator { executor }
    }
}

impl<T: OrderExecutor> OrderExecutor for ValidationDecorator<T> {
    fn execute_order(&self, order: &Order) -> Result<(), String> {
        if self.validate(order) {
            println!("Validated Order: {:?}", order);
            self.executor.execute_order(order)
        } else {
            Err(String::from("Validation failed"))
        }
    }
}

impl<T: OrderExecutor> ValidationDecorator<T> {
    fn validate(&self, order: &Order) -> bool {
        // Implement validation logic
        order.quantity > 0 && order.price > 0.0
    }
}

Client Code

fn main() {
    let order = Order {
        symbol: String::from("AAPL"),
        quantity: 100,
        price: 150.0,
    };

    // Basic executor
    let basic_executor = BasicOrderExecutor;

    // Decorate with validation
    let validated_executor = ValidationDecorator::new(basic_executor);

    // Further decorate with logging
    let logged_executor = LoggingDecorator::new(validated_executor);

    // Execute the order
    let result = logged_executor.execute_order(&order);
    match result {
        Ok(_) => println!("Order executed successfully."),
        Err(e) => println!("Order execution failed: {}", e),
    }
}

Notes on Rust Implementation

In this example, I used generics allowing static dispatch. By the way, in Rust, traits are not types themselves; they are a collection of methods that types can implement. You cannot instantiate a trait directly or store it as a field without using a pointer or a generic parameter. The following will result in compilation error because OrderExecutor is a trait.

struct ValidationDecorator {
    executor: OrderExecutor,
}

In Rust, all types must have a known size at compile time unless they are behind a pointer (like &, Box, or Rc) or used as a generic type parameter with trait bounds. By default, all generic type parameters and struct fields have an implicit Sized bound. This means that the compiler needs to know the size of the type at compile time.

The choice between generics and trait objects depends on your specific needs for performance and flexibility.

Instead of generics, you can use Trait Objects as follows:

struct ValidationDecorator {
    executor: Box<dyn OrderExecutor>,
}

This uses dynamic dispatch, which introduces a slight runtime overhead due to the use of a vtable.

Factory Method Pattern

Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. This definition is by the refactoring.guru. The “Head First Design Pattern” more or less says the same:

The Factory Method Patterns defines an interface for creating an object, but lets subclasses decide which class to instantiate. Factory method lets a class defer instantiation to subclasses.

Structure

  • Product Interface: Defines the interface of objects the factory method creates.

  • Concrete Products: Various implementations of the product interface.

  • Creator (Factory): Declares the factory method that returns new product objects.

  • Concrete Creators: Override the factory method to change the resulting product's type.

UML Diagram

+------------------+
|    Creator       |<---------------------------+
+------------------+                            |
| + factoryMethod()|                            |
+------------------+                            |
            ^                                   |
            |                                   |
+------------------+          +------------------+
| ConcreteCreator  |          |   Product        |
+------------------+          +------------------+
| + factoryMethod()|          | + operation()    |
+------------------+          +------------------+
                                         ^
                                         |
                          +---------------------------+
                          |                           |
                +------------------+        +------------------+
                | ConcreteProductA |        | ConcreteProductB |
                +------------------+        +------------------+
                | + operation()    |        | + operation()    |
                +------------------+        +------------------+

Example: Order Creation Factory

Let's consider an example where we need to create different types of orders based on the current state of our trading strategy.

Order Types

  • Market Order: Executes immediately at the current market price.

  • Limit Order: Executes at a specified price or better.

  • Stop Order: Becomes a market order when a specified price is reached.

Product Interface

This interface is common to all the objects (in our case order types) that can be produced by the creator and its subclasses.

trait Order {
    fn place(&self);
}

Concrete Products

Concrete Products are different implementations of the product interface. In our case different implementation of order types.

struct MarketOrder {
    symbol: String,
    quantity: u32,
}

impl Order for MarketOrder {
    fn place(&self) {
        println!("Placing Market Order: Buy {} units of {} at market price.",
            self.quantity, self.symbol
        );
        // Implement order placement logic here
    }
}
struct LimitOrder {
    symbol: String,
    quantity: u32,
    limit_price: f64,
}

impl Order for LimitOrder {
    fn place(&self) {
        println!("Placing Limit Order: Buy {} units of {} at ${}.",
            self.quantity, self.symbol, self.limit_price
        );
        // Implement order placement logic here
    }
}
struct StopOrder {
    symbol: String,
    quantity: u32,
    stop_price: f64,
}

impl Order for StopOrder {
    fn place(&self) {
        println!(
            "Placing Stop Order: Buy {} units of {} when price reaches ${}.",
            self.quantity, self.symbol, self.stop_price
        );
        // Implement order placement logic here
    }
}

Creator (Order Factory)

It returns a different type of order.

enum OrderType {
    Market,
    Limit(f64), // Limit price
    Stop(f64),  // Stop price
}
struct OrderFactory;

impl OrderFactory {
    fn create_order(order_type: OrderType, symbol: String, quantity: u32) -> Box<dyn Order> {
        match order_type {
            OrderType::Market => Box::new(MarketOrder { symbol, quantity }),
            OrderType::Limit(limit_price) => Box::new(LimitOrder {
                symbol,
                quantity,
                limit_price,
            }),
            OrderType::Stop(stop_price) => Box::new(StopOrder {
                symbol,
                quantity,
                stop_price,
            }),
        }
    }
}

Client Code

fn main() {
    let symbol = String::from("AAPL");
    let quantity = 100;
    // Decide which order type to use
    let order_type = OrderType::Limit(149.0);
    // Create the order using the factory
    let order = OrderFactory::create_order(order_type, symbol.clone(), quantity);
    // Place the order
    order.place();
}

Singleton Pattern

Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance. Overall Singleton has almost the same pros and cons as global variables.

In Rust, implementing singletons are pretty tricky due to its ownership model and emphasis on safe concurrency. It must solve these two problem:

  • Ensure a Class Has Only One Instance: Prevents multiple instances, guaranteeing that all clients use the same instance.

  • Provide a Global Point of Access: Offers a way to access the single instance from anywhere in the application.

Rust discourages global mutable state to prevent data races and ensure thread safety. And any global state must be safe to access from multiple threads. All these makes implementing singletons non-trivial. We can achieve this using different approach like using Mutex, static lifetime, OnceCell crate.

While a mutable singleton can be convenient, global mutable state can lead to code that's difficult to maintain and test. You always have option to pass the instance as a parameter to the parts of your code that need it. As we are discussing singletons, lets see the implementation using OnceCell.

OnceCell<T>

When you need to initialize data at runtime, possibly in a lazy manner, and ensure it is set only once. The core API looks roughly like

impl OnceCell<T> {
    fn new() -> OnceCell<T> { ... }
    fn set(&self, value: T) -> Result<(), T> { ... }
    fn get(&self) -> Option<&T> { ... }
}

OnceCell has two varient std::cell::OnceCell<T> for single-threaded scenarios and std::sync::OnceCell<T> for thread-safe scenarios, can be shared between threads.

https://github.com/matklad/once_cell

However, since OnceCell provides immutable access to the initialized value, we need a way to mutate the instance after it's been set because singleton patterns allows mutable instance. To achieve a mutable singleton, we need combine OnceCell with interior mutability patterns. This involves wrapping the struct in types that allow mutation through immutable references, such as RefCell for single-threaded applications or Mutex/RwLock for multi-threaded applications.

Example: Trading System ConfigManager

Config

The Config struct holds various configuration settings. It derives Deserialize to allow loading from a JSON file.

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Config {
    pub api_key: String,
    pub db_connection_string: String,
    pub trading_parameters: TradingParameters,
}

#[derive(Debug, Deserialize)]
pub struct TradingParameters {
    pub max_positions: usize,
    pub risk_tolerance: f64,
    // Add other parameters as needed
}

ConfigManager Singleton

OnceCell ensures that the ConfigManager is initialized only once in a thread-safe manner. instance method provides global access to the singleton instance. The new method is private to prevent direct instantiation.

use once_cell::sync::OnceCell;
use std::fs;

pub struct ConfigManager {
    config: Config,
}

impl ConfigManager {
    fn new() -> Self {
        let config_data = fs::read_to_string("config.json")
            .expect("Failed to read configuration file");
        let config: Config = serde_json::from_str(&config_data)
            .expect("Failed to parse configuration file");
        ConfigManager { config }
    }

    pub fn instance() -> &'static ConfigManager {
        static INSTANCE: OnceCell<ConfigManager> = OnceCell::new();
        INSTANCE.get_or_init(|| ConfigManager::new())
    }
    pub fn get_config(&self) -> &Config {
        &self.config
    }
}

Use ConfigManager

Now we can’t just use this TWAP that depends on a ConfigManager in some other context, without carrying over the ConfigManager to the other context, e.g. Unit Tests.

pub trait OrderExecution {
    fn execute(&self);
}
pub struct TWAP;

impl OrderExecution for TWAP {
    fn execute(&self) {
        let config_manager = ConfigManager::instance();
        let max_positions = config_manager.get_config().trading_parameters.max_positions;
        let risk_tolerance = config_manager.get_config().trading_parameters.risk_tolerance;
        // Implement execution logic using the configurations
    }
}
fn main() {
    let strategy = TWAP;
    strategy.execute();

    // Access the ConfigManager directly elsewhere
    let api_key = ConfigManager::instance().get_config().api_key.clone();
    println!("Using API Key: {}", api_key);
}

In this example, we can't change the ConfigManager instance. We've basically set up an immutable singleton (or a single Flyweight object).

To implement a mutable singleton ConfigManager struct using OnceCell, you need to wrap the ConfigManager in an Interior Mutability Type like using RefCell<ConfigManager> for single-threaded applications or using using Mutex<ConfigManager> or RwLock<ConfigManager> for multi-threaded applications. For example:

static CONFIG: OnceCell<RefCell<ConfigManager>> = OnceCell::new();
//Multi-threaded
static CONFIG: OnceCell<Mutex<ConfigManager>> = OnceCell::new();

More Notes on Rust

What is Mutex<T>

Mutex stands for mutual exclusion, which means it is used to protect shared data by allowing only one thread to access a resource or critical section at a time e.g. only exclusive borrows. Once a thread acquires a mutex, all other threads attempting to acquire the same mutex are blocked until the first thread releases the mutex.

The Rust standard library provides std::sync::Mutex<T>. It is generic over a type T, which is the type of the data the mutex is protecting. Its lock() method returns a special type called a MutexGuard. This guard represents the guarantee that we have locked the mutex. You unlock the mutex simply by dropping the guard.

Rust’s Mutex holds the data it’s protecting. In C++, on the other hand, std::mutex doesn’t hold the data it’s protecting and doesn’t even know what it’s protecting.

What is Arc<Mutex<T>>

Here, we want to share ownership of the data (Mutex<T>) between multiple threads. Arc provides shared ownership with thread-safe reference counting. Arc<Mutex<T>> allows multiple threads to own the same data and mutate it safely.

Command Pattern

Command is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request’s execution, and support undoable operations. This definition is by the refactoring.guru. The “Head First Design Pattern” says:

The command pattern encapsulates a request as an object, thereby letting you parameterize other objects with different requests, queue or log requests and support undoable operations.

Let consider an example where we have a Portfolio that keeps track of stock positions and cash balance. We support actions like Add, Remove, or Reduce positions, as well as Deposit and Withdraw funds to our portfolio. Lets make it more interesting and take a step forward and consider multiple treading scenario as one tread could be buying SPY, another selling MSFT and another thread depositing funds. We need to share Portfolio with different threads and it handles different commends.

Structure

  • Command interface: The Command interface usually methods for executing the command. We can defines execute and rollback methods with thread safety in mind.

  • Receiver: The receiver class contains the business logic. Most commands only handle the details of how a request is passed to the receiver, while the receiver itself does the actual work. In our case the Portfolio is a receiver that stores positions and balance, protected by Mutex for safe concurrent access.

  • Concrete Commands: Implement the Command trait for specific actions. A concrete command isn’t supposed to perform the work on its own, but rather to pass to the receiver. In our case we will implement add position and add funds commends.

  • Invoker: The Invoker class is responsible for initiating requests. In our example we define a Broker that executes commands and maintains a history.

  • Client: The application that creates commands and interacts with the broker.

Command Trait

use std::error::Error;
trait Command: Send + 'static {
    fn execute(&mut self) -> Result<(), Box<dyn Error>>;
    fn rollback(&mut self) -> Result<(), Box<dyn Error>>;
}

Send Trait ensures commands can be transferred across threads. The Box<dyn Error> is for flexible error types.

Receiver: Portfolio

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct Portfolio {
    positions: Arc<Mutex<HashMap<String, i32>>>, // symbol -> quantity
    balance: Arc<Mutex<f64>>,                    // cash balance
}
impl Portfolio {
    fn new() -> Self {
        Portfolio {
            positions: Arc::new(Mutex::new(HashMap::new())),
            balance: Arc::new(Mutex::new(0.0)),
        }
    }
    fn add_position(&self, symbol: &str, quantity: i32) -> Result<(), String> {
        let mut positions = self.positions.lock().unwrap();
        let entry = positions.entry(symbol.to_string()).or_insert(0);
        *entry += quantity;
        println!("Added {} shares of {}", quantity, symbol);
        Ok(())
    }
    fn reduce_position(...){...} //Similar
    fn deposit(&self, amount: f64) -> Result<(), String> {
        let mut balance = self.balance.lock().unwrap();
        *balance += amount;
        println!("Deposited ${}", amount);
        Ok(())
    }
    fn withdraw(...){...} //Similar to deposit
    fn get_balance(&self) -> f64 {
        let balance = self.balance.lock().unwrap();
        *balance
    }

    fn get_positions(&self) -> HashMap<String, i32> {
        let positions = self.positions.lock().unwrap();
        positions.clone()
    }
}

We used Arc<Mutex<HashMap<String, i32>>> to allow shared mutable access across threads for position.

Concrete Commands

We implement two concreate commands AddPositionCommand and WithdrawCommand. Both command interacts with the Portfolio in a thread-safe manner and implements rollback functionality.

struct AddPositionCommand {
    receiver: Arc<Portfolio>,
    symbol: String,
    quantity: i32,
}
impl Command for AddPositionCommand {
    fn execute(&mut self) -> Result<(), Box<dyn Error>> {
        self.receiver.add_position(&self.symbol, self.quantity)?;
        Ok(())
    }
    fn rollback(&mut self) -> Result<(), Box<dyn Error>> {
        self.receiver.reduce_position(&self.symbol, self.quantity)?;
        Ok(())
    }
}
struct WithdrawCommand {
    receiver: Arc<Portfolio>,
    amount: f64,
}
impl Command for WithdrawCommand {
    fn execute(&mut self) -> Result<(), Box<dyn Error>> {
        self.receiver.withdraw(self.amount)?;
        Ok(())
    }
    fn rollback(&mut self) -> Result<(), Box<dyn Error>> {
        self.receiver.deposit(self.amount)?;
        Ok(())
    }
}

Invoker: Broker

struct Broker {
    history: Vec<Box<dyn Command>>,
}
impl Broker {
    fn new() -> Self {
        Broker {
         history: Vec::new(),
        }
    }
    fn execute_command(&mut self, mut command: Box<dyn Command>) -> Result<(), Box<dyn Error>> {
        command.execute()?;
        self.history.push(command);
        Ok(())
    }
    fn rollback_last(&mut self) -> Result<(), Box<dyn Error>> {
        if let Some(mut command) = self.history.pop() {
            command.rollback()?;
            Ok(())
        } else {
            Err("No commands to rollback.".into())
        }
    }
}

Client Code

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let portfolio = Arc::new(Portfolio::new());
    let broker = Arc::new(Mutex::new(Broker::new()));

    let portfolio_clone1 = Arc::clone(&portfolio);
    let broker_clone1 = Arc::clone(&broker);
    // Add position
    let add_position_command = AddPositionCommand {
        receiver: portfolio.clone(),
        symbol: "AAPL".to_string(),
        quantity: 50,
    };
    let deposit_handle = thread::spawn(move || {
        let deposit_command = DepositCommand {
            receiver: portfolio_clone1,
            amount: 10000.0,
        };
        let mut broker1 = broker_clone1.lock().unwrap();
        broker1.execute_command(Box::new(deposit_command)).unwrap();
        drop(broker1);
    });
    let broker_clone2 = Arc::clone(&broker);
    {
        let mut broker2 = broker_clone2.lock().unwrap();
        broker2.execute_command(Box::new(add_position_command))?;
    }

    deposit_handle.join().unwrap();
    // Print current portfolio state
    println!("Current balance: ${}", portfolio.get_balance());
    println!("Current positions: {:?}", portfolio.get_positions());
    // Rollback last command (Withdraw)
    {
        let mut broker3 = broker.lock().unwrap();
        broker3.rollback_last()?;
    }
    println!("\nAfter rollback of last command:");
    println!("Current balance: ${}", portfolio.get_balance());
    println!("Current positions: {:?}", portfolio.get_positions());
    Ok(())
}

Here, I used Arc<Mutex<Broker>> to let multiple threads share access to the broker, ensuring the execute_command method is synchronized. While this isn't the perfect example since locking Broker might slow things down, it gives you a taste of multithreading and the command pattern.

As Broker might be doing tons of other operation we should at least minimize locking and lock only history to prevent contention and improve performance.

So, let's take out the Mutex around the Broker and redesign the broker as:

struct Broker {
    history: Arc<Mutex<Vec<Box<dyn Command>>>>,
}

Now, the history is an Arc<Mutex<Vec<Box<dyn Command>>>>, which lets multiple parts of the program access the Broker at the same time while keeping the history synchronized.

Conclusion

Design patterns like Strategy, Decorator, Observer, and Factory Method provide proven and adaptable solutions that help developers create strong and efficient trading systems. By learning and using these patterns, developers can greatly improve the quality, maintainability, and performance of algorithmic trading systems. By abstracting the creation of objects, by encapsulating execution and trading strategies separately, we can build systems that are easier to maintain, extend, and adapt to changing market conditions.

References

https://refactoring.guru/design-patterns/strategy

https://rust-unofficial.github.io/patterns/patterns/index.html

Rust Atomics and Locks-O'Reilly Media (2023)-Mara Bos

Did you find this article valuable?

Support Siddharth by becoming a sponsor. Any amount is appreciated!