← Case studies

Chapter 2 — Architecture Design

Why event-driven. How the layers connect. The decision to share one strategy codebase across backtest and live.

March 23, 2026

The First Design Question

Before writing a single line of trading logic, I had to answer a more fundamental question:

How should the system be structured?

Get this wrong and every layer built on top inherits the problem. Get it right and the system becomes easy to extend, test, and reason about.

Three decisions shaped everything: event-driven communication, strict layer separation, and a shared codebase for backtest and live.


Why Event-Driven

KaizEn is built around an event bus.

Every meaningful thing that happens — a candle closes, a fill arrives, a position changes state — becomes an event. Modules publish events. Other modules subscribe to them and react.

This maps naturally to how markets actually work.

A new candle doesn't arrive on a schedule you control. A fill from the exchange doesn't wait for you to ask. The world pushes data at you, and your system needs to respond — not poll, not wait, not block.

Event-driven also means modules stay decoupled. The data layer doesn't know the strategy layer exists. The execution layer doesn't care how signals are generated. Each module does one thing and communicates through a shared contract — the event.

This makes the system easier to test in isolation, easier to swap components, and easier to reason about when something goes wrong.


The Layers

KaizEn is organised into six distinct layers. Each one has a single responsibility and a defined interface.

Data Layer — Ingests candles from Binance via WebSocket, aggregates them across timeframes (1m → 5m → 1h → 4h → 1d), computes features, and stores everything to the database. Nothing else.

Research Layer — Runs backtests deterministically. Given a dataset, a config, and a strategy, it always produces the same result. Every run emits an artifact bundle: metrics, equity curve, trade list, and the exact config used.

Strategy Layer — Reads features, detects the current market regime, and decides whether to act. If conditions are right, it emits a Trade Intent — not an order, just an intent. The strategy never touches the exchange directly.

Execution Layer — Receives Trade Intents and converts them into real orders. It manages the full lifecycle: placing, tracking partial fills, handling cancellations, and reconciling state against the exchange on startup.

AI Layer — An optional advisor. It can generate a signal score that the strategy layer considers — but it cannot place orders, cannot control sizing, and cannot bypass risk checks. If the model drifts from its expected behaviour, the system falls back to rule-based logic automatically.

Monitoring Layer — Tracks equity, drawdown, order latency, error rates, and emits alerts when thresholds are crossed.

The flow is one-directional:

Data → Strategy (+ AI as advisor) → Execution → Monitoring
         ↑
      Research (offline validation)

AI as Advisor, Not Trader

The AI layer deserves a specific note.

It would be tempting to let a model drive the whole thing — feed it data, let it decide when to buy and sell.

I chose not to.

The reason is accountability. When a model makes a bad call, I need to understand why, reproduce it, and fix it. A model that controls execution directly makes that much harder. It also bypasses the risk controls that exist specifically to protect capital.

So in KaizEn, AI has a clearly bounded role: it produces a score. The strategy layer decides what to do with that score — and all the same risk rules still apply regardless.

AI is an advisor. The strategy is the decision-maker. The execution layer is the gatekeeper.


One Codebase for Backtest and Live

This was one of the most important decisions in the entire project.

The strategy code that runs in backtests is the exact same code that runs live. No translation layer. No "research version" and "production version". One implementation.

This matters because the moment you maintain two versions, they drift. A bug fix in one doesn't make it to the other. A behaviour that looked fine in backtesting turns out to work differently in production because the code paths diverged.

To make this work, the strategy is environment-agnostic. It receives candles and features as input. It returns a Trade Intent as output. It has no knowledge of whether it's running against historical data or a live WebSocket stream.

The backtest engine and the live engine are just different hosts for the same strategy logic.


What This Enables

This architecture creates a system where:

  • Each layer can be tested in complete isolation
  • A new strategy only needs to implement one interface
  • Bugs in live can be reproduced exactly in backtest
  • The AI component can be swapped or disabled without touching anything else
  • Adding a new symbol or timeframe is a configuration change, not a rewrite

The architecture isn't the most minimal possible. But every decision has a reason, and the reasons all come back to the same principle: build something you can trust.