ZenQuant.Options (zen_quant v0.2.0)

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).

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.chain: exchange_data, spot: value
moneyness_skew3Build per-expiry moneyness skew observations from an option chain.chain: exchange_data, spot: value
atm_iv_term_structure3Build per-expiry ATM IV observations from an option chain.chain: exchange_data, spot: value
atm_iv2Extract ATM implied volatility from an option chain.chain: exchange_data, spot: value
expected_range3Calculate expected 1-sigma price range for given IV and time horizon.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.chain: exchange_data
hot_zone3Detect hot zone / pin risk at expiry.chain: exchange_data, spot: value
pin_magnets3Find high-gamma strikes near spot (pin magnets).chain: exchange_data, spot: value
gamma_flip2Find gamma flip level where GEX crosses zero.chain: exchange_data, spot: value
gex_by_strike2Calculate gamma exposure (GEX) by strike.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.chain: exchange_data, n: value
session_phase1Determine trading session phase based on time to expiry.expiry: value
put_call_ratio1Calculate put/call ratio from open interest.chain: exchange_data
max_pain1Calculate max pain strike where most options expire worthless.chain: exchange_data
filter_by_dte3Filter option chain by maximum days to expiry.chain: exchange_data, max_dte: value
time_to_expiry1Calculate time remaining until expiry.expiry: value
oi_by_expiry1Aggregate open interest by expiry date.chain: exchange_data
oi_by_strike1Aggregate open interest by strike price.chain: exchange_data

Summary

Functions

Aggregate open interest across multiple option chains.

Extract ATM implied volatility from an option chain.

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.

Filter option chain by maximum days to expiry.

Find gamma flip level where GEX crosses zero.

Calculate gamma exposure (GEX) by strike.

Sum Greeks across all options in a chain.

Detect hot zone / pin risk at expiry.

Check if a strike is within threshold of spot price.

Get the N largest positions by open interest.

Calculate max pain strike where most options expire worthless.

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

Aggregate open interest by expiry date.

Aggregate open interest by strike price.

Find high-gamma strikes near spot (pin magnets).

Assess pin risk for strikes near spot price.

Calculate put/call ratio from open interest.

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.

Types

expiry()

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

option_chain()

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

strike()

@type strike() :: float()

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: %{
      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}

Extract ATM implied volatility from an option chain.

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

  • :no_iv_data
# descripex:contract
%{
  params: %{
    chain: %{
      description: "Option chain with IV data in :raw",
      source: "Options.Deribit.chain(exchange_mod, enrich: :greeks)",
      kind: :exchange_data
    },
    spot: %{description: "Current spot price", kind: :value}
  },
  errors: [: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.

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.

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: 1.0, lower: 1.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: 1.0, lower: 1.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)

Options

  • now_dt - Current time (for testing) (default: "DateTime.utc_now()")

Returns

Filtered chain with only options within max_dte (map)

Example

%{value: 1.0}
# descripex:contract
%{
  opts: %{
    now_dt: %{
      default: "DateTime.utc_now()",
      type: :datetime,
      description: "Current time (for testing)"
    }
  },
  params: %{
    chain: %{
      description: "Option chain map",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    },
    max_dte: %{description: "Maximum days to expiry", kind: :value}
  },
  returns: %{
    type: :map,
    description: "Filtered chain with only options within max_dte"
  },
  returns_example: %{value: 1.0}
}

gamma_flip(chain, spot)

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

Find gamma flip level where GEX crosses zero.

Parameters

  • chain - Option chain with greeks (exchange_data)
  • spot - Current spot price (value)

Returns

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

Example

{:ok, 83250.0}

Errors

  • :no_flip_found
# descripex:contract
%{
  params: %{
    chain: %{
      description: "Option chain with greeks",
      source: "Options.Deribit.chain(exchange_mod, enrich: :greeks)",
      kind: :exchange_data
    },
    spot: %{description: "Current spot price", kind: :value}
  },
  errors: [:no_flip_found],
  returns: %{
    type: :tuple,
    description: "{:ok, strike} or {:error, :no_flip_found}"
  },
  returns_example: {:ok, 83250.0}
}

gex_by_strike(chain, spot)

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

Calculate gamma exposure (GEX) by strike.

Parameters

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

Returns

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

Example

%{value: 1.0}

Composes With

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

greeks_sum(chain)

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

Sum Greeks across all options in a chain.

Parameters

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

Returns

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

Example

%{gamma: 1.0, delta: 1.0, vega: 1.0, theta: 1.0}
# descripex:contract
%{
  params: %{
    chain: %{
      description: "Option chain with greeks in :raw data",
      source: "Options.Deribit.chain(exchange_mod, enrich: :greeks)",
      kind: :exchange_data
    }
  },
  returns: %{
    type: :map,
    description: "Map with aggregated :delta, :gamma, :theta, :vega"
  },
  returns_example: %{gamma: 1.0, delta: 1.0, vega: 1.0, theta: 1.0}
}

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

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

Detect hot zone / pin risk at expiry.

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}
# 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: %{
    chain: %{
      description: "Option chain map",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    },
    spot: %{description: "Current spot price", kind: :value}
  },
  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},
    strike: %{description: "Strike price", kind: :value},
    threshold_pct: %{description: "Maximum distance percentage", 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()}]

