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

Neutral comparisons between observed option quotes and caller-supplied model values.

`scan/3` never prices an option or infers volatility. The caller supplies a
quote map, a model-value map, and every comparison policy. Each model entry
includes the inputs used to produce it and volatility provenance declaring
whether that volatility came from the observed quote.

Quotes and model values are keyed by the same caller-selected instrument ID.
A quote contains `:bid`, `:ask`, and `:observed_at`; a model entry contains
`:value`, `:inputs`, and `:volatility_source`. `:inputs` must expose a
non-negative `:volatility`. The volatility source has this contract:

    %{
      name: :realized_30_day,
      derived_from_observed_quote: false
    }

The settings map has no defaults. It requires:

  * `:quote_side` — `:bid`, `:ask`, or `:mid`
  * `:as_of` — a `DateTime` or Unix epoch milliseconds
  * `:max_quote_age_ms` — a non-negative staleness limit
  * `:transaction_costs` — non-negative `:fees` and `:slippage` estimates,
    expressed in the same units as each quote and model value
  * `:action_thresholds` — `:absolute` and `:relative` non-negative
    deviation thresholds; either may be `nil`, but not both. Relative
    deviation uses model value as its denominator, and every enabled
    threshold must be met.

Valid rows report the signed raw deviation (`observed - model`) separately
from estimated costs. Costs reduce the deviation magnitude without changing
its sign. Thresholds are evaluated against the remaining magnitude and do
not produce a trade recommendation.

Missing, crossed, stale, and future-dated quotes are excluded and labelled.
A model whose volatility is declared as derived from the same observed quote
is also excluded, so the quote is never labelled deviant from a model that
circularly used it as the volatility input.

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `scan` | 3 | Compare observed quotes with independent caller-supplied model values. | `quotes: exchange_data`, `model_values: value`, `settings: value` |

# `error_reason`

```elixir
@type error_reason() ::
  {:invalid_input, :quotes | :model_values | :settings}
  | {:missing_setting, atom()}
  | {:invalid_setting, atom()}
```

Top-level input or policy validation failure.

# `model_value`

```elixir
@type model_value() :: %{
  value: number(),
  inputs: %{:volatility =&gt; number(), optional(term()) =&gt; term()},
  volatility_source: volatility_source()
}
```

Caller-supplied model value and the inputs that produced it.

# `quote`

```elixir
@type quote() :: %{
  bid: number(),
  ask: number(),
  observed_at: DateTime.t() | integer()
}
```

Caller-selected quote observation in model-value currency units.

# `result`

```elixir
@type result() :: %{assumptions: map(), results: [map()]}
```

Successful scan with explicit assumptions and per-instrument results.

# `settings`

```elixir
@type settings() :: %{
  quote_side: :bid | :ask | :mid,
  as_of: DateTime.t() | integer(),
  max_quote_age_ms: non_neg_integer(),
  transaction_costs: %{fees: number(), slippage: number()},
  action_thresholds: %{absolute: number() | nil, relative: number() | nil}
}
```

Explicit policies applied to every row in one scan.

# `volatility_source`

```elixir
@type volatility_source() :: %{
  :name =&gt; atom() | String.t(),
  :derived_from_observed_quote =&gt; boolean(),
  optional(term()) =&gt; term()
}
```

Provenance for the model's volatility input.

# `scan`

```elixir
@spec scan(map() | term(), map() | term(), settings() | term()) ::
  {:ok, result()} | {:error, error_reason()}
```

Compares matching quote and model maps under fully explicit caller policies.

---

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