Indicators

The quaver.strategies.indicators module provides a pure-NumPy library of technical-analysis primitives shared by the bundled strategy engines. All functions accept one-dimensional numpy.ndarray inputs and return arrays of the same length, padding leading positions with NaN when insufficient history is available.

Note

The indicator API has no dependency on pandas, talib, or any other charting library. This keeps the strategy layer light and deterministic across platforms.

Overview

The 21 indicators fall into five families.

Trend / smoothing

sma()

Simple Moving Average — arithmetic mean over a rolling window.

ema()

Exponential Moving Average — weighted toward recent values.

wilder_smooth()

Wilder smoothing — slower EMA variant used in ADX / RSI.

macd()

Moving Average Convergence Divergence (MACD line, signal, histogram).

Volatility / range

true_range()

Per-bar True Range (max of three high / low / prev-close ranges).

atr()

Average True Range — Wilder-smoothed True Range.

bollinger_bands()

Upper, middle, and lower Bollinger Bands.

bollinger_band_width()

Normalised band width — proxy for volatility regime.

donchian()

Donchian channel — rolling max / min of high / low.

keltner()

Keltner channel — EMA ± ATR multiple.

Momentum / oscillators

rsi()

Relative Strength Index — 0–100 momentum oscillator.

adx()

Average Directional Index — trend-strength magnitude.

stochastic()

Stochastic %K and %D oscillators.

cci()

Commodity Channel Index — deviation from typical price.

Volume / flow

obv()

On-Balance Volume — cumulative signed volume.

vwap()

Volume-Weighted Average Price (rolling).

volume_relative()

Current volume as a multiple of recent average.

Statistical helpers

daily_returns()

Bar-over-bar percentage returns.

rolling_max()

Rolling maximum over a window.

rolling_min()

Rolling minimum over a window.

rolling_percentile()

Rolling quantile of a series.

Conventions

  • NaN padding — every indicator pads leading positions with NaN until enough history exists. Strategy code should filter or guard against NaN before making decisions.

  • No look-ahead — indicators only consume values up to and including the current index; the engine layer is responsible for excluding the current bar when point-in-time evaluation is required.

  • Float64 throughout — inputs are expected to be np.float64 arrays. Pass df["close"].to_numpy(dtype=float) from a pandas DataFrame.

Reference

Shared pure-numpy indicator library for strategy engines.

This module provides a collection of technical analysis indicator functions implemented exclusively with NumPy. It is intended to be imported by any strategy that requires price-based signal computation. All functions operate on numpy.ndarray objects and return arrays of the same length as their primary input, padding leading positions with NaN wherever insufficient data exists to produce a meaningful value.

quaver.strategies.indicators.sma(values, period)[source]

Compute the Simple Moving Average using convolution.

Calculates a rolling arithmetic mean of period consecutive elements via numpy.convolve(). Positions 0 through period - 2 are filled with NaN because fewer than period samples are available.

Parameters:
  • values (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of input values.

  • period (int) – Number of elements in the rolling window. Must be >= 1.

Returns:

Array of the same length as values containing the SMA values, with NaN for the first period - 1 positions.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.true_range(high, low, close)[source]

Compute the True Range for each bar.

The True Range at index i is defined as:

TR[i] = max(H[i] - L[i], |H[i] - C[i-1]|, |L[i] - C[i-1]|)

Index 0 is always NaN because no previous close exists.

Parameters:
Returns:

Array of the same length as high containing True Range values, with NaN at index 0.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.wilder_smooth(values, period)[source]

Apply Wilder’s smoothing (RMA) to an array.

The seed value is the arithmetic mean of the first period valid elements (indices 1 through period, because index 0 is NaN for True Range and Directional Movement series). Subsequent values are computed as:

out[i] = out[i-1] * (period - 1) / period + values[i]

Note

CORRECTNESS NOTE: The seed must use numpy.mean(), not numpy.sum(). Using np.sum produces a seed that is period times too large and corrupts all downstream ATR and ADX values.

Parameters:
  • values (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array to smooth. Index 0 is expected to be NaN (consistent with True Range and Directional Movement outputs).

  • period (int) – Smoothing period (Wilder period). Must be >= 1 and the array must contain at least period + 1 elements.

Returns:

Array of the same length as values containing the smoothed values, with NaN for all positions before the first valid seed.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.adx(high, low, close, period=14)[source]

Compute Wilder’s Average Directional Index (ADX) and Directional Indicators.

Implements the full ADX calculation pipeline:

  1. Computes raw Directional Movement (+DM and -DM).

  2. Smooths True Range and Directional Movement with wilder_smooth().

  3. Derives +DI and -DI as percentages of smoothed ATR.

  4. Computes DX from the divergence of +DI and -DI.

  5. Smooths DX with a second Wilder pass to produce the ADX line.

At least 2 * period + 1 bars are required; a tuple of three all-NaN arrays is returned when this threshold is not met.

Parameters:
Returns:

A three-element tuple (adx_arr, plus_di, minus_di) where

  • adx_arr – Wilder-smoothed ADX values (0-100 scale).

  • plus_di – Positive Directional Indicator (+DI, 0-100 scale).

  • minus_di – Negative Directional Indicator (-DI, 0-100 scale).

All three arrays have the same length as high and contain NaN wherever insufficient data is available.

Return type:

tuple[ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]]]

