ZenQuant.Options (zen_quant v0.11.2)

Copy Markdown View Source

Options analytics and aggregation functions.

Pure functions for analyzing option chains, calculating open interest distributions, and identifying key market levels. Works with plain maps containing option data (e.g., :open_interest, :raw with greeks). time_to_expiry/2 and session_phase/2 are deterministic given a reference DateTime; the one-argument forms read DateTime.utc_now/0.

All functions that need strike/expiry data parse it from symbols using ZenQuant.Options.Deribit.parse_option/1.

Example

chain = %{
  "BTC-31JAN26-84000-C" => %{open_interest: 100.0, raw: %{"gamma" => 0.00001}},
  "BTC-31JAN26-84000-P" => %{open_interest: 50.0, raw: %{"gamma" => 0.00001}}
}

ZenQuant.Options.oi_by_strike(chain)
# => %{84000.0 => 150.0}

ZenQuant.Options.put_call_ratio(chain)
# => {:ok, 0.5}

API Functions

FunctionArityDescriptionParam Kinds
breakeven_move2Calculate percentage move to breakeven for an option.option: value, spot: value
aggregate_oi1Aggregate open interest across multiple option chains.chains: exchange_data
pin_risk3Assess pin risk for strikes near spot price. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
moneyness_skew3Build per-expiry moneyness skew observations from an option chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
atm_iv_term_structure3Build per-expiry ATM IV observations from an option chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
atm_iv2Extract ATM implied volatility from an option chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
expected_range3Calculate expected 1-sigma price range for given IV and time horizon. Scalar inputs only; derive chain IV with atm_iv/2, which rejects mixed underlyings or settlement regimes with {:error, :heterogeneous_chain}.spot: value, iv: value, hours_to_expiry: value
theta_per_hour1Calculate hourly theta decay rate for an option.option: value
greeks_sum1Sum Greeks across all options in a chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data
hot_zone3Detect hot zone / pin risk at expiry. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
pin_magnets3Find high-gamma strikes near spot (pin magnets). Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
zero_gamma3Find aggregate dealer zero-gamma spot levels under sticky-strike repricing. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
gamma_flip2Find the first per-strike GEX sign change scanning strikes upward. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
gex_by_strike2Calculate gamma exposure (GEX) by strike. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, spot: value
in_play?3Check if a strike is within threshold of spot price.strike: value, spot: value, threshold_pct: value
strike_distance2Calculate percentage distance from spot to strike.strike: value, spot: value
largest_positions2Get the N largest positions by open interest. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data, n: value
session_phase2Determine trading session phase based on time to expiry.expiry: value, now: value
put_call_ratio1Calculate put/call ratio from open interest. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data
max_pain1Calculate max pain strike where most options expire worthless. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data
filter_by_dte3Filter option chain by maximum days to expiry.chain: exchange_data, max_dte: value, now_dt: value
time_to_expiry2Calculate time remaining until expiry.expiry: value, now: value
oi_by_expiry1Aggregate open interest by expiry date. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data
oi_by_strike1Aggregate open interest by strike price. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.chain: exchange_data

Summary

Types

Aggregate zero-gamma validation or solver failure.

A leg excluded from zero-gamma analysis by explicit caller policy.

Aggregate dealer zero-gamma result under sticky-strike repricing.

Functions

Aggregate open interest across multiple option chains.

Extract ATM implied volatility from an option chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Per-expiry ATM implied volatility term structure from a chain.

Calculate percentage move to breakeven for an option.

Calculate expected 1-sigma price range for given IV and time horizon. Scalar inputs only; derive chain IV with atm_iv/2, which rejects mixed underlyings or settlement regimes with {:error, :heterogeneous_chain}.

Filter option chain by maximum days to expiry.

Returns the first per-strike GEX sign change while scanning strikes upward.

Calculate gamma exposure (GEX) by strike. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Sum Greeks across all options in a chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Detect hot zone / pin risk at expiry. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Check if a strike is within threshold of spot price.

