6 Things I'd Tell Myself Before Building a Trading Bot Platform
This closes out a short series about building Freya Finance. Rather than repeat the articles, here is what I would actually say to myself at the start, ordered by how much each one cost to learn.
The common thread is uncomfortable: almost none of these failures announced themselves. They produced plausible numbers, valid-looking orders, and screens that looked correct. In a system that acts on someone's money, the bug that throws an exception is the friendly kind.
Key Takeaways
- Third-party APIs fail quietly. An HTTP 200 carrying less data than you asked for is the failure mode to design against, not the 500.
- Test the simulator before you test anything it simulates. A broken simulator returns numbers that look exactly like working ones.
- Report what you could not simulate. A result with a stated gap is worth more than a clean-looking result that quietly dropped a condition.
- Precision is a domain, not a utility function. Step sizes, tick sizes and minimums differ per pair, per exchange and per market type.
- Choose constraints early. Deciding never to hold user funds deleted an entire category of problems instead of solving them.
- In a money interface, audit the gestures the platform treats as edits. A mouse wheel over a focused number input is one of them.
1. Assume every API succeeds incorrectly sometimes
The most expensive assumption I made was that a failed request would look like a failure.
Exchanges under load return HTTP 200 with fewer candles than requested, no error field, no warning. One of our deep backtests asked for roughly 536,000 candles and received 58,000. The same request an hour later returned the full set. Nothing in the response distinguished the two.
If you are not checking, the engine simulates a strategy over a fraction of the intended history and reports a confident number. That is worse than a crash, because a crash is honest.
Two rules came out of it: validate the shape of a successful response, not just its status code, and distinguish transient failures from permanent ones before deciding what to do. They look identical at the moment they happen and deserve opposite responses.
Full story: what I underestimated
2. Test the simulator, not the strategy
Every instinct pushes you to test strategy logic, because that is the interesting part. The trap is that a broken simulator produces output indistinguishable from a working one. A run that silently processed a tenth of the data still returns a number. A run whose exits never evaluated returns a suspiciously flat one.
The shift that fixed this was asserting on things the strategy cannot change: that the candles processed match the candles fetched, that a run over a known period actually spans it, that a configuration guaranteed to trigger produces trades.
When those invariants hold, a strange result is a real finding. When they do not, the result is noise wearing a suit.
3. Report what you could not simulate
This one is a design choice rather than a bug, and I think it is the most underappreciated.
Some conditions cannot be replayed. An external webhook signal has no historical record. The tempting move is to skip it and evaluate the rest, which makes the output look complete.
Look at what that does to a strategy requiring A AND B. If B cannot be simulated, A decides alone, so the simulated bot enters where the real one would have stayed out. The error is not random: dropping a condition under AND logic can only produce more trades than reality, so the bias points at a better-looking result every single time.
We kept running the simulation, but the result carries an explicit warning naming what was skipped. A simulator that cannot fail loudly will fail quietly, and quiet failure in something that produces numbers is the worst failure mode there is.
4. A smooth equity curve is a question, not an answer
Related, and worth separating out because it fools humans rather than code.
A DCA grid without a stop loss almost never realises a loss. It holds and adds. So closed trades are nearly all winners, win rate looks excellent, and the equity curve is a tidy staircase.
The risk did not go anywhere. It was deferred, and the deferral has a hard limit: the capital left for further safety orders. The useful question is not "what was the drawdown" but "if price keeps going against this, at what point does the strategy run out of room?"
Drawdown itself needs the same scepticism. It is measured against allocated capital at its peak, so the same 200 USDT loss shows as 40% on a 500 USDT allocation and 4% on 5,000. Two bots trading identically can display wildly different numbers.
Full story: understanding drawdown
5. Precision is a domain, not a helper
I filed "round quantities to valid steps" as one utility function. It is a category of work, and it hides in a place where mistakes cost money.
Floating point starts it: scaling 0.29 by a step of 0.01 yields 28.999999999999996, which floors one full step too low, silently, toward a smaller order.
Then the exchanges disagree about the rules themselves. One checks order value in USDT, one checks contract count with no value check at all, one checks both. Our worst bug in this area came from inventing a minimum that one venue does not have, then rejecting valid orders against it. The tell, in hindsight, was that our version of the constraint depended on price while the real one did not.
Anywhere you write a single constant for something the exchange defines per pair, you are writing a bug with a delay fuse.
Full story: floating point will cost you money
6. Choose your constraints early
The last one is not a mistake. It is a decision whose consequences I underestimated in a good way.
Freya never holds user trading funds. Keys carry read and trade permission, never withdrawal, and a key that arrives with withdrawal permission is refused rather than accepted and ignored. That distinction matters more than it sounds: "we will not withdraw" is a claim about our behaviour that a user cannot verify, while "the key cannot withdraw" is enforced by the exchange and checkable by the user in their own settings.
I expected this to be a marketing point. It turned out to be architectural, and it made the system smaller: no custody ledger, no reconciliation, no withdrawal queue, no cold storage policy for trading capital. An entire class of the hardest problems in financial software is absent because we never took on the thing that creates them.
What replaces it is narrower and sharper: the system has to be correct in real time, because it can never undo anything. An order filled on someone else's exchange account is final.
Full story: designing non-custodial trading
The one I did not expect to need
A bonus, because it surprised me most: in a money interface, the front end needs the same paranoia as the engine.
A focused <input type="number"> steps its value when you scroll the wheel over it. On a quantity picker that is an annoyance. On leverage, order size and price it silently rewrites what the user chose, and the form still validates. Nothing about that bug is visible in a screenshot.
Full story: the mouse wheel is editing your number inputs
If I compressed all of it into one line
Build for the failures that do not announce themselves. The ones that throw will find you on their own.
