Terminal risk-neutral probability estimates from vertical option spreads.
A call vertical uses the lower-strike call price minus the upper-strike call price. A put vertical uses the upper-strike put price minus the lower-strike put price. Dividing that spread price by discounted strike spacing gives the risk-neutral expectation of the vertical's normalized terminal payoff.
This is a finite-strike terminal-state estimate. It is not a touch probability, a physical/real-world probability, or a forecast of where the underlying will trade before expiry.
Input contract
estimate/1 accepts a map with:
:side—:callor:put:lower_strikeand:upper_strike— strictly increasing strikes:lower_priceand:upper_price— non-negative option prices:time_to_expiry_years— non-negative caller-computed year fraction:annual_discount_rate— annualized rate as a decimal:discount_convention—:continuousor:simple:quote_side—:midor:natural
With :mid, both prices are midpoint quotes. With :natural, the long leg is
priced at its ask and the short leg at its bid: lower ask/upper bid for calls,
and upper ask/lower bid for puts.
Continuous discounting uses exp(-rate * years). Simple discounting uses
1 / (1 + rate * years) and requires a positive denominator.
Results outside [0, 1] return {:error, {:probability_out_of_bounds, details}}; they are not silently clamped.
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
estimate | 1 | Estimate a terminal risk-neutral probability from a vertical option spread. | inputs: value |
Summary
Types
Calculation assumptions returned with a probability estimate.
Annual discounting convention.
Validation or no-arbitrage failure.
Explicit vertical-spread inputs.
Option side used to construct the vertical.
Quote selection assumption for the two spread legs.
Successful risk-neutral terminal probability estimate.
Functions
Estimate a terminal risk-neutral probability from a vertical option spread.
Types
@type assumptions() :: %{ side: option_side(), terminal_state: :above_strike_interval | :below_strike_interval, lower_strike: float(), upper_strike: float(), strike_spacing: float(), lower_price: float(), upper_price: float(), spread_price: float(), time_to_expiry_years: float(), annual_discount_rate: float(), discount_convention: discount_convention(), discount_factor: float(), quote_side: quote_side(), quote_application: :both_mid | :lower_ask_upper_bid | :lower_bid_upper_ask }
Calculation assumptions returned with a probability estimate.
@type discount_convention() :: :continuous | :simple
Annual discounting convention.
@type error_reason() :: {:invalid_input, :spread | atom()} | {:missing_input, atom()} | {:invalid_strike_order, %{lower_strike: number(), upper_strike: number()}} | {:invalid_discount_factor, float()} | {:probability_out_of_bounds, %{probability: float(), minimum: float(), maximum: float()}}
Validation or no-arbitrage failure.
@type inputs() :: %{ side: option_side(), lower_strike: number(), upper_strike: number(), lower_price: number(), upper_price: number(), time_to_expiry_years: number(), annual_discount_rate: number(), discount_convention: discount_convention(), quote_side: quote_side() }
Explicit vertical-spread inputs.
@type option_side() :: :call | :put
Option side used to construct the vertical.
@type quote_side() :: :mid | :natural
Quote selection assumption for the two spread legs.
@type result() :: %{ type: :risk_neutral_terminal_probability, probability: float(), assumptions: assumptions() }
Successful risk-neutral terminal probability estimate.
Functions
@spec estimate(inputs() | term()) :: {:ok, result()} | {:error, error_reason()}
Estimate a terminal risk-neutral probability from a vertical option spread.
Parameters
inputs- Map with side, ordered strikes, leg prices, expiry years, annual discount rate/convention, and :mid or :natural quote-side assumption (value)
Returns
{:ok, %{type: :risk_neutral_terminal_probability, probability, assumptions}} or {:error, reason} (result_tuple)
Example
{:ok,
%{
type: :risk_neutral_terminal_probability,
assumptions: %{
side: :call,
time_to_expiry_years: 0.5,
lower_price: 6.0,
upper_price: 1.0,
lower_strike: 100.0,
upper_strike: 110.0,
annual_discount_rate: 0.0,
discount_convention: :continuous,
quote_side: :mid,
terminal_state: :above_strike_interval,
strike_spacing: 10.0,
spread_price: 5.0,
discount_factor: 1.0,
quote_application: :both_mid
},
probability: 0.5
}}Errors
:missing_input- A required input field is absent:invalid_input- An input has an unsupported type, value, or convention:invalid_strike_order- Lower strike must be strictly below upper strike:invalid_discount_factor- Discounting did not produce a strictly positive factor:probability_out_of_bounds- Spread prices violate the zero-to-one no-arbitrage bounds
# descripex:contract
%{
params: %{
inputs: %{
description: "Map with side, ordered strikes, leg prices, expiry years, annual discount rate/convention, and :mid or :natural quote-side assumption",
kind: :value
}
},
errors: [
missing_input: "A required input field is absent",
invalid_input: "An input has an unsupported type, value, or convention",
invalid_strike_order: "Lower strike must be strictly below upper strike",
invalid_discount_factor: "Discounting did not produce a strictly positive factor",
probability_out_of_bounds: "Spread prices violate the zero-to-one no-arbitrage bounds"
],
returns: %{
type: :result_tuple,
description: "`{:ok, %{type: :risk_neutral_terminal_probability, probability, assumptions}}` or `{:error, reason}`"
},
returns_example: {:ok,
%{
type: :risk_neutral_terminal_probability,
assumptions: %{
side: :call,
time_to_expiry_years: 0.5,
lower_price: 6.0,
upper_price: 1.0,
lower_strike: 100.0,
upper_strike: 110.0,
annual_discount_rate: 0.0,
discount_convention: :continuous,
quote_side: :mid,
terminal_state: :above_strike_interval,
strike_spacing: 10.0,
spread_price: 5.0,
discount_factor: 1.0,
quote_application: :both_mid
},
probability: 0.5
}}
}