Metadata-Version: 2.4
Name: indicatorPy
Version: 0.2.1
Summary: Yet Another Library for technical indicators, but this time with more stationarity.
Author-email: Mayank Khanna <cryptex.mk@gmail.com>
Project-URL: Homepage, https://github.com/monk-boop/indicatorPy
Project-URL: Bug Tracker, https://github.com/monk-boop/indicatorPy/issues
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: pandas
Requires-Dist: numpy
Requires-Dist: scipy

# indicatorPy

Yet another technical-indicator library — but this one is built around **stationarity**.
Most indicators are normalized (typically through the normal CDF) into a bounded,
zero-centred range, so their output is comparable across instruments and regimes and
is ready to use as a machine-learning feature.

```bash
pip install indicatorPy
```

## Quick start

```python
import numpy as np
from indicators.trend import RSI, ADAPTIVE_BANDPASS
from indicators.volume import MONEY_FLOW

# bring your own OHLCV as numpy arrays / lists
open_, high, low, close, volume = load_my_data()

rsi = RSI().calculate(14, close)                       # centered RSI, ~[-50, +50]
mf  = MONEY_FLOW().calculate(20, high, low, close, volume)

bp   = ADAPTIVE_BANDPASS()
sig  = bp.calculate(0.3, 3, close)                     # Ehlers adaptive bandpass
lead = bp.lead_signal                                  # extra "lead" series it exposes
```

## Conventions (read once, applies to all)

- **Construct, then call:** every class indicator is used as `IND().calculate(...)`.
  A handful are plain functions (`cmma`, `entropy`, `rangeInterQuartileRangeRatio`).
- **Output shape:** `calculate` returns a `numpy` array the same length as the input
  (one value per bar). Plain functions noted below return a scalar or a `pandas` Series.
- **Stationarity / range:** most indicators pass through the standard normal CDF and
  land in a bounded, mean-centred range — usually **`[-50, +50]`** (a few are `[-1, +1]`,
  `[0, 1]`, or `0–100`). The bound is noted per indicator.
- **Warm-up:** the first few bars (until the lookback/filter is satisfied) are filled
  with **`0`** (a few use `NaN` — noted explicitly).
- **Integer params:** numeric lengths are rounded internally via `int(x + 0.5)`, so
  floats are accepted.
- **Variant selector:** some indicators take a string first argument (`var_num`) that
  chooses which output to return (e.g. AROON `"up"`/`"down"`/`"diff"`).
- **Extra attributes:** `ADAPTIVE_BANDPASS` exposes `.lead_signal`; several filters also
  store their output on `.result`.

## Indicators at a glance

