0013. Time injection: Domain takes timestamps as arguments, Application reads from an injected Clock

#Context

The system needs to operate in two modes:

Until now, places in the codebase that needed an ambient timestamp called Unix.gettimeofday () directly. This is harmless in live mode and structurally wrong in backtest mode for three reasons:

  1. Non-determinism. Two backtest runs over the same input stamp events with different times, so any snapshot test or diff-against-reference comparison becomes brittle.
  2. Audit-timeline divergence. A backtest over 2023 data run in 2026 stamps every derived event with 2026, breaking correlation with the underlying bar timestamps.
  3. Mixed clocks. Three independent ACL handlers (PM, PTR, EMS kill-switch) each call Unix.gettimeofday separately for the same upstream fact, so they record three different times for the one event.

The Domain Layer separately must not read ambient time. Domain methods are pure functions of their inputs; an embedded gettimeofday call would make them non-reproducible and would defeat formal verification (Why3 proofs do not see a "current time" oracle).

#Decision

Adopt a three-rule discipline for time:

#Rule 1 — Domain takes timestamps as explicit arguments

Domain methods that need a timestamp accept it as a named parameter (~occurred_at, ~fill_ts, etc.). They never read ambient time. This is the existing convention for most of the Domain Layer; the rule makes it explicit and project-wide.

This rule is enforced by dune: the Domain Layer's library does not list unix in its (libraries …) clause, so a stray Unix.gettimeofday would fail to link.

#Rule 2 — Application Layer obtains time from a Clock

The Application Layer is the boundary where ambient time enters the system. It does so through a first-class Datetime.Clock.t value — an opaque carrier of a "read current time" thunk that returns int64 epoch seconds.

To prevent the Clock abstraction from bleeding into every workflow / handler, BC factories accept a closed-over ~now : unit -> int64 argument rather than the Clock.t itself. The composition root builds the Clock.t, derives the closure via Datetime.Clock.now, and threads it through the factory.

#Rule 3 — Composition root chooses the implementation

The composition root (bin/main.ml) instantiates exactly one clock for the lifetime of the process:

In both cases, the resulting Clock.t is converted to a now closure and passed to every BC factory that needs ambient time.

#Scope

This ADR governs Application-Layer ambient time — workflows, ACL handlers, factory wiring. It explicitly does not govern:

If a future Application-Layer caller has a genuine need for wall-clock independent of the simulated timeline (e.g., deadline-driven retry inside a workflow), it takes an extra ~wall_now : unit -> int64 injected from the composition root, distinct from ~now. We expect this to be rare.

#Bar-stream subscription details (backtest)

The composition root subscribes the virtual clock to in-memory://broker.bar-updated with its own consumer group (clock-tick), distinct from every BC's group. The handler parses the bar's candle.ts and calls Virtual_clock.set.

Ordering caveat. The in-memory bus dispatches subscribers within different groups independently. The virtual clock's tick is therefore not strictly ordered before any BC handler's processing of the same bar. In practice this is benign:

If a future scenario requires stricter ordering (e.g., a synchronous "tick before publish" guarantee), the composition root can advance the clock inline with Bus.publish bar instead of subscribing — without touching any BC code.

#Consequences

Easier:

Harder:

To watch for:

#References