Skip to content

MaxPain

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-expiration max-pain strike — the strike at which the total dollar-payoff of all in-the-money options expiring on that date is minimised. Calculated from the per-strike open-interest snapshot plus the live chain so the value reflects the current term-structure ladder, not an end-of-day batch.

The analytic emits one tick per (symbol, watermark) carrying one MaxPainLeg per registered forward expiration; each leg reports the max-pain strike + the per-strike payoff curve so a consumer can draw the standard "pinning landscape" chart used on dealer desks.

Methodology

For each (symbol, expiration) with OpenInterest[K] rows the analytic computes per candidate strike K*:

text
payoff(K*) = sum over K of
  ( max(K* - K, 0) · OI_call[K]
  + max(K - K*, 0) · OI_put[K]  ) · 100  (contract multiplier)

The max-pain strike is argmin K* payoff(K*). The intuition is the pin-risk argument from Stoll (1969) extended by 0DTE / weekly desks: at expiration, dealer-hedged option books finance the payoff to writers; the strike that makes the option-writer wallet "hurt least" is the natural gravitational centre on pin days.

References:

  • Stoll, H. (1969). The Relationship Between Put and Call Option Prices. Journal of Finance 24(5): 801–824.
  • Garleanu, Pedersen, Poteshman (2009). Demand-Based Option Pricing. Review of Financial Studies 22(10): 4259–4299.
  • Avellaneda, Lipkin (2003). A Market-Induced Mechanism for Stock Pinning. Quantitative Finance 3(6): 417–425.

Inputs

  • Per-strike open-interest snapshot (engine open-interest cache) — drives the payoff calculation.
  • Option TradeTick and QuoteTick on the chain — drives the emission cadence + registry hydration.
  • conditions admission policy filters non-regular prints.

Per-expiration leg (MaxPainLeg)

Each legs[i] row carries: expiration: i32 (YYYYMMDD), tenor_days: f64, the resolved max_pain_strike: f64, plus the per-strike payoff distribution (strikes: Vec<f64>, payoffs: Vec<f64>) so a consumer can render the full pin-risk curve.

Output schema (MaxPainTick)

The field / type / description table below is regenerated from the MaxPainTick 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 at emission time.
legsVec<MaxPainLeg>Per-expiration max-pain ladder. Ordered ascending by tenor_days.

Configuration (MaxPainRequest)

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

FieldTypeDescription
contractsSecurityFilterContracts the subscription tracks.
conditionsConditionPolicyTrade-condition admission policy.
venuesExchangeFilterExchange / venue admission policy.
emitEmitPolicyEmission policy. Defaults to [EmitPolicy::OnExchangeInterval] at one row per second per symbol: the max-pain ladder is a property of the whole chain snapshot, so the per-second event-time cadence carries the full signal without paying a chain sweep per inbound trade. OnEveryTick fires per admitted Trade; OnClose accumulates and fires on watermark.

Operational characteristics

  • Per-tick latency. O(N²) over the N chain strikes per expiration — for a 200-strike SPX chain that is 40_000 cheap arithmetic ops per leg, completed well inside one watermark tick.
  • Allocation discipline. Per-strike payoff vector is preallocated and reused per leg.
  • Replay parity. Deterministic given identical engine open-interest cache
    • 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):
    for leg in tick.legs:
        print(
            f"{tick.symbol} {leg.expiration} "
            f"t={leg.tenor_days:.1f}d max_pain={leg.max_pain_strike:.2f}"
        )

sub = client.live().max_pain(["SPX"]).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().maxPain(["SPX"]).onEvent((tick) => {
  for (const leg of tick.legs) {
    console.log(
      `${tick.symbol} ${leg.expiration} t=${leg.tenorDays.toFixed(1)}d ` +
        `maxPain=${leg.maxPainStrike.toFixed(2)}`,
    );
  }
});

Proprietary. All rights reserved.