**Trend, momentum & normalized oscillators** ·
[RSI](#rsi) · [DETRENDED_RSI](#detrended_rsi) · [STOCHASTIC](#stochastic) ·
[STOCHASTIC_RSI](#stochastic_rsi) · [MA_DIFFERENCE](#ma_difference) · [PRICE_INT](#price_int) ·
[PPO](#ppo) · [MACD](#macd) · [POLY_TREND](#poly_trend) · [ADX](#adx) · [AROON](#aroon) · [FTI](#fti)

**Ehlers cycle & adaptive indicators** ·
[ROOFING_FILTER](#roofing_filter) · [ADAPTIVE_RSI](#adaptive_rsi) ·
[ADAPTIVE_STOCHASTIC](#adaptive_stochastic) · [ADAPTIVE_BANDPASS](#adaptive_bandpass) ·
[EVEN_BETTER_SINEWAVE](#even_better_sinewave) ·
[INVERSE_FISHER_STOCHASTIC](#inverse_fisher_stochastic) · [DECYCLER_OSCILLATOR](#decycler_oscillator)

**Deviation & expectation** ·
[CLOSE_MINUS_MOVING_AVERAGE](#cmma) · [POLY_DEVIATION](#poly_deviation) ·
[PRICE_CHANGE_OSCILLATOR](#price_change_oscillator) · [VARIANCE_RATIO](#variance_ratio)

**Information content** ·
[entropy](#entropy) · [rangeInterQuartileRangeRatio](#rangeinterquartilerangeratio) ·
[ENTROPY](#entropy-rolling) · [MUTUAL_INFORMATION](#mutual_information)

**Volume** ·
[VSA](#vsa) · [INTRADAY_INTENSITY](#intraday_intensity) · [MONEY_FLOW](#money_flow) ·
[REACTIVITY](#reactivity) · [PRICE_VOLUME_FIT](#price_volume_fit) ·
[VOLUME_WEIGHTED_MA_RATIO](#volume_weighted_ma_ratio) · [ON_BALANCE_VOLUME](#on_balance_volume) ·
[VOLUME_INDEX](#volume_index) · [VOLUME_MOMENTUM](#volume_momentum)

**Multi-market** · [JANUS](#janus) — **Utilities** · [CommonMethods](#commonmethods)

---

## Trend, momentum & normalized oscillators

`from indicators.trend import ...`

### RSI

Relative Strength Index, **centred** (raw RSI − 50) so it is stationary.

```python
RSI().calculate(period, close)          # e.g. RSI().calculate(14, close)
```

`period` — RSI lookback (Wilder smoothing). **In:** close · **Out:** array, ≈`[-50, +50]`;
first `min(period, N)` bars are `0`. (When `period == 2` a log transform is applied for near-normality.)

RSI measures momentum by comparing average upward price changes to total average movement (up and down). The algorithm applies Wilder's exponential smoothing to positive and negative price deltas separately, then outputs `100 * upsum / (upsum + dnsum) - 50`, centering the classic 0–100 RSI scale at `0` for stationarity. This places traditional overbought territory (RSI > 70) at centered value **+20** and oversold (RSI < 30) at **−20**, with the zero-crossing at the neutral RSI point. For `period == 2` only, a logarithmic transform is applied to approximate a normal distribution. Typical periods are 14 or higher; the centered variant prioritizes statistical stationarity over the traditional 70/30 thresholds.

### DETRENDED_RSI

A short-period RSI **regressed against** a long-period RSI; the residual isolates
short-term momentum from the longer trend.

```python
DETRENDED_RSI().calculate(short_len, long_len, reg_len, close)   # e.g. (9, 25, 20, close)
```

`short_len` — short RSI length · `long_len` — long/detrender RSI length (must be > short) ·
`reg_len` — regression lookback. **In:** close · **Out:** array; first `long_len + reg_len − 1` bars are `0`.

Detrended RSI removes the trend component from short-period momentum by computing a short-period RSI and a longer-period RSI (the detrender), then linearly regressing the short RSI against the long RSI over a `reg_len`-bar window and outputting the residual: `(short_RSI − mean_short) − coef · (long_RSI − mean_long)`, where `coef` is the regression slope. The result is a zero-mean oscillator: positive values mean momentum is stronger than the trend predicts, negative means weaker. When `short_len == 2`, the short RSI is log-transformed before regression. Requires `long_len > short_len`.

### STOCHASTIC

Position of the close within the high–low range, scaled and centred to `[-50, +50]`.

```python
STOCHASTIC().calculate(lookback, smoothing, high, low, close)    # e.g. (14, 1, high, low, close)
```

`lookback` — range window · `smoothing` — `0` raw, `1` %K, `2` %D (1/3–2/3 EMA passes).
**In:** high, low, close · **Out:** array, `[-50, +50]`; first `lookback − 1` bars are `0`.

The Stochastic oscillator measures where the close sits within the high–low range over the lookback, answering whether price is near overbought or oversold levels. It computes the raw stochastic `(close − min_low) / (max_high − min_low)`, then applies exponential smoothing controlled by `smoothing`: `0` uses raw values, `1` applies one smooth (the %K line, with 1/3–2/3 coefficients), `2` applies a second smooth (%D). The output is scaled and centred to `[-50, +50]`, where **+50** is overbought (close at the high), **−50** is oversold (close at the low), and `0` is neutral. Use `smoothing=1` for a standard %K or `2` for the smoother %D commonly used in strategies.

### STOCHASTIC_RSI

A stochastic applied to the RSI, with optional EMA smoothing.

```python
STOCHASTIC_RSI().calculate(rsi_len, stoch_len, ema_len, close)   # e.g. (14, 14, 3, close)
```

`rsi_len` — RSI period · `stoch_len` — stochastic window · `ema_len` — EMA smoothing
(`≤ 1` = none). **In:** close · **Out:** array, `[-50, +50]`; first `rsi_len + stoch_len − 1` bars are `0`.

Stochastic RSI measures where the current RSI ranks within its own recent range — i.e. whether momentum itself is at an extreme. It first computes RSI (Wilder smoothing) over `rsi_len`, then applies the stochastic formula `100 * (RSI − min_RSI) / (max_RSI − min_RSI) - 50` over a `stoch_len`-bar window of prior RSI values. The result spans `[-50, +50]`: positive = RSI near its recent highs (potential overbought), negative = near recent lows (oversold), `0` = midpoint. Optional EMA smoothing of `ema_len` periods can be applied (`≤ 1` disables it). A tiny epsilon (`1e-60`) guards the denominator when the RSI range is flat.

### MA_DIFFERENCE

ATR-normalized difference between a short and a (lagged) long moving average, CDF-mapped.

```python
MA_DIFFERENCE().calculate(short, long, lag, open_, high, low, close)   # e.g. (10, 20, 0, o, h, l, c)
```

`short`, `long` — MA periods · `lag` — lag applied to the long MA. **In:** OHLC ·
**Out:** array, `[-50, +50]`; first `min(long + lag, N)` bars are `0`.

MA_DIFFERENCE compares a short- and long-term moving average, normalized for volatility into a stationary oscillator. It computes `100 * norm.cdf(1.5 * normalized_diff) - 50`, where `normalized_diff = (short_ma − long_ma) / (sqrt(|lag_adjustment|) · ATR)` and `lag_adjustment = 0.5·(long − short) + lag` (accounting for the temporal offset between the two MAs). The output oscillates in `[-50, +50]` **centered at 0**: above `0` the short MA leads the long (bullish), below `0` it lags (bearish). ATR normalization makes it scale-invariant across volatility regimes. Typical settings: `short` 5–13, `long` 20–50, `lag` 1–10.

### PRICE_INT

Price intensity: candle body `(close − open)` relative to the true range, optionally
EMA-smoothed, then CDF-normalized.

```python
PRICE_INT().calculate(smooth, open_, high, low, close)    # e.g. (20, o, h, l, c)
```

`smooth` — EMA smoothing period (`1` = none). **In:** OHLC · **Out:** array, ≈`[-50, +50]`.

PRICE_INT measures directional intrabar intensity as close-minus-open over a true-range-like denominator. The first bar uses `high − low`; later bars use the max of `high − low`, `high − prior_close`, and `prior_close − low`. The raw ratio is optionally EMA-smoothed (`alpha = 2/(n+1)`, where `n = round(smooth)`), then mapped through the normal CDF as `100 * norm.cdf(0.8 · sqrt(n) · x) - 50`, giving a bounded `(-50, +50)` output centered at `0`. Positive = bullish bars (close above open), negative = bearish, with magnitude reflecting directional strength relative to range. Larger `smooth` both smooths and compresses extremes (the `sqrt(n)` factor). The denominator has a `1e-60` floor against division by zero.

### PPO

Percentage Price Oscillator, `100·(shortEMA − longEMA)/longEMA`, CDF-normalized; when
`smooth > 1` it returns the normalized PPO **minus its own smoothed version**.

```python
PPO().calculate(short, long, smooth, close)    # e.g. (12, 26, 9, close)
```

`short`, `long` — EMA lengths · `smooth` — smoothing of the PPO line. **In:** close ·
**Out:** array, ≈`[-50, +50]`.

The Percentage Price Oscillator compares a short- and long-term EMA of price as a percentage: `100 * (shortEMA − longEMA) / longEMA`. The implementation normalizes that raw PPO through the normal CDF as `100 * cdf(0.2 · raw_PPO) - 50`, bounding values to `[-50, +50]` and taming outliers. If `smooth > 1`, the output becomes the normalized PPO **minus its own exponential smoothing** — i.e. the deviation from its trend. Positive values indicate bullish momentum (short-term strength), negative bearish; zero-crossings flag trend changes, and because the range is normalized, actionable signals come from directional shifts rather than fixed thresholds.

### MACD

EMA difference **normalized by ATR**, CDF-mapped to ≈`[-50, +50]`; when `smooth > 1` the
smoothed value is subtracted back out.

```python
MACD().calculate(short, long, smooth, open_, high, low, close)    # e.g. (12, 26, 9, o, h, l, c)
```

`short`, `long` — EMA lengths · `smooth` — second smoothing pass. **In:** OHLC
(`open_` is accepted but unused) · **Out:** array, ≈`[-50, +50]`; first value is `0`.

MACD measures momentum and trend direction from the gap between a short- and long-term EMA of price. Two EMAs are formed with decay factors `2/(short+1)` and `2/(long+1)`; their difference is normalized by `sqrt(|0.5·(long − short)|) · ATR` (volatility scaling) and mapped through the normal CDF as `100·Φ(x) − 50`, which bounds the output to `[-50, +50]` **centered at 0** (equal EMAs → `Φ(0)=0.5` → `0`). When `smooth > 1`, that output is exponentially smoothed and subtracted from itself, producing a tighter MACD-histogram-style oscillator around zero. Positive = bullish (short-term strength); negative = bearish.

### POLY_TREND

Polynomial (Legendre) trend on log price, normalized by ATR and goodness-of-fit, CDF-mapped.

```python
POLY_TREND().calculate(degree, fit_len, atr_len, open_, high, low, close)   # e.g. (1, 20, 14, o, h, l, c)
```

`degree` — `1` linear / `2` quadratic / `3` cubic (numeric) · `fit_len` — trend window ·
`atr_len` — ATR window. **In:** OHLC · **Out:** array, ≈`[-50, +50]`; first `max(fit_len − 1, atr_len)` bars are `0`.

POLY_TREND fits an orthogonal Legendre polynomial (linear, quadratic, or cubic) to log-prices over the window and reports trend strength as a probability-scaled statistic. The trend coefficient is the dot product of the log-price window with the chosen Legendre basis, normalized by log-scale ATR times `k` (`k = fit_len − 1`, except `k = 2` when `fit_len == 2`) and weighted by the fit's R². It is then scaled by `2.0` and mapped through the normal CDF as `100·Φ(x) − 50`, giving `[-50, +50]` where `0` = no trend, `< 0` = downtrend, `> 0` = uptrend. ATR normalization adapts to volatility, and the R² weighting pulls weak fits toward `0` regardless of raw slope.

### ADX

Average Directional Index — trend **strength** (direction-agnostic) on a `0–100` scale.

```python
ADX().calculate(period, high, low, close)    # e.g. (14, high, low, close)
```

`period` — DM/ATR lookback. **In:** high, low, close · **Out:** array, `0–100`; fully
smoothed from bar `2·period` onward.

ADX measures the **strength** of a trend (not its direction), on a 0–100 scale. It derives directional movement (DM+, DM−) from successive high/low changes — keeping only the larger per bar — and normalizes by True Range to form the directional indicators DI+ and DI−. The raw ADX is `100 · |DI+ − DI−| / (DI+ + DI−)`. The implementation uses three stages: cumulative sums over the first `period` bars, transitional averaging over the next `period − 1`, then exponential smoothing with factor `(period − 1)/period` applied to the directional components **and to the ADX line itself**. Interpretation: ADX 0–25 = weak trend, 50+ = strong; inspect DI+ vs DI− separately for direction. Typical `period` is 14.

### AROON

How recently the highest high / lowest low occurred within the lookback.

```python
AROON().calculate(variant, lookback, high, low)    # e.g. ("up", 25, high, low)
```

`variant` — `"up"`/`"down"` (`0–100`) or `"diff"` (`−100..+100`) · `lookback` — scan window.
**In:** high, low · **Out:** array (range depends on variant).

AROON tracks how recently the highest high and lowest low occurred within the window: `Aroon_Up = 100 · (lookback − bars_since_highest_high) / lookback` and `Aroon_Down = 100 · (lookback − bars_since_lowest_low) / lookback`, each in 0–100; the `"diff"` variant returns `Up − Down` (−100..+100). Values near 100 mean a very recent extreme (strong directional bias); near 0 means the extreme was `lookback` bars ago (waning trend). In the diff variant, zero-crossings mark momentum reversals. The first bar is seeded to 50 (up/down) or 0 (diff) for lack of history. Typical `lookback` is 14 or 25.

### FTI

Govinda Khalsa's **Follow-Through Index** — finds the dominant cycle by testing symmetric
lowpass filters over a band of periods.

```python
FTI().calculate(variant, lookback, half_len, min_period, max_period, close)   # e.g. ("best_fti", 128, 32, 5, 65, close)
```

`variant` — `"lowpass"` / `"best_period"` / `"best_width"` / `"best_fti"` · `lookback` —
processing block · `half_len` — FIR half-width (`2·half_len ≥ max_period`) · `min_period`,
`max_period` — period band. **In:** close · **Out:** array (`"best_fti"` ≈`[-50, +50]`);
first `min(lookback − 1, N)` bars are `0`.

The Follow-Through Index gauges trend strength by comparing the mean size of significant price moves (turning-point "legs") to the channel width. It applies a symmetric FIR lowpass filter at each test period over log prices, finds direction reversals in the filtered signal, and measures legs above a noise threshold (20% of the longest move). FTI is `mean_leg / channel_width` (the width being the beta-fractile of `|price − filtered|` via a partition index with `beta = 0.95`), normalized through the regularized incomplete gamma CDF as `100 · gammainc(2, fti/3) − 50` for a bounded `[-50, +50]` score. It auto-selects the best period in `[min_period, max_period]`, adapting to changing market rhythm. Other variants expose the filtered value, the chosen period, or the channel width.

---

## Ehlers cycle & adaptive

`from indicators.trend import ...` — John Ehlers' cycle toolkit (2013).

### ROOFING_FILTER

Two-pole highpass + Super Smoother lowpass → a clean, roughly zero-centred passband.

```python
ROOFING_FILTER().calculate(lp_period, hp_period, close)    # e.g. (40, 80, close)
```

`lp_period` — Super Smoother period · `hp_period` — highpass period (detrend). **In:** close ·
**Out:** array (also on `.result`); bars `0–1` are `0`.

The Roofing Filter is Ehlers' two-stage bandpass that isolates a chosen frequency band by removing both long-term trend and short-term noise. A two-pole highpass (period `hp_period`) first removes cycles longer than that period (detrending); the result is then smoothed by a Super Smoother lowpass (period `lp_period`) to suppress cycles shorter than that. The output is a roughly zero-centred smooth signal capturing intermediate-band momentum — positive = upward, negative = downward. Ehlers' defaults are `hp_period=80`, `lp_period=40`; widen or narrow the band by adjusting them. The first two bars are `0` (filter initialization starts the loop at index 2).

### ADAPTIVE_RSI

RSI computed over **half the autocorrelation-detected dominant cycle**, Super-Smoothed.

```python
ADAPTIVE_RSI().calculate(avg_len, close)    # e.g. (3, close)
```

`avg_len` — autocorrelation averaging length (Ehlers uses `3`; `0` = per-lag). **In:** close ·
**Out:** array, ≈`[0, 1]` (the Super Smoother can overshoot slightly; overbought 0.7 / oversold 0.3); also on `.result`.

Ehlers' Adaptive RSI tunes the RSI period to the price series' dominant cycle. The price is first run through a roofing filter — a 48-bar two-pole highpass followed by a 10-bar Super Smoother — then an autocorrelation periodogram detects the dominant cycle each bar. RSI is computed on the filtered data over **half** the detected cycle, as `up_sum / (up_sum + down_sum)` (sums of positive/negative filtered differences), and smoothed again with the Super Smoother. Output sits near `[0, 1]` with 0.7/0.3 as overbought/oversold and ~0.5 neutral. Set `avg_len` to `3` (Ehlers' default) or `0` to average each lag over its own length.

### ADAPTIVE_STOCHASTIC

Stochastic over **one full dominant cycle** (roofing-filtered), Super-Smoothed.

```python
ADAPTIVE_STOCHASTIC().calculate(avg_len, close)    # e.g. (3, close)
```

`avg_len` — autocorrelation averaging length (`3`; `0` = per-lag). **In:** close ·
**Out:** array, ≈`[0, 1]` (can overshoot slightly; 0.7 / 0.3 guides); also on `.result`.

Measures overbought/oversold conditions using a stochastic computed over the dominant cycle rather than a fixed lookback. The price is roofing-filtered (48-bar highpass + 10-bar Super Smoother), an autocorrelation periodogram detects the dominant cycle length each bar, and the stochastic `(current − lowest) / (highest − lowest)` is taken over that cycle window and Super-Smoothed. Output is approximately `[0, 1]` (the Super Smoother can overshoot slightly), with 0.7/0.3 as Ehlers' overbought/oversold guides. `avg_len` controls autocorrelation averaging (typical `3`, or `0` for per-lag). In degenerate flat windows during warm-up, the previous stochastic value is held.

### ADAPTIVE_BANDPASS

Bandpass filter **tuned to the dominant cycle**, normalized by a fast AGC peak detector.

```python
bp   = ADAPTIVE_BANDPASS()
sig  = bp.calculate(bandwidth, avg_len, close)    # e.g. (0.3, 3, close)
lead = bp.lead_signal                             # the advanced "lead" series
```

`bandwidth` — fraction of centre frequency (Ehlers `0.3`) · `avg_len` — autocorrelation
averaging (`3`; `0` = per-lag). **In:** close · **Out:** Signal array ≈`[-1, +1]`; **also sets
`.lead_signal`** (a more aggressively-AGC'd lead version) and `.result`.

Isolates the dominant cyclical oscillation in price. A roofing filter (48-bar highpass + 10-bar Super Smoother) extracts the cyclic component; an autocorrelation periodogram detects the dominant period each bar; a bandpass filter tuned to 90% of that period isolates its energy; and a fast AGC (decaying peak detector, decay `0.991`) normalizes the result to roughly `[-1, +1]`. The returned **Signal** oscillates around zero as cycles strengthen and weaken. The `.lead_signal` attribute is an *advanced* companion (`1.3×` the 4-bar-average difference of the signal, normalized by its own AGC with decay `0.93` and scaled by `0.7`) — useful as an early cross against the signal. `bandwidth` (≈0.3) sets filter width; `avg_len` (≈3, or 0) sets autocorrelation averaging. ~2-bar warm-up.

### EVEN_BETTER_SINEWAVE

Ehlers' EBSW: one-pole highpass + Super Smoother, power-normalized; hugs `+1` in uptrends
and `−1` in downtrends, crossing zero at turns.

```python
EVEN_BETTER_SINEWAVE().calculate(duration, close)    # e.g. (40, close)
```

`duration` — highpass period (≈`20` swing, `40` default, `160` long trend). **In:** close ·
**Out:** array, `[-1, +1]`; also on `.result`.

EVEN_BETTER_SINEWAVE detects trend direction and mode changes by separating price into cyclical and noise components. A one-pole highpass removes cycles longer than `duration`, followed by a 10-bar Super Smoother that damps short-period noise. The smoothed wave is then power-normalized: `output = wave / sqrt(pwr)`, where `wave` and `pwr` are 3-bar rolling averages of the smoothed amplitude and its square. The output is bounded to ≈`[-1, +1]`, hugging `+1` in uptrends, `−1` in downtrends, and crossing zero at reversals. Use `duration ≈ 40` (default), ~20 for swing trading, ~160 for long-term trends; valid after ~2 warm-up bars.

### INVERSE_FISHER_STOCHASTIC

Inverse Fisher transform `tanh(3v)` of the Adaptive Stochastic — saturates hard toward the
`±1` bounds and sharpens transitions.

```python
INVERSE_FISHER_STOCHASTIC().calculate(avg_len, close)    # e.g. (3, close)
```

`avg_len` — autocorrelation averaging length (`3`). **In:** close · **Out:** array, `(-1, +1)`;
also on `.result`.

Applies Ehlers' inverse Fisher transform to the Adaptive Stochastic to sharpen momentum signals. It first computes the Adaptive Stochastic (a dominant-cycle-normalized oscillator of roofing-filtered price, ~0–1), recenters it to `[-1, 1]` (multiply by 2, offset by −1 via the `−0.5` shift), and applies `tanh(3v)` = `(exp(6v)−1)/(exp(6v)+1)`. This nonlinearity saturates output hard toward `±1`, dramatically sharpening swings and quieting noise near zero-crossings. Positive = upward momentum, negative = downward. `avg_len` is the autocorrelation averaging length of the underlying Adaptive Stochastic (default `3`, or `0` for per-lag). Best for spotting sharp regime changes and divergences rather than precise overbought/oversold levels.

### DECYCLER_OSCILLATOR

Difference of two two-pole highpass filters — keeps the trend band between the two periods.

```python
DECYCLER_OSCILLATOR().calculate(hp1, hp2, close)    # e.g. (30, 60, close)
```

`hp1` — shorter highpass period · `hp2` — longer highpass period. **In:** close ·
**Out:** array, zero-centred (price units); also on `.result`; bars `0–1` are `0`.

The Decycler Oscillator (Ehlers, 2013) reveals trend by isolating a frequency band: it takes the difference of two two-pole highpass filters, `output = HP(hp2) − HP(hp1)`. Each highpass passes cycles shorter than its period and attenuates longer ones, so the difference rejects both very short cycles (below `hp1`) and very long cycles (above `hp2`), leaving the intermediate trend-revealing band. The output is zero-centred in price units — sign gives trend direction, magnitude gives strength. Defaults are `hp1=30`, `hp2=60`; smaller periods isolate shorter-term trends. Output starts at the third bar (two-bar lookback in the highpass recursion).

---

## Deviation & expectation

`from indicators.deviationExpectation import ...`

### cmma

_(function)_ Close **minus its moving average** (on log price), normalized by a log-ATR
scaled by `sqrt(lookback + 1)`, then CDF-mapped. A mean-reversion gauge.

```python
from indicators.deviationExpectation import cmma
cmma(ohlc, lookback, atr_lookback=168)    # e.g. cmma(df, 14, 168)
```

`ohlc` — pandas DataFrame with `high`, `low`, `close` · `lookback` — MA window ·
`atr_lookback` — ATR window (default `168`). **Out:** pandas Series, `[-50, +50]`; warm-up bars are `0`.

cmma measures the normalized distance between the current log-close and a log-space moving average of the previous `lookback` bars. The deviation `log_close − ma` is normalized by `atr · sqrt(lookback + 1.0)` to account for volatility, then mapped through the normal CDF as `100 · norm.cdf(z) − 50`, giving a bounded `(-50, +50)` probabilistic gauge. Positive = price above its moving average (bullish deviation); negative = below (underperformance). The output is stationary and useful for regime-relative momentum / mean-reversion. The first `max(lookback, atr_lookback)` bars are set to `0` during warm-up.

### POLY_DEVIATION

Deviation of price from a fitted **Legendre polynomial trend**, scaled by the window's RMS
error and CDF-compressed.

```python
POLY_DEVIATION().calculate(degree, lookback, close)    # e.g. ("1", 20, close)
```

`degree` — **string** `"1"`/`"2"`/`"3"` · `lookback` — fit window (min 3/4/5 by degree).
**In:** close · **Out:** array, ≈`[-50, +50]`; leading bars `0`.

POLY_DEVIATION measures how far the current price sits from a polynomial trend fitted to log prices, normalized by the historical fit error. It fits a linear, quadratic, or cubic orthonormal Legendre polynomial to the window and computes the signed deviation `(log_price − predicted) / rms_error` (a z-score against typical fit error). This is mapped through a scaled normal CDF, `100 · norm.cdf(0.6 · z) − 50`, compressing it to ≈`[-50, +50]`. Positive = price above trend (bullish), negative = below; magnitude reflects how extreme the deviation is. Minimum `lookback` is enforced (3/4/5 for degree 1/2/3) for a stable fit; the first `lookback − 1` bars are `0`.

### PRICE_CHANGE_OSCILLATOR

Short- vs long-window **average absolute log price change**, ATR-normalized and CDF-mapped.

```python
PRICE_CHANGE_OSCILLATOR().calculate(short, mult, open_, high, low, close)    # e.g. (14, 2, o, h, l, c)
```

`short` — short window · `mult` — long = `short × mult` (min 2). **In:** OHLC ·
**Out:** array, ≈`[-50, +50]`; first `long` bars are `0`.

The Price Change Oscillator compares short- vs long-term absolute price changes relative to volatility. It takes the **difference** of the mean log-return magnitude over a short window minus that over a longer window (`long = short × mult`), normalized by ATR with an adaptive denominator `0.36 + 1.0/short + 0.7·log(0.5·mult)/1.609`. The normalized difference is mapped through the normal CDF via `100 · Φ(4 · ratio) − 50`, giving ≈`[-50, +50]`. Above `0`, recent price action is more active than the long-term baseline (momentum); below `0` suggests mean reversion. `mult` sets the long/short ratio and is clamped to a minimum of 2; the first `long` bars are `0`.

### VARIANCE_RATIO

Ratio of short- to long-window variance, transformed through the **F-distribution CDF**.

```python
VARIANCE_RATIO().calculate(variant, short, mult, close)    # e.g. ("price", 5, 4, close)
```

`variant` — `"price"` (variance of log prices) or `"change"` (of log changes) · `short` —
short window · `mult` — long = `short × mult`. **In:** close · **Out:** array, `[-50, +50]`; leading bars `0`.

The Variance Ratio contrasts short- vs long-term volatility to flag trending vs mean-reverting regimes. It takes the ratio of variance over `short` to variance over `short × mult`, then maps it through the F-distribution CDF with mode-specific degrees of freedom. In `"change"` mode the raw ratio is used directly with `df=(4, 4·mult)`; in `"price"` mode the ratio is first **scaled by `mult`** and uses `df=(2, 2·mult)`. The result is `100 · CDF − 50`, spanning ≈`[-50, +50]`: positive = short-term volatility exceeds long-term (trending/divergence), negative = consolidation/mean reversion. If the long-window variance is ≤ 0 (rare), the ratio defaults to 1.0. Warm-up is `long − 1` bars (price) or `long` bars (change). Typical: `short` 5–10, `mult` 2–4.

---

## Information content

`from indicators.informationContent import ...`

### entropy

_(function)_ Normalized **Shannon entropy** of the price histogram — `0` = ordered/predictable,
`1` = maximally random.

```python
from indicators.informationContent import entropy
entropy(x)
```

`x` — price series. **Out:** scalar float `[0, 1]`. Skips the first 168 points; bin count adapts to length.

entropy quantifies the disorder of the price distribution — whether prices cluster in narrow ranges (low entropy) or spread across their historical range (high entropy). It histogram-bins the input (after skipping the first 168 warm-up observations), forms bin proportions `p_i`, and sums `−p_i · log(p_i)` over non-empty bins. The bin count adapts: 3 for `n<100`, 5 for `n<1000`, 10 for `n<10000`, 20 for `n≥10000`. Dividing by `log(nbins)` normalizes the result to `[0, 1]`, where `0` = all prices in one bin and `1` = uniform spread. Higher entropy suggests random / trendless action; lower suggests tight, potentially consolidating ranges.

<a id="entropy-rolling"></a>

### ENTROPY

**Rolling** entropy of up/down move patterns ("words"), CDF-normalized — a per-bar series.

```python
ENTROPY().calculate(word_len, mult, close)    # e.g. (2, 3, close)
```

`word_len` — bits per pattern · `mult` — data-requirement multiplier. **In:** close ·
**Out:** array, ≈`[-50, +50]`; first `min(2^word_len · mult, N − 1)` bars are `0`.

ENTROPY measures the information content of price movement by computing the Shannon entropy of up/down bit patterns. Price changes are encoded as binary words of `word_len` bits (1 = rise, 0 = fall) and the Shannon entropy across observed words is computed, normalized to `[0, 1]` by dividing by `log(2^word_len)`. A shaping transform follows — for `word_len == 1` it uses `1 − exp(log(1.00000001 − value)/5)` (hardcoded exponent 5, with the `1e-8` guard against `log(0)`); for `word_len > 1`, `1 − exp(log(1 − value)/word_len)` — centered on an adaptive mean (`0.6` for `word_len==1`, else `1/word_len + 0.35`). The result is mapped through the normal CDF as `100 · norm.cdf(8·(value − mean)) − 50`, giving a `[-50, +50]` oscillator: values near `0` indicate random movement, large deviations flag predictable (low-entropy) patterns.

### rangeInterQuartileRangeRatio

_(function)_ Ratio of the **mean to the interquartile range** — a scale-invariant spread metric.

```python
from indicators.informationContent import rangeInterQuartileRangeRatio
rangeInterQuartileRangeRatio(x)
```

`x` — price/indicator series. **Out:** scalar float. Skips the first 168 points.

This metric relates a series' mean level to its dispersion: `mean / IQR`, where IQR is the 75th-minus-25th percentile spread. A larger absolute value means a larger mean relative to the middle-50% spread (from a strong mean and/or a concentrated distribution). It runs on the window after discarding the first 168 bars as warm-up. Interpretation is context-dependent: a high positive ratio suggests a strong uptrend relative to central dispersion, a low positive one indicates greater relative spread; a negative mean yields a negative ratio (uncommon in raw price series).

### MUTUAL_INFORMATION

Mutual information between the next move and the **preceding up/down pattern**, CDF-normalized.

```python
MUTUAL_INFORMATION().calculate(word_len, mult, close)    # e.g. (3, 2, close)
```

`word_len` — pattern length · `mult` — scaling / min count per bin. **In:** close ·
**Out:** array, `[-50, +50]`; leading bars `0`.

MUTUAL_INFORMATION quantifies how much a history "word" of preceding up/down moves predicts the next move. Prices are converted to binary up/down comparisons, grouped into words of length `word_len`, and mutual information `MI = Σ p(w,c) · log(p(w,c) / (p(w)·p(c)))` is computed over word/next-move combinations, then normalized via `100 · Φ(3·(MI · mult · √word_len − 0.12·word_len − 0.04)) − 50` to a bounded `[-50, +50]`. Positive values mean history biases toward up moves, negative toward down, near-zero = weak predictability. Requires `2^(word_len+1) · mult + 1` warm-up bars; typical `word_len` 1–3, with `mult` requiring that many occurrences per history bin.

---

## Volume

`from indicators.volume import ...`

### VSA

**Volume-Spread Analysis** — regresses bar range (÷ATR) on volume (÷rolling median); the output
is the **residual** (actual minus volume-expected range): positive = unusually wide, negative = compressed.

```python
VSA().calculate(norm_lookback, open_, high, low, close, volume)    # e.g. (14, o, h, l, c, v)
```

`norm_lookback` — ATR / median / regression window. **In:** OHLCV · **Out:** array (on `.result`);
first `2·norm_lookback` bars are `NaN`.

Volume-Spread Analysis relates price range to volume to spot momentum divergences and shifting supply/demand. It normalizes the bar's range `high − low` by ATR (`norm_range`) and volume by its rolling median (`norm_volume`), then runs a rolling linear regression of `norm_range` on `norm_volume` over the lookback. If the regression slope is positive **and** the correlation coefficient magnitude is ≥ 0.2, the output is the deviation of the actual normalized range from its volume-predicted value: positive = wider range than volume implies (potential strength), negative = narrower (potential weakness). Output is `0` when the slope is ≤ 0 or `|r| < 0.2`, and `NaN` during the ~`2·norm_lookback`-bar warm-up.

### INTRADAY_INTENSITY

`100·(2·close − high − low)/(high − low) · volume`, moving-averaged; if `smooth > 1` it is
normalized by smoothed volume (an oscillator).

```python
INTRADAY_INTENSITY().calculate(ma_len, smooth, high, low, close, volume)    # e.g. (14, 1, h, l, c, v)
```

`ma_len` — MA window · `smooth` — volume EMA (`≤ 1` = none). **In:** high, low, close, volume ·
**Out:** array; leading bars `0`.

Intraday Intensity relates where a bar closes within its range to its volume. Each bar computes `100 · (2·close − high − low)/(high − low) · volume`: the intensity component `(2·close − high − low)/(high − low)` is in `[-1, +1]` (×100 → ±100) and is then **multiplied by volume**, so the raw per-bar value is volume-weighted and unbounded. The result is averaged over `ma_len` bars; when `smooth > 1`, it is divided by an EMA of volume (`alpha = 2/(smooth+1)`), recovering a normalized intensity-like oscillator. Positive = bullish (closes high in the range on strong volume), negative = bearish; equal high/low gives `0`. Warm-up zeros run to `min(ma_len − 1 + first_volume, N)`.

### MONEY_FLOW

**Chaikin's Money Flow** — MA of intraday-intensity ÷ MA of volume.

```python
MONEY_FLOW().calculate(lookback, high, low, close, volume)    # e.g. (20, h, l, c, v)
```

`lookback` — averaging window. **In:** high, low, close, volume · **Out:** array; leading bars `0`.

Chaikin's Money Flow measures buying vs selling pressure by combining the close's position in the range with volume. For each bar (from the first non-zero-volume bar) it computes intraday intensity `100 · (2·close − high − low)/(high − low) · volume` — where the close sits in the range (±100) weighted by volume; equal high/low gives `0`. It then divides a `lookback`-period moving average of that intensity by the moving average of volume over the same window. Positive = accumulation (closes in the upper range with volume), negative = distribution. Bounds depend on the volume scale but it oscillates around zero. Warm-up zeros span the first non-zero-volume bar plus `lookback − 1`.

### REACTIVITY

Volume-weighted range "reactivity" × price change, normalized by smoothed range and CDF-mapped.

```python
REACTIVITY().calculate(lookback, mult, high, low, close, volume)    # e.g. (20, 1, h, l, c, v)
```

`lookback` — range / change window · `mult` — smoothing-constant multiplier. **In:** high, low,
close, volume · **Out:** array, `[-50, +50]`; leading bars `0`.

REACTIVITY gauges how aggressively price moves relative to recent volatility and volume. It forms an aspect ratio of the current range vs a smoothed range, scaled by the current-vs-smoothed volume ratio, then multiplies by the `lookback` price change. That raw reactivity is normalized by the smoothed range and mapped through the normal CDF with scale `0.6`, yielding output bounded exactly in `[-50, +50]`. Positive = upward reactivity, negative = downward, near-zero = muted response. The exponential smoothing constant is `alpha = 2.0 / (lookback · mult + 1)`; `lookback` sets the window and `mult` the smoothing weight. Warm-up begins after the first non-zero-volume bar plus `lookback`.

### PRICE_VOLUME_FIT

Rolling **regression slope of `log(close)` on `log(volume)`** — price-to-volume elasticity, CDF-mapped.

```python
PRICE_VOLUME_FIT().calculate(lookback, close, volume)    # e.g. (20, close, volume)
```

`lookback` — regression window. **In:** close, volume · **Out:** array, `[-50, +50]`; leading bars `0`.

PRICE_VOLUME_FIT measures the strength and direction of the price–volume relationship. For each bar it regresses `log(close)` on `log(volume + 1)` over a rolling `lookback` window and maps the regression coefficient through `100 · norm.cdf(9.0 · coef) − 50` to a bounded `[-50, +50]`. Positive = price tends to rise with volume; negative = an inverse relationship; `0` = neutral. The `9.0` scaling plus the CDF turn the raw slope into a stationary, comparable reading. Warm-up is automatic: output is `0` until both the window is full and the first non-zero-volume bar is seen, accommodating data with missing early volume.

### VOLUME_WEIGHTED_MA_RATIO

Ratio of the **volume-weighted MA to the simple MA**, log-scaled and CDF-normalized.

```python
VOLUME_WEIGHTED_MA_RATIO().calculate(lookback, close, volume)    # e.g. (20, close, volume)
```

`lookback` — MA window. **In:** close, volume · **Out:** array, ≈`[-50, +50]`; leading bars `0`.

VOLUME_WEIGHTED_MA_RATIO measures whether volume concentrates at higher or lower prices over the window. It computes `1000 · log(lookback · numer / (total · denom)) / sqrt(lookback)`, where `numer = Σ(volume·close)`, `total = Σ(volume)`, `denom = Σ(close)` — essentially the log-ratio of the volume-weighted average price to the simple average — then maps it through the normal CDF as `100 · cdf(z) − 50`, bounding output to `[-50, +50]`. Positive = volume concentrated on higher prices (potential strength); negative = on lower prices (potential weakness). Output is `0` until enough non-zero-volume data accrues (about `lookback − 1` bars after the first non-zero-volume bar).

### ON_BALANCE_VOLUME

**Signed-volume momentum** over a window, volume-normalized and CDF-mapped to `[-50, +50]`.

```python
ON_BALANCE_VOLUME().calculate(variant, lookback, delta_lag, close, volume)    # e.g. ("normalized", 20, 0, c, v)
```

`variant` — `"normalized"` or `"delta"` (difference vs `delta_lag` bars ago) · `lookback` —
window · `delta_lag` — used only for `"delta"`. **In:** close, volume · **Out:** array, `[-50, +50]`; leading bars `0`.

On-Balance Volume measures the directional flow of volume. Over the last `lookback` bars it accumulates volume as positive when `close > prior close` (buying pressure) and negative when `close < prior close` (selling), divides by total volume, scales by `sqrt(lookback)`, and normalizes through the normal CDF as `100 · cdf(0.6 · value) − 50`, giving a zero-centred `[-50, +50]` reading. Positive = net buying volume, negative = net selling, `0` = balanced. Bars before the first non-zero volume are skipped. With `variant="delta"`, the output switches to a momentum form: the change in this value from `delta_lag` bars earlier.

### VOLUME_INDEX

Sum of log-returns on **volume-up** (`"positive"`) or **volume-down** (`"negative"`) bars,
volatility-normalized and CDF-mapped.

```python
VOLUME_INDEX().calculate(variant, lookback, close, volume)    # e.g. ("positive", 20, c, v)
```

`variant` — `"positive"` / `"negative"` · `lookback` — accumulation window. **In:** close, volume ·
**Out:** array, `[-50, +50]`; leading bars `0`.

VOLUME_INDEX measures price movement that occurs on volume changes. The `"positive"` variant accumulates log returns on bars where volume **rises**; `"negative"` accumulates returns where volume **falls**. The sum is normalized by `sqrt(lookback)` (period-independence), then divided by historical volatility (std of close over a window of at least 250 bars) to adjust for regime, and finally mapped through the normal CDF as `100 · Φ(0.5·x) − 50`, an oscillator in `[-50, +50]` centered at `0`. Positive = sustained moves on increasing (positive) or decreasing (negative) volume; values near `±50` signal strong volume–price alignment. Needs ≥250 bars of history plus the first non-zero-volume position; outputs `0` where data or volatility is insufficient.

### VOLUME_MOMENTUM

Short- vs long-window **average volume**, as a log-ratio, CDF-mapped. (Volume only.)

```python
VOLUME_MOMENTUM().calculate(short, mult, volume)    # e.g. (10, 2, volume)
```

`short` — short window · `mult` — long = `short × mult` (min 2). **In:** volume ·
**Out:** array, `[-50, +50]`; leading bars `0`.

Volume Momentum compares short- to long-term average volume — is volume accelerating or decelerating? It computes `log(short_avg / long_avg)` (where `long = short × mult`), normalized by a scale factor `exp(log(mult)/3.0)`, then mapped through the normal CDF and scaled as `100 · cdf(3 · normalized_log) − 50`, giving a zero-centred reading in ≈`[-50, +50]`. Above `0` = short-term volume strength relative to the long-term average; below `0` = relative weakness. `short` sets the short window and `mult` defines the long one (clamped to a minimum of 2). Leading bars up to the long lookback (after the first non-zero volume) are `0`.

---

## Multi-market

### JANUS

Gary Anderson's **Janus** family — relative strength (RS) and relative momentum (RM) of each
market in a universe against a median index, plus leader/laggard equity, RSS, DOM/DOE and CMA
trend signals. Operates on a **2-D** close matrix.

```python
from indicators.multiMarket import JANUS
JANUS().calculate(var_num, lookback, market_or_smooth, delta_lag, closes)   # e.g. ("rss", 20, 5, 3, closes)
```

`var_num` — output selector (e.g. `"raw_rs"`, `"fractile_rs"`, `"delta_fractile_rs"`, `"rss"`,
`"delta_rss"`, `"dom"`, `"doe"`, `"raw_rm"`, `"fractile_rm"`, `"rs_leader_equity"`,
`"rs_laggard_equity"`, `"rm_ps"`, `"cma_oos"`, `"leader_cma_oos"`, …) · `lookback` — window ·
`market_or_smooth` — 1-based market number for per-market variants (`0` = index for `dom`/`doe`),
or the smoothing length for `rss`/`delta_rss` · `delta_lag` — differencing lag for the `delta_*`
variants · `closes` — **2-D** array `(nbars, n_markets)`. **Out:** array `(nbars,)`; first `lookback` bars are `0`.

JANUS measures how each market in a universe performs relative to the median index. It computes offensive/defensive relative strength as `70.710678 · (market_off/index_off − market_def/index_def)`, where bars are split by whether returns exceeded the period median (offensive ≥ median, defensive < median), capped at ±200. From this it derives Direction of Momentum (DOM) and Direction of Entropy (DOE) — cumulative returns while the relative-strength spread (RSS, the gap between strongest and weakest performers) expands or contracts — and Relative Momentum (RM), the same strength calculation applied to DOM changes (capped at ±300). Both RS and RM are rank-transformed to fractiles (0–1). Variants expose raw/fractile strength, spreads and their changes, out-of-sample leader/laggard equity, and a conditional moving-average (CMA) system that trades only when DOM exceeds its adaptive EMA. `lookback` (≈5–50) sets the strength window; the first `lookback` bars are `0`.

> `JanusEngine` (the underlying engine) is also exported if you want the full set of computed
> series at once rather than one `var_num` at a time.

---

## Utilities

### CommonMethods

`from indicators.common.common import CommonMethods` — shared building blocks used by the
indicators above. These helpers take **no `self`**; call them on the **class** directly:

```python
from indicators.common.common import CommonMethods

tr  = CommonMethods.true_range_series(use_log, high, low, close)            # per-bar true range, len N
atr = CommonMethods.atr_series(use_log, length, open_, high, low, close)    # rolling ATR, len N (NaN before `length`)
a   = CommonMethods.atr_single(use_log, icase, length, open_, high, low, close)   # scalar ATR at bar icase
v   = CommonMethods.variance(use_change, icase, length, prices)             # scalar historical variance
c1, c2, c3 = CommonMethods.legendre_3(n)                                    # orthonormal Legendre coefficients
```

- `use_log` — work in log space (ratios) when truthy, else absolute differences.
- `use_change` — variance of log **changes** (truthy) vs log **prices**.
- In the ATR helpers `open_` is accepted but unused (true range needs only high/low/close).
- `atr_series` is NaN before index `length`; `atr_single` requires `icase >= length`.

`CommonMethods` provides the foundational computations the indicators reuse. `true_range_series`
gives per-bar true range (bar 0 uses only `high − low`; later bars use the max of `high − low`,
`high − prior_close`, `prior_close − low`). The ATR helpers average true range over a `length`
window (arithmetic or log-ratio via `use_log`), with the first valid value at index `length`
(earlier entries are `NaN`). `variance` returns sample variance of log prices or log changes over
the window, and `legendre_3` returns first-, second-, and third-order Legendre coefficients each
normalized to unit length (c3 is orthogonalized against c1). All methods take NumPy arrays and
return new arrays.

---

## License

MIT.