quaver.strategies.indicators.bollinger_bands(close, period=20, num_std=2.0)[source]

Compute Bollinger Bands around a Simple Moving Average.

The middle band is the SMA of close over period bars. The upper and lower bands are offset by num_std population standard deviations (ddof=0) of the same rolling window:

upper  = SMA + num_std * std
middle = SMA
lower  = SMA - num_std * std
Parameters:
  • close (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of closing prices.

  • period (int) – Rolling window length for the SMA and standard deviation. Must be >= 1. Defaults to 20.

  • num_std (float) – Number of standard deviations for the band width. Defaults to 2.0.

Returns:

A three-element tuple (upper, middle, lower) where

  • upper – Upper Bollinger Band array.

  • middle – Middle band (SMA) array.

  • lower – Lower Bollinger Band array.

All three arrays have the same length as close and contain NaN for the first period - 1 positions.

Return type:

tuple[ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]]]

quaver.strategies.indicators.bollinger_band_width(upper, middle, lower)[source]

Compute the Bollinger Band Width (BBW) normalised by the middle band.

Band width is calculated as:

BBW = (upper - lower) / middle

The result is NaN wherever middle equals zero or any of the three input arrays carry a NaN value at that position.

Parameters:
Returns:

Array of BBW values with the same length as upper, containing NaN where middle is zero or any input is NaN.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.rolling_percentile(values, window, percentile)[source]

Compute a rolling percentile over a fixed-size sliding window.

For each position i >= window - 1, the percentile is computed over values[i - window + 1 : i + 1], excluding any NaN elements within the window. Positions before window - 1 are filled with NaN.

Parameters:
  • values (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of input values.

  • window (int) – Number of elements in the rolling window. Must be >= 1 and <= len(values).

  • percentile (float) – Desired percentile in the range [0, 100].

Returns:

Array of the same length as values containing the rolling percentile, with NaN for the first window - 1 positions or whenever the window contains no valid (non-NaN) values.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.daily_returns(close)[source]

Compute bar-over-bar percentage returns.

Returns are defined as:

ret[t] = (close[t] - close[t-1]) / close[t-1]

Index 0 is always NaN because no prior close is available. Positions where the previous close is zero are also set to NaN to avoid division-by-zero artefacts.

Parameters:

close (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of closing prices.

Returns:

Array of the same length as close containing percentage returns, with NaN at index 0 and wherever the previous close equals zero.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.volume_relative(volume, period=20)[source]

Compute the Relative Volume ratio against a simple moving average.

Relative Volume is defined as:

RVOL = volume / SMA(volume, period)

The result is NaN wherever the SMA is zero or NaN (i.e. for the first period - 1 positions).

Parameters:
  • volume (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of bar volume values.

  • period (int) – Rolling window length for the volume SMA. Defaults to 20.

Returns:

Array of the same length as volume containing the relative volume ratio, with NaN for the first period - 1 positions or wherever the SMA is zero.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.atr(high, low, close, period=14)[source]

Compute the Average True Range using a simple moving average.

Calculates the True Range for each bar and then smooths it with a sma() of length period. The first valid ATR value appears at index period (because True Range index 0 is NaN).

Parameters:
Returns:

Array of the same length as high containing ATR values, with NaN for leading positions where insufficient data exists.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.rsi(close, period=14)[source]

Compute the Relative Strength Index (SMA-based).

Uses simple moving averages of gains and losses over period bars:

RSI = 100 - 100 / (1 + avg_gain / avg_loss)

The first valid RSI appears at index period. Returns 100.0 when average loss is zero (all gains).

Parameters:
  • close (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of closing prices.

  • period (int) – Lookback period for gain/loss averages. Must be >= 1. Defaults to 14.

Returns:

Array of the same length as close containing RSI values in [0, 100], with NaN for the first period positions.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.rolling_max(values, period)[source]

Compute the rolling maximum over a fixed-size sliding window.

For each position i >= period - 1, the result is the maximum of values[i - period + 1 : i + 1]. Leading positions are NaN.

Parameters:
  • values (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of input values.

  • period (int) – Number of elements in the rolling window. Must be >= 1.

Returns:

Array of the same length as values containing rolling maximum values, with NaN for the first period - 1 positions.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.rolling_min(values, period)[source]

Compute the rolling minimum over a fixed-size sliding window.

For each position i >= period - 1, the result is the minimum of values[i - period + 1 : i + 1]. Leading positions are NaN.

Parameters:
  • values (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of input values.

  • period (int) – Number of elements in the rolling window. Must be >= 1.

Returns:

Array of the same length as values containing rolling minimum values, with NaN for the first period - 1 positions.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.ema(values, period)[source]

Compute the Exponential Moving Average.

The seed value is the SMA of the first period elements. Subsequent values use the standard EMA recursion:

EMA[i] = close[i] * k + EMA[i-1] * (1 - k)

where k = 2 / (period + 1).

Parameters:
Returns:

Array of the same length as values with NaN for the first period - 2 positions (first valid value at index period - 1).

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.macd(close, fast=12, slow=26, signal=9)[source]

Compute Moving Average Convergence Divergence.

The MACD line is the difference between the fast and slow EMAs. The signal line is an EMA of the MACD line. The histogram is their difference:

macd_line  = EMA(close, fast) - EMA(close, slow)
signal_line = EMA(macd_line, signal)
histogram   = macd_line - signal_line
Parameters:
  • close (ndarray[tuple[Any, ...], dtype[double]]) – One-dimensional array of closing prices.

  • fast (int) – Fast EMA period. Defaults to 12.

  • slow (int) – Slow EMA period. Defaults to 26.

  • signal (int) – Signal EMA period. Defaults to 9.

Returns:

(macd_line, signal_line, histogram) tuple. All arrays have the same length as close.

Return type:

tuple[ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]]]

quaver.strategies.indicators.stochastic(high, low, close, period_k=14, period_d=3)[source]

Compute the Stochastic Oscillator (%K and %D).

%K measures where the close sits relative to the high-low range over period_k bars:

%K = (close - lowest_low) / (highest_high - lowest_low) * 100

%D is the SMA of %K over period_d bars.

Parameters:
Returns:

(%K, %D) tuple. Both arrays have the same length as close.

Return type:

tuple[ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]]]

quaver.strategies.indicators.obv(close, volume)[source]

Compute On-Balance Volume.

OBV is a cumulative total of volume, where volume is added on up-close bars and subtracted on down-close bars:

OBV[0] = NaN
OBV[i] = OBV[i-1] + sign(close[i] - close[i-1]) * volume[i]
Parameters:
Returns:

Array of the same length as close with NaN at index 0.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.vwap(high, low, close, volume)[source]

Compute cumulative Volume-Weighted Average Price.

VWAP is the ratio of cumulative typical-price-weighted volume to cumulative volume:

TP = (high + low + close) / 3
VWAP = cumsum(TP * volume) / cumsum(volume)
Parameters:
Returns:

Array of the same length as close. NaN wherever cumulative volume is zero.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.cci(high, low, close, period=20, scalar=0.015)[source]

Compute the Commodity Channel Index.

CCI measures how far the typical price deviates from its SMA, normalised by mean absolute deviation:

TP  = (high + low + close) / 3
CCI = (TP - SMA(TP, period)) / (scalar * MAD(TP, period))
Parameters:
Returns:

Array of the same length as close with NaN for the first period - 1 positions.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

quaver.strategies.indicators.donchian(high, low, period=20)[source]

Compute Donchian Channels.

The upper channel is the rolling max of high and the lower channel is the rolling min of low. The middle is their average:

upper  = rolling_max(high, period)
lower  = rolling_min(low, period)
middle = (upper + lower) / 2
Parameters:
Returns:

(upper, middle, lower) tuple. All arrays have the same length as high.

Return type:

tuple[ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]]]

quaver.strategies.indicators.keltner(high, low, close, period=20, multiplier=2.0)[source]

Compute Keltner Channels.

The middle band is an EMA of close. Upper and lower bands are offset by a multiple of the ATR:

middle = EMA(close, period)
upper  = middle + multiplier * ATR(high, low, close, period)
lower  = middle - multiplier * ATR(high, low, close, period)
Parameters:
Returns:

(upper, middle, lower) tuple. All arrays have the same length as close.

Return type:

tuple[ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]], ndarray[tuple[Any, ...], dtype[double]]]