Skip to content

The Historical Surface ​

Context ​

The same analytics need to run on three ingest sources behind one analytic pipeline:

  1. Live — the upstream live feed via Client::connect.
  2. Historical — internally downloaded from the upstream historical service for a given date range. Institutional users replay last week's option flow before opening their book on Monday; quants regression-test model changes against a year of intraday Greeks; risk runs end-of-day reports against a synthetic intraday volatility surface.
  3. User-supplied datasets — pre-fetched trade + quote ticks staged on the engine link. Required for back-tests where the user already holds the data, for replay against vendor-recorded fills, and for offline correctness regressions.

The analytic outputs stay bit-identical across modes. An analytic that fires on a live upstream quote and the same analytic fired on the same (date, ms_of_day, bid, ask, ...) replayed from the upstream historical service or from a recorded dataset MUST produce identical output.

Surface ​

1. Client shape — mode namespaces on Client ​

The thin client exposes three mode namespaces. Each returns the SAME per-analytic builder accessors; the underlying engine call differs by mode:

rust
impl Client {
    pub fn live(&self) -> Scope<'_>;

    #[cfg(feature = "historical-mode")]
    pub fn historical(&self, start: i32, end: i32) -> Scope<'_>;

    #[cfg(feature = "replay-mode")]
    pub fn replay(&self) -> Scope<'_>;
}

impl<'a> Scope<'a> {
    pub fn volatility(&self) -> AnalyticBuilder<'a, VolatilityRequest>;
    pub fn greeks(&self)     -> AnalyticBuilder<'a, GreeksRequest>;
    pub fn gex(&self)        -> AnalyticBuilder<'a, GexRequest>;
    // … one per-analytic accessor …
}

The historical and replay namespaces are gated behind the matching Cargo features (historical-mode / replay-mode). They remain preview while the C-ABI staging entries land.

2. Terminal verb — .on_event in every mode ​

rust
use kairos::GreeksRow;

// Live — unbounded; the callback fires until you unsubscribe.
client.live().greeks(["QQQ"])
    .on_event(|row: &GreeksRow| { /* … */ })?;

// Historical — bounded over the `YYYYMMDD` date range; the callback
// fires once per row, then the stream ends.
client
    .historical(20251103, 20251107)
    .greeks(["QQQ"])
    .on_event(|row: &GreeksRow| { /* … */ })?;

// Replay — bounded over the engine-staged dataset.
client.replay().greeks(["QQQ"])
    .on_event(|row: &GreeksRow| { /* … */ })?;

start / end are YYYYMMDD-encoded i32s per the engine's integer-time convention. .on_event is the terminal verb in every mode; it returns an EventSubscription handle. The only difference is lifetime — on the bounded historical / replay namespaces the stream ends after the last row and the handle goes inactive (sub.is_active() returns false); a live subscription runs until you unsubscribe().

3. Synthetic watermark — derived from tick event-time ​

Live mode advances watermarks on a fixed interval. Historical mode replaces that interval with a tick-coupled advance:

  • The historical orchestrator downloads one (contract, date) batch at a time.
  • It walks the tick stream in source order and feeds each quote / trade event into the engine.
  • Every N ticks (default 256) and at every source-data minute boundary the engine advances the watermark to the most recent tick's (date, ms_of_day).
  • At the end of each unit the engine emits a final watermark at (date, end_of_session_ms) to flush any window still open at the close.

The minute-boundary advance is the load-bearing rule: a worst-case 256-tick gap inside a minute cannot delay an OHLCVC bar flush past the boundary.

4. Dataset replay surface ​

The replay dataset is staged on the engine link. The thin client does not marshal a dataset over the wire; the FFI staging entry arms the dataset on the client handle, and the in-tree test link owns it directly. From the caller's point of view, the dataset is provisioned out of band; the public client call is just client.replay().<analytic>([...]).on_event(|row| …).

