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

Deterministic term structures from normalized option-skew observations.

`term_structure/1` accepts a list of maps with this contract:

| Field | Contract |
| --- | --- |
| `:expiry` | `Date.t()` |
| `:tenor_days` | Non-negative integer number of calendar days |
| `:measure` | Non-empty atom or string identifying the caller-selected skew measure |
| `:units` | Non-empty atom or string identifying the value units |
| `:value` | Numeric skew observation, or `nil` for an explicitly missing observation |

Every observation in one call must use the same measure and units. The module
does not interpret those identifiers; examples include
`:delta_25_risk_reversal` measured in `:decimal_volatility`.

Results are sorted by expiry and then tenor. Exact duplicate expiry/tenor
coordinates are reduced to the arithmetic mean of their numeric values,
independent of input order. Missing duplicates do not enter that mean, and a
coordinate with no numeric values remains `nil`.

`:change_from_previous` is the current value minus the immediately preceding
sorted value. It is `nil` for the first point or when either adjacent point is
missing. No missing value is filled and no interpolation is performed.

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `term_structure` | 1 | Build a sorted skew term structure without interpolation. | `observations: value` |

# `error_reason`

```elixir
@type error_reason() ::
  :invalid_observations
  | {:invalid_observation, non_neg_integer()}
  | {:missing_field, non_neg_integer(), atom()}
  | {:invalid_field, non_neg_integer(), atom()}
  | {:mixed_measure, atom() | String.t(), atom() | String.t()}
  | {:mixed_units, atom() | String.t(), atom() | String.t()}
```

Validation failure returned by `term_structure/1`.

# `observation`

```elixir
@type observation() :: %{
  expiry: Date.t(),
  tenor_days: non_neg_integer(),
  measure: atom() | String.t(),
  units: atom() | String.t(),
  value: number() | nil
}
```

Normalized caller-selected skew observation.

# `term_point`

```elixir
@type term_point() :: %{
  expiry: Date.t(),
  tenor_days: non_neg_integer(),
  value: float() | nil,
  sample_count: non_neg_integer(),
  change_from_previous: float() | nil
}
```

A sorted and duplicate-reduced term point.

# `term_structure`

```elixir
@type term_structure() :: %{
  measure: atom() | String.t() | nil,
  units: atom() | String.t() | nil,
  duplicate_policy: :mean_observed_values,
  missing_policy: :preserve_nil,
  observations: [term_point()]
}
```

Skew term structure with its explicit measure, units, and policies.

# `term_structure`

```elixir
@spec term_structure([observation()] | term()) ::
  {:ok, term_structure()} | {:error, error_reason()}
```

Build a sorted skew term structure without interpolation.

## Parameters

  * `observations` - Normalized maps with Date :expiry, non-negative :tenor_days, caller-defined :measure/:units, and numeric or nil :value (value)

## Returns

\{:ok, %\{measure, units, duplicate_policy, missing_policy, observations\}\} or \{:error, reason\} (`result_tuple`)

### Example

```elixir
{:ok,
 %{
   units: :decimal_volatility,
   measure: :delta_25_risk_reversal,
   observations: [
     %{
       value: 0.03,
       expiry: ~D[2026-08-28],
       sample_count: 1,
       tenor_days: 30,
       change_from_previous: nil
     }
   ],
   duplicate_policy: :mean_observed_values,
   missing_policy: :preserve_nil
 }}
```

## Errors

  * `:invalid_observations` - Input must be a list
  * `:invalid_observation` - Each item must be a normalized observation map
  * `:missing_field` - A required observation field is absent
  * `:invalid_field` - An observation field has an unsupported type or domain
  * `:mixed_measure` - All observations must identify the same skew measure
  * `:mixed_units` - All observations must identify the same value units

```elixir
# descripex:contract
%{
  params: %{
    observations: %{
      description: "Normalized maps with Date :expiry, non-negative :tenor_days, caller-defined :measure/:units, and numeric or nil :value",
      kind: :value
    }
  },
  errors: [
    invalid_observations: "Input must be a list",
    invalid_observation: "Each item must be a normalized observation map",
    missing_field: "A required observation field is absent",
    invalid_field: "An observation field has an unsupported type or domain",
    mixed_measure: "All observations must identify the same skew measure",
    mixed_units: "All observations must identify the same value units"
  ],
  returns: %{
    type: :result_tuple,
    description: "{:ok, %{measure, units, duplicate_policy, missing_policy, observations}} or {:error, reason}"
  },
  returns_example: {:ok,
   %{
     units: :decimal_volatility,
     measure: :delta_25_risk_reversal,
     observations: [
       %{
         value: 0.03,
         expiry: ~D[2026-08-28],
         sample_count: 1,
         tenor_days: 30,
         change_from_previous: nil
       }
     ],
     duplicate_policy: :mean_observed_values,
     missing_policy: :preserve_nil
   }}
}
```

---

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