Get the N largest positions by open interest. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Calculate max pain strike where most options expire worthless. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Per-expiry moneyness skew from a chain at caller-supplied moneyness levels.

Aggregate open interest by expiry date. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Aggregate open interest by strike price. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Find high-gamma strikes near spot (pin magnets). Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Assess pin risk for strikes near spot price. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Calculate put/call ratio from open interest. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Determine trading session phase based on time to expiry.

Calculate percentage distance from spot to strike.

Calculate hourly theta decay rate for an option.

Calculate time remaining until expiry.

Solves for aggregate dealer GEX sign changes as spot varies.

Types

expiry()

@type expiry() :: Date.t()

option_chain()

@type option_chain() :: %{optional(String.t()) => map()}

strike()

@type strike() :: float()

zero_gamma_error_reason()

@type zero_gamma_error_reason() ::
  {:invalid_zero_gamma_input, :chain | :spot | :options}
  | {:invalid_zero_gamma_option,
     :search_range | :relative_tolerance | :min_gross_gex | :on_unpriceable}
  | {:unpriceable_leg, zero_gamma_omission()}
  | :heterogeneous_chain
  | :no_gamma_signal
  | :no_zero_gamma_found
  | :no_zero_gamma_in_populated_range
  | {:zero_gamma_did_not_converge, map()}

Aggregate zero-gamma validation or solver failure.

zero_gamma_omission()

@type zero_gamma_omission() :: %{
  symbol: term(),
  reason: :missing_iv | :degenerate_input
}

A leg excluded from zero-gamma analysis by explicit caller policy.

zero_gamma_result()

@type zero_gamma_result() :: %{
  level: float(),
  roots: [float()],
  current_gex: float(),
  current_gross_gex: float(),
  gex_at_level: float(),
  gross_gex_at_level: float(),
  searched_range: {float(), float()},
  populated_strike_range: {float(), float()},
  relative_tolerance: float(),
  omitted: [zero_gamma_omission()]
}

Aggregate dealer zero-gamma result under sticky-strike repricing.

Functions

aggregate_oi(chains)

@spec aggregate_oi([option_chain()]) :: %{
  calls: float(),
  puts: float(),
  total: float(),
  ratio: float() | nil
}

Aggregate open interest across multiple option chains.

Parameters

  • chains - List of option chain maps (exchange_data)

Returns

Map with :calls, :puts, :total, :ratio (puts/calls) (map)

Example

%{calls: 1800.0, total: 3000.0, puts: 1200.0, ratio: 0.667}
# descripex:contract
%{
  params: %{
    chains: %{
      scope: %{
        reason: "Takes a LIST of chains, so summing across them is the caller's explicit request.",
        mode: :cross_chain
      },
      description: "List of option chain maps",
      source: "multiple Options.Deribit.chain() calls",
      kind: :exchange_data
    }
  },
  returns: %{
    type: :map,
    description: "Map with :calls, :puts, :total, :ratio (puts/calls)"
  },
  returns_example: %{calls: 1800.0, total: 3000.0, puts: 1200.0, ratio: 0.667}
}

atm_iv(chain, spot)

@spec atm_iv(option_chain(), number()) ::
  {:ok, float()} | {:error, :no_iv_data} | {:error, :heterogeneous_chain}

Extract ATM implied volatility from an option chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain with IV data in :raw (exchange_data)
  • spot - Current spot price (value)

Returns

{:ok, iv_percentage} or {:error, :no_iv_data} (tuple)

Example

{:ok, 46.3}

Errors

  • :heterogeneous_chain
  • :no_iv_data
