European Black-Scholes-Merton pricing, analytic greeks, and implied volatility.
The model assumes European exercise and a continuous dividend or carry yield. Every rate is continuously compounded and annualized. Callers supply time directly in years, so this module applies no calendar or trading-day count.
Input contract
Pricing and greeks accept :call or :put plus a map containing:
| Field | Unit and convention |
|---|---|
:spot | Positive underlying price in currency units |
:strike | Positive strike price in the same currency units |
:time_to_expiry_years | Non-negative remaining time in caller-computed years |
:risk_free_rate | Annualized continuously compounded rate as a decimal |
:dividend_yield | Annualized continuous dividend/carry yield as a decimal |
:volatility | Annualized standard deviation as a decimal; non-negative |
Output contract
Prices use the same currency units as spot and strike. Greek conventions are:
| Greek | Unit and sign convention |
|---|---|
:delta | Price change per one-unit spot increase; call positive, put negative |
:gamma | Delta change per one-unit spot increase |
:vega | Price change per 1.0 absolute volatility increase |
:theta | Price change per one year of calendar-time passage (-dV/dT) |
:rho | Price change per 1.0 absolute risk-free-rate increase |
Vega and rho are not divided into one-percentage-point units. Theta is not divided by 365 or 252.
At expiry, price/2 returns intrinsic value. At positive time and zero
volatility it returns the discounted deterministic payoff. Analytic greeks
are undefined at expiry and at zero volatility, so greeks/2 returns an
explicit error for those boundaries.
implied_volatility/4 uses bisection over a default volatility bracket of
[0.0, 5.0], an absolute price tolerance of 1.0e-8, and at most 100
iterations. A successful result includes the volatility, iterations,
residual, and converged: true. Prices outside the bracket and iteration
exhaustion return explicit errors. Volatility is unidentifiable at expiry.
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
implied_volatility | 4 | Solve annualized decimal volatility from a European option price. | option_type: value, market_price: value, inputs: value |
greeks | 2 | Calculate analytic European Black-Scholes-Merton greeks. | option_type: value, inputs: value |
price | 2 | Price a European call or put under Black-Scholes-Merton. | option_type: value, inputs: value |
Summary
Types
Pricing or solver failure reason.
Analytic sensitivities using the module's documented units.
Successful implied-volatility convergence details.
Black-Scholes input map; implied-volatility calls omit :volatility.
European option side.
Functions
Calculate analytic European Black-Scholes-Merton greeks.
Solve annualized decimal volatility from a European option price.
Price a European call or put under Black-Scholes-Merton.
Types
@type error_reason() :: {:invalid_option_type, term()} | {:missing_input, atom()} | {:invalid_input, atom()} | :greeks_undefined_at_expiry | :greeks_undefined_at_zero_volatility | :implied_volatility_undefined_at_expiry | {:invalid_solver_option, atom()} | {:not_bracketed, map()} | {:did_not_converge, map()}
Pricing or solver failure reason.
Analytic sensitivities using the module's documented units.
@type implied_volatility_result() :: %{ volatility: float(), iterations: non_neg_integer(), residual: float(), converged: true }
Successful implied-volatility convergence details.
@type inputs() :: %{ :spot => number(), :strike => number(), :time_to_expiry_years => number(), :risk_free_rate => number(), :dividend_yield => number(), optional(:volatility) => number() }
Black-Scholes input map; implied-volatility calls omit :volatility.
@type option_type() :: :call | :put
European option side.
Functions
@spec greeks(option_type() | term(), inputs() | term()) :: {:ok, greeks()} | {:error, error_reason()}
Calculate analytic European Black-Scholes-Merton greeks.
Parameters
option_type-:callor:put(value)inputs- Price inputs; outputs use per-unit delta/gamma, per-1.0 volatility vega, annual calendar-time theta, and per-1.0 rate rho (value)
Returns
{:ok, %{delta, gamma, vega, theta, rho}} or {:error, reason} (result_tuple)
Example
{:ok,
%{
delta: 0.586851146134764,
gamma: 0.018950578755008718,
vega: 37.901157510017434,
theta: -5.089318913998333,
rho: 49.45810910532236
}}Errors
:invalid_option_type- Option type is not :call or :put:missing_input- A required map field is absent:invalid_input- A field has an unsupported type or domain:greeks_undefined_at_expiry- Payoff derivatives are discontinuous at expiry:greeks_undefined_at_zero_volatility- The deterministic payoff has a strike kink
# descripex:contract
%{
params: %{
option_type: %{description: "`:call` or `:put`", kind: :value},
inputs: %{
description: "Price inputs; outputs use per-unit delta/gamma, per-1.0 volatility vega, annual calendar-time theta, and per-1.0 rate rho",
kind: :value
}
},
errors: [
invalid_option_type: "Option type is not :call or :put",
missing_input: "A required map field is absent",
invalid_input: "A field has an unsupported type or domain",
greeks_undefined_at_expiry: "Payoff derivatives are discontinuous at expiry",
greeks_undefined_at_zero_volatility: "The deterministic payoff has a strike kink"
],
returns: %{
type: :result_tuple,
description: "`{:ok, %{delta, gamma, vega, theta, rho}}` or `{:error, reason}`"
},
returns_example: {:ok,
%{
delta: 0.586851146134764,
gamma: 0.018950578755008718,
vega: 37.901157510017434,
theta: -5.089318913998333,
rho: 49.45810910532236
}}
}
@spec implied_volatility( option_type() | term(), number() | term(), inputs() | term(), keyword() | term() ) :: {:ok, implied_volatility_result()} | {:error, error_reason()}
Solve annualized decimal volatility from a European option price.
Parameters
option_type-:callor:put(value)market_price- Non-negative option price in spot currency units (value)inputs- Pricing input map without :volatility; time is years and both rates are annual continuously compounded decimals (value)
Options
bracket- Inclusive{lower, upper}annual decimal-volatility bracket (default:{0.0, 5.0})tolerance- Positive absolute option-price residual tolerance (default:1.0e-8)max_iterations- Positive bisection iteration limit (default:100)
Returns
{:ok, %{volatility, iterations, residual, converged: true}} or {:error, reason} (result_tuple)
Example
{:ok, %{volatility: 0.2, residual: 2.0e-9, iterations: 31, converged: true}}Errors
:invalid_option_type- Option type is not :call or :put:missing_input- A required map field is absent:invalid_input- A field or market price has an unsupported type or domain:implied_volatility_undefined_at_expiry- Price is independent of volatility at expiry:invalid_solver_option- Bracket, tolerance, or iteration limit is invalid:not_bracketed- Target price is outside the prices attainable at the volatility endpoints:did_not_converge- Absolute price tolerance was not met before the iteration limit
# descripex:contract
%{
opts: %{
tolerance: %{
default: 1.0e-8,
type: :float,
description: "Positive absolute option-price residual tolerance"
},
bracket: %{
default: {0.0, 5.0},
type: :tuple,
description: "Inclusive `{lower, upper}` annual decimal-volatility bracket"
},
max_iterations: %{
default: 100,
type: :integer,
description: "Positive bisection iteration limit"
}
},
params: %{
market_price: %{
description: "Non-negative option price in spot currency units",
kind: :value
},
option_type: %{description: "`:call` or `:put`", kind: :value},
inputs: %{
description: "Pricing input map without :volatility; time is years and both rates are annual continuously compounded decimals",
kind: :value
}
},
errors: [
invalid_option_type: "Option type is not :call or :put",
missing_input: "A required map field is absent",
invalid_input: "A field or market price has an unsupported type or domain",
implied_volatility_undefined_at_expiry: "Price is independent of volatility at expiry",
invalid_solver_option: "Bracket, tolerance, or iteration limit is invalid",
not_bracketed: "Target price is outside the prices attainable at the volatility endpoints",
did_not_converge: "Absolute price tolerance was not met before the iteration limit"
],
returns: %{
type: :result_tuple,
description: "`{:ok, %{volatility, iterations, residual, converged: true}}` or `{:error, reason}`"
},
returns_example: {:ok,
%{volatility: 0.2, residual: 2.0e-9, iterations: 31, converged: true}}
}
@spec price(option_type() | term(), inputs() | term()) :: {:ok, float()} | {:error, error_reason()}
Price a European call or put under Black-Scholes-Merton.
Parameters
option_type-:callor:put(value)inputs- Map with positive :spot/:strike, :time_to_expiry_years, continuous annual :risk_free_rate/:dividend_yield, and decimal annual :volatility (value)
Returns
{:ok, price} in spot currency units or {:error, reason} (result_tuple)
Example
{:ok, 9.227005508154036}Errors
:invalid_option_type- Option type is not :call or :put:missing_input- A required map field is absent:invalid_input- A field has an unsupported type or domain
Composes With
implied_volatility
# descripex:contract
%{
params: %{
option_type: %{description: "`:call` or `:put`", kind: :value},
inputs: %{
description: "Map with positive :spot/:strike, :time_to_expiry_years, continuous annual :risk_free_rate/:dividend_yield, and decimal annual :volatility",
kind: :value
}
},
errors: [
invalid_option_type: "Option type is not :call or :put",
missing_input: "A required map field is absent",
invalid_input: "A field has an unsupported type or domain"
],
returns: %{
type: :result_tuple,
description: "`{:ok, price}` in spot currency units or `{:error, reason}`"
},
returns_example: {:ok, 9.227005508154036},
composes_with: [:implied_volatility]
}