Thanh's Islet 🏝️

Rust × Algotrading

I’ve been pursuing a Master of Finance Engineering degree, and also been playing with Rust for a while, so I figured: why don’t I try to combine both by implementing an algotrading (short for algorithmic trading) bot? I eventually implemented some strategies and backtesting from scratch (spoiler: the result is worse than just holding, not counting trading fee). I’m on my way improving the result, and putting it into paper trade. This post is a nice distraction from that, where I’ll show you some code 1, then go with some thoughts on Rust.

Strategy Implementation

I thought of trying to incorporate risk management/portfolio management to the strategies at first, but then feel like it would complicate the logic without real obvious gains, so my implementations focused on managing one position, and making one action at a time. Here is the model at a high level:

state_new, action = advance(state_current, tick)

Before we continue on details of a “Hello World” strategy, Moving Average Crossover 2, let’s assume some common data structures:

#[derive(Debug, PartialEq, Clone)]
pub enum Action {
    Entry { price_record: PriceRecord },
    Exit { price_record: PriceRecord },
}

#[derive(Debug, PartialEq, Clone)]
pub struct PriceRecord {
    pub value: f64,
    pub timestamp_ms: u64,
}

The state for the strategy doesn’t have anything special, but we need them for completeness’s sake:

pub struct MovingAverage {
    n: usize,
    values: VecDeque<f64>,
    averages: VecDeque<f64>,
    sum: f64,
}

pub struct Strategy {
    pub ma_shorter: MovingAverage,
    pub ma_longer: MovingAverage,

    pub actions: Vec<Action>,
    pub actions_filled: Vec<Option<Action>>,
    pub price_records: Vec<PriceRecord>,
}

The interesting part is in advance implementations, where the calculation is “pure” and change incremental with new data and supposedly works for any time range:

impl MovingAverage {
    // ...

    pub fn advance(&mut self, value: f64) {
        if self.values.len() == self.n {
            if let Some(value_first) = self.values.pop_front() {
                self.sum -= value_first;
            }
            self.averages.pop_front();
        }

        self.values.push_back(value);
        self.sum += value;
        self.averages.push_back(self.sum / self.values.len() as f64);
    }

    // ...
}

impl Engine {
    // ...

    pub fn advance(&mut self, price_record: &PriceRecord) {
        self.ma_shorter.advance(price_record.value);
        self.ma_longer.advance(price_record.value);
        self.price_records.push(price_record.clone());

        let crossed = is_crossed(&self.ma_shorter, &self.ma_longer);
        let action_opt: Option<Action>;
        match (crossed, self.actions.last()) {
            (Crossed::Golden, Some(Action::Exit { price_record: _ }) | None) => {
                action_opt = Some(Action::Entry {
                    price_record: price_record.clone(),
                });
            }
            (Crossed::Death, Some(Action::Entry { price_record: _ })) => {
                action_opt = Some(Action::Exit {
                    price_record: price_record.clone(),
                });
            }
            _ => {
                action_opt = None;
            }
        }

        self.actions_filled.push(action_opt.clone());
        if let Some(action) = action_opt {
            (self.on_new_action)(&action);
            self.actions.push(action);
        }
    }
}

Using:

The result looks promising:

2025-09-03T02:33:30.787618Z  INFO downloader: Loaded total 731 records
2025-09-03T02:33:30.787675Z  INFO backtests: Action: Entry { price_record: PriceRecord { value: 27151.31, timestamp_ms: 1679097600000 } }
2025-09-03T02:33:30.787722Z  INFO backtests: Action: Exit { price_record: PriceRecord { value: 27613.515, timestamp_ms: 1683676800000 } }
...
2025-09-03T02:33:30.788032Z  INFO backtests: Action: Entry { price_record: PriceRecord { value: 62353.985, timestamp_ms: 1726704000000 } }
2025-09-03T02:33:30.788088Z  INFO backtests: Action: Exit { price_record: PriceRecord { value: 93265.12, timestamp_ms: 1735516800000 } }
2025-09-03T02:33:30.788102Z  INFO backtests: Total return percent: 1.396501006474551
2025-09-03T02:33:30.788107Z  INFO backtests: Total fee: 704.2855500000001
2025-09-03T02:33:30.788111Z  INFO backtests: Number of trade made: 14

Where we made around 139% returns over 2 years with 14 trades. However, I doubt that this is a good strategy as we didn’t take into account trading fee and the period is favorable already. Based roughly on the first and the last actions, just holding would gives us >200% returns ((93,265 - 27,151) / 27,151).

On Rust Usage

I think most people coming here would have this question in mind: is Rust good for algotrading?

Let’s say we have the research/backtests side, and the low-latency side, then Rust isn’t too good nor too bad at either. It doesn’t help with fast iteration like Python, but not suited for ultra-low latency trading (where C++ dominates) 3. If you’re building a full trading system from scratch and value type safety and reproducibility across your entire pipeline, Rust becomes more compelling.

Therefore, one takeaway is: you won’t go wrong with either Python (if you are on the strategy camp like doing researches and backtesting) or C++ (if you are on the low-latency trading camp).


Another thing I think people would be interested about is an unique review of Rust. It doesn’t stray far from this GitHub’s blog post 4:

Another takeaway: the language is okay. You’ll learn a ton from using it and have fun on the way, but don’t expect you can find a job easily.


Finally, we have an interesting question: what is Rust good for?

To be honest, I don’t have a clear answer, even after I used it to implement a half-finished toy KV database 5, an HTTP backend for a personal financial dashboard 6, and a trading bot 1. Recently, I was asked the same question was asked when in a backend position 7. The company was using Go heavily, and looking to improve the performance of their services, and was considering Rust. I mentioned correctness and binary build, but and my interviewer said that the points didn’t really hold against Go. I quoted Discord’s switching from Go to Rust 8, and talked about better memory management, but I guess I couldn’t dive deep enough and convince my interviewer.

Going back to answering the question, let’s put it in terms of:

Then I think Rust is good if:

uv 9 is a successful example: the functional requirements of Python tooling doesn’t change that much after the language is stable, but this new tool nailed the DX and performance.

The final takeaway: Rust is good for rewrites.

Conclusion

While it’s a fun experiment implementing algotrading in Rust, I would say for job hunting and ease of starting, you should consider using Python. If you are targeting HFT positions, then do something with C++. For me, I would continue with Rust as I got something working, and… it’s fun doing stuff from scratch anyway.