VIZANIXTrading Software Development
Bot architectureArchitecture9 min read

Trading bot architecture: the layers you must not merge

Almost every bot that becomes unmaintainable made the same mistake — it let strategy logic reach directly for the exchange.

Vizanix engineering · about the author

ARTICLE
9 minreading time
SECTION
Bot architecture
PUBLISHED
2026-08-28
CHAPTERS
6
READ NEXT
3
LANGUAGE
written in English
An engineering breakdown, not a rewrite of the docs.
LAYERSSTRATEGYEXECUTIONRISKTRANSPORT

A trading bot is not one program. It is five concerns that happen to run in the same process, and the difference between a system you can change and one you cannot is whether those concerns can see each other.

The five layers

LayerOwnsMust not know about
TransportREST client, WebSocket, signing, rate limits, retriesStrategies, positions, P&L
Instrument registrySymbols, filters, categories, tick and lot sizesAnything time-varying
Market stateOrder books, candles, funding, sequence integrityOrders, intentions
StrategySignals: I want to be long 3 units of XExchange APIs, order IDs, HTTP
Risk & executionTurning intent into orders under constraintsWhy the strategy wanted it

The critical boundary is the fourth row. A strategy expresses intent — a desired position — and never places an order itself. Everything downstream is free to refuse, resize, defer or split that intent.

Why the strategy boundary matters

When strategy code calls the exchange directly, four things become impossible at once, and you usually discover all four on the same bad day:

  • Backtesting. You cannot replay a strategy that expects an HTTP client. Mocking the exchange gets you a test of your mock.
  • Paper trading. Same problem, live.
  • Global risk. With three strategies each placing their own orders, nothing knows the total exposure until it is already on.
  • Venue changes. A new exchange means rewriting every strategy rather than one adapter.
python
# Strategy speaks intent. It has no idea an exchange exists.
@dataclass(frozen=True)
class Intent:
    symbol: str
    target_qty: Decimal        # signed: negative is short
    stop: Decimal | None
    reason: str                # goes into the audit log verbatim

class ImpulseReversal:
    def on_bar(self, bar, state) -> Intent | None:
        ...
        return Intent(bar.symbol, -size, stop=level, reason="rsi_overbought_series_4")

# Risk and execution decide what actually happens to that intent.
def handle(intent: Intent):
    approved = risk.evaluate(intent, portfolio.snapshot())
    if approved.rejected:
        audit.log(intent, approved.reason)
        return
    executor.reconcile_to(approved.target_qty, intent.symbol)

The reason field looks like a nicety. It is the thing that lets you answer “why did the bot do that” six weeks later. Our signal engine stores the reason for every evaluation including the rejections, which is why “why are there no entries today” is a query rather than a debugging session.

Target position, not buy and sell

Express intent as a desired end state rather than an action. “Be long 3” is idempotent: if the bot restarts and re-emits it while already holding 3, the reconciler does nothing. “Buy 3” is not: the same restart buys 3 more.

This one choice removes an entire class of double-execution bugs, and it makes recovery after a disconnect trivial — reconcile to target, whatever happened during the gap.

The control plane runs beside, not inside

Operators need to start, stop, adjust and inspect. That interface must not be able to stall the trading loop. In our microstructure engine the Telegram control plane is a separate process talking over a Unix socket; the realtime loop never makes a network call to a chat API, never waits on one, and cannot be blocked by one being slow.

The general rule: anything with unpredictable latency — chat APIs, dashboards, log shipping, model training — lives outside the loop that must respond to a market event in milliseconds.

Storage is not on the hot path

Recording everything is what makes a system honest, and writing synchronously is what makes it slow. Our signal engine keeps 5.76 million evaluations from a fifteen-day window — every evaluation, not only the ones that fired — and it does that with batched writes off the evaluation path and indexes chosen for the queries actually run.

A scanner that blocks on a disk write has stopped being a scanner. Buffer, batch, and let the writer fall behind if it must; the trading decision does not depend on the row being durable yet.

What good looks like

  1. You can run the strategy layer against historical data with no network at all.
  2. You can add a second venue by writing one adapter.
  3. You can answer “what was the total exposure at 14:32” from storage.
  4. Killing the control plane does not affect trading.
  5. Restarting the process re-derives state from the exchange, not from a local file.

None of this is exotic. It is ordinary layering applied to a domain where the cost of a leaky boundary is measured in money rather than in refactoring time.

This article describes engineering practice. It is not investment advice. Vizanix develops software and does not promise trading returns.

Blog

Read next

Want this running for you?

We write about what we build. If you need it built, get in touch — scoping is free.

Brief

Get a project estimate

Four questions and your contact. No deposit required to talk — if the job is not a fit, we say so straight away.

01What do you need
02Exchange
03Market
04Strategy
05Contacts

Prefer to write directly? Telegram @vx_ceo

Discuss a system