5 Years, 3 Languages, One Trading System

I built a trading bot in Dart. It lost a few thousand dollars. Best investment I ever made.

Not because the bot eventually worked. It didn’t. But because chasing that problem through three languages over five years taught me more about software design than any project before it.

Starting with what I knew

In 2021, I was deep in Flutter. HabitChallenge was running, and I had the itch to build something else. Markets felt interesting. At its core, it’s just data transformation: ingest price data, compute indicators, act on signals.

So I built a bot in Dart. It connected to Binance, computed moving averages, sent signals to Telegram, tracked positions in Google Sheets, and executed real trades on live markets. Real money in, real money out. It worked, in the sense that it ran. It also lost money.

Dart is a genuinely good language: strongly typed, null-safe, pattern matching, standalone Linux executables with no system dependencies. But its concurrency model is event-based and single-threaded, much like JavaScript. That simplifies a lot of things, but not the thing I needed: always-on computation across multiple data streams with minimal latency. To get real concurrency, I’d have had to manage isolates manually, building the resilience layer myself. I knew there were platforms built for exactly this problem. I just wasn’t using one.

After three architectural rewrites, I understood what I wanted to build. I also started to suspect the real problem wasn’t the system. It was the strategy which had never been properly backtested. But I wasn’t ready to hear that yet.

The Elixir pivot

I’d been researching stream processing systems and landed on Elixir. I knew about the BEAM VM from dabbling with Erlang years earlier, and I’d heard about Elixir at conferences. It felt like a perfect fit: a runtime designed from the ground up for concurrent, fault-tolerant, always-on systems. Exactly what a trading bot needs.The paradigm shift was real. I rebuilt the system using GenServer and GenStage to parallelise indicator computation. The architecture was cleaner. The concurrency model was a genuine fit.

Looking back, parallelising those math operations was overkill at my scale. The computations were too small to benefit from it, and the overhead might have made it slower. But I learned to think in Elixir: immutable data, functional pipelines, GenServer for state, GenStage for parallelism. That rewired how I approach problems, and that was worth something.

The Elixir version never reached production. Before I got to order execution, I hit a wall that had nothing to do with concurrency: the type system. Elixir is dynamically typed. I tried to work around it with typespecs and typed_struct, but it wasn’t the same. I’m a strong-type-system developer, and without it I couldn’t trust the code the way I needed to.

That’s also when the real insight landed. The problem had never been the language or the runtime. It was the strategy. I’d been trading on repaints without backtesting. No amount of architectural elegance, performance tuning, or latency reduction fixes that. I needed a backtesting system that could simulate real market conditions based on actual trade operations, not just candlestick closes. And it needed to be fast: the quicker you get results, the more strategies and parameter combinations you can test. Hours per backtest means you stop experimenting. I hoped the alpha was in the simulation fidelity. Wishful thinking, but productive wishful thinking.

Landing on Rust

I approached Rust sceptically. I assumed it meant managing memory manually, and after years of garbage-collected languages I had no interest in that. I just wanted to form an educated opinion. A side quest while taking a break from the trading system.

Then I realised the borrow checker is the memory management. There’s no GC, but there’s no malloc and free either. The compiler handles it. That, combined with a type system that was everything I’d wanted in Elixir, made me stay.

Rust, like Elixir before it, was eye-opening. The borrow checker still bites me, though for mostly number-crunching code it doesn’t come up that often. But the contracts are explicit in a way no other language I’ve used achieves.

The bot is on hold. Before building the backtesting engine, I needed the foundation: reliable, fast technical indicators. The kind of building block any strategy engine needs. That became quantedge-ta, a streaming technical analysis library. SMA, EMA, Bollinger Bands, RSI. O(1) per bar, tested against talipp reference data, under 8 nanoseconds per tick on Apple Silicon.

A few thousand dollars and five years. The losses were the lab bill. The crate is the output.

Leave a Reply

Your email address will not be published. Required fields are marked *