# `ZenQuant.Options.Pricing`
[🔗](https://github.com/ZenHive/zen_quant/blob/v0.8.1/lib/zen_quant/options/pricing.ex#L1)

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` |

# `error_reason`

```elixir
@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`

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

Analytic sensitivities using the module's documented units.

# `implied_volatility_result`

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

Successful implied-volatility convergence details.

# `inputs`

```elixir
@type inputs() :: %{
  :spot =&gt; number(),
  :strike =&gt; number(),
  :time_to_expiry_years =&gt; number(),
  :risk_free_rate =&gt; number(),
  :dividend_yield =&gt; number(),
  optional(:volatility) =&gt; number()
}
```

Black-Scholes input map; implied-volatility calls omit `:volatility`.

# `option_type`

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

European option side.

# `greeks`

```elixir
@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

```elixir
{:ok,
 %{
   delta: 0.586851146134764,
   gamma: 0.018950578755008718,
   theta: -5.089318913998333,
   vega: 37.901157510017434,
   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

```elixir
# 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,
     theta: -5.089318913998333,
     vega: 37.901157510017434,
     rho: 49.45810910532236
   }}
}
```

# `implied_volatility`

```elixir
@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

```elixir
{:ok, %{volatility: 0.2, iterations: 31, residual: 2.0e-9, 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

```elixir
# descripex:contract
%{
  opts: %{
    bracket: %{
      default: {0.0, 5.0},
      type: :tuple,
      description: "Inclusive `{lower, upper}` annual decimal-volatility bracket"
    },
    tolerance: %{
      default: 1.0e-8,
      type: :float,
      description: "Positive absolute option-price residual tolerance"
    },
    max_iterations: %{
      default: 100,
      type: :integer,
      description: "Positive bisection iteration limit"
    }
  },
  params: %{
    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
    },
    market_price: %{
      description: "Non-negative option price in spot currency units",
      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, iterations: 31, residual: 2.0e-9, converged: true}}
}
```

# `price`

```elixir
@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

```elixir
{: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`

```elixir
# 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]
}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
