DEV Community

Tristan
Tristan

Posted on

I'm building an algorithmic trading system in Python

Hello world, my name is Tristan and I'm building a multi-market algorithmic trading system (in Python, of course). Before I get into the algo itself, a quick word about me: I studied international business. But my passion since I was a kid has always been computers and everything digital. Completely self-taught there, learned the way you probably learn best in this field: by building things and failing at them (A LOT). Same story with trading. Self-taught as well, but I also learned by doing, by messing up, by reading, and by going out to ask advice from people who know far more than I do.

Before getting into the architecture, I need to lay out the system's constraints, because they explain a lot of the technical choices that follow. The algo trades spot only (you buy the asset and actually own it, unlike derivatives where you own nothing), no leverage, and no short selling (betting on an asset's price going down).

That closes some doors, obviously. But it opens plenty of others: no margin management, no margin calls, no forced liquidation risk, no overnight funding costs to model (without leverage nothing is borrowed, so there's nothing to finance). Entire classes of complexity disappear (and with them, entire classes of bugs!)

These constraints aren't negotiable and they aren't parameters. They're enforced by the architecture itself: there is no code path that can open a short position. I'll get into the why in a future post.

Now, the architecture. The system is built around a simple principle: one engine per asset class, behind a shared protocol. Concretely there's one engine per market, and for now the crypto engine is the first one built, the others will follow. Each engine knows the specifics of its market: trading hours, venue constraints, order granularity, price behaviour. A dispatcher just routes symbols to the right engine, and it holds no trading logic whatsoever. Zero. If you find a conditional in there that mentions strategy or risk, that's a bug. Above that sits a shared layer for everything market-agnostic: trade logging, client configuration, data models, protocols.

I could have written one generic engine parameterised by market. I went with separation, for a specific reason: markets don't resemble each other enough. Crypto runs 24/7, equities close and reopen with gaps, forex has its own sessions, and so on. A generic engine would have ended up riddled with if crypto: ... else: ..., and every new market would have raised the risk of breaking the previous ones (plus an unmaintainable codebase). This way, adding a market means writing a new engine that satisfies the protocol, without touching anything that already works.

Now, the life of a trade, from birth to death.

It starts with detection. Several strategies analyse the market in parallel, each looking for a different kind of setup. When one finds something, it emits a signal with a confidence score.

Then come the filters. The signal has to survive a series of checks: does the current market regime allow this strategy? Is the higher timeframe trend pointing the same way? Do we already hold a correlated position? Does the expected gain cover transaction fees? Most signals die right here, and that's intentional.

If the signal makes it through, we compute position size. It depends on capital, on the client's risk profile (which they choose themselves: conservative, aggressive, and so on), and above all on the distance to the stop-loss. A wider stop means a smaller position, so that the maximum loss stays constant regardless of the trade.

Then execution, and this is where it gets interesting.

When an order goes out, the first question to ask is: what happens if my program dies right after?

It's a mundane scenario. A network outage, a server restart, a Python crash, an update. If the position is open and the protection only lives in my code, it dies with it. The client is left holding a naked position in a moving market, with nobody around to close it.

Hence the golden rule: the stop-loss is a real order, resting at the broker, existing independently of my system. It's registered on their servers. If my program dies, it stays alive.

Sounds obvious put that way, but plenty of systems keep their stops in memory and just poll the price in a loop. As long as the program runs, it works. The day it stops at the wrong moment, the loss is no longer bounded.

And it makes execution significantly harder. You have to place the entry order, wait for confirmation that it filled, then place the protection. Between those two, there's a window where the position exists unprotected. That window has to be as short as possible, detected if it lingers, and alerted on if it doesn't close.

You also have to handle the fact that protection moves: when a trade turns profitable, the stop trails up. Every adjustment means cancelling the existing order and placing a new one. Another window. And if the cancel succeeds but the placement fails, the position sits naked with nothing having flagged it.

The result: what looked like "send an order" turned into a state machine with a ladder of degraded states, retry logic, and alerting. It's probably the most complex part of the system, and by far where I've spent the most time so far.

Another choice that looks trivial and isn't: all monetary calculations use Decimal, never float.

Open a Python interpreter and type 0.1 + 0.2. You get 0.30000000000000004. That's normal, it's how binary floating point works, and in 99% of programs it makes no difference at all.

Except when you're handling money and comparing prices against thresholds. A position whose value is computed in floats can land just above or just below an exposure limit depending on operation order. A stop-loss can trigger or not based on an error in the fifteenth decimal place.

So the rule is: anything touching an amount, a price or a quantity enters the system as Decimal and only leaves it at the boundary with the broker API. And the conversion goes through a string, never directly from a float, otherwise you inherit exactly the imprecision you were trying to avoid.

That's it for this first write-up. The system isn't finished and I won't pretend otherwise.

What's still open, roughly: full convergence between the backtest engine and the production code (two paths executing the same logic is one path too many), parameter calibration which hasn't started yet, and integrating markets beyond crypto.

And above all, the thing eating most of my time right now: making sure what the backtest measures actually corresponds to what would happen for real. That's much harder than it sounds, and it's probably what I'll write about next.

A couple of things I'd genuinely like input on:

On the window between entry fill and protection placement — how do you handle it on your side? My impression is that it's irreducible, but I could be wrong.

And for anyone who does backtesting: what's the trap that cost you the most?

Thanks for reading. Criticism welcome, including the harsh kind.

Top comments (0)