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
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
breakeven_move | 2 | Calculate percentage move to breakeven for an option. | option: value, spot: value |
aggregate_oi | 1 | Aggregate open interest across multiple option chains. | chains: exchange_data |
pin_risk | 3 | Assess pin risk for strikes near spot price. | chain: exchange_data, spot: value |
moneyness_skew | 3 | Build per-expiry moneyness skew observations from an option chain. | chain: exchange_data, spot: value |
atm_iv_term_structure | 3 | Build per-expiry ATM IV observations from an option chain. | chain: exchange_data, spot: value |
atm_iv | 2 | Extract ATM implied volatility from an option chain. | chain: exchange_data, spot: value |
expected_range | 3 | Calculate expected 1-sigma price range for given IV and time horizon. | spot: value, iv: value, hours_to_expiry: value |
theta_per_hour | 1 | Calculate hourly theta decay rate for an option. | option: value |
greeks_sum | 1 | Sum Greeks across all options in a chain. | chain: exchange_data |
hot_zone | 3 | Detect hot zone / pin risk at expiry. | chain: exchange_data, spot: value |
pin_magnets | 3 | Find high-gamma strikes near spot (pin magnets). | chain: exchange_data, spot: value |
gamma_flip | 2 | Find gamma flip level where GEX crosses zero. | chain: exchange_data, spot: value |
gex_by_strike | 2 | Calculate gamma exposure (GEX) by strike. | chain: exchange_data, spot: value |
in_play? | 3 | Check if a strike is within threshold of spot price. | strike: value, spot: value, threshold_pct: value |
strike_distance | 2 | Calculate percentage distance from spot to strike. | strike: value, spot: value |
largest_positions | 2 | Get the N largest positions by open interest. | chain: exchange_data, n: value |
session_phase | 1 | Determine trading session phase based on time to expiry. | expiry: value |
put_call_ratio | 1 | Calculate put/call ratio from open interest. | chain: exchange_data |
max_pain | 1 | Calculate max pain strike where most options expire worthless. | chain: exchange_data |
filter_by_dte | 3 | Filter option chain by maximum days to expiry. | chain: exchange_data, max_dte: value |
time_to_expiry | 1 | Calculate time remaining until expiry. | expiry: value |
oi_by_expiry | 1 | Aggregate open interest by expiry date. | chain: exchange_data |
oi_by_strike | 1 | Aggregate 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
Functions
@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}
}
@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}
}
@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 nearestspot(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_pctpercent of spot. An empty window yieldsstatus: :no_ivandvalue: nilfor 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.
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}
}
@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}
}
@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}
}
@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}
}
@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]
}
@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}
}
@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}
}
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
}
@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}}]
}
@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}
}
@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.
@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}
}
@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]
}
@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}]
}
@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}
]
}
@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}
}
@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
}
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
}
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
}
@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}
}