# descripex:contract
%{
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    chain: %{
      scope: :single,
      description: "Option chain with IV data in :raw",
      source: "Options.Deribit.chain(Bourse, exchange, enrich: :greeks) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain, :no_iv_data],
  returns: %{
    type: :tuple,
    description: "{:ok, iv_percentage} or {:error, :no_iv_data}"
  },
  returns_example: {:ok, 46.3}
}

atm_iv_term_structure(chain, spot, opts \\ [])

@spec atm_iv_term_structure(option_chain() | term(), number() | term(), keyword()) ::
  {:ok, [map()]} | {:error, atom()}

Per-expiry ATM implied volatility term structure from a chain.

Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Groups legs by expiry, selects ATM strikes relative to spot, and returns one observation per expiry. Observations carry the Options.Skew.term_structure/1 contract (:expiry, :tenor_days, :measure, :units, :value) plus :open_interest and an explicit :status — feed the list straight into Skew.term_structure/1 without reshaping.

ATM selection (:window_pct)

  • 0 (default) — the single strike nearest spot (ties break to the lower strike). Call and put IVs at that strike are averaged.
  • > 0 — arithmetic mean of per-strike IVs for every strike within ±window_pct percent of spot. An empty window yields status: :no_iv and value: nil for that expiry (no silent nearest-strike fallback).

Missing IV

Every expiry present in the chain appears in the result. When no readable IV is available under the selection policy, value is nil and status: :no_iv — the expiry is never dropped and never given a fabricated numeric IV.

:tenor_days is max(0, Date.diff(expiry, as_of_date)) from the required caller-supplied :as_of (Date or DateTime); the function never reads system time.

breakeven_move(option, spot)

@spec breakeven_move(map(), number()) :: {:ok, float()} | {:error, :missing_data}

Calculate percentage move to breakeven for an option.

Parameters

  • option - Option map with :symbol, :mark_price, :underlying_price (value)
  • spot - Current spot price (value)

Returns

{:ok, percentage} or {:error, :missing_data} (tuple)

Example

{:ok, 4.2}

Errors

  • :missing_data
# descripex:contract
%{
  params: %{
    option: %{
      description: "Option map with :symbol, :mark_price, :underlying_price",
      kind: :value
    },
    spot: %{description: "Current spot price", kind: :value}
  },
  errors: [:missing_data],
  returns: %{
    type: :tuple,
    description: "{:ok, percentage} or {:error, :missing_data}"
  },
  returns_example: {:ok, 4.2}
}

expected_range(spot, iv, hours_to_expiry)

@spec expected_range(number(), number(), number()) :: %{
  lower: float(),
  upper: float(),
  move_pct: float()
}

Calculate expected 1-sigma price range for given IV and time horizon. Scalar inputs only; derive chain IV with atm_iv/2, which rejects mixed underlyings or settlement regimes with {:error, :heterogeneous_chain}.

Parameters

  • spot - Current spot price (value)
  • iv - Annualized IV as percentage (e.g., 46.3) (value)
  • hours_to_expiry - Hours until expiry (value)

Returns

Map with :lower, :upper, :move_pct (map)

Example

%{upper: 101.0, lower: 99.0, move_pct: 1.0}
# descripex:contract
%{
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    iv: %{description: "Annualized IV as percentage (e.g., 46.3)", kind: :value},
    hours_to_expiry: %{description: "Hours until expiry", kind: :value}
  },
  returns: %{type: :map, description: "Map with :lower, :upper, :move_pct"},
  returns_example: %{upper: 101.0, lower: 99.0, move_pct: 1.0}
}

filter_by_dte(chain, max_dte, now_dt \\ DateTime.utc_now())

@spec filter_by_dte(option_chain(), pos_integer(), DateTime.t()) :: option_chain()

Filter option chain by maximum days to expiry.

Parameters

  • chain - Option chain map (exchange_data)
  • max_dte - Maximum days to expiry (value)
  • now_dt - Current time (for testing) (default: "DateTime.utc_now()", value)

Returns

Filtered chain with only options within max_dte (map)

Example

