Module Algostream_backtest.Engine

The backtest loop.

The authoritative clock

The engine carries its own int64 event clock, taken from the data. Every timestamp in Result.t comes from it. The Timestamp.t fields on Portfolio / Position / Trade are populated via the ?ts parameters and are advisory metadata only — no analytics path reads them. That is why the ~240 ns quantization of Timestamp.of_ns cannot affect any reported number.

Step ordering

Per market record, in this order — the ordering is contract, because changing it changes results:

  1. advance the event clock; drop and count out-of-order records
  2. update Market_view; feed bar builders and per-pair state
  3. release inbound messages whose delivery time has arrived (fills, order updates, timers)
  4. release outbound orders that have now reached the venue
  5. run one Fill_engine matching pass; book each fill into the portfolio and blotter
  6. deliver the market event to the strategy; collect its actions
  7. gate actions against risk limits, assign order ids, admit them with outbound latency
  8. mark to market, accrue financing, and sample the equity curve

A fill is booked into the portfolio before the strategy is told about it — the portfolio is the venue's view, the strategy's view is delayed by inbound latency. That asymmetry is real and is what makes a latency-sensitive strategy behave differently here than in a naive simulator.

Pairs

A Strategy.Pair subscription causes the engine to drive a Pairs.Per_pair.t inline and emit Event.Pair_snapshot. Pairs.Processor is deliberately bypassed: its Domain, SPSC queue and bus subscription are eventual-consistency machinery that would make results depend on scheduling. This mirrors what test/pairs/test_determinism.ml already does.

module Rng = Algostream_rng.Rng
type config = {
  1. initial_capital : float;
  2. account_id : string;
  3. venue : Venue.t;
  4. slippage : Slippage.model;
  5. latency : Latency.t;
  6. cost : Cost_model.config;
  7. risk_limits : Risk_limits.t option;
    (*

    None disables the pre-trade gate

    *)
  8. maker_fill : Fill_engine.maker_fill_model;
  9. stop_trigger : Fill_engine.stop_trigger_ref;
  10. equity_sample_interval_ns : int64;
    (*

    0L samples on every event

    *)
  11. pairs_config : Algostream_pairs.Config.t;
  12. bar_interval_ns : int64 option;
    (*

    emit Event.Bar at this cadence when set

    *)
  13. root_seed : int64;
  14. run_index : int;
  15. flatten_at_end : bool;
  16. max_events : int option;
}
val default_config : venue:Venue.t -> initial_capital:float -> config

Zero latency, book-walk slippage, queue-position maker fills, no risk gate, equity sampled every event. A deliberately frictionless starting point — add costs explicitly so that what you are assuming is visible in the config rather than buried in a default.

val run : (module Algostream_strategy.Strategy.S with type params = 'p) -> params:'p -> config:config -> data:Data_source.t -> Result.t

Run a strategy over historical data.

Randomness comes from two disjoint substreams of root_seed: index 2·run_index drives anything data-related, index 2·run_index + 1 drives execution noise (latency jitter). Keeping them separate means changing the execution model does not shift the price path — common random numbers, which is what makes A/B comparisons across model variants low-variance.