Kairos · Algorithmic Trading · Go

An automated trading system, built end to end in Go

Kairos is an automated algorithmic trading system I designed and built on my own with heavy lifting from AI coding agents. It reads live market data, decides when to act, places and manages real orders through a broker, and runs itself through the trading day without supervision. This case study is about the architecture behind that — the concurrency model, the interfaces, the failure handling, and the operational tooling that let a system handle money on its own. The trading strategy itself is out of scope here; the engineering is the point.

Role
Solo — architecture, engine, execution, infrastructure & dashboard
Stack
Go · PostgreSQL · Docker · WebSockets · Kite Connect · Gin
Scope
~52k lines of Go · five services · running live
Type
Algorithmic trading · Personal product
~52klines of Go
5containerised services
24/5unattended, self-scheduling

What it does, in plain terms

Left to a person, running a strategy means sitting at a screen all day: watching prices, placing an order when the setup appears, managing the position, and closing it when the rules say so. Kairos does that end to end, every trading day, on its own. It starts before the market opens, connects to the broker, works the session, and shuts down after the close. If the process dies mid-session, it comes back and resumes from the real state of the account. The hard part was never the trading logic — it was building something trustworthy enough to handle real money while no one is looking.

Architecture — independent workers over shared services

Kairos can run several strategies at once, each on a different market, inside a single process. Each one runs as an independent worker with its own state and its own path to the broker — in Go terms, its own goroutine guarded by its own lock. The parts that are expensive or unsafe to duplicate are shared as singletons: one WebSocket connection to the market-data feed, one database, one risk manager, one alerting channel. A small router reads the incoming tick stream and dispatches each update to the worker responsible for that instrument.

The reason for that shape is fault isolation. Because every worker owns its execution path, a slow or stuck order in one market cannot stall the others. Within a worker, actions are serialized so two decisions never interleave; across workers, they proceed in parallel. The concurrency model is doing load-bearing work here — isolation where correctness demands it, and sharing only where it is safe.

Market-data feed one WebSocket · all markets Router · by market Worker · market A candles → strategy → decision own goroutine · own lock Worker · market B candles → strategy → decision own goroutine · own lock Order state machine → broker idle → opening → open Order state machine → broker idle → opening → open SHARED SERVICES risk · circuit breaker database alerts · email/chat
The static structure: each market runs as its own isolated worker; the data feed, database, risk manager and alerts are shared. Strategy and execution are swappable pieces behind clean interfaces.

Everything behind an interface

The system is organised around a handful of interfaces, so its parts stay swappable and independently testable. A strategy is defined purely as bars in, a decision out — a direction to hold and a protective stop. It knows nothing about brokers, instruments, or order types. Execution sits behind its own interface and can carry out the same decision in more than one way, chosen by configuration; the strategy neither knows nor needs to. The broker is an interface too, with a live implementation and a simulated one that produces paper fills over the identical code path above it.

Holding a firm line between what to do and how to do it is what keeps a system like this from ossifying as it grows. Adding a strategy is essentially a new file behind the existing contract; changing how orders reach the market touches none of the strategies. Where a strategy needs something beyond the base contract, it advertises that through an optional capability interface the runtime discovers by type assertion — so nothing is ever coupled to a concrete type.

The path of a single decision

It helps to follow one decision from tick to fill, because most of the system's guarantees live along that path. A supervisor process opens the day: it refreshes the broker's access token (these expire every morning), then starts a session. The session builds one worker per configured market, subscribes every instrument on a single WebSocket, and — before it trades anything — reconciles against the broker's own position book so its in-memory view matches reality. From there the loop is continuous: ticks arrive, the router hands each to its worker, the worker aggregates them into candles, and on each candle's close it runs the strategy. Between candles, every tick still drives the stop-loss check, so protection is never a full bar behind. When the strategy calls for a change, the worker hands an Intent to its executor — and that hand-off is where care matters most.

