# `ZenQuant.Risk`
[🔗](https://github.com/ZenHive/zen_quant/blob/v0.8.1/lib/zen_quant/risk.ex#L1)

Position risk analysis for trading systems.

Pure functions for analyzing portfolio concentration, position limits,
and risk metrics.

## Example

    positions = [
      %{symbol: "BTC/USDT", value: 50_000},
      %{symbol: "ETH/USDT", value: 30_000},
      %{symbol: "SOL/USDT", value: 20_000}
    ]

    ZenQuant.Risk.concentration(positions)
    # => %{max: 0.5, hhi: 0.38, top3: 1.0}

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `exposure_by_asset` | 1 | Group positions by base asset and compute delta-weighted exposure per asset. | `positions: value` |
| `portfolio_delta` | 1 | Calculate net delta exposure across all positions. | `positions: value` |
| `calmar_ratio` | 2 | Calculate Calmar ratio (annualized return / max drawdown). | `returns: value` |
| `max_drawdown` | 2 | Calculate maximum drawdown from equity curve or returns. | `values: value` |
| `sortino_ratio` | 3 | Calculate Sortino ratio (excess return per unit of downside risk). | `returns: value` |
| `sharpe_ratio` | 2 | Calculate Sharpe ratio (excess return per unit of total risk). | `returns: value` |
| `beta` | 2 | Calculate portfolio beta to benchmark. | `portfolio_returns: value`, `benchmark_returns: value` |
| `var` | 4 | Calculate parametric Value at Risk (VaR) assuming normal distribution. | `position_value: value`, `volatility: value` |
| `stress_test` | 3 | Apply caller-supplied asset price shocks to portfolio positions. | `positions: value`, `scenarios: value`, `opts: value` |
| `liquidation_headroom` | 2 | Calculate liquidation-price and maintenance-margin headroom. | `position: value`, `margin_inputs: value` |
| `check_limits` | 2 | Check if positions comply with risk limits. | `positions: value`, `limits: value` |
| `max_position_size` | 2 | Calculate maximum position size based on risk limits. | `account_size: value` |
| `concentration` | 1 | Calculate portfolio concentration metrics (max weight, HHI, top-3). | `positions: value` |

# `concentration_metrics`

```elixir
@type concentration_metrics() :: %{max: float(), hhi: float(), top3: float()}
```

Concentration metrics for portfolio analysis

# `delta_position`

```elixir
@type delta_position() :: %{
  optional(:symbol) =&gt; String.t() | nil,
  optional(:delta) =&gt; number() | nil,
  optional(:notional) =&gt; number() | nil
}
```

Position with delta exposure for directional risk calculations

# `liquidation_position`

```elixir
@type liquidation_position() :: %{
  side: :long | :short,
  margin_mode: :isolated | :cross,
  mark_price: number(),
  liquidation_price: number()
}
```

Caller-supplied liquidation position inputs

# `margin_inputs`

```elixir
@type margin_inputs() :: %{
  :collateral =&gt; number(),
  :maintenance_requirement =&gt; number(),
  optional(:margin_mode) =&gt; :isolated | :cross,
  optional(:liquidation_threshold_pct) =&gt; number(),
  optional(:maintenance_threshold_pct) =&gt; number()
}
```

Caller-supplied collateral, maintenance, and alert threshold inputs

# `shock_scenario`

```elixir
@type shock_scenario() :: %{
  asset: term(),
  type: :relative | :absolute,
  value: number(),
  units: :fraction | :price
}
```

Price shock applied to one asset

# `stress_metrics`

```elixir
@type stress_metrics() :: %{
  baseline_exposure: float(),
  stressed_exposure: float(),
  exposure_change: float(),
  pnl: float()
}
```

Aggregated baseline, stressed exposure, and P&L metrics

# `stress_position`

```elixir
@type stress_position() :: %{
  :asset =&gt; term(),
  :venue =&gt; term(),
  :kind =&gt; atom(),
  optional(:quantity) =&gt; number(),
  optional(:mark_price) =&gt; number()
}
```

Position accepted by stress_test/3

# `valued_position`

```elixir
@type valued_position() :: %{optional(:symbol) =&gt; String.t(), value: number()}
```

Position with value for risk calculations

# `violation`

```elixir
@type violation() ::
  {:max_position, String.t() | nil, number(), number()}
  | {:max_concentration, float(), float()}
  | {:max_total_exposure, number(), number()}
```

Risk limit violation tuple

# `beta`

```elixir
@spec beta([number()], [number()]) :: float() | nil
```

Calculate portfolio beta to benchmark.

## Parameters

  * `portfolio_returns` - List of portfolio returns (value)
  * `benchmark_returns` - List of benchmark returns (same length) (value)

## Returns

Beta coefficient (1.5 = moves 1.5x benchmark), or nil if insufficient data (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  params: %{
    portfolio_returns: %{description: "List of portfolio returns", kind: :value},
    benchmark_returns: %{
      description: "List of benchmark returns (same length)",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Beta coefficient (1.5 = moves 1.5x benchmark), or nil if insufficient data"
  },
  returns_example: 0.1095
}
```

# `calmar_ratio`

```elixir
@spec calmar_ratio([number()], pos_integer()) :: float() | nil
```

Calculate Calmar ratio (annualized return / max drawdown).

## Parameters

  * `returns` - List of period returns (value)

## Options

  * `periods_per_year` - Periods per year for annualization (default: `365`)

## Returns

Calmar ratio, or nil if insufficient data or zero drawdown (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  opts: %{
    periods_per_year: %{
      default: 365,
      type: :integer,
      description: "Periods per year for annualization"
    }
  },
  params: %{returns: %{description: "List of period returns", kind: :value}},
  returns: %{
    type: :float,
    description: "Calmar ratio, or nil if insufficient data or zero drawdown"
  },
  returns_example: 0.1095
}
```

# `check_limits`

```elixir
@spec check_limits(
  [valued_position()],
  keyword()
) :: {:ok, [valued_position()]} | {:error, [violation()]}
```

Check if positions comply with risk limits.

## Parameters

  * `positions` - List of positions with :value field (value)
  * `limits` - Keyword list with :max_position, :max_concentration, :max_total_exposure (value)

## Returns

\{:ok, positions\} if compliant, \{:error, violations\} with list of violated limits (`tuple`)

### Example

```elixir
{:ok, [%{value: 50000.0, symbol: "BTC/USDT"}]}
```

## Errors

  * `:max_position`
  * `:max_concentration`
  * `:max_total_exposure`

```elixir
# descripex:contract
%{
  params: %{
    limits: %{
      description: "Keyword list with :max_position, :max_concentration, :max_total_exposure",
      kind: :value
    },
    positions: %{
      description: "List of positions with :value field",
      kind: :value
    }
  },
  errors: [:max_position, :max_concentration, :max_total_exposure],
  returns: %{
    type: :tuple,
    description: "{:ok, positions} if compliant, {:error, violations} with list of violated limits"
  },
  returns_example: {:ok, [%{value: 50000.0, symbol: "BTC/USDT"}]}
}
```

# `concentration`

```elixir
@spec concentration([valued_position()]) :: concentration_metrics() | nil
```

Calculate portfolio concentration metrics (max weight, HHI, top-3).

## Parameters

  * `positions` - List of positions with :value field (absolute value) (value)

## Returns

Map with :max (largest weight), :hhi (Herfindahl-Hirschman), :top3 (top 3 combined), or nil (`map`)

### Example

```elixir
%{max: 1.0, hhi: 1.0, top3: 1.0}
```

```elixir
# descripex:contract
%{
  params: %{
    positions: %{
      description: "List of positions with :value field (absolute value)",
      kind: :value
    }
  },
  returns: %{
    type: :map,
    description: "Map with :max (largest weight), :hhi (Herfindahl-Hirschman), :top3 (top 3 combined), or nil"
  },
  returns_example: %{max: 1.0, hhi: 1.0, top3: 1.0}
}
```

# `exposure_by_asset`

```elixir
@spec exposure_by_asset([delta_position()]) :: %{required(String.t()) =&gt; float()}
```

Group positions by base asset and compute delta-weighted exposure per asset.

## Parameters

  * `positions` - List of maps with optional :symbol, :delta, :notional fields (value)

## Returns

Map of %\{asset => net_delta_exposure\} (`map`)

### Example

```elixir
%{"BTC" => 42500.0, "ETH" => -12000.0}
```

```elixir
# descripex:contract
%{
  params: %{
    positions: %{
      description: "List of maps with optional :symbol, :delta, :notional fields",
      kind: :value
    }
  },
  returns: %{type: :map, description: "Map of %{asset => net_delta_exposure}"},
  returns_example: %{"BTC" => 42500.0, "ETH" => -12000.0}
}
```

# `liquidation_headroom`

```elixir
@spec liquidation_headroom(liquidation_position(), margin_inputs()) ::
  {:ok, map()} | {:error, {atom(), atom()}}
```

Calculate liquidation-price and maintenance-margin headroom.

## Parameters

  * `position` - Map with :side, :margin_mode, :mark_price, and venue-supplied :liquidation_price (value)
  * `margin_inputs` - Map with :collateral, venue-supplied :maintenance_requirement, and optional percentage thresholds (value)

## Returns

\{:ok, metrics\} with liquidation/maintenance absolute and percentage headroom, status, and threshold breaches (`tuple`)

### Example

```elixir
{:ok,
 %{
   status: :healthy,
   margin_mode: :isolated,
   liquidation: %{absolute_headroom: 20.0, percentage_headroom: 0.2},
   maintenance: %{
     requirement: 250.0,
     absolute_headroom: 750.0,
     percentage_headroom: 0.75
   },
   threshold_breaches: []
 }}
```

## Errors

  * `:missing_field` - A required position or margin field is absent
  * `:invalid_input` - A field has an unsupported type or value
  * `:non_positive_input` - A price, collateral, or maintenance requirement is not positive
  * `:inconsistent_input` - Position and margin-input margin modes disagree

```elixir
# descripex:contract
%{
  params: %{
    position: %{
      description: "Map with :side, :margin_mode, :mark_price, and venue-supplied :liquidation_price",
      kind: :value
    },
    margin_inputs: %{
      description: "Map with :collateral, venue-supplied :maintenance_requirement, and optional percentage thresholds",
      kind: :value
    }
  },
  errors: [
    missing_field: "A required position or margin field is absent",
    invalid_input: "A field has an unsupported type or value",
    non_positive_input: "A price, collateral, or maintenance requirement is not positive",
    inconsistent_input: "Position and margin-input margin modes disagree"
  ],
  returns: %{
    type: :tuple,
    description: "{:ok, metrics} with liquidation/maintenance absolute and percentage headroom, status, and threshold breaches"
  },
  returns_example: {:ok,
   %{
     status: :healthy,
     margin_mode: :isolated,
     liquidation: %{absolute_headroom: 20.0, percentage_headroom: 0.2},
     maintenance: %{
       requirement: 250.0,
       absolute_headroom: 750.0,
       percentage_headroom: 0.75
     },
     threshold_breaches: []
   }}
}
```

# `max_drawdown`

```elixir
@spec max_drawdown(
  [number()],
  keyword()
) ::
  %{
    max_drawdown: float(),
    peak_index: non_neg_integer(),
    trough_index: non_neg_integer()
  }
  | nil
```

Calculate maximum drawdown from equity curve or returns.

## Parameters

  * `values` - List of portfolio values or cumulative returns (value)

## Options

  * `type` - :values (equity curve) or :returns (will convert) (default: `:values`)

## Returns

Map with :max_drawdown (decimal), :peak_index, :trough_index, or nil (`map`)

### Example

```elixir
%{max_drawdown: 0.18, peak_index: 12, trough_index: 27}
```

```elixir
# descripex:contract
%{
  opts: %{
    type: %{
      default: :values,
      type: :atom,
      description: ":values (equity curve) or :returns (will convert)"
    }
  },
  params: %{
    values: %{
      description: "List of portfolio values or cumulative returns",
      kind: :value
    }
  },
  returns: %{
    type: :map,
    description: "Map with :max_drawdown (decimal), :peak_index, :trough_index, or nil"
  },
  returns_example: %{max_drawdown: 0.18, peak_index: 12, trough_index: 27}
}
```

# `max_position_size`

```elixir
@spec max_position_size(
  number(),
  keyword()
) :: float()
```

Calculate maximum position size based on risk limits.

## Parameters

  * `account_size` - Total account equity (value)

## Options

  * `max_position_pct` - Max single position as fraction of account (default: `0.2`)
  * `max_loss_pct` - Max loss per position as fraction of account (default: `0.02`)
  * `expected_drawdown` - Expected max drawdown fraction (default: `0.2`)

## Returns

Maximum position value in account currency (conservative of two limits) (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  opts: %{
    max_position_pct: %{
      default: 0.2,
      type: :float,
      description: "Max single position as fraction of account"
    },
    max_loss_pct: %{
      default: 0.02,
      type: :float,
      description: "Max loss per position as fraction of account"
    },
    expected_drawdown: %{
      default: 0.2,
      type: :float,
      description: "Expected max drawdown fraction"
    }
  },
  params: %{account_size: %{description: "Total account equity", kind: :value}},
  returns: %{
    type: :float,
    description: "Maximum position value in account currency (conservative of two limits)"
  },
  returns_example: 0.1095
}
```

# `portfolio_delta`

```elixir
@spec portfolio_delta([delta_position()]) :: float()
```

Calculate net delta exposure across all positions.

## Parameters

  * `positions` - List of maps with optional :delta and :notional fields (value)

## Returns

Net delta exposure (positive = net long, negative = net short) (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  params: %{
    positions: %{
      description: "List of maps with optional :delta and :notional fields",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Net delta exposure (positive = net long, negative = net short)"
  },
  returns_example: 0.1095
}
```

# `sharpe_ratio`

```elixir
@spec sharpe_ratio([number()], number()) :: float() | nil
```

Calculate Sharpe ratio (excess return per unit of total risk).

## Parameters

  * `returns` - List of period returns (value)

## Options

  * `risk_free_rate` - Risk-free rate per period (default: `0`)

## Returns

Sharpe ratio, or nil if insufficient data or zero volatility (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  opts: %{
    risk_free_rate: %{
      default: 0,
      type: :number,
      description: "Risk-free rate per period"
    }
  },
  params: %{returns: %{description: "List of period returns", kind: :value}},
  returns: %{
    type: :float,
    description: "Sharpe ratio, or nil if insufficient data or zero volatility"
  },
  returns_example: 0.1095
}
```

# `sortino_ratio`

```elixir
@spec sortino_ratio([number()], number(), number()) :: float() | nil
```

Calculate Sortino ratio (excess return per unit of downside risk).

## Parameters

  * `returns` - List of period returns (value)

## Options

  * `risk_free_rate` - Risk-free rate per period (default: `0`)
  * `target_return` - Minimum acceptable return for downside calc (default: `0`)

## Returns

Sortino ratio, or nil if insufficient data or zero downside deviation (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  opts: %{
    risk_free_rate: %{
      default: 0,
      type: :number,
      description: "Risk-free rate per period"
    },
    target_return: %{
      default: 0,
      type: :number,
      description: "Minimum acceptable return for downside calc"
    }
  },
  params: %{returns: %{description: "List of period returns", kind: :value}},
  returns: %{
    type: :float,
    description: "Sortino ratio, or nil if insufficient data or zero downside deviation"
  },
  returns_example: 0.1095
}
```

# `stress_test`

```elixir
@spec stress_test([stress_position()], [shock_scenario()], keyword()) ::
  {:ok,
   %{
     scenarios: [shock_scenario()],
     by_asset: %{required(term()) =&gt; stress_metrics()},
     by_venue: %{required(term()) =&gt; stress_metrics()},
     portfolio: stress_metrics()
   }}
  | {:error, term()}
```

Apply caller-supplied asset price shocks to portfolio positions.

## Parameters

  * `positions` - Positions with :asset, :venue, and :kind. Linear positions also require :quantity and :mark_price. (value)
  * `scenarios` - Shocks with :asset, :type (:relative or :absolute), :value, and :units (:fraction or :price) (value)
  * `opts` - Optional :repricer arity-2 function returning \{:ok, %\{baseline_exposure: number, stressed_exposure: number\}\} (default: `[]`, value)

## Returns

\{:ok, result\} with scenarios and P&L/exposure grouped by asset, venue, and portfolio (`tuple`)

### Example

```elixir
{:ok,
 %{
   scenarios: [%{type: :relative, value: -0.1, asset: "BTC", units: :fraction}],
   portfolio: %{
     pnl: -10.0,
     baseline_exposure: 100.0,
     stressed_exposure: 90.0,
     exposure_change: -10.0
   }
 }}
```

## Errors

  * `:missing_field` - A required position or scenario field is absent
  * `:invalid_input` - A position or scenario field is invalid
  * `:inconsistent_input` - More than one shock targets the same asset
  * `:repricing_required` - A nonlinear position needs a caller-supplied repricer

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Optional :repricer arity-2 function returning {:ok, %{baseline_exposure: number, stressed_exposure: number}}",
      kind: :value
    },
    positions: %{
      description: "Positions with :asset, :venue, and :kind. Linear positions also require :quantity and :mark_price.",
      kind: :value
    },
    scenarios: %{
      description: "Shocks with :asset, :type (:relative or :absolute), :value, and :units (:fraction or :price)",
      kind: :value
    }
  },
  errors: [
    missing_field: "A required position or scenario field is absent",
    invalid_input: "A position or scenario field is invalid",
    inconsistent_input: "More than one shock targets the same asset",
    repricing_required: "A nonlinear position needs a caller-supplied repricer"
  ],
  returns: %{
    type: :tuple,
    description: "{:ok, result} with scenarios and P&L/exposure grouped by asset, venue, and portfolio"
  },
  returns_example: {:ok,
   %{
     scenarios: [
       %{type: :relative, value: -0.1, asset: "BTC", units: :fraction}
     ],
     portfolio: %{
       pnl: -10.0,
       baseline_exposure: 100.0,
       stressed_exposure: 90.0,
       exposure_change: -10.0
     }
   }}
}
```

# `var`

```elixir
@spec var(number(), number(), float(), pos_integer()) :: float()
```

Calculate parametric Value at Risk (VaR) assuming normal distribution.

## Parameters

  * `position_value` - Total position value (value)
  * `volatility` - Daily volatility (standard deviation) as decimal (value)

## Options

  * `confidence` - Confidence level (0-1) (default: `0.95`)
  * `days` - Time horizon in days (default: `1`)

## Returns

Maximum expected loss at given confidence level (`float`)

### Example

```elixir
0.1095
```

```elixir
# descripex:contract
%{
  opts: %{
    days: %{default: 1, type: :integer, description: "Time horizon in days"},
    confidence: %{
      default: 0.95,
      type: :float,
      description: "Confidence level (0-1)"
    }
  },
  params: %{
    volatility: %{
      description: "Daily volatility (standard deviation) as decimal",
      kind: :value
    },
    position_value: %{description: "Total position value", kind: :value}
  },
  returns: %{
    type: :float,
    description: "Maximum expected loss at given confidence level"
  },
  returns_example: 0.1095
}
```

---

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