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

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` — `:call` or `:put`
  * `:lower_strike` and `:upper_strike` — strictly increasing strikes
  * `:lower_price` and `: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` — `:continuous` or `:simple`
  * `:quote_side` — `:mid` or `: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` |

# `assumptions`

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

# `discount_convention`

```elixir
@type discount_convention() :: :continuous | :simple
```

Annual discounting convention.

# `error_reason`

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

# `inputs`

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

# `option_side`

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

Option side used to construct the vertical.

# `quote_side`

```elixir
@type quote_side() :: :mid | :natural
```

Quote selection assumption for the two spread legs.

# `result`

```elixir
@type result() :: %{
  type: :risk_neutral_terminal_probability,
  probability: float(),
  assumptions: assumptions()
}
```

Successful risk-neutral terminal probability estimate.

# `estimate`

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

```elixir
{:ok,
 %{
   type: :risk_neutral_terminal_probability,
   probability: 0.5,
   assumptions: %{
     side: :call,
     time_to_expiry_years: 0.5,
     lower_price: 6.0,
     upper_price: 1.0,
     quote_side: :mid,
     lower_strike: 100.0,
     upper_strike: 110.0,
     annual_discount_rate: 0.0,
     discount_convention: :continuous,
     terminal_state: :above_strike_interval,
     strike_spacing: 10.0,
     spread_price: 5.0,
     discount_factor: 1.0,
     quote_application: :both_mid
   }
 }}
```

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

```elixir
# 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,
     probability: 0.5,
     assumptions: %{
       side: :call,
       time_to_expiry_years: 0.5,
       lower_price: 6.0,
       upper_price: 1.0,
       quote_side: :mid,
       lower_strike: 100.0,
       upper_strike: 110.0,
       annual_discount_rate: 0.0,
       discount_convention: :continuous,
       terminal_state: :above_strike_interval,
       strike_spacing: 10.0,
       spread_price: 5.0,
       discount_factor: 1.0,
       quote_application: :both_mid
     }
   }}
}
```

---

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