Skip to content

12.0.0: lots of new features (~95) - #14

Merged
MathisWellmann merged 115 commits into
mainfrom
new-features
Oct 12, 2026
Merged

MathisWellmann merged 115 commits into
mainfrom
new-features

Conversation

@MathisWellmann

@MathisWellmann MathisWellmann commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

John Ehlers filters

  • HighPass (1 and 2 pole)
  • BandPass
  • UltimateSmoother,
  • UltimateBands
  • Decycler
  • DecyclerOscillator
  • GaussianFilter (1 to 4 poles)
  • Butterworth (2 and 3 poles)
  • EhlersFilter (nonlinear FIR)
  • DSMA (Deviation Scaled Moving Average)
  • EmpiricalModeDecomposition
  • ContinuationIndex
  • John Ehlers cycle measurement and adaptive indicators
  • HomodyneDominantCycle
  • HilbertPhasor
  • SineWave
  • InstantaneousTrendline
  • HilbertTrendMode
  • MAMA / FAMA (MESA Adaptive Moving Average)
  • AutocorrelationPeriodogram
  • EvenBetterSinewave
  • AdaptiveRsi
  • AdaptiveLaguerreFilter
  • AutoTuneFilter
  • AdaptiveSuperSmoother
  • John Ehlers oscillators
  • InverseFisherTransform
  • EhlersStochastic
  • UniversalOscillator
  • ReversionIndex
  • Oscillators
  • MACD
  • PPO
  • APO
  • TRIX
  • TSI
  • CMO
  • StochRSI
  • RSX
  • ConnorsRSI
  • QQE
  • Schaff Trend Cycle
  • DPO
  • Coppock Curve
  • KST (Know Sure Thing)
  • Trend and regression
  • Linear regression slope, intercept, angle, R², TSF, standard error
  • Kaufman Efficiency Ratio
  • VHF (Vertical Horizontal Filter)
  • QuantileNormalizer (rank to normal score), PercentileRank
  • Box-Cox and Yeo-Johnson transforms

Rolling statistics

  • Min, Max (amortized O(1) monotonic deque)
  • RollingQuantile
  • RollingMedian
  • IQR
  • Skewness
  • Kurtosis
  • Coefficient of Variation
  • EWMA variance / volatility

Complexity and dependence

  • Shannon, permutation and sample entropy
  • Hurst exponent (R/S)
  • variance ratio
  • autocorrelation
  • Ornstein-Uhlenbeck half-life

Adaptive estimators and robust filters

  • One Euro filter
  • alpha-beta filter
  • 1D Kalman filter
  • LMS, NLMS and RLS adaptive linear predictors
  • Huber EMA
  • Hampel filter

Change detection

  • CUSUM, Page-Hinkley, ADWIN, BOCPD (Bayesian online changepoint detection)

Risk

  • Value at Risk
  • Conditional Value at Risk
  • Ulcer Index
  • Sharpe ratio
  • Sortino ratio

Max and Min rescanned the whole window when the current extreme was evicted, which is O(window_len) per update in the worst case (e.g. monotone input). They now keep a monotonic deque of (index, value) candidates (Lemire 2006, streaming max-min filter): each value is pushed and popped at most once, so updates are amortized O(1).

Public API and semantics are unchanged: last() is Some after the first value and is the extreme of the last window_len values (of all values during warm-up). Equal values are collapsed to the newest one, which does not change the result. MinMaxNormalizer and the pipeline do not depend on Max/Min internals (pipeline only uses Max::new / Min::new).

Tests compare against a naive rescan on seeded random data, data with many ties, and monotone / constant sequences. Adds benches/max_min.rs (window 1024).
HighPass removes periods longer than cutoff_period. HighPassPoles::One is the first order filter from Ehlers, Cycle Analytics for Traders (2013); HighPassPoles::Two is the second order filter from 'Predictive Indicators for Effective Trading Strategies', the same formula as the high pass stage of RoofingFilter (which uses 4.4422 ~= 0.707*2pi). Other agents need a standalone high-pass building block.

O(1) update and memory. Both orders share one branch-free update with precomputed coefficients.

