Appearance
Consuming events
The Kairos client delivers analytic output through a typed callback. The fluent chain names the analytic and its universe, then ends in .on_event, which registers a closure that fires once per emission:
rust
use kairos::{Client, GreeksRow};
let client = Client::connect(("me@example.com", "secret"))?;
let sub = client
.live()
.greeks(["QQQ"])
.on_event(|row: &GreeksRow| {
println!("delta={} gamma={}", row.delta, row.gamma);
})?;Each emission arrives as a typed <Camel>Row struct — GreeksRow for greeks, VpinRow for vpin, VolatilityRow for volatility, and so on across the row catalogue. The consumer reads the fields it needs directly; there is no batch to decode and no poll loop to drive.
An emission is not always a tick
.on_event fires per emission, and an emission is whatever the analytic publishes: a per-trade Greek, a closed OHLCVC bar, a debounced volatility snapshot, a rolling-window flow reading. The callback is named for the event it delivers, not for a tick — the unit of delivery is the analytic's output row, whatever its cadence.
Cadence is configured on the analytic, not on the callback
Delivery and cadence are separate axes. .on_event says what runs per emission; how often an analytic emits — its bar size, rolling window, or trigger — is set on the builder with the analytic's fluent setters, before the terminal .on_event:
rust
use kairos::{Client, VpinRow};
let sub = client
.live()
.vpin(["QQQ"])
.bucket_volume(100_000) // cadence: emit one reading per closed bucket
.window_size(50)
.on_event(|row: &VpinRow| println!("vpin={}", row.vpin))?;The setters a given analytic exposes are documented on its page and in the subscription builder reference. An analytic with no cadence knobs (Greeks, GEX) emits on its natural trigger and takes no setters — the chain is just the accessor and .on_event.
The subscription handle
.on_event returns an EventSubscription handle. Hold it to control the stream:
rust
let sub = client.live().greeks(["QQQ"]).on_event(|row: &GreeksRow| {
// ...
})?;
// … later, from anywhere …
sub.unsubscribe(); // stops delivery, releases the subscription| Method | Purpose |
|---|---|
unsubscribe() | Stop delivery and release the engine subscription. Idempotent; a second call is a no-op. At most the in-flight emission's callback completes before delivery ceases. |
is_active() | false once unsubscribe() has been called or the stream has ended. |
panic_count() | Cumulative count of callback panics caught on this subscription (see below). |
Dropping the handle does not stop delivery — a fire-and-forget let _ = builder.on_event(...) keeps streaming. Call unsubscribe() to stop, or bind the handle to keep control.
Shared delivery
All .on_event callbacks in a process share one delivery path. A desk that streams a Greeks subscription, a GEX subscription, and a raw quote feed at the same time does not pay a thread per stream — the client services every active subscription off a single shared worker and hands each decoded row to the right callback.
The trade-off is that callbacks share that worker, so a slow callback delays its siblings. Callbacks are meant to be cheap: stash the row, enqueue it, update a widget. A consumer that needs to do heavy work per emission should hand the row off to its own thread from inside the callback and return immediately.
Panic isolation
A panic inside a callback is caught, counted on the subscription's panic_count(), and delivery continues with the next emission. One panicking callback never tears down the shared worker or the sibling subscriptions — the row that triggered the panic is skipped and the stream stays live.
Bounded consumption (historical / replay)
.on_event is the same terminal verb on the bounded preview namespaces. client.historical(start, end) and client.replay() fire the callback once per row over the finite result set rather than an open-ended live feed:
rust
use kairos::{Client, VolatilityRow};
let sub = client
.historical(20251103, 20251107)
.volatility(["SPX"])
.on_event(|row: &VolatilityRow| println!("{} {}", row.ms_of_day, row.value))?;A replay over a captured Parquet dataset binds the file on the chain before the terminal:
rust
use kairos::{Client, OhlcvcRow};
let sub = client
.replay()
.ohlcvc(["SPX"])
.from_parquet("ticks.parquet")
.on_event(|bar: &OhlcvcRow| println!("close={}", bar.close))?;The accessor, the universe argument, and the callback shape are identical across live, historical, and replay — only the namespace changes.
Emission contract — suppress when undefined
An analytic emits only when its output is defined. During a warmup window that has not yet collected enough observations, while a required spot price is missing, or on a degenerate sample, the analytic suppresses the emission rather than delivering a row of NaN sentinels. The first row a callback receives is therefore the analytic's first defined reading, and consumers never filter "not-ready" rows out of the stream. Fields that are individually undefined within an otherwise-defined row still carry NaN — documented per field on each analytic's page.
Push-callback streaming (Python / TypeScript SDK)
The Python and TypeScript SDKs expose the same event-driven model through their own idioms. On Python, .on_event(callback) hands each emission to a callback and returns a handle that is both .unsubscribe()-able and a context manager (drains on exit):
python
import kairos_thetadata as kt
client = kt.Client.connect(kt.Credentials.from_env())
def on_event(tick):
... # handle each emission
with client.live().volatility(["SPX"]).on_event(on_event) as sub:
sub.wait(timeout_seconds=60.0)Without the with wrapper, .on_event returns the same handle; call sub.unsubscribe() to tear the dispatcher down. On TypeScript the method is .onEvent(callback), returning a handle with .unsubscribe(). See universe selection for the full comparison.
See also
- Client — the connect + namespace surface.
- Subscription builder — the per-analytic fluent builder and its cadence setters.
- Tick types — the typed row catalogue
.on_eventdelivers. - Backpressure — what happens when a consumer falls behind the live feed.