Get the N largest positions by open interest.

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}}]
# descripex:contract
%{
  params: %{
    n: %{description: "Number of positions to return", kind: :value},
    chain: %{
      description: "Option chain map",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    }
  },
  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}

Calculate max pain strike where most options expire worthless.

Parameters

  • chain - Option chain map (exchange_data)

Returns

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

Example

{:ok, 84000.0}

Errors

  • :empty_chain
# descripex:contract
%{
  params: %{
    chain: %{
      description: "Option chain map",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    }
  },
  errors: [: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.

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()}

Aggregate open interest by expiry date.

Parameters

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

Returns

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

Example

%{value: 1.0}
# descripex:contract
%{
  params: %{
    chain: %{
      description: "Option chain map %{symbol => option_map}",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    }
  },
  returns: %{type: :map, description: "Map of %{Date => total_oi}"},
  returns_example: %{value: 1.0}
}

oi_by_strike(chain)

@spec oi_by_strike(option_chain()) :: %{required(strike()) => float()}

Aggregate open interest by strike price.

Parameters

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

Returns

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

Example

%{value: 1.0}

Composes With

  • max_pain
# descripex:contract
%{
  params: %{
    chain: %{
      description: "Option chain map %{symbol => option_map}",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    }
  },
  returns: %{type: :map, description: "Map of %{strike => total_oi}"},
  returns_example: %{value: 1.0},
  composes_with: [:max_pain]
}

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

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

Find high-gamma strikes near spot (pin magnets).

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}]
# 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: %{
    chain: %{
      description: "Option chain map",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    },
    spot: %{description: "Current spot price", kind: :value}
  },
  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()]

Assess pin risk for strikes near spot price.

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, risk: :high, distance_pct: 0.6}]
# 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: %{
    chain: %{
      description: "Option chain map",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    },
    spot: %{description: "Current spot price", kind: :value}
  },
  returns: %{
    type: :list,
    description: "List of %{strike, oi, distance_pct, risk: :high|:medium|:low}"
  },
  returns_example: [
    %{strike: 84000.0, oi: 1250.0, risk: :high, distance_pct: 0.6}
  ]
}

put_call_ratio(chain)

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

Calculate put/call ratio from open interest.

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

  • :no_calls
# descripex:contract
%{
  params: %{
    chain: %{
      description: "Option chain map",
      source: "Options.Deribit.chain(exchange_mod)",
      kind: :exchange_data
    }
  },
  errors: [: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)

@spec session_phase(Date.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)

Returns

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

Example

:final_hour
# descripex:contract
%{
  params: %{
    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

0.1095
# 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: 0.1095
}

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.1095
# 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.1095
}

time_to_expiry(expiry)

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

Calculate time remaining until expiry.

Parameters

  • expiry - Expiry Date (assumes 08:00 UTC) or DateTime (value)

Returns

Map with :hours and :minutes remaining (map)

Example

%{hours: 1, minutes: 1}
# descripex:contract
%{
  params: %{
    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, minutes: 1}
}