Deviations / choices: the input history is seeded with the first value, so the filter starts at rest instead of seeing a step from 0 (replaces Ehlers' CurrentBar guard). last() is None for the first cutoff_period - 1 values, as in SuperSmoother. The constructor asserts cutoff_period >= 5 (one pole) / >= 3 (two poles), because otherwise cos(angle) <= 0 and a1 leaves (0, 1), giving an unstable or degenerate filter.

Adds benches/ehlers_filters.rs.
BandPass passes cycles near the center period with unit gain and attenuates trend and noise. Formula from Ehlers, Cycle Analytics for Traders (2013) ch. 5 and Ehlers & Way, Empirical Mode Decomposition (2010): L1 = cos(2pi/P), G1 = cos(delta*2pi/P), S1 = 1/G1 - sqrt(1/G1^2 - 1), BP = 0.5(1 - S1)(x - x[2]) + L1(1 + S1)BP[1] - S1*BP[2].

O(1) update and memory.

Deviations / choices: S1 is computed as cos(theta) / (1 + sin(theta)), which is algebraically equal but avoids cancellation when G1 is near 0. The EMD article's delta is a half bandwidth (720*delta); here bandwidth follows the book (360*bandwidth). The book's pre high-pass, AGC and trigger line are not included (they could not be verified against the source and the output should keep the input scale); they can be composed with HighPass and other views. Input history is seeded with the first value (filter starts at rest), last() is None for the first period - 1 values. Asserts period >= 2, 0 < bandwidth < 1 and 4*bandwidth < period (G1 > 0).
Wma weights the last n values 1..n, newest heaviest, and divides by n(n+1)/2. Other indicators (e.g. Hull MA) need it as a building block.

Update is amortized O(1): a running sum S and weighted sum W are updated with W' = W - S + n*x and S' = S - oldest + x. Because W - S accumulates rounding error over long series, both sums are recomputed exactly from the window every window_len evictions (O(n) every n steps, O(1) amortized). last() is None until the window is full, like Sma.

Adds benches/moving_averages.rs.
…elper

RollingQuantile computes the q-quantile of the last window_len values with linear interpolation between order statistics (Hyndman & Fan type 7, default of R and numpy). q = 0 / 0.5 / 1 give min / median / max. last() is None until the window is full; q outside [0, 1] (or NaN) panics in the constructor.

The order statistics live in the new crate-private SortedWindow<T>: a FIFO VecDeque plus a sorted Vec maintained with binary-search insert/remove. Per push this is O(log n) search plus an O(n) memmove, which is fast for typical windows and much simpler than an order-statistic tree; queries are O(1) (quantile/median) or O(log n) (count_less/count_less_equal). A Vec (not VecDeque) is used so sorted() can return a contiguous slice. SortedWindow is meant to be reused by other indicators (median, percentile rank, ...). Methods not yet used by library code are #[cfg(test)] to avoid dead_code warnings; remove the attribute when using one. MadScaler is intentionally not refactored here.

Adds benches/statistics.rs.
Wilder's smoothing (RMA/SMMA): alpha = 1/n, seeded with the SMA of the
first n values per the TA-Lib convention (Wilder's original seeds with the
first value; the difference washes out after a few window lengths).
Reference: J. W. Wilder Jr., New Concepts in Technical Trading Systems
(1978). O(1) per update after warm-up.
DEMA = 2*EMA(x) - EMA(EMA(x)), reducing lag vs a plain EMA at the cost of
overshoot on reversals (Patrick Mulloy, Technical Analysis of Stocks &
Commodities, 1994). Composes two internal Ema<Echo> stages fed by the
chained input. O(1) per update, O(1) memory.
TEMA = 3*EMA(x) - 3*EMA(EMA(x)) + EMA(EMA(EMA(x))), reducing lag further
than DEMA at the cost of more overshoot on reversals (Patrick Mulloy,
Technical Analysis of Stocks & Commodities, 1994). Composes three internal
Ema<Echo> stages. O(1) per update, O(1) memory.
Tillson's T3 moving average: six chained EMAs combined with the standard
c1..c4 coefficients derived from the volume factor v (default 0.7, asserted
in [0, 1]) (Tim Tillson, Technical Analysis of Stocks & Commodities, 1998;
TA-Lib's T3). O(1) per update, O(1) memory.
Zero-lag EMA (Ehlers & Way, Technical Analysis of Stocks & Commodities,
2010): EMA of (2x - x[lag]) with lag = (n-1)/2, floored. Composes an
internal Lag<Echo> and Ema<Echo>. O(1) per update, O(lag) memory.
Hull Moving Average (Alan Hull, 2005): WMA(2*WMA(n/2) - WMA(n),
round(sqrt(n))), with n/2 and sqrt(n) rounded half away from zero (some
implementations floor n/2 instead). Requires n >= 2. Composes three
internal Wma<Echo> stages. O(1) per update, O(n) memory.
Exponential Hull MA: the same max(n, n/2) + sqrt(n) construction as Hma,
but with every WMA stage replaced by an EMA. n/2 and sqrt(n) rounded half
away from zero, n >= 2 required. Composes three internal Ema<Echo> stages.
O(1) per update, O(1) memory.
Triangular MA (TA-Lib TRIMA): implemented as a double SMA per TA-Lib's
odd/even split (p=q=(n+1)/2 for odd n; p=n/2, q=p+1 for even n), giving a
triangular weight shape of width n without computing the weights directly.
Composes two internal Sma<Echo> stages. O(1) per update, O(n) memory.
McGinley Dynamic (John R. McGinley, Technical Analysis of Stocks &
Commodities, 1997): MD = MD[1] + (x - MD[1]) / (k*n*(x/MD[1])^4), seeded
with the first value, k default 0.6. Requires strictly positive input.
Unlike windowed averages, ready from the first update (no warm-up). O(1)
per update, O(1) memory.
Kaufman Efficiency Ratio (Perry J. Kaufman, Smarter Trading, 1995): ER =
|x - x[n]| / sum(|x[i]-x[i-1]|) over window_len, in [0, 1] by the triangle
inequality. 0/0 (a flat window) is defined as 0. O(1) per update via two
bounded VecDeques, O(window_len) memory.
Kaufman's Adaptive Moving Average (Perry J. Kaufman, Smarter Trading,
1995): sc = (ER*(fast_sc - slow_sc) + slow_sc)^2, fast/slow periods default
2/30, reusing KaufmanEfficiencyRatio internally. Seeded with the first
value for which ER is ready. O(1) per update, O(window_len) memory.
Chande Momentum Oscillator (Tushar Chande, The New Technical Trader,
1994): 100*(sum_up - sum_down)/(sum_up + sum_down) over window_len, in
[-100, 100]. 0/0 (a flat window) is defined as 0. O(1) per update via a
bounded VecDeque of (up, down) pairs, O(window_len) memory.
Chande's Variable Index Dynamic Average (Tushar Chande, 'Adapting Moving
Averages To Market Volatility', TASC 1992): an EMA whose smoothing factor
alpha * k scales with the inner CMO's volatility index k = |CMO| / 100 in
[0, 1], so it moves close to the input during a strong trend and barely
moves in a flat/choppy market.

Reference: VIDYA = alpha * k * x + (1 - alpha * k) * VIDYA[1], alpha = 2 /
(window_len + 1), seeded with the first value for which the inner CMO is
ready (after window_len + 1 inputs). Reuses the existing
sliding_windows::Cmo<T, Echo<T>> internally rather than re-deriving the
up/down move sums.

Complexity: O(1) per update (the inner CMO is O(1) amortized); O(window_len)
memory for its window.

Deviation: tested the naive-reference step-by-step formula against a gentle
multiplicative random walk rather than i.i.d. uniform noise, matching the
convention already used for McGinleyDynamic and Kama, since CMO-driven
smoothing factors are more representative of real price series that way.
Also added a dedicated test asserting VIDYA freezes exactly when the
volatility index is 0 (fully flat window), which is the degenerate case
unique to this indicator.
Mark Jurik's JMA ('JMA - Jurik Moving Average', Technical Analysis of
Stocks & Commodities, 1999): a low-lag adaptive three-stage filter.

