Brindle
Automated trading bots, and the risk limits that actually stop them
A platform that runs automated trading strategies against broker adapters on a one-second tick. The strategies were never the engineering problem. Enforcing risk limits that protect a real account is, because the cheap limits and the expensive ones can't be checked by the same mechanism.
- Built for
- Ballo Innovations
- My role
- Architecture and implementation of the execution gateway, the risk engine, and the runtime supervision loop.
- When
- 2026
- Python
- FastAPI
- Pydantic v2
- SQLAlchemy 2
- Alembic
- asyncio
- Prometheus
The longer version
The problem
The failure I was designing against is a bot that keeps trading while it loses, because the limit that would have caught it was too expensive to check on the order path and so never got checked at all.
That isn't hypothetical. An earlier version of this system accepted a
max_drawdown_pct in its configuration. It validated the value, displayed it,
and enforced it only in the pre-order gate, which is precisely where the number
it needed wasn't available. The runtime ignored it completely. The schema was
making a promise to the operator that the running system never kept.
What I built
One execution gateway that every order passes through, a pure risk function in front of it that does no I/O, and a separate supervision loop for the limits that need a broker round-trip and only make sense across a series of trades. Rejections are persisted and audited, because a rejection is an event rather than a silent no-op. When state is uncertain, whether an adapter is unhealthy or market data is stale, the system does nothing and alerts instead of guessing.
The class of bug, and the test that catches it
The drawdown gap was not a one-off. It is an instance of a class: a limit that is schema-validated, bounded and unit-tested against a synthetic snapshot can still be unreachable from the snapshot the runtime actually constructs. The tests stay green and the configuration reads as enforced either way. That's why the audit runs against the runtime rather than against the schema.
So the fix I care about isn't patching a field. It's a test that walks the risk configuration schema and asserts every limit in it is reachable from the snapshot the runtime builds, which turns a whole class of silent gap into a failing build. That's the check I'd want in any system where a configured limit and an enforced limit can drift apart, and it's why this platform isn't carrying a funded account yet.
What made it hard
The problems worth reading about. Everything else in this system was ordinary work.
- 01
Position size can be checked in microseconds from a snapshot already sitting in memory. Drawdown needs the broker's real balance, which means a network call. Put both in front of every order and you get one of two things: broker latency on every tick, or a limit that's configured and never actually checked.
- 02
So they run in two places with two different consequences. The pre-order gate rejects the order, and the runtime breaker pauses the bot. A position breach means "not this order". A drawdown breach means "no more orders until a human looks at it".
- 03
Strategies never call a broker adapter directly. An earlier version let each one size its own orders, and one promptly computed its own stake without reading the risk context, so the configured per-trade cap just didn't apply to it. A limit every caller has to remember to apply isn't a limit.
What it measures
Each figure says where it came from, so you can judge how much weight to give it. Some are measurements and some are chosen thresholds; the basis line tells you which.
- 0
- I/O calls on the pre-order path
- 4
- Runtime conditions that pause a bot
The risk engine takes an injected portfolio snapshot and mark price, and makes no network or database calls. Verifiable rather than measured, since its unit tests run with no fixtures and no database.
Consecutive risk rejections, daily loss or drawdown breach, consecutive losing trades, and allocation depletion. Each one writes its reason into the pause record.
The close calls, written up
Where a choice was genuinely arguable, I wrote it up the way a team records a design decision internally: the constraint, the options I turned down and why, and what it would cost to reverse.
- ADR-002acceptedTrading Systems08 May 2026
Two-tier risk limits
Some risk limits can be checked in microseconds from state you already hold. Others need a broker round-trip and only make sense over a series of trades. Putting both in front of every order would have meant either blocking the order path on network I/O or leaving the slow limits unenforced, so they run in two places with two different consequences: reject the order, or pause the bot.
3 options evaluated