Appearance
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
TradeTickandQuoteTickon the chain — drives the emission cadence + registry hydration. conditionsadmission 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.
| Field | Type | Description |
|---|---|---|
symbol | Arc<str> | Underlying symbol. |
date | i32 | Trading-session date (YYYYMMDD). |
ms_of_day | i32 | ms_of_day at emission time. |
legs | Vec<MaxPainLeg> | Per-expiration max-pain ladder. Ordered ascending by tenor_days. |
Configuration (MaxPainRequest)
Regenerated from the MaxPainRequest Rust source — see the note above.
| Field | Type | Description |
|---|---|---|
contracts | SecurityFilter | Contracts the subscription tracks. |
conditions | ConditionPolicy | Trade-condition admission policy. |
venues | ExchangeFilter | Exchange / venue admission policy. |
emit | EmitPolicy | Emission 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 theNchain strikes per expiration — for a 200-strike SPX chain that is40_000cheap 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)}`,
);
}
});