Module Algostream_backtest.Fill_engine

Order matching against historical data.

This is where the order types acquire behaviour. Nothing else in the tree interpreted time_in_force, evaluated a Stop trigger, or exposed only an Iceberg's display slice — the constructors existed in Order.order_type and were pattern-matched nowhere.

Maker fills

Passive fills are simulated with an explicit queue position. When a resting limit order arrives at the venue, its queue_ahead is seeded from Order_book.depth_at_price ~side ~price — the liquidity already resting at or better than our price. Every subsequent tape print at or through that price decrements it, and we only begin filling once it reaches zero.

That model is approximate in a specific, documented way: depth_at_price is cumulative at or better, so it counts better-priced orders that are not literally in our queue. Since those orders do fill ahead of us anyway, the estimate is conservative in the right direction — it makes maker fills harder, not easier. What it cannot capture is order-by-order priority within a level, which would need L3 data the ingestion layer does not capture.

An iceberg that refreshes a slice goes to the back of the queue — queue_ahead is reseeded from current depth. That is real venue behaviour and it is the main reason naive iceberg simulation overstates fill rates.

Ordering

A pass over one market record proceeds: not-yet-arrived orders are skipped (latency), stops are evaluated, marketable orders cross, then resting orders advance their queue. TIF is applied last, because IOC and FOK are decisions about what to do with whatever did not fill.

module Rng = Algostream_rng.Rng
type maker_fill_model =
  1. | Queue_position
    (*

    the default; seeds queue_ahead from book depth and drains it on tape prints

    *)
  2. | Touch_cross
    (*

    fill when the market trades through our price, ignoring queue. More optimistic, much cheaper, adequate when the strategy is not queue-sensitive

    *)
  3. | Optimistic
    (*

    fill the moment the touch reaches our price. An upper bound on achievable passive performance; useful for bracketing, dishonest as a headline

    *)
type stop_trigger_ref =
  1. | Trigger_last
  2. | Trigger_mid
  3. | Trigger_touch
    (*

    bid for sell-stops, ask for buy-stops — the conservative choice

    *)
type config = {
  1. slippage : Slippage.model;
  2. latency : Latency.t;
  3. maker_fill : maker_fill_model;
  4. stop_trigger : stop_trigger_ref;
  5. allow_partial : bool;
    (*

    when false, a partially fillable order fills fully or not at all

    *)
}
val default_config : config
type t
val create : config:config -> cost:Cost_model.t -> rng:Rng.t -> t
val admit : t -> now_ns:int64 -> Action.intent -> order_id:string -> decision_price:float -> Order.order

Accept an intent. Returns the Order.order the engine created, stamped with now_ns as its decision time; it becomes eligible to match only after the outbound latency has elapsed.

val on_market : t -> now_ns:int64 -> Data_source.record -> ctx:Slippage.market_ctx -> Event.fill list * Event.t list

Advance matching by one market record. Returns the fills that occurred and any order status changes (expiry, IOC/FOK cancellation). Both are in event time.

val cancel : t -> now_ns:int64 -> client_order_id:string -> unit

Request cancellation. Takes effect after the cancel latency, so a cancel can lose a race with a fill — which is exactly what happens on a real venue.

val working_orders : t -> Order.order list
val is_working : t -> client_order_id:string -> bool
val cancel_all : t -> now_ns:int64 -> Event.t list

Cancel everything outstanding; used at end of run.

val tca : t -> client_order_id:string -> market_vwap:float -> Execution_quality.report option

Post-trade TCA for a completed order, via the existing Order_management.Execution_quality.analyze. None if the order never existed or never filled.

type stats = {
  1. n_admitted : int;
  2. n_fills : int;
  3. n_maker_fills : int;
  4. n_taker_fills : int;
  5. n_cancelled : int;
  6. n_expired : int;
  7. n_fok_killed : int;
  8. n_ioc_remainder_cancelled : int;
  9. n_stops_triggered : int;
  10. unfilled_quantity : float;
}
val stats : t -> stats