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

#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