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.
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.
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.
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.
- Recovery from the source of truth. After any restart — a crash, a deploy, a reboot — the engine rebuilds its picture of open positions from the broker's records before it does anything else. The broker, not local memory, is the source of truth, so a restart neither loses a position nor books one twice. A position book it can't interpret unambiguously stops the system rather than acting on a guess.
- A circuit breaker. Trading halts automatically on a loss limit for the day, a market-data feed that keeps dropping, or an indeterminate order result. The last of these is treated as serious and stays latched until cleared by hand.
- Unattended daily login. The broker token expires every morning, so a dedicated service performs the login itself — a headless browser driving the flow with a time-based one-time password — ahead of the open, with the main process as a backstop.
- A self-scheduling supervisor. It wakes before the open, runs the session, honours weekends and the exchange holiday calendar, sends a summary at the close, and sleeps until the next trading day.
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.)
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
- Go 1.24Concurrent engine — goroutines, channels, per-worker locks
- PostgreSQLJournal & single source of truth · idempotent migrations
- WebSocketsLive market feed in, real-time dashboard out
- Kite ConnectBroker API — real orders & live market data
- Gin + PrometheusHTTP API and metrics behind the dashboard
- Docker ComposeFive services on one server · health checks, volumes
- Headless ChromeAutomated daily broker login (browser + one-time password)
- CloudflareZero-inbound tunnel + authenticated access gate
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.