Skip to content

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_legs but 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:

  1. Roll the window forward if the new print is more than same_ts_window_ms past the active group's earliest leg.
  2. Group active prints by distinct contract_id — additional prints on a contract already in the group accumulate size at the most-recent price.
  3. Sort the distinct legs by (expiration, right, strike).
  4. 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).
  5. Fire a SpreadDetectorTick if the total absolute premium crosses min_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 TradeTick admitted by conditions.

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.

FieldTypeDescription
symbolArc<str>Underlying symbol.
datei32Trading session date (YYYYMMDD).
ms_of_dayi32ms_of_day of the earliest leg in the complex.
shapeSpreadShapeClassified shape — see [SpreadShape].
legsVec<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_countu32Number of distinct contracts in the complex — same as legs.len(), carried as a separate field for downstream columnar filtering.
net_premium_usdf64Total absolute premium across the complex (USD: sum of `

Configuration (SpreadDetectorRequest)

Regenerated from the SpreadDetectorRequest Rust source — see the note above.

FieldTypeDescription
contractsSecurityFilterUnderlyings the subscription tracks. The detector expands a SecurityFilter::Symbols(["SPX"]) over every option contract on the underlying chain — leg correlation is per-symbol.
conditionsConditionPolicyTrade-condition admission policy. Default [ConditionPolicy::OpraRegular] — cancels, openings, late, and halt-rule prints are excluded.
same_ts_window_msu32Window 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_legsu8Minimum distinct contracts required to fire one emission. Default 2 — the smallest spread (verticals, straddles, strangles, calendars, diagonals).
max_legsu8Maximum 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_usdf64Minimum 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_legsusizeMaximum 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 by max_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}`,
    );
  }
});

Proprietary. All rights reserved.