I worked on TrendHound as a full-stack developer for a startup in London. It is a no-code platform where people build a trading strategy from indicators, backtest it and run it live. It is someone else's company, so I will not talk about its users, its numbers or its code. The architecture idea at its centre is general, though, and this post is about that idea.
The problem
Every exchange has its own way of doing everything. They differ in how you open a stream, what a message looks like, how a trading pair is spelled, what unit a timestamp is in, how fast you may ask for things and what happens when you ask too fast. Their connections also drop, each in its own style.
A platform that supports several exchanges has two choices. It can let those differences spread through the code, so every feature knows about every exchange. Or it can stop them at the edge.
The first choice works for the first exchange. By the third, the strategy engine is full of if (exchange === ...) and every new feature has to be written and tested once per exchange.
What we did
Each exchange gets an adapter. An adapter owns everything about that one exchange:
- opening its connection and reconnecting when it drops
- respecting its rate limits
- translating its messages and symbols
- turning its failures into a small set of errors the rest of the system understands
Nothing outside an adapter knows what the exchange's raw messages look like. What comes out of an adapter is one internal model: the same candle, the same trade, the same order shape, whichever exchange it came from.
Here is a sketch of the shape in TypeScript. It is an illustration, not the platform's code (the real backend is Go microservices):
// The only things the rest of the system ever sees.
type Candle = {
symbol: string; // our spelling, e.g. "BTC/USDT"
openTime: number; // always milliseconds, always UTC
open: number; high: number; low: number; close: number;
volume: number;
closed: boolean; // a candle still forming is not a finished one
};
interface MarketAdapter {
readonly id: string;
candles(symbol: string, from: number, to: number): AsyncIterable<Candle>; // history
subscribe(symbol: string, onCandle: (c: Candle) => void): () => void; // live
}Behind MarketAdapter each exchange does its own thing. One adapter might reconnect with a backoff, another might have to subscribe to every symbol again after a drop, another might pull its history in pages because of a rate limit. The strategy engine sees none of it.
Why live and backtest benefit most
The part I care about is that history and live data come out of the same door. A backtest reads candles through candles(), live trading reads them through subscribe(), and both produce Candle. The strategy code that decides what to do with a candle is the same code in both cases.
That matters because of the failure it prevents. Suppose backtests used one data path and live trading another. Sooner or later the two disagree. A result that looked good in the backtest does not repeat live, and you cannot tell whether the strategy is bad or the data is different. You end up debugging two systems to find out which one is lying.
With one model that question has fewer answers. If live and backtest disagree on the same input, the bug is in one place.
What the pattern costs
I do not want to oversell it.
An adapter is real work. You have to learn an exchange's quirks and write them down in code, and the quirks do not announce themselves. The internal model is also a promise. If it is too small, you lose information some exchange offers. If it is too big, every adapter has to fake fields it does not have. Deciding what is in Candle takes more arguing than writing it.
There is also a pull to leak. The easiest fix for a one-off problem is a small exchange check in the engine. Every one of those is a crack, and the whole point of the design is that there are none.
The lesson
Put the mess at the edge and make the edge pay for it. The rest of the system should be written against the model you chose, not against the services you happen to call today.
If you are building something that reads from several outside services, ask early which parts of your code should be allowed to know their names. The answer should be a short list.