Appearance
SpreadDetector
Preview on the public Rust client. The analytic ships in the engine and is reachable today through the Python wheel and the TypeScript / Node addon; a per-analytic builder on the public Rust thin client (
kairos::Client) is tracked on the roadmap. The output tick schema below is stable.
What it computes
Per-underlying same-timestamp spread detector. When trades print on ≥ min_legs distinct (expiration, right, strike) triples on one underlying within same_ts_window_ms milliseconds, the analytic emits one SpreadDetectorTick carrying the per-leg breakdown and a canonical shape label.
The shapes recognised are the standard ones used on the institutional complex-order book:
- Vertical — same expiration, same right, distinct strikes.
- Calendar — same right + strike, distinct expirations.
- Diagonal — same right, distinct expirations and strikes.
- Straddle — same expiration + strike, both rights.
- Strangle — same expiration, both rights, distinct strikes.
- Butterfly — 3 legs, same expiration + right, 1:2:1 size ratio.
- Condor — 4 legs, 1:1:1:1 size ratio (single-right or iron).
- Ratio — 2 legs with a non-unit size ratio.
- Unknown — leg set passed
min_legsbut matched no rule.
Spread detection is the multi-leg sibling of SweepDetector: sweeps fire on one contract across multiple venues; spreads fire on multiple contracts at the same instant on the same underlying.
Methodology
The analytic maintains a per-underlying active window. On every admitted option trade:
- Roll the window forward if the new print is more than
same_ts_window_mspast the active group's earliest leg. - Group active prints by distinct
contract_id— additional prints on a contract already in the group accumulate size at the most-recent price. - Sort the distinct legs by
(expiration, right, strike). - Apply the shape classifier in priority order over leg count: 2-leg (vertical / straddle / strangle / calendar / diagonal / ratio) → 3-leg (butterfly) → 4-leg (condor, iron condor).
- Fire a
SpreadDetectorTickif the total absolute premium crossesmin_total_premium_usd. Dedup against the previous emission for the same(date, window_start_ms, leg-set)triple so an unchanged leg set does not re-emit.
References:
- CBOE Complex Order Book Specification — the wire format that defines the leg-grouping conventions classified here.
- Hull, Options, Futures, and Other Derivatives, 11e — chapters on standard combinations (verticals, calendars, butterflies, condors, straddles, strangles).
Inputs
- Option
TradeTickadmitted byconditions.
Output schema (SpreadDetectorTick)
The field / type / description table below is regenerated from the SpreadDetectorTick Rust source by docs-site/scripts/inject-doc-tables.ts on every npm run docs:build. Do not hand-edit between the sentinels.
| Field | Type | Description |
|---|---|---|
symbol | Arc<str> | Underlying symbol. |
date | i32 | Trading session date (YYYYMMDD). |
ms_of_day | i32 | ms_of_day of the earliest leg in the complex. |
shape | SpreadShape | Classified shape — see [SpreadShape]. |
legs | Vec<SpreadLeg> | Per-leg breakdown. Ordered by ascending (expiration, right, strike) so verticals / butterflies / condors arrive in a deterministic sequence the classifier can be inverted against. |
leg_count | u32 | Number of distinct contracts in the complex — same as legs.len(), carried as a separate field for downstream columnar filtering. |
net_premium_usd | f64 | Total absolute premium across the complex (USD: sum of ` |
Configuration (SpreadDetectorRequest)
Regenerated from the SpreadDetectorRequest Rust source — see the note above.
| Field | Type | Description |
|---|---|---|
contracts | SecurityFilter | Underlyings the subscription tracks. The detector expands a SecurityFilter::Symbols(["SPX"]) over every option contract on the underlying chain — leg correlation is per-symbol. |
conditions | ConditionPolicy | Trade-condition admission policy. Default [ConditionPolicy::OpraRegular] — cancels, openings, late, and halt-rule prints are excluded. |
same_ts_window_ms | u32 | Window in milliseconds. Trades that print within this many milliseconds of the active group's earliest leg are folded into the same complex-order candidate. Default 1 — strict same-tick correlation. Loosen to 50 for the "human millisecond" convention some venues advertise. |
min_legs | u8 | Minimum distinct contracts required to fire one emission. Default 2 — the smallest spread (verticals, straddles, strangles, calendars, diagonals). |
max_legs | u8 | Maximum legs the classifier inspects on a single emission. Default 4 — the institutional convention covers verticals through condors; 5+ leg combos fall to [SpreadShape::Unknown]. |
min_total_premium_usd | f64 | Minimum total absolute premium (dollars) summed across legs for the candidate to fire. Default 0.0 — fire on every classified group regardless of size. |
max_window_legs | usize | Maximum entries kept in the active per-symbol window. Bounds memory growth on a busy underlying. Default 32 — far above the 4-leg condor ceiling, leaves room for 4-leg combos that arrive interleaved with single-leg flow. |
Operational characteristics
- Per-tick cost. O(
max_window_legs) on every admitted print — bounded constant in the engine's hot path. The classifier itself is O(legs) with leg count capped bymax_legs(default 4). - Allocation discipline. Per-symbol active window + dedup stamp are preallocated; the per-emission leg vector is the only per-tick allocation.
- Replay parity. Deterministic given identical input tick order.
Example
Preview. This analytic ships in the engine and is reachable today through the Python wheel and the TypeScript / Node addon. A per-analytic builder on the public Rust thin client (
kairos::Client) is tracked on the roadmap — bare-string symbol filters passed to the analytic accessor will match the other analytics already exposed there. The output tick rows are stable and documented above.
Cross-language surface
python
import kairos_thetadata as kt
client = kt.Client.connect(kt.Credentials.from_env())
def on_event(tick):
print(
f"{tick.symbol} shape={tick.shape} "
f"legs={tick.leg_count} premium=${tick.net_premium_usd:.0f}"
)
for leg in tick.legs:
print(
f" {leg.expiration}/{leg.right}/{leg.strike} "
f"x{leg.size} @ ${leg.price}"
)
sub = client.live().spread_detector(["SPY"]).on_event(on_event)
sub.wait(timeout_seconds=60.0)typescript
import { Client, Credentials } from "kairos-thetadata";
const client = await Client.connect(Credentials.fromEnv());
const sub = await client.live().spreadDetector(["SPY"]).onEvent((tick) => {
console.log(
`${tick.symbol} shape=${tick.shape} legs=${tick.legCount} ` +
`premium=$${tick.netPremiumUsd.toFixed(0)}`,
);
for (const leg of tick.legs) {
console.log(
` ${leg.expiration}/${leg.right}/${leg.strike} ` +
`x${leg.size} @ $${leg.price}`,
);
}
});