The same synthetic-watermark contract applies: the engine walks the dataset in event-time order and synthesises watermarks exactly the same way. Replay is byte-equivalent to historical except the source is the user's staged dataset instead of an upstream historical download.

5. Progress reporting ​

The thin client surfaces progress through the callback itself — each emission is one row delivered to .on_event. A consumer can drive any progress UI (CLI bar, web-UI WebSocket push, Prometheus gauge, log lines) from inside the callback without engine-side hooks, and watch sub.is_active() to detect completion on a bounded run.

6. Upstream endpoint selection and concurrency ​

The engine resolves the contract filter against its symbol classifier to produce the concrete contract list and fans out the historical download per (contract, date). The per-contract concurrency cap is an engine config knob; the public client surfaces the bounded stream uniformly.

7. Output shape — same typed rows ​

The .on_event callback and the typed <Camel>Row it receives are the same in every mode. The unbounded live case runs until you unsubscribe(); the bounded historical / replay cases end after the last row and mark the handle inactive.

8. Backpressure differs by mode ​

  • Live — drop-oldest if the consumer falls behind. The live feed cannot be slowed without losing the connection's reconnect-replay window. If a callback consumer falls far enough behind that the buffer overflows, the subscription ends rather than skipping frames silently, and its handle goes inactive.
  • Historical / Replay — block the producer. The bounded stream suspends until the callback keeps up.

9. Tier checks — per-asset historical entitlement ​

The upstream account model carries separate streaming and historical entitlements per asset class. The historical() namespace verifies the per-asset historical tier before issuing the first upstream call. A user with streaming Pro / historical Standard sees Free-tier rejections from historical(...).<analytic>(...).on_event(…) while the live builder (client.live().<analytic>([...]).on_event(…)) continues to work.

The replay path (client.replay()) does not check entitlement: the dataset came from the user, the engine is not the gatekeeper.

10. Memory — one (contract, date) batch in flight per concurrent unit ​

The historical orchestrator fans out one (contract, date) unit at a time per concurrent unit; the per-unit watermark publisher streams ticks into the engine as they arrive rather than buffering the whole unit. The bounded peak memory tracks the engine's per-unit concurrency cap and the typical per-day tick count for the contract class — the public client does not own a buffer that grows with the unit count.

Surface trade-offs ​

Strengths ​

  • One client, one per-analytic builder shape, three ingest sources.
  • The same source-order processing holds across all three sources — every event reaches the analytic in source order.
  • The mode is opt-in via Cargo feature; production FFI builds that do not enable the preview features cannot reach the unwired paths.
  • Replay-from-staged-dataset gives correctness regressions a determinism harness: stage the same dataset twice, expect bit-identical output.

Trade-offs ​

  • The synthetic-watermark contract is enforced engine-side, not by the public type system. A future replay surface that exposed a custom iterator constructor would need to enforce ordering separately.
  • Multi-day historical queries fan out per (contract, date). The upstream historical endpoint set drives this shape.
  • Consumption is uniform across modes: .on_event fires per row in every namespace. A bounded caller that wants the rows collected gathers them inside the callback (push onto a Vec) and watches sub.is_active() for completion.
  • The replay() namespace bypasses tier checks. A user with a Free account cannot drive the live builder (client.live().<analytic>()...) or the historical namespace but can still drive replay() with their own staged data. This is correct behaviour.

Industry comparables ​

  • databento-rs — split client design with separate live::Client and historical::Client. The namespace design here preserves the same downstream call-site uniformity without splitting client identity.
  • Bloomberg BLPAPI — single Session exposing both subscribe(...) (real-time) and sendRequest(...) (request / reply). The namespace design mirrors the single-session model.
  • Refinitiv RTSDK — same data types across modes. The mode-agnostic data type contract is what this surface commits to: the .on_event callback and its typed rows are the same regardless of which namespace produced them.

Proprietary. All rights reserved.