ZenQuant.Options.Pricing (zen_quant v0.2.0)

Copy Markdown View Source

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:

FieldUnit and convention
:spotPositive underlying price in currency units
:strikePositive strike price in the same currency units
:time_to_expiry_yearsNon-negative remaining time in caller-computed years
:risk_free_rateAnnualized continuously compounded rate as a decimal
:dividend_yieldAnnualized continuous dividend/carry yield as a decimal
:volatilityAnnualized standard deviation as a decimal; non-negative

Output contract

Prices use the same currency units as spot and strike. Greek conventions are:

GreekUnit and sign convention
:deltaPrice change per one-unit spot increase; call positive, put negative
:gammaDelta change per one-unit spot increase
:vegaPrice change per 1.0 absolute volatility increase
:thetaPrice change per one year of calendar-time passage (-dV/dT)
:rhoPrice 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

FunctionArityDescriptionParam Kinds
implied_volatility4Solve annualized decimal volatility from a European option price.option_type: value, market_price: value, inputs: value
greeks2Calculate analytic European Black-Scholes-Merton greeks.option_type: value, inputs: value
price2Price 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

error_reason()

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

greeks()

@type greeks() :: %{
  delta: float(),
  gamma: float(),
  vega: float(),
  theta: float(),
  rho: float()
}

Analytic sensitivities using the module's documented units.

implied_volatility_result()

@type implied_volatility_result() :: %{
  volatility: float(),
  iterations: non_neg_integer(),
  residual: float(),
  converged: true
}

Successful implied-volatility convergence details.

inputs()

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

option_type()

@type option_type() :: :call | :put

European option side.

Functions

greeks(option_type, inputs)

@spec greeks(option_type() | term(), inputs() | term()) ::
  {:ok, greeks()} | {:error, error_reason()}

Calculate analytic European Black-Scholes-Merton greeks.

Parameters

  • option_type - :call or :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
   }}
}

implied_volatility(option_type, market_price, inputs, opts \\ [])

@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 - :call or :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}}
}

price(option_type, inputs)

@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 - :call or :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]
}