Reference: Jurik Research has never published the original algorithm, so
this implements the widely-circulated open reconstruction (beta/alpha/
phase_ratio filter chain over e0/e1/e2) independently confirmed across
pandas-ta's jma() core loop, TradingView Pine jma() libraries and MQL
ports, itself reverse-engineered from
https://c.mql5.com/forextsd/forum/164/jurik_1.pdf. This is the simplified,
non-adaptive-volatility variant (period/phase/power only); it is a close
approximation, not a bit-exact match of Jurik's proprietary binary.

Complexity: O(1) per update, O(1) memory - no window buffer is needed, only
three running filter states (e0, e1, e2).

Deviation: per task instructions, JMA was only attempted because the
three-stage phase/power reconstruction above is confidently and
consistently documented across multiple independent open-source ports (not
just one blog post), unlike some other 'community JMA' variants that
reconstruct Jurik's additional adaptive-volatility bands, which differ more
between implementations and are not attempted here. phase is clamped to
[-100, 100] rather than rejected (matching every reference above); power is
asserted to be in 1..=4 (the range used by existing open crates) since
larger exponents make alpha collapse towards 0 and the filter degenerate.
MACD (Appel, 1979): EMA(fast) - EMA(slow), signal = EMA(macd, signal_period), histogram = macd - signal. Classic defaults 12/26/9. Composes the existing Ema view internally (Ema<T, Echo<T>>) for fast/slow/signal, each O(1) per update, O(1) memory. Creates benches/oscillators.rs (this group's bench file) with one criterion benchmark_group per indicator; registers it in Cargo.toml.
Percentage Price Oscillator: 100*(EMA(fast)-EMA(slow))/EMA(slow), signal = EMA(ppo, signal_period), histogram = ppo - signal. Deviation: TA-Lib's PPO defaults to SMA; here EMA is always used (matches the common MACD-style PPO on most charting platforms), documented in the struct doc. O(1) per update via three chained Ema<T, Echo<T>>. Adds the ppo benchmark_group to benches/oscillators.rs.
Absolute Price Oscillator (TA-Lib APO): EMA(fast) - EMA(slow), no signal line. Classic defaults 12/26. O(1) per update via two chained Ema<T, Echo<T>>. Adds the apo benchmark_group to benches/oscillators.rs.
TRIX (Hutson, 1983; TA-Lib TRIX): 1-bar percent rate of change of a triple-cascaded EMA (EMA(EMA(EMA(x,n),n),n)). Warm-up gates each stage on the previous being ready, matching Macd/Ppo. O(1) per update via three chained Ema<T, Echo<T>>. Adds the trix benchmark_group to benches/oscillators.rs.
True Strength Index (William Blau, 1991): 100 * EMA(EMA(m,r),s) / EMA(EMA(|m|,r),s), m = x - x_prev; defaults r=25, s=13. Deviation: adds a signal line (EMA of TSI, default 7-period) via Tsi::signal, an extension not in Blau's original definition but common on charting platforms; documented in the struct doc. 0/0 is defined as 0. O(1) per update via four chained Ema<T, Echo<T>> plus the signal EMA. Adds the tsi benchmark_group to benches/oscillators.rs.
Stochastic RSI (Chande & Kroll, 1994; TA-Lib STOCHRSI fastk): (RSI - min(RSI,n)) / (max(RSI,n) - min(RSI,n)), %D = SMA(%K, d_period). Deviation: kept in [0, 1] instead of TA-Lib's *100 scaling (documented). Flat RSI window (max == min) defines %K as 0. Reuses Rsi, the monotonic Max/Min deques and Sma internally, all O(1) amortized per update. Adds the stoch_rsi benchmark_group to benches/oscillators.rs.
RSX - Jurik's 'noise free' RSI: a direct transliteration of the widely published open-source RSX recursion (f8..f90, matching the ProRealTime and everget/Pine Script ports byte-for-byte in formula terms), triple-smoothing momentum and |momentum| through cascaded EMA pairs blended as 1.5*fast - 0.5*slow, then ratioed and scaled to [0, 100]. Deviation: last() is Some from the first update (the formula defines its own short, period-independent internal warm-up that outputs the neutral 50), unlike this crate's usual None-during-warmup convention; documented in the struct doc. O(1) per update, O(1) memory (13 scalar state variables, no buffers).
Schaff Trend Cycle (Doug Schaff, 1999), the classic 'double stochastic of MACD' variant used by most MT4/MT5 ports: MACD(23,50) -> stochastic over a 10-bar cycle, smoothed with factor 0.5 -> stochastic again over the same cycle, smoothed with factor 0.5. Flat stochastic ranges hold the previous raw ratio instead of NaN. Output stays in [0, 100] since 0.5-smoothing is a convex combination of two in-range ratios. Reuses Ema and the monotonic Max/Min deques internally, all O(1) amortized per update. Adds the schaff_trend_cycle benchmark_group to benches/oscillators.rs.
Quantitative Qualitative Estimation (Yuri Reshetov): smooths RSI with an
EMA (SF=5 default) into RsiMa, then builds a volatility band from a
twice Wilder-smoothed ATR-of-RSI (period = 2*rsi_period-1), scaled by
factor (default 4.236). A trailing stop-like level follows RsiMa and
flips trend on a band cross (Qqe::trailing_level getter).

Deviation: the original's trend/band update compares RsiMa against a
one-bar-shifted view of the band series itself (a 'crossover of a
crossover'), fragile to transcribe outside Pine Script. This instead
detects the cross using the previous bar's RsiMa against the previous
bar's band value directly, which matches the original closely while
being well-defined bar-by-bar.

RsiMa is in [0, 100]; last() is None until RSI+EMA warm up, and
trailing_level() additionally needs both Wilder stages to warm up.
O(1) per update, O(1) memory.

Reused the crate's existing sliding-window Rsi (not Wilder-exponential)
for consistency with StochRsi's prior deviation.

Tests: plot, warm-up gating for both last() and trailing_level(),
range check, trailing_level finiteness, constant-input RSI saturation
at 100, and a naive-reference test independently reimplementing the
sliding RSI, EMA, Wilder smoothing and band/trend logic against 5000
random values (max abs diff < 1e-9).
Bayesian online changepoint detection (Adams & MacKay 2007) with constant hazard 1/lambda and a Normal-Inverse-Gamma prior (mu0, kappa0, alpha0, beta0) for unknown mean and variance, Student-t predictive, all probabilities in log space (log-sum-exp) to avoid underflow. The ln Gamma ratio of the Student-t density is carried per run length with the recurrence ln G(a+1) - ln G(a+1/2) = ln a - (ln G(a+1/2) - ln G(a)), so only the constructor evaluates ln Gamma (private Stirling-series helper). Output is the MAP run length; changepoint_probability() is P(r_t <= 1) because P(r_t = 0) always equals the hazard under a constant hazard; expected_run_length() is the posterior mean. Run lengths are truncated at max_run_length: mass growing beyond it is merged into the last bin (keeping the statistics of the more probable merged run) and the distribution renormalized, so the MAP saturates instead of collapsing on long stationary data (deviation from plain drop-and-renormalize). O(max_run_length) time and preallocated memory per update.
Historical (non-parametric) Value at Risk over a sliding window of returns: VaR_c = -Q(1 - c) with the type 7 quantile of the crate-private SortedWindow, so a positive value is a loss. Useful as a tail-risk feature and position sizing input. O(log n) search + O(n) memmove per update, O(1) last(), O(n) memory. Adds benches/risk.rs.
Historical Conditional Value at Risk / expected shortfall (Rockafellar & Uryasev 2002) over a sliding window of returns: -mean of the returns <= the type 7 quantile Q(1 - c) (ties included, the tail is never empty), so CVaR >= VaR (clamped against ulp rounding on all-tie tails). Positive = loss. Captures tail severity that VaR ignores. Uses SortedWindow::sorted and count_less_equal, whose #[cfg(test)] lines are removed. O(n) memmove + O(n (1 - c)) tail sum per update, O(1) last(), O(n) memory.
Peter Martin's Ulcer Index (Martin & McCann 1989) over a window of n prices: sqrt(mean of the last n squared percent drawdowns), each drawdown measured from the classic rolling n-period max ending at that price (not the max within the averaging window), so the first output comes after 2n - 1 prices. Captures depth and duration of drawdowns, unlike volatility. A monotonic deque gives the window max and a running sum the mean square (recomputed every n updates and reset to exactly zero when no drawdown is in the window, as sqrt amplifies rounding residuals): amortized O(1) per update, O(n) preallocated memory.
Per-period ex-post Sharpe ratio (Sharpe 1966/1994) over a sliding window of returns: (mean - risk_free) / sample std (n - 1), not annualized. Risk-adjusted performance feature. O(1) per update with running sums of deviations from a shift value taken from the window (avoids catastrophic cancellation), recomputed and re-centered every window_len updates (amortized O(1)) to bound drift; O(n) memory. Zero std -> None, where zero means the sample variance is below 4 eps times the mean square deviation from the shift (rounding noise, e.g. constant returns). window_len must be >= 2.
Per-period Sortino ratio (Sortino & Price 1994) over a sliding window of returns: (mean - target) / downside deviation, with DD = sqrt(sum(min(0, r - target)^2) / n) over the whole window, not annualized. Penalizes only harmful volatility, unlike Sharpe. O(1) per update with running sums and a count of returns below the target, so the downside sum is exactly zero without downside (-> None, as the ratio is undefined); sums are recomputed every window_len updates (amortized O(1)) to bound drift; O(n) memory. Also exposes downside_deviation().
The crate gained about 95 views (Ehlers filters and cycle tools, classic moving averages and oscillators, regression, rolling statistics, complexity measures, adaptive estimators, change detection, risk). The README list is the entry point for users, so group them by category there.
The DSL can now build the Ehlers filters added to the crate at runtime:
high_pass, band_pass, ultimate_smoother, ultimate_bands, decycler,
decycler_oscillator, gaussian_filter, butterworth, ehlers_filter, dsma,
continuation_index and emd. The pipeline text comes from untrusted
authors (LLMs), so check_params is the validation boundary: parameters
that pass it must never reach a constructor assert.

Infrastructure, for this and the following categories:
- Param::Integer { min, max }: a small integer choice (pole count,
  embedding order), mapped to the view's enum in build. Not a window,
  so warm_up does not sum it.
- Param::Positive: a finite real above 0.
- Optional table fields: check (constraints between parameters and caps
  that keep constructor asserts unreachable; errors are prefixed with the
  signature) and warm_up (overrides the sum of windows for views with
  fixed warm-ups or chained stages).
- Params accessors assert the declared kind, so a wiring mistake in the
  table fails on the first call instead of misreading a value.
- example_params/larger_params (tests): valid parameters at the small
  and larger end. The property tests now run every function at both.
- A table-driven test compares each call with the directly constructed
  view, to catch parameter order and pole mapping mistakes.

Band-pass and Butterworth periods are capped at 100000: their recursions
lose accuracy to rounding beyond (the three-pole Butterworth diverges by
1e7) and narrow band-passes round to an unstable pole. Views above about
200 bytes are boxed in State, so they do not inflate every state slot.
Adds homodyne_dominant_cycle, hilbert_phasor, sine_wave,
instantaneous_trendline, hilbert_trend_mode, mama,
autocorrelation_periodogram, even_better_sinewave, adaptive_rsi,
adaptive_laguerre_filter, auto_tune_filter and adaptive_super_smoother.

- The Hilbert transform views and the autocorrelation periodogram family
  have fixed internals, so they take no parameters and override the
  warm-up (50, 63, 32 bars; 109 and 119 for the periodogram and the
  adaptive RSI, from their filter chains). The periodogram keeps the
  book's periods 10 to 48: its O(L^2) update stays small, and two period
  parameters would need disjoint ranges to be checkable.
- auto_tune_filter costs O(window^2) per update, so its window is capped
  at 100 by a check; mama checks slow limit <= fast limit.
- hilbert_trend_mode requires positive input: its 1.5% trendline rule is
  relative and always says trend near zero.
- Ranges: cycle periods [6, 50] and [10, 48], phasor angle [-180, 180],
  sine waves [-1, 1], trend mode [0, 1]; mama and the instantaneous
  trendline stay within the input range (convex averages). The adaptive
  RSI is real-valued: its super smoother can overshoot [0, 1].
- Views above 200 bytes are boxed in State.

View fix (squashed into 'sliding_windows: add HomodyneDominantCycle'):
the periods are smoothed from zero, so after the 50-bar warm-up they were
still below 6 on constant input (5.9997); the reported periods are now
clamped to [6, 50] as documented.
Adds inverse_fisher, ehlers_stochastic, universal_oscillator and
reversion_index.

- inverse_fisher is stateless, so it is a Pure variant like tanh:
  tanh(gain * a) with a positive gain, and its range maps the argument's
  domain through the increasing function.
- universal_oscillator is in [-1, 1] by construction (divided by a peak
  of at least |filt|). ehlers_stochastic and reversion_index end in a
  super smoother of a bounded ratio, which can overshoot, so their
  ranges are real.
- Warm-ups: ehlers_stochastic needs the roofing filter, a full
  stochastic window and the smoother of the same length; the universal
  oscillator two inputs before its smoother.
- EhlersStochastic is boxed in State (above 200 bytes).

No view needed a fix: a suspected underflow of the universal
oscillator's decaying peak on long constant input does not happen (0.991
times the smallest subnormal rounds back to it).
Adds wma, rma, dema, tema, t3, zlema, hma, ehma, trima, mc_ginley_dynamic, kama, vidya and jma.

The convex combinations of past inputs (wma, rma, trima, kama, vidya and the
now step-capped mc_ginley_dynamic) declare the input range; the lag-reducing
averages (dema, tema, t3, zlema, hma, ehma, jma) overshoot on reversals, so
they declare the real line. Chained stages override the warm-up: dema
2w - 1, tema 3w - 2, t3 6w - 5, zlema w + (w - 1) / 2, hma/ehma w + isqrt(w),
kama/vidya w + 1.

t3 exposes the classic volume factor as a fraction; jma exposes the phase,
checked to lie in [-100, 100] because the view clamps beyond it silently;
kama and mc_ginley_dynamic use the views' defaults (fast 2, slow 30; k 0.6).
mc_ginley_dynamic requires positive input (it divides by the ratio of input to
average). tema, t3, hma and ehma are boxed (over 200 bytes).

View fixes found by the adversarial property tests, squashed into the views'
revisions: Cmo and KaufmanEfficiencyRatio floor their running sums at 0 and
clamp the ratio to its proven bound (rounding broke the debug asserts and the
ranges); McGinleyDynamic caps its step at 1, as a drop made the raw formula
overshoot negative and diverge to infinity.
The running average gain and loss are updated by adding and subtracting
change / window. Moves leave the window in another order than they entered
it, so rounding can leave an average slightly below zero once its moves
left the window: the RSI then fell below 0 or above 100, or became infinite
when 1 + rs rounded to 0. Integer prices (e.g. ticks) hit this, as does
ConnorsRsi's streak RSI. Floor the averages at 0 and clamp the RSI.

Rsi's own history is immutable, so the fix is its own revision.
Adds cmo, macd, ppo, trix, tsi, stoch_rsi, rsx, schaff_trend_cycle,
connors_rsi, dpo, coppock and kst.

Multi-output views expose their main line only (MACD, PPO, TSI, %K, KST);
signal lines compose, e.g. ema(macd(x, 12, 26), 9). macd, ppo and
schaff_trend_cycle check fast < slow, which their constructors assert.

ppo, trix, coppock and kst are percentages of the input level, so they
require positive input; that bounds them below (-100, -100, -200, -1000).
cmo and tsi lie in [-100, 100], stoch_rsi in [0, 1], rsx,
schaff_trend_cycle and connors_rsi in [0, 100]. Warm-up overrides: cmo
w + 1, macd/ppo the slow window, trix 3w - 1, schaff_trend_cycle slow +
2 cycle, dpo max(w, w / 2 + 2); coppock and kst count until their rates
of change compare against full windows (the views report earlier against
the first value). kst takes no parameters (Pring's nine classic periods).

Skipped: Apo, whose output equals macd's MACD line; Qqe, whose main output
RsiMa is exactly ema(rsi(x, n), sf) and whose QQE-specific trailing level
is a getter.

A fuzz over integer price walks found ConnorsRsi leaving [0, 100] through
Rsi's rounding; that fix is the parent revision, as Rsi's history is
immutable.
Adds efficiency_ratio (KaufmanEfficiencyRatio), lin_reg_slope,
lin_reg_intercept, lin_reg_angle, r_squared, tsf, standard_error and vhf.

min_window follows the constructors' asserts: 2 for the regression views
and vhf, 3 for standard_error (n - 2 degrees of freedom), 1 for the
efficiency ratio. efficiency_ratio, r_squared and vhf lie in [0, 1] (vhf by
the triangle inequality: the window's range is at most the path between
its extremes, although its docs call it unbounded), lin_reg_angle in
[-90, 90], standard_error is non-negative; slope, intercept and tsf are
real (the fitted line extrapolates past the input range). efficiency_ratio
and vhf need one value before their window (warm-up window + 1). vhf costs
O(window) per update, said in its doc.

The rounding fix of KaufmanEfficiencyRatio (clamp to [0, 1]) was squashed
into its revision with the moving average functions, where kama found it.
Adds quantile (RollingQuantile), median (RollingMedian), iqr,
percentile_rank, gaussian_rank (QuantileNormalizer), skewness, kurtosis,
coefficient_of_variation, ewma_variance, and the stateless power
transforms box_cox and yeo_johnson as Pure variants with a real lambda.

min_window follows the constructors' asserts (iqr and the coefficient of
variation 2, skewness 3, kurtosis 4) and is 2 elsewhere, where a single
value is degenerate. quantile takes q as a Fraction: q = 0 and 1 are min
and max. coefficient_of_variation requires a positive input: it is a
dispersion relative to the level, which explodes near a zero mean.

Ranges: quantile and median stay within the input; iqr lies in
[0, width of the input] and ewma_variance in [0, (width / 2)^2]
(Popoviciu: Finch's recursion is an exactly weighted variance);
percentile_rank in [0, 100] (the view's scale); gaussian_rank within the
normal quantiles of the extreme plotting positions 1 / (n + 1) and
n / (n + 1), computed with the view's inverse_normal_cdf (now pub(crate));
skewness and kurtosis are real, as their exact-arithmetic bounds fail for
windows that are flat up to round-off. The transforms map the argument
domain, as both are increasing in it for every lambda.

BoxCox and YeoJohnson asserted a finite output in debug builds, which a
large lambda overflows (1e12^30); they now return None there. The fixes
were squashed into the revisions that added them.
Adds shannon_entropy, permutation_entropy, sample_entropy, hurst
(HurstExponent), variance_ratio, autocorrelation and ou_half_life.

shannon_entropy bins over [lo, lo + width] (lo Real, width Positive)
instead of lo and hi: two Reals cannot express lo < hi, and the test
parameters (both 0.5) would violate that check. A check still rejects a
width that rounding erases at lo or an end that overflows, which the
constructor asserts. Bins are Integer 2 to 100, the permutation order
Integer 2 to 7 (the view's limits, delay fixed at 1) and the sample
entropy template length Integer 1 to 5.

Costs: sample_entropy is O(window^2 m) per update and capped at window 100
(check_quadratic_window); hurst is O(window log window) and capped at 1000
(about 10^4 operations per update). sample_entropy needs a window of 20:
shorter windows have so few template pairs that it is mostly undefined.

The lag of autocorrelation and the period of variance_ratio are windows
that come before the window that must exceed them, as macd's fast window
does. Ranges: the entropies in [0, 1], sample_entropy in [0, ln(pairs)]
(at least one pair matches), autocorrelation in [-1, 1], variance_ratio
non-negative, ou_half_life non-negative (the view reports exactly 0 for an
AR(1) coefficient of 0, so positive would be unsound), hurst real (not
clamped). Warm-ups: permutation_entropy window + order - 1, ou_half_life
window + 1, autocorrelation and variance_ratio their window. No view
needed a fix.
Adds one_euro, alpha_beta, kalman, huber_ema, nlms, rls and hampel.

- one_euro(min_cutoff, beta): derivative cutoff fixed at the reference
  default 1; beta is a real checked to be non-negative, as 0 is a plain
  EMA. kalman, huber_ema, hampel and one_euro are convex updates or pick
  an input value, so they keep the argument's range.
- alpha_beta(alpha, beta): checks beta <= 2 and 4 - 2 alpha - beta > 0
  exactly as the constructor computes it, since rounding decides near
  alpha = 1, beta = 2. Range is any real (it overshoots turns).
- kalman(q, r): checks 2 (q + r) is finite, so the variance sums of the
  update cannot overflow.
- nlms(order 1..=64, mu < 2): regularizer fixed at 1e-12, which bounds a
  weight step while the taps are near zero. rls(order 1..=32, lambda):
  O(order^2) per update, so the order is capped; delta fixed at 0.01.
  Both override the warm-up with order + 1.
- hampel(window 3..=1000, t): O(window log window) per update, capped
  like hurst.
- LmsFilter is not offered: its step must suit the unknown input power,
  so no parameter check keeps it from diverging; nlms is the same
  predictor with a scale-free step.

RlsFilter was fixed in its own revision: zeros, constants or too short a
memory for the order let P wind up by 1 / lambda per update until
rounding made it indefinite and it overflowed (a debug assert fired on
a random walk with order 4, lambda 0.5 after 8000 updates). It now resets
P to I / delta (covariance resetting) once an entry exceeds 1e6 / delta
or h^T P h turns negative.
Adds cusum, page_hinkley, bocpd, adwin, var, cvar, ulcer_index, sharpe
and sortino. Detectors output their statistic (the views' last()), not
their alarms.

- cusum(k, h): target mean 0 and std 1 are fixed, as the DSL's
  arithmetic already standardizes (`cusum((x - m) / s, k, h)` or
  `cusum(zscore(x, n), k, h)`). Range: -max(0, h - lo - k) to
  max(0, h + hi - k), as both statistics are at most h before an update.
- page_hinkley(min_instances, delta, lambda, direction): direction is an
  integer, 0 both, 1 increases, 2 decreases; forgetting fixed at
  scikit-multiflow's 0.9999. Range: |statistic| <= max(min_instances w,
  lambda + w) with w = max(0, hi - lo - delta), one-sided per direction.
- bocpd(lambda, max_run_length): both windows (lambda > 1 must hold for
  the 0.5 example reals too), max_run_length capped at 1000 (O(n) per
  update), prior for standardized input; output in [0, max_run_length];
  warm-up 0, ready from the first value.
- adwin(delta): MOA's 5 buckets and clock 32; the window mean keeps the
  input's range.
- var/cvar(window, confidence): negated input range; O(window) per update
  accepted and documented like quantile.
- ulcer_index(window): requires positive prices, in [0, 100], warm-up
  2 window - 1.
- sharpe/sortino(window, rate): |rate| <= 1e6, so the excess over the
  smallest positive deviation cannot overflow. Sortino is at least -1
  (the mean excess is at least minus the downside deviation). The
  warm-up test feeds sortino returns (roc), as on prices around 100 no
  value is ever below its target.

View fixes, each in the view's own revision:
- Bocpd panicked in release (assert) once a value's squared deviation
  overflowed (|x| beyond about 1e154): it maps a NaN predictive to an
  impossible value and starts a new run when every run is impossible.
- SortinoRatio hit a debug assert (and returned -inf) when the downside
  deviation underflowed to 0 for deviations below about 1e-162: it is now
  not ready then.
Without getter outputs its DSL function returned the center line, i.e. exactly ultimate_smoother, and its num_sds had no effect. A duplicate name only confuses pipeline authors (an LLM reading the reference), so remove it. The UltimateBands view keeps its band getters for code users.
RoofingFilter::new takes the high-pass cutoff period first (the HP alpha uses it) and the SuperSmoother period second. The DSL doc said 'low-pass window, high-pass window', which reads the other way round, so an LLM following the reference would swap them. Name both by what they do and give Ehlers' usual 48, 10 as example.
@MathisWellmann MathisWellmann changed the title 11.1: lots of new features 12.0.0: lots of new features Oct 12, 2026
@MathisWellmann MathisWellmann changed the title 12.0.0: lots of new features 12.0.0: lots of new features (~85) Oct 12, 2026
Adding a pipeline function or parameter kind changed these public exhaustive enums, which made every such addition a semver-major change (cargo-semver-checks: enum_variant_added). Since 12.0.0 is a major release anyway, mark them non-exhaustive now so future functions and parameter kinds only need a minor bump. Downstream code iterates Function::ALL rather than matching every variant.
Breaking per cargo-semver-checks against 11.0.0: the public exhaustive enums pipeline::Function and pipeline::Param gained variants (enum_variant_added), and Param::Integer { min, max } is a struct variant, so Param's discriminants are no longer defined (enum_discriminants_undefined_non_unit_variant). Both enums are now #[non_exhaustive], so future additions only need a minor bump. Everything else is additive.
@MathisWellmann MathisWellmann changed the title 12.0.0: lots of new features (~85) 12.0.0: lots of new features (~95) Oct 12, 2026
@MathisWellmann
MathisWellmann merged commit efbc1e0 into main Oct 12, 2026
1 check passed
@MathisWellmann
MathisWellmann deleted the new-features branch October 12, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant