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

Range geometry and momentum mechanics over chronologically ordered candles.

Every lookback, smoothing length, and pivot-selection rule is supplied by the
caller. Range boundaries are envelopes over the selected pivots (not mid-lines
through them), and distances to those boundaries are signed. The functions
report quantities only: they do not name chart patterns, apply thresholds, or
emit trading signals.

Pivot prominence is measured within each requested range window. A side with
no more-extreme pivot uses the window border as its search limit. If a pivot
lies on a window border, its own value is that side's base, so its prominence
is zero. `range_geometry/5` itself detects pivots only where the requested
lookback exists on both sides, so it does not return border extrema.

`dss_bressert/3` follows the
[published ProRealTime construction](https://www.prorealcode.com/prorealtime-indicators/dss-bressert-double-smoothed-stochastic/):
stochastic close position, EMA, normalization of that smoothed series against
its own rolling bounds, then a second EMA. This differs from prose descriptions
that instead smooth separate stochastic numerators and denominators.

`stoch_rsi_kd/5` follows the TradingView built-in construction:
`K = SMA(stoch(RSI, RSI, RSI, stochastic_period), k_smoothing)` and
`D = SMA(K, d_smoothing)`. Unlike `stoch_rsi/3`, it returns the smoothed K and
D lines rather than the raw stochastic of RSI.

`dss_bressert_hlc/4` starts with the stochastic position of close within the
rolling high/low range, applies an SMA-seeded EMA, normalizes that smoothed
series within its own rolling range, then applies an SMA-seeded EMA for DSS
and another for its trigger. Unlike close-only `dss_bressert/3`, it uses candle
highs and lows and returns both DSS and trigger values.

Positional OHLCV rows can be converted to the shared candle-map shape with
`ZenQuant.Volatility.normalize_candles/1` before calling this module.

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `dss_bressert_hlc` | 4 | Calculate stoch1 from close within rolling candle lows/highs, smoothed = EMA(stoch1), stoch2 from smoothed within its rolling range, DSS = EMA(stoch2), and trigger = EMA(DSS), using SMA-seeded EMAs; unlike close-only dss_bressert/3, return DSS/trigger pairs. | `candles: exchange_data`, `stochastic_period: value`, `ema_length: value`, `trigger_length: value` |
| `dss_bressert` | 3 | Calculate DSS Bressert using the published double-smoothed construction. | `candles: exchange_data`, `period: value`, `ema_length: value` |
| `stoch_rsi_kd` | 5 | Calculate K = SMA(stoch(Wilder RSI, Wilder RSI, Wilder RSI, stochastic_period), k_smoothing) and D = SMA(K, d_smoothing); unlike stoch_rsi/3, return smoothed K/D pairs. | `candles: exchange_data`, `rsi_period: value`, `stochastic_period: value`, `k_smoothing: value`, `d_smoothing: value` |
| `stoch_rsi` | 3 | Normalize Wilder RSI within a caller-selected rolling RSI range. | `candles: exchange_data`, `rsi_period: value`, `stochastic_period: value` |
| `rsi` | 2 | Calculate Wilder's Relative Strength Index from candle closes. | `candles: exchange_data`, `period: value` |
| `rate_of_change` | 2 | Calculate close-to-close percentage rate of change. | `candles: exchange_data`, `period: value` |
| `range_geometry` | 5 | Measure pivot-envelope range boundaries for every requested window. | `candles: exchange_data`, `windows: value`, `pivot_lookback: value`, `price: value`, `select_pivots: value` |
| `prominence_selector` | 1 | Build a pivot selector using a caller-supplied minimum prominence. | `minimum_prominence: value` |

# `boundary`

```elixir
@type boundary() :: %{
  slope: float(),
  intercept: float(),
  start_value: float(),
  end_value: float()
}
```

An envelope boundary evaluated at both ends of its range window

# `geometry`

```elixir
@type geometry() :: %{
  upper_pivots: [pivot()],
  lower_pivots: [pivot()],
  upper_boundary: boundary() | nil,
  lower_boundary: boundary() | nil,
  upper_slope: float() | nil,
  lower_slope: float() | nil,
  converging?: boolean() | nil,
  width_start: float() | nil,
  width_end: float() | nil,
  distance_to_upper: float() | nil,
  distance_to_lower: float() | nil
}
```

Measured range geometry for one requested window

# `oscillator_point`

```elixir
@type oscillator_point() :: {float(), float()}
```

Two aligned oscillator lines at one chronological observation

# `pivot`

```elixir
@type pivot() :: %{index: non_neg_integer(), price: float(), prominence: float()}
```

A local high or low with topographic prominence in price units

# `pivot_selector`

```elixir
@type pivot_selector() :: ([pivot()] -&gt; [pivot()])
```

Caller-supplied rule that chooses which detected pivots enter the envelope fit

# `dss_bressert`

```elixir
@spec dss_bressert([ZenQuant.Volatility.candle()], pos_integer(), pos_integer()) :: [
  float()
]
```

Calculate DSS Bressert using the published double-smoothed construction.

## Parameters

  * `candles` - Chronological candle maps with a :close field (exchange_data)
  * `period` - Caller-selected stochastic and normalization period (value)
  * `ema_length` - Caller-selected length for both EMA smoothing passes (value)

## Returns

Chronological DSS values on a 0-100 scale (`list`)

### Example

```elixir
[36.2, 48.9, 67.4]
```

```elixir
# descripex:contract
%{
  params: %{
    period: %{
      description: "Caller-selected stochastic and normalization period",
      kind: :value
    },
    candles: %{
      description: "Chronological candle maps with a :close field",
      source: "fetch_ohlcv(symbol) |> ZenQuant.Volatility.normalize_candles()",
      kind: :exchange_data
    },
    ema_length: %{
      description: "Caller-selected length for both EMA smoothing passes",
      kind: :value
    }
  },
  returns: %{
    type: :list,
    description: "Chronological DSS values on a 0-100 scale"
  },
  returns_example: [36.2, 48.9, 67.4]
}
```

# `dss_bressert_hlc`

```elixir
@spec dss_bressert_hlc(
  [ZenQuant.Volatility.candle()],
  pos_integer(),
  pos_integer(),
  pos_integer()
) :: [oscillator_point()]
```

Calculate stoch1 from close within rolling candle lows/highs, smoothed = EMA(stoch1), stoch2 from smoothed within its rolling range, DSS = EMA(stoch2), and trigger = EMA(DSS), using SMA-seeded EMAs; unlike close-only dss_bressert/3, return DSS/trigger pairs.

## Parameters

  * `candles` - Chronological candle maps with :high, :low, and :close fields (exchange_data)
  * `stochastic_period` - Caller-selected period for both stochastic passes (value)
  * `ema_length` - Caller-selected SMA-seeded EMA length for both DSS smoothing passes (value)
  * `trigger_length` - Caller-selected SMA-seeded EMA length for the trigger line (value)

## Returns

Chronological \{DSS, trigger\} pairs on a 0-100 scale, with no threshold or signal classification (`list`)

### Example

```elixir
[{92.08, 86.6}]
```

```elixir
# descripex:contract
%{
  params: %{
    candles: %{
      description: "Chronological candle maps with :high, :low, and :close fields",
      source: "fetch_ohlcv(symbol) |> ZenQuant.Volatility.normalize_candles()",
      kind: :exchange_data
    },
    stochastic_period: %{
      description: "Caller-selected period for both stochastic passes",
      kind: :value
    },
    ema_length: %{
      description: "Caller-selected SMA-seeded EMA length for both DSS smoothing passes",
      kind: :value
    },
    trigger_length: %{
      description: "Caller-selected SMA-seeded EMA length for the trigger line",
      kind: :value
    }
  },
  returns: %{
    type: :list,
    description: "Chronological {DSS, trigger} pairs on a 0-100 scale, with no threshold or signal classification"
  },
  returns_example: [{92.08, 86.6}]
}
```

# `prominence_selector`

```elixir
@spec prominence_selector(number()) :: pivot_selector()
```

Build a pivot selector using a caller-supplied minimum prominence.

## Parameters

  * `minimum_prominence` - Minimum pivot prominence in the same units as candle prices (value)

## Returns

Function ([pivot] -> [pivot]) retaining pivots at or above the supplied threshold (`function`)

### Example

```elixir
"fn pivots -> Enum.filter(pivots, &(&1.prominence >= minimum_prominence)) end"
```

```elixir
# descripex:contract
%{
  params: %{
    minimum_prominence: %{
      description: "Minimum pivot prominence in the same units as candle prices",
      kind: :value
    }
  },
  returns: %{
    type: :function,
    description: "Function ([pivot] -> [pivot]) retaining pivots at or above the supplied threshold"
  },
  returns_example: "fn pivots -> Enum.filter(pivots, &(&1.prominence >= minimum_prominence)) end"
}
```

# `range_geometry`

```elixir
@spec range_geometry(
  [ZenQuant.Volatility.candle()],
  [pos_integer()],
  pos_integer(),
  number(),
  pivot_selector()
) ::
  %{required(pos_integer()) =&gt; geometry()}
  | {:error, {:insufficient_candles, pos_integer()}}
```

Measure pivot-envelope range boundaries for every requested window.

## Parameters

  * `candles` - Chronological candle maps with :high and :low fields (exchange_data)
  * `windows` - Set of trailing window lengths to measure (value)
  * `pivot_lookback` - Bars required on each side of a strict local high or low (value)
  * `price` - Price whose signed distance to each ending boundary is measured (price - end_value; positive means above the boundary) (value)
  * `select_pivots` - Function ([pivot] -> [pivot]) choosing which detected pivots enter each envelope fit; applied independently to upper and lower pivots (value)

## Returns

Map of window to prominent pivots, envelope boundaries, slopes, convergence, widths, and signed price distances; error if a window exceeds the series (`map`)

### Example

```elixir
%{
  60 => %{
    upper_slope: -0.08,
    lower_slope: 0.03,
    converging?: true,
    width_start: 12.4,
    width_end: 5.91,
    distance_to_upper: -1.2,
    distance_to_lower: 2.4
  }
}
```

## Errors

  * `:insufficient_candles`

```elixir
# descripex:contract
%{
  params: %{
    windows: %{
      description: "Set of trailing window lengths to measure",
      kind: :value
    },
    price: %{
      description: "Price whose signed distance to each ending boundary is measured (price - end_value; positive means above the boundary)",
      kind: :value
    },
    candles: %{
      description: "Chronological candle maps with :high and :low fields",
      source: "fetch_ohlcv(symbol) |> ZenQuant.Volatility.normalize_candles()",
      kind: :exchange_data
    },
    pivot_lookback: %{
      description: "Bars required on each side of a strict local high or low",
      kind: :value
    },
    select_pivots: %{
      description: "Function ([pivot] -> [pivot]) choosing which detected pivots enter each envelope fit; applied independently to upper and lower pivots",
      kind: :value
    }
  },
  errors: [:insufficient_candles],
  returns: %{
    type: :map,
    description: "Map of window to prominent pivots, envelope boundaries, slopes, convergence, widths, and signed price distances; error if a window exceeds the series"
  },
  returns_example: %{
    60 => %{
      upper_slope: -0.08,
      lower_slope: 0.03,
      converging?: true,
      width_start: 12.4,
      width_end: 5.91,
      distance_to_upper: -1.2,
      distance_to_lower: 2.4
    }
  }
}
```

# `rate_of_change`

```elixir
@spec rate_of_change([ZenQuant.Volatility.candle()], pos_integer()) :: [float()]
```

Calculate close-to-close percentage rate of change.

## Parameters

  * `candles` - Chronological candle maps with a :close field (exchange_data)
  * `period` - Caller-selected comparison period (value)

## Returns

Chronological percentage rate-of-change values (`list`)

### Example

```elixir
[2.5, -1.2, 0.8]
```

```elixir
# descripex:contract
%{
  params: %{
    period: %{description: "Caller-selected comparison period", kind: :value},
    candles: %{
      description: "Chronological candle maps with a :close field",
      source: "fetch_ohlcv(symbol) |> ZenQuant.Volatility.normalize_candles()",
      kind: :exchange_data
    }
  },
  returns: %{
    type: :list,
    description: "Chronological percentage rate-of-change values"
  },
  returns_example: [2.5, -1.2, 0.8]
}
```

# `rsi`

```elixir
@spec rsi([ZenQuant.Volatility.candle()], pos_integer()) :: [float()]
```

Calculate Wilder's Relative Strength Index from candle closes.

## Parameters

  * `candles` - Chronological candle maps with a :close field (exchange_data)
  * `period` - Caller-selected Wilder smoothing period (value)

## Returns

Chronological RSI values on a 0-100 scale (`list`)

### Example

```elixir
[48.3, 51.7, 55.1]
```

```elixir
# descripex:contract
%{
  params: %{
    period: %{
      description: "Caller-selected Wilder smoothing period",
      kind: :value
    },
    candles: %{
      description: "Chronological candle maps with a :close field",
      source: "fetch_ohlcv(symbol) |> ZenQuant.Volatility.normalize_candles()",
      kind: :exchange_data
    }
  },
  returns: %{
    type: :list,
    description: "Chronological RSI values on a 0-100 scale"
  },
  returns_example: [48.3, 51.7, 55.1]
}
```

# `stoch_rsi`

```elixir
@spec stoch_rsi([ZenQuant.Volatility.candle()], pos_integer(), pos_integer()) :: [
  float()
]
```

Normalize Wilder RSI within a caller-selected rolling RSI range.

## Parameters

  * `candles` - Chronological candle maps with a :close field (exchange_data)
  * `rsi_period` - Caller-selected Wilder RSI period (value)
  * `stochastic_period` - Caller-selected RSI normalization period (value)

## Returns

Chronological StochRSI values on a 0-100 scale (`list`)

### Example

```elixir
[18.4, 42.7, 100.0]
```

```elixir
# descripex:contract
%{
  params: %{
    candles: %{
      description: "Chronological candle maps with a :close field",
      source: "fetch_ohlcv(symbol) |> ZenQuant.Volatility.normalize_candles()",
      kind: :exchange_data
    },
    rsi_period: %{
      description: "Caller-selected Wilder RSI period",
      kind: :value
    },
    stochastic_period: %{
      description: "Caller-selected RSI normalization period",
      kind: :value
    }
  },
  returns: %{
    type: :list,
    description: "Chronological StochRSI values on a 0-100 scale"
  },
  returns_example: [18.4, 42.7, 100.0]
}
```

# `stoch_rsi_kd`

```elixir
@spec stoch_rsi_kd(
  [ZenQuant.Volatility.candle()],
  pos_integer(),
  pos_integer(),
  pos_integer(),
  pos_integer()
) :: [oscillator_point()]
```

Calculate K = SMA(stoch(Wilder RSI, Wilder RSI, Wilder RSI, stochastic_period), k_smoothing) and D = SMA(K, d_smoothing); unlike stoch_rsi/3, return smoothed K/D pairs.

## Parameters

  * `candles` - Chronological candle maps with a :close field (exchange_data)
  * `rsi_period` - Caller-selected Wilder RSI period (value)
  * `stochastic_period` - Caller-selected RSI normalization period (value)
  * `k_smoothing` - Caller-selected SMA length for the K line (value)
  * `d_smoothing` - Caller-selected SMA length for the D line (value)

## Returns

Chronological \{K, D\} pairs on a 0-100 scale, with no threshold or signal classification (`list`)

### Example

```elixir
[{87.68, 90.73}]
```

```elixir
# descripex:contract
%{
  params: %{
    candles: %{
      description: "Chronological candle maps with a :close field",
      source: "fetch_ohlcv(symbol) |> ZenQuant.Volatility.normalize_candles()",
      kind: :exchange_data
    },
    rsi_period: %{
      description: "Caller-selected Wilder RSI period",
      kind: :value
    },
    stochastic_period: %{
      description: "Caller-selected RSI normalization period",
      kind: :value
    },
    k_smoothing: %{
      description: "Caller-selected SMA length for the K line",
      kind: :value
    },
    d_smoothing: %{
      description: "Caller-selected SMA length for the D line",
      kind: :value
    }
  },
  returns: %{
    type: :list,
    description: "Chronological {K, D} pairs on a 0-100 scale, with no threshold or signal classification"
  },
  returns_example: [{87.68, 90.73}]
}
```

---

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