Supervisor · self-scheduling daemon wakes before the open · refreshes the broker token · skips weekends & holidays Session start build one worker per market · subscribe every instrument on a single WebSocket reconcile open positions from the broker's book — halt if it is ambiguous Market-data feed → tick router one WebSocket · each tick routed to its market's worker by instrument token Per-market worker · ×N, isolated ticks → candles; on each close, run the strategy → Intent (direction + stop) every tick also runs the intrabar stop-loss check actions serialized by the worker's own lock; workers run in parallel Execution · order state machine idle → opening → open → closing rejects duplicates while busy · halts on an indeterminate fill, never guesses Broker seam · one interface, two implementations live orders | simulated paper fills — identical code path above the seam realized P&L computed from the actual fills that come back SHARED SERVICES · written on every close risk · breaker Postgres journal alerts · email/chat Prometheus metrics Housekeeping & close expiry rollover in place · end-of-day summary · supervisor reschedules NEXT TRADING DAY
The runtime flow, top to bottom: a self-scheduling supervisor brings up the session, the live loop turns ticks into decisions into orders, and every close is written through to risk, the journal, alerts and metrics before the day rolls over.

An order is modelled as a small state machine — idle → opening → open → closing. That single mechanism is the guarantee against the worst failure mode in trading software: firing the same order twice. A second attempt while a position is opening is simply rejected. If the broker's response is ambiguous — filled, or not? — the executor stops and raises an alarm rather than assume an answer, and that alarm latches until a human clears it. Profit and loss is always derived from the real fills the broker returns, never from an estimate, so the recorded result and the account can't quietly drift apart.

Recovery and safety

Most of the substantive engineering is in the unhappy paths.

Observability and control

A system that handles money unattended has to be observable and stoppable. Kairos exposes a real-time operations dashboard — a Go (Gin) HTTP API with a WebSocket feed pushing live updates to the browser, plus a Prometheus metrics endpoint for monitoring. Everything the dashboard shows is read back from the trade journal in Postgres, so it reflects the recorded truth of what happened rather than a hopeful in-memory view. Alongside the read-only pages it carries the controls that matter operationally: a one-click flatten of any open position, per-strategy start/stop, and a restart that refuses to run unless the book is already flat. (Figures in the screenshots below are blurred; the run shown is on paper.)

The dashboard home view: status cards for the session, data feed and safety switch, a summary of results, an open-positions table and a recent-orders panel, in a dark UI.
The home view — session, data-feed and safety-switch status up top, with any open positions and recent orders below.
The performance view: summary stat cards above an equity curve, a year-long activity heatmap, and a small results table.
Performance — an equity curve, a year-long activity heatmap and summary stats, all computed from the trade journal.
The trades view: a history table listing each closed trade with its entry, exit, result, duration and the reason it closed.
Every closed trade, read straight from the journal — entry, exit, result, and why it closed.
The controls view: a one-click Exit button for an open position, start/stop toggles, and a restart control that is blocked while a position is open.
Controls — a one-click flatten for open positions, per-strategy start/stop, and a restart that refuses to run unless everything is already closed.

How it's shipped

The system runs as five Docker containers on a single modest server, orchestrated with Compose: Postgres, the always-on engine, the token-refresh service, an optional paper-shadow runner, and a tunnel for the dashboard. Nothing is exposed to the public internet — the dashboard is reachable only through an outbound Cloudflare tunnel behind an authenticated access gate, and the server's firewall is closed to everything except SSH. Configuration lives in plain text under version control, and the database schema is managed as ordered, idempotent migrations that are safe to re-run. Health checks and named volumes keep the stack recoverable across restarts.

Tested against history first

Nothing reaches the live account untested. A separate backtesting engine replays years of historical market data through the exact same strategy code that runs in production, which keeps the tested behaviour and the deployed behaviour from diverging — there is no second implementation to drift out of sync. The same strategy can also run in a lightweight shadow mode beside the live engine, trading only on paper, as a continuous real-world check.

Stack

The measure of a system like this is simple: does it keep behaving correctly on a live account with nobody watching? That reliability comes from a few principles held consistently — treat the broker as the source of truth and always recover to it, make every order safe against duplication and ambiguity, and keep firm boundaries between the strategy, the execution, and the infrastructure around them.

← Selected work