%{"BTC-31JAN26-84000-C" => %{open_interest: 1200.0}}
# descripex:contract
%{
  params: %{
    chain: %{
      scope: %{
        reason: "Returns a filtered chain without aggregating across legs.",
        mode: :per_leg
      },
      description: "Option chain map",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    },
    max_dte: %{description: "Maximum days to expiry", kind: :value},
    now_dt: %{
      default: "DateTime.utc_now()",
      description: "Current time (for testing)",
      kind: :value
    }
  },
  returns: %{
    type: :map,
    description: "Filtered chain with only options within max_dte"
  },
  returns_example: %{"BTC-31JAN26-84000-C" => %{open_interest: 1200.0}}
}

gamma_flip(chain, spot)

@spec gamma_flip(option_chain(), number()) ::
  {:ok, strike()} | {:error, :no_flip_found} | {:error, :heterogeneous_chain}

Returns the first per-strike GEX sign change while scanning strikes upward.

Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

This is the put/call open-interest boundary evaluated at the supplied spot. It is not the aggregate zero-gamma level; use zero_gamma/3 for that.

gex_by_strike(chain, spot)

@spec gex_by_strike(option_chain(), number()) ::
  %{required(strike()) => float()} | {:error, :heterogeneous_chain}

Calculate gamma exposure (GEX) by strike. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain with greeks in :raw data (exchange_data)
  • spot - Current spot price (value)

Returns

Map of %{strike => gex_value} (map)

Example

%{84000.0 => 141120.0}

Errors

  • :heterogeneous_chain

Composes With

  • gamma_flip
# descripex:contract
%{
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    chain: %{
      scope: :single,
      description: "Option chain with greeks in :raw data",
      source: "Options.Deribit.chain(Bourse, exchange, enrich: :greeks) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{type: :map, description: "Map of %{strike => gex_value}"},
  returns_example: %{84000.0 => 141120.0},
  composes_with: [:gamma_flip]
}

greeks_sum(chain)

@spec greeks_sum(option_chain()) ::
  %{delta: float(), gamma: float(), theta: float(), vega: float()}
  | {:error, :heterogeneous_chain}

Sum Greeks across all options in a chain. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain with greeks in :raw data (exchange_data)

Returns

Map with aggregated :delta, :gamma, :theta, :vega (map)

Example

%{delta: 1.0, gamma: 1.0, theta: 1.0, vega: 1.0}

Errors

  • :heterogeneous_chain
# descripex:contract
%{
  params: %{
    chain: %{
      scope: :single,
      description: "Option chain with greeks in :raw data",
      source: "Options.Deribit.chain(Bourse, exchange, enrich: :greeks) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{
    type: :map,
    description: "Map with aggregated :delta, :gamma, :theta, :vega"
  },
  returns_example: %{delta: 1.0, gamma: 1.0, theta: 1.0, vega: 1.0}
}

hot_zone(chain, spot, opts \\ [])

@spec hot_zone(option_chain(), number(), keyword()) ::
  {:hot, strike(), float()} | :clear | {:error, :heterogeneous_chain}

Detect hot zone / pin risk at expiry. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map (exchange_data)
  • spot - Current spot price (value)

Options

  • expiry - Expiry date to check (default: "Date.utc_today()")
  • threshold_pct - Max distance for hot zone (default: 1.0)

Returns

{:hot, strike, oi} or :clear (tuple)

Example

{:hot, 84000.0, 1250.0}

Errors

  • :heterogeneous_chain
# descripex:contract
%{
  opts: %{
    expiry: %{
      default: "Date.utc_today()",
      type: :date,
      description: "Expiry date to check"
    },
    threshold_pct: %{
      default: 1.0,
      type: :float,
      description: "Max distance for hot zone"
    }
  },
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    chain: %{
      scope: :single,
      description: "Option chain map",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{type: :tuple, description: "{:hot, strike, oi} or :clear"},
  returns_example: {:hot, 84000.0, 1250.0}
}

in_play?(strike, spot, threshold_pct)

@spec in_play?(strike(), number(), number()) :: boolean()

Check if a strike is within threshold of spot price.

Parameters

  • strike - Strike price (value)
  • spot - Current spot price (value)
  • threshold_pct - Maximum distance percentage (value)

Returns

true if abs(distance) <= threshold (boolean)

Example

true
# descripex:contract
%{
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    threshold_pct: %{description: "Maximum distance percentage", kind: :value},
    strike: %{description: "Strike price", kind: :value}
  },
  returns: %{type: :boolean, description: "true if abs(distance) <= threshold"},
  returns_example: true
}

largest_positions(chain, n)

@spec largest_positions(option_chain(), pos_integer()) ::
  [{String.t(), map()}] | {:error, :heterogeneous_chain}

Get the N largest positions by open interest. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map (exchange_data)
  • n - Number of positions to return (value)

Returns

List of {symbol, option_map} sorted by OI desc (list)

Example

[{"BTC-31JAN26-84000-C", %{open_interest: 1200.0}}]

Errors

  • :heterogeneous_chain
# descripex:contract
%{
  params: %{
    n: %{description: "Number of positions to return", kind: :value},
    chain: %{
      scope: :single,
      description: "Option chain map",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{
    type: :list,
    description: "List of {symbol, option_map} sorted by OI desc"
  },
  returns_example: [{"BTC-31JAN26-84000-C", %{open_interest: 1200.0}}]
}

max_pain(chain)

@spec max_pain(option_chain()) ::
  {:ok, strike()} | {:error, :empty_chain} | {:error, :heterogeneous_chain}

Calculate max pain strike where most options expire worthless. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map (exchange_data)

Returns

{:ok, strike} or {:error, :empty_chain} (tuple)

Example

{:ok, 84000.0}

Errors

  • :heterogeneous_chain
  • :empty_chain
# descripex:contract
%{
  params: %{
    chain: %{
      scope: :single,
      description: "Option chain map",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain, :empty_chain],
  returns: %{
    type: :tuple,
    description: "{:ok, strike} or {:error, :empty_chain}"
  },
  returns_example: {:ok, 84000.0}
}

moneyness_skew(chain, spot, opts \\ [])

@spec moneyness_skew(option_chain() | term(), number() | term(), keyword()) ::
  {:ok, [map()]} | {:error, atom()}

Per-expiry moneyness skew from a chain at caller-supplied moneyness levels.

Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

For each expiry, selects the strike(s) nearest spot * low and spot * high (defaults 0.9 and 1.1), reads IV with the same policy as atm_iv/2, and returns value = iv_low - iv_high (percentage points).

Observations match the Options.Skew.term_structure/1 contract and can be passed through without reshaping. Missing wings are never dropped: value is nil and :status is :no_iv (unreadable IV) or :no_strike (no strike within selection policy / :max_distance_pct).

ATM/wing selection uses the same :window_pct rules as atm_iv_term_structure/3. Optional :max_distance_pct rejects a nearest strike farther than that percent of spot from the target moneyness level.

oi_by_expiry(chain)

@spec oi_by_expiry(option_chain()) ::
  %{required(expiry()) => float()} | {:error, :heterogeneous_chain}

Aggregate open interest by expiry date. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map %{symbol => option_map} (exchange_data)

Returns

Map of %{Date => total_oi} (map)

Example

%{~D[2026-01-31] => 1200.0}

Errors

  • :heterogeneous_chain
# descripex:contract
%{
  params: %{
    chain: %{
      scope: :single,
      description: "Option chain map %{symbol => option_map}",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{type: :map, description: "Map of %{Date => total_oi}"},
  returns_example: %{~D[2026-01-31] => 1200.0}
}

oi_by_strike(chain)

@spec oi_by_strike(option_chain()) ::
  %{required(strike()) => float()} | {:error, :heterogeneous_chain}

Aggregate open interest by strike price. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map %{symbol => option_map} (exchange_data)

Returns

Map of %{strike => total_oi} (map)

Example

%{84000.0 => 1200.0}

Errors

  • :heterogeneous_chain

Composes With

  • max_pain
# descripex:contract
%{
  params: %{
    chain: %{
      scope: :single,
      description: "Option chain map %{symbol => option_map}",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{type: :map, description: "Map of %{strike => total_oi}"},
  returns_example: %{84000.0 => 1200.0},
  composes_with: [:max_pain]
}

pin_magnets(chain, spot, opts \\ [])

@spec pin_magnets(option_chain(), number(), keyword()) ::
  [{strike(), float()}] | {:error, :heterogeneous_chain}

Find high-gamma strikes near spot (pin magnets). Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map (exchange_data)
  • spot - Current spot price (value)

Options

  • threshold_pct - Max distance from spot (default: 5.0)
  • min_oi - Minimum OI to consider (default: 0)

Returns

List of {strike, oi} sorted by OI desc (list)

Example

[{84000.0, 1250.0}]

Errors

  • :heterogeneous_chain
# descripex:contract
%{
  opts: %{
    threshold_pct: %{
      default: 5.0,
      type: :float,
      description: "Max distance from spot"
    },
    min_oi: %{default: 0, type: :number, description: "Minimum OI to consider"}
  },
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    chain: %{
      scope: :single,
      description: "Option chain map",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{type: :list, description: "List of {strike, oi} sorted by OI desc"},
  returns_example: [{84000.0, 1250.0}]
}

pin_risk(chain, spot, opts \\ [])

@spec pin_risk(option_chain(), number(), keyword()) ::
  [map()] | {:error, :heterogeneous_chain}

Assess pin risk for strikes near spot price. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map (exchange_data)
  • spot - Current spot price (value)

Options

  • threshold_pct - Max distance from spot (default: 5.0)
  • min_oi - Minimum OI to include (default: 0)

Returns

List of %{strike, oi, distance_pct, risk: :high|:medium|:low} (list)

Example

[%{strike: 84000.0, oi: 1250.0, distance_pct: 0.6, risk: :high}]

Errors

  • :heterogeneous_chain
# descripex:contract
%{
  opts: %{
    threshold_pct: %{
      default: 5.0,
      type: :float,
      description: "Max distance from spot"
    },
    min_oi: %{default: 0, type: :number, description: "Minimum OI to include"}
  },
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    chain: %{
      scope: :single,
      description: "Option chain map",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain],
  returns: %{
    type: :list,
    description: "List of %{strike, oi, distance_pct, risk: :high|:medium|:low}"
  },
  returns_example: [
    %{strike: 84000.0, oi: 1250.0, distance_pct: 0.6, risk: :high}
  ]
}

put_call_ratio(chain)

@spec put_call_ratio(option_chain()) ::
  {:ok, float()} | {:error, :no_calls} | {:error, :heterogeneous_chain}

Calculate put/call ratio from open interest. Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Parameters

  • chain - Option chain map (exchange_data)

Returns

{:ok, float()} put/call ratio, or {:error, :no_calls} if no call OI (result_tuple)

Example

{:ok, 0.53}

Errors

  • :heterogeneous_chain
  • :no_calls
# descripex:contract
%{
  params: %{
    chain: %{
      scope: :single,
      description: "Option chain map",
      source: "Options.Deribit.chain(Bourse, exchange) :chain",
      kind: :exchange_data
    }
  },
  errors: [:heterogeneous_chain, :no_calls],
  returns: %{
    type: :result_tuple,
    description: "{:ok, float()} put/call ratio, or {:error, :no_calls} if no call OI"
  },
  returns_example: {:ok, 0.53}
}

session_phase(expiry, now \\ DateTime.utc_now())

@spec session_phase(Date.t() | DateTime.t(), DateTime.t()) ::
  :early | :final_hour | :last_15min | :expired

Determine trading session phase based on time to expiry.

Parameters

  • expiry - Expiry Date (assumes 08:00 UTC) or DateTime (value)
  • now - Reference DateTime for the session-phase thresholds. Omit to read the wall clock. (default: "DateTime.utc_now()", value)

Returns

:early | :final_hour | :last_15min | :expired (atom)

Example

:final_hour
# descripex:contract
%{
  params: %{
    now: %{
      default: "DateTime.utc_now()",
      description: "Reference DateTime for the session-phase thresholds. Omit to read the wall clock.",
      kind: :value
    },
    expiry: %{
      description: "Expiry Date (assumes 08:00 UTC) or DateTime",
      kind: :value
    }
  },
  returns: %{
    type: :atom,
    description: ":early | :final_hour | :last_15min | :expired"
  },
  returns_example: :final_hour
}

strike_distance(strike, spot)

@spec strike_distance(strike(), number()) :: float()

Calculate percentage distance from spot to strike.

Parameters

  • strike - Strike price (value)
  • spot - Current spot price (value)

Returns

Percentage distance (positive = strike above spot) (float)

Example

4.761904761904762
# descripex:contract
%{
  params: %{
    spot: %{description: "Current spot price", kind: :value},
    strike: %{description: "Strike price", kind: :value}
  },
  returns: %{
    type: :float,
    description: "Percentage distance (positive = strike above spot)"
  },
  returns_example: 4.761904761904762
}

theta_per_hour(option)

@spec theta_per_hour(map()) :: float() | nil

Calculate hourly theta decay rate for an option.

Parameters

  • option - Option map with :raw containing theta (value)

Returns

Per-hour theta decay, or nil if theta unavailable (float)

Example

-0.5208333333333334
# descripex:contract
%{
  params: %{
    option: %{
      description: "Option map with :raw containing theta",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Per-hour theta decay, or nil if theta unavailable"
  },
  returns_example: -0.5208333333333334
}

time_to_expiry(expiry, now \\ DateTime.utc_now())

@spec time_to_expiry(Date.t() | DateTime.t(), DateTime.t()) :: %{
  hours: float(),
  minutes: float()
}

Calculate time remaining until expiry.

Parameters

  • expiry - Expiry Date (assumes 08:00 UTC) or DateTime (value)
  • now - Reference DateTime for the remaining-time calculation. Omit to read the wall clock. (default: "DateTime.utc_now()", value)

Returns

Map with :hours and :minutes remaining (map)

Example

%{hours: 1.0, minutes: 60.0}
# descripex:contract
%{
  params: %{
    now: %{
      default: "DateTime.utc_now()",
      description: "Reference DateTime for the remaining-time calculation. Omit to read the wall clock.",
      kind: :value
    },
    expiry: %{
      description: "Expiry Date (assumes 08:00 UTC) or DateTime",
      kind: :value
    }
  },
  returns: %{type: :map, description: "Map with :hours and :minutes remaining"},
  returns_example: %{hours: 1.0, minutes: 60.0}
}

zero_gamma(chain, spot, opts \\ [])

@spec zero_gamma(option_chain() | term(), number() | term(), keyword() | term()) ::
  {:ok, zero_gamma_result()} | {:error, zero_gamma_error_reason()}

Solves for aggregate dealer GEX sign changes as spot varies.

Requires a single underlying and settlement regime across all parseable legs; returns {:error, :heterogeneous_chain} otherwise, before filtering.

Each candidate replaces only :spot in every enriched leg's retained :pricing_inputs; IV, time to expiry, rates, and carry remain fixed. This is the sticky-strike assumption. The default search range is 0.5–1.5 times the supplied spot and can be replaced with an absolute :search_range tuple.

The range is scanned over 512 equal intervals and every observed sign-changing bracket is bisected. Roots outside the minimum and maximum strikes carrying positive open interest are discarded. The primary :level is the remaining root nearest the supplied spot, while :roots contains every remaining root in ascending order.

Calls contribute positive dealer GEX and puts negative dealer GEX. A candidate is zero when abs(net GEX) <= relative_tolerance * gross GEX. Empty or near-empty gamma mass returns :no_gamma_signal instead of treating every spot as a root.