What I Underestimated Building a No-Code Trading Bot Platform
The pitch for a no-code trading bot is simple: you describe a strategy in a form, and software runs it for you. The hard part is not the form. It is that every simplification you offer the user has to be paid for somewhere in the system, usually in a place you did not expect.
These are three things I got wrong about that, and what each one actually taught us. They are ordered by how badly I misjudged them.
Key Takeaways
- Exchange APIs fail quietly more often than they fail loudly. An HTTP 200 with less data than you asked for is the failure mode you have to design against.
- Never trust a backtest result until you have tested the simulator itself. A clean-looking equity curve can come from a broken run.
- Floating point arithmetic on order quantities produces off-by-one-step errors that only appear on specific pairs.
- Deciding never to hold user funds removes an entire class of problems and creates a smaller, sharper one: your system must be correct without ever being able to undo anything.
- Every convenience you offer the user becomes a constraint somewhere in the engine. That trade is usually worth making, but it is never free.
1. I assumed a failed request would look like a failure
This is the one that cost the most time, and it is the one I would warn any developer about first.
We fetch historical candles from exchanges to run backtests. The naive mental model is that a request either succeeds or errors: you get your data, or you get a non-200 and handle it. Real exchange APIs do not behave that way under load. They return HTTP 200 with fewer candles than you asked for, and no indication that anything went wrong.
Here is a real case from our logs, and the numbers are exact. A long-period backtest requested roughly 536,000 candles. At 16:34 it received 58,000. Status 200. No error field. No warning. The same request, unchanged, at 17:54 received the full 535,000.
Think about what that means if you are not checking. The engine happily simulates a strategy over eleven percent of the intended history, produces a plausible-looking equity curve, and reports a result. The user reads a number that is wrong in a way nothing in the output reveals. That is worse than a crash. A crash is honest.
The fix is conceptually boring and was easy to skip: after fetching, verify that the data you received actually covers the period you asked for, and treat a shortfall as a failure even though the transport said everything was fine. Coverage is a property you have to assert, not one you can assume.
There was a second-order lesson hiding behind the first. Once we started detecting truncation, the obvious move was to fail the run. But the numbers above show these truncations are transient: the identical request an hour later returned complete data. Failing outright would have burned the user's quota for what amounts to a temporary hiccup on the exchange side. So the correct behaviour is to retry once before failing, which meant reworking the failure path so that a retryable condition does not get permanently marked as a failure on the way out.
Two rules came out of this, and I would take both to any project that talks to a third-party API:
- Validate the shape of a successful response, not just its status code. The status code tells you the request completed. It does not tell you the response is what you asked for.
- Distinguish transient failures from permanent ones before you decide what to do. They look identical at the moment they happen and they deserve opposite responses.
2. I assumed a backtest result was a result
Related, but a distinct mistake, and it goes deeper than the truncation problem.
When you build a simulator, you spend your testing effort on strategies. Does the DCA logic behave correctly? Do exits fire where they should? That is the interesting question, so it is where attention naturally goes.
The problem is that a broken simulator produces output that looks exactly like a working one. A run that silently processed a fraction of the data still yields a number. A run whose exit conditions never evaluated still yields a number, and a suspiciously flat one. We have seen results come back at exactly +0.00% with zero trades, which is the loudest possible signal that something is wrong with the machinery, and the easiest one to misread as "this strategy does nothing."
The shift that fixed this was changing what we test. Test the simulator, not the strategy. Concretely, that means asserting on things the strategy should not be able to change: that the number of candles processed matches the number fetched, that a run over a known period actually spans that period, that a configuration guaranteed to trigger produces trades. When those invariants hold, a strange-looking result is a real finding about the strategy. When they do not, the result is noise wearing a suit.
The general form of this, for anything that simulates: a simulator that cannot fail loudly will fail quietly, and quiet failure in a system that produces numbers is the worst failure mode there is. People act on numbers. They do not act on stack traces.
3. I underestimated how much precision work order sizing really is
This one is not intellectually hard. It is just much larger than it looks, and it hides in a place where mistakes are expensive.
Exchanges do not accept arbitrary quantities. Each trading pair has a step size for quantity and a tick size for price, and orders that do not align get rejected. So every quantity the system computes has to be floored to a valid step before it goes out. That sounds like one utility function.
Then floating point arrives. Consider flooring 0.29 to a step of 0.01. Scale it up and IEEE-754 gives you 28.999999999999996, which floors to 28, which scales back to 0.28. The value was already step-aligned. The arithmetic moved it one full step down, and it did so silently, in the direction of a smaller order. Fixing it takes an epsilon during the scaling step, small enough that a genuinely-below value like 0.289999 still floors down correctly, large enough to absorb the representation noise.
That is a one-line fix that took real debugging to find, because the symptom is not an exception. It is an order that is slightly smaller than intended, on some pairs and not others, depending on where the step size falls relative to binary representation.
The broader point, and the reason I list this as something I underestimated: precision in a trading system is not a utility function, it is a category of work. Step sizes and tick sizes differ per pair, per exchange, and between spot and futures. Minimum order values are not one number you can look up and hardcode. They come from the exchange per symbol, and on some venues they depend on the current price. Any time you find yourself writing a single constant for something the exchange defines per pair, you are writing a bug with a delay fuse.
We surface this to users rather than hiding it, because the alternative is an order that gets rejected for reasons they cannot see.
4. The constraint I did not expect to like
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. Bots trade through exchange API keys with permission to read and trade, and never to withdraw. Money stays in the user's exchange account throughout.
I expected this to be a marketing point. It turned out to be an architectural one, and it made the system smaller. There is no custody ledger, no internal balance to reconcile against an external one, no withdrawal queue, no cold-wallet policy for trading capital. An entire category of the hardest problems in financial software is simply absent, because we never took on the thing that creates them.
What replaces it is narrower and sharper: your system has to be correct in real time, because it cannot undo anything. With custody you can, in principle, reverse an internal transfer. Placing an order on someone else's exchange account is final the moment it fills. That pushes all of the engineering weight onto correctness before the request goes out rather than reconciliation after, which is a trade I would make again.
What I would tell myself at the start
- Assume every third-party API succeeds incorrectly sometimes, and design detection for it before you need it.
- Write invariant tests for your simulator before you write assertions about strategies. The strategy tests are worthless until the simulator ones pass.
- Treat precision as a domain, not a helper.
- Choose your constraints early. The ones you accept up front are much cheaper than the ones you discover later.
None of these were exotic problems. They were ordinary engineering problems that I filed as "small" during planning, which is exactly how they got expensive.
