Architecture overview
#The whole system in one picture
╔═══════════════╗ ╔════════════╗ ╔════════════════╗
║ Sources ║ ║ Pure core ║ ║ Effects ║
╟───────────────╢ ╟────────────╢ ╟────────────────╢
║ list (BT) ║ ──► ║ Stream ║ ──► ║ Portfolio acc. ║ (Backtest)
║ Eio.Stream(L) ║ ──► ║ pipeline ║ ──► ║ Broker.place ║ (Live)
╚═══════════════╝ ╚════════════╝ ╚════════════════╝
scan_map /
fold_left /
iter
over Seq.t
- Sources and effects differ between Backtest and Live; the pure core is the same code in both.
-
Domain (
Stream,Step,Pipeline,Backtest,Strategy) knows nothing about Eio or brokers. -
Infrastructure (
Eio_stream, Finam/BCS/Paper adapters) implements sources and effects. -
Application (
Live_engine) is a thin wrapper tying a source to the pipeline to an effect.
#Layering
The project follows hexagonal architecture (ports & adapters): pure domain logic in the center, orchestration around it, infrastructure at the edges. Directory layout mirrors the layers:
lib/
domain/ ← pure, no IO, no external deps beyond core
core/ ← Instrument, Candle, Order, Signal, Decimal
indicators/ ← SMA, EMA, RSI, MACD, …
strategies/ ← SMA_Crossover, RSI_MeanReversion, …
engine/ ← Portfolio, Risk, Step, Pipeline, Backtest
stream/ ← functional streams on Seq.t
ml/ ← logistic regression (offline training)
application/ ← orchestrates the domain via ports
broker/ ← Broker.S port + existential Broker.client
live_engine/ ← streams bars → pipeline → broker orders
infrastructure/ ← adapters implementing ports, touching IO
acl/ ← external broker translators
finam/
bcs/
alor/
synthetic/
paper/ ← Paper_broker decorator (simulated fills)
inbound/http/ ← HTTP API + SSE stream registry
websocket/ ← shared WS primitives (frame codec, Resilient)
eio_stream/ ← Eio.Stream → Stream.t adapter
http_transport/ ← cohttp-eio wrapper
log/ ← wrapper over Logs library
#The dependency rule
Arrows point inward — nothing in domain/ imports from
application/ or infrastructure/. This is the anti-corruption
rule that keeps domain pure and testable. The compiler enforces it
through dune library dependencies and .mli files; see
ADR 0002: .mli guardrails.
┌─────────────────────────────────────┐
│ Domain (pure) │ ← no IO, no external deps
│ core / engine / strategies / │
│ indicators / stream / ml │
└─────────────────────────────────────┘
▲
│ (application imports domain)
│
┌─────────────────────────────────────┐
│ Application │ ← orchestrates via ports
│ broker (port Broker.S) │
│ live_engine │
└─────────────────────────────────────┘
▲
│ (infra implements ports)
│
┌─────────────────────────────────────┐
│ Infrastructure │ ← IO, adapters
│ acl/finam, acl/bcs, acl/alor, │
│ acl/synthetic │
│ paper, inbound/http, eio_stream │
│ websocket, http_transport, log │
└─────────────────────────────────────┘
Broker.S is the key port: it lives in Application
(lib/application/broker/) and declares what the engine needs
from a broker. Infrastructure adapters (Finam, BCS, Alor, Paper,
Synthetic) implement it. The engine programs against the
existential Broker.client without naming a concrete adapter,
so swapping brokers is a one-line wiring change.
#The core abstraction: Broker.S
Everything the system needs from an external broker is captured in a single OCaml module type:
(* lib/application/broker/broker.ml *)
module type S = sig
type t
val name : string
val bars : t -> n:int -> instrument:Instrument.t ->
timeframe:Timeframe.t -> Candle.t list
val venues : t -> Mic.t list
val place_order : t -> instrument:Instrument.t -> side:Side.t ->
quantity:Decimal.t -> kind:Order.kind ->
tif:Order.time_in_force ->
client_order_id:string -> Order.t
val get_orders : t -> Order.t list
val get_order : t -> client_order_id:string -> Order.t
val cancel_order: t -> client_order_id:string -> Order.t
end
type client = E : (module S with type t = 't) * 't -> client
An existential wrapper Broker.client hides the concrete adapter
from callers. Finam, BCS, Alor, Synthetic and Paper all implement this
same port. Adding a new broker means writing one .ml file that
satisfies S — no other change is needed upstream.
#Data flow: bar → order
┌─────────────────┐
WS upstream (Finam/BCS/Alor/Synth.) │ HTTP /api/stream│
▼ │ (SSE) │
┌────────────────┐ └────────▲────────┘
│ Stream │◀── fan-out ──┐ │
│ registry │ │ │
└────────────────┘ │ │
│ │ │
▼ ▼ │
Eio.Stream ──────► Live_engine.run fiber
(of candles) │
▼
Stream.of_eio ──► Pipeline.run (Step) ──► Broker.place_order
(pull) (pure transducer) │
▼
Paper / Finam / BCS / Alor
│
fill event
│
▼
Live_engine.on_fill_event
│
▼
Portfolio.commit_fill
A single candle arriving from the WS bridge flows through an
Eio.Stream, crosses into the pure domain via
Pipe.Eio_stream.of_eio_stream,
drives the shared state machine
(Engine.Pipeline), emits an intent event
that the engine translates to a broker order, and eventually
returns as a fill event that updates the
reservation ledger.
The same Pipeline.run function is driven by Backtest.run over
a historical Candle.t list — so paper and backtest P&L agree
bit-for-bit on identical inputs. See
testing.md for the differential test that
enforces this invariant.
#Layering discipline in dune
Each layer is a separate library with an explicit libraries
clause; the compiler rejects upward imports:
lib/domain/engine/dune:
(libraries core indicators strategies stream)
lib/application/live_engine/dune:
(libraries core broker strategies engine log stream eio_stream eio)
lib/infrastructure/paper/dune:
(libraries core broker engine)
Domain has no eio dependency and no broker-specific code. If
someone tried to open Eio inside lib/domain/engine/step.ml the
build would fail.
#See also
- Domain model — the types that flow through the system.
- State machine — how bars become intents.
- Reservations — how intents become ledger entries.
- Live engine — the orchestration layer.
-
Transport supervisor — how broker
ACL adapters keep
Broker.eventflowing across WS disconnects by falling back to REST polling transparently. - WebSocket protocol layer — reader / consumer split that isolates RFC 6455 heartbeats from downstream-handler latency.
- Hexagonal ADR — why this layering over alternatives.