diff --git a/lib/dynamics/impulse/Impulse.md b/lib/dynamics/impulse/Impulse.md index a5f77a54..9859a266 100644 --- a/lib/dynamics/impulse/Impulse.md +++ b/lib/dynamics/impulse/Impulse.md @@ -7,14 +7,14 @@ | **Parameters** | `emaPeriod` (default 13), `macdFast` (default 12), `macdSlow` (default 26), `macdSignal` (default 9) | | **Outputs** | Single series (Impulse) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `Math.Max(emaPeriod, macdSlow) + macdSignal - 1` bars (default 34) | ### TL;DR - The Elder Impulse System combines a 13-period EMA (trend inertia) with the MACD(12,26,9) histogram (momentum acceleration) to classify each bar as ... - Parameterized by `emaperiod` (default 13), `macdfast` (default 12), `macdslow` (default 26), `macdsignal` (default 9). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `Math.Max(emaPeriod, macdSlow) + macdSignal - 1` bars (default 34) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The Impulse System identifies inflection points where a trend speeds up or slows down." -- Alexander Elder, *Come Into My Trading Room* diff --git a/lib/dynamics/ravi/Ravi.md b/lib/dynamics/ravi/Ravi.md index 99f84928..43f31501 100644 --- a/lib/dynamics/ravi/Ravi.md +++ b/lib/dynamics/ravi/Ravi.md @@ -7,14 +7,14 @@ | **Parameters** | `shortPeriod` (default 7), `longPeriod` (default 65) | | **Outputs** | Single series (Ravi) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `longPeriod` bars (default 65) | ### TL;DR - RAVI (Range Action Verification Index) measures trend strength by computing the absolute percentage divergence between a short-period SMA and a lon... - Parameterized by `shortperiod` (default 7), `longperiod` (default 65). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `longPeriod` bars (default 65) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The simplest question in technical analysis is also the most important: is this market trending or not? RAVI answers it with two moving averages and a division." diff --git a/lib/errors/huber/Huber.md b/lib/errors/huber/Huber.md index 047e477c..ee47fb1d 100644 --- a/lib/errors/huber/Huber.md +++ b/lib/errors/huber/Huber.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period`, `delta` (default 1.345) | | **Outputs** | Single series (Huber) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/logcosh/LogCosh.md b/lib/errors/logcosh/LogCosh.md index 6b740e21..27317c74 100644 --- a/lib/errors/logcosh/LogCosh.md +++ b/lib/errors/logcosh/LogCosh.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | -| **Outputs** | Single series (UNKNOWN) | +| **Outputs** | Single series (LogCosh) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/maape/Maape.md b/lib/errors/maape/Maape.md index 850c738d..86282b14 100644 --- a/lib/errors/maape/Maape.md +++ b/lib/errors/maape/Maape.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAAPE) | -| **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Output range** | $[0, \pi/2]$ | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mae/Mae.md b/lib/errors/mae/Mae.md index b8d4d4a0..29133e10 100644 --- a/lib/errors/mae/Mae.md +++ b/lib/errors/mae/Mae.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mapd/Mapd.md b/lib/errors/mapd/Mapd.md index 57604b0c..84350f33 100644 --- a/lib/errors/mapd/Mapd.md +++ b/lib/errors/mapd/Mapd.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAPD) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mape/Mape.md b/lib/errors/mape/Mape.md index 3a938cf8..797900b0 100644 --- a/lib/errors/mape/Mape.md +++ b/lib/errors/mape/Mape.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAPE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mase/Mase.md b/lib/errors/mase/Mase.md index c4b0ab81..24e6f96b 100644 --- a/lib/errors/mase/Mase.md +++ b/lib/errors/mase/Mase.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Mase) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/mdae/Mdae.md b/lib/errors/mdae/Mdae.md index 9e58dcc3..8357ea96 100644 --- a/lib/errors/mdae/Mdae.md +++ b/lib/errors/mdae/Mdae.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Mdae) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/mdape/Mdape.md b/lib/errors/mdape/Mdape.md index be37b67b..4733e197 100644 --- a/lib/errors/mdape/Mdape.md +++ b/lib/errors/mdape/Mdape.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Mdape) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/me/Me.md b/lib/errors/me/Me.md index 6c090ab3..7f205cbb 100644 --- a/lib/errors/me/Me.md +++ b/lib/errors/me/Me.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (ME) | -| **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Output range** | Any (positive or negative) | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mpe/Mpe.md b/lib/errors/mpe/Mpe.md index f4bfd5e4..fcc119ed 100644 --- a/lib/errors/mpe/Mpe.md +++ b/lib/errors/mpe/Mpe.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MPE) | -| **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Output range** | Any (positive or negative) | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mrae/Mrae.md b/lib/errors/mrae/Mrae.md index 58f16d00..0d84ffb7 100644 --- a/lib/errors/mrae/Mrae.md +++ b/lib/errors/mrae/Mrae.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MRAE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mse/Mse.md b/lib/errors/mse/Mse.md index 168e5dfc..34c4236f 100644 --- a/lib/errors/mse/Mse.md +++ b/lib/errors/mse/Mse.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MSE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/msle/Msle.md b/lib/errors/msle/Msle.md index 86b35540..e2b1fdc2 100644 --- a/lib/errors/msle/Msle.md +++ b/lib/errors/msle/Msle.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MSLE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/pseudohuber/PseudoHuber.md b/lib/errors/pseudohuber/PseudoHuber.md index 1cdc332e..704e9633 100644 --- a/lib/errors/pseudohuber/PseudoHuber.md +++ b/lib/errors/pseudohuber/PseudoHuber.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period`, `delta` (default 1.0) | -| **Outputs** | Single series (UNKNOWN) | +| **Outputs** | Single series (PseudoHuber) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Pseudo-Huber Loss (also called Charbonnier Loss) is a smooth approximation to the Huber loss function. - Parameterized by `period`, `delta` (default 1.0). - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "All the robustness of Huber, none of the discontinuities." diff --git a/lib/errors/quantileloss/QuantileLoss.md b/lib/errors/quantileloss/QuantileLoss.md index ff024fad..830963de 100644 --- a/lib/errors/quantileloss/QuantileLoss.md +++ b/lib/errors/quantileloss/QuantileLoss.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period`, `quantile` (default 0.5) | -| **Outputs** | Single series (Quantile) | +| **Outputs** | Single series (QuantileLoss) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/rae/Rae.md b/lib/errors/rae/Rae.md index cd0c3796..464114f9 100644 --- a/lib/errors/rae/Rae.md +++ b/lib/errors/rae/Rae.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Rae) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/rmse/Rmse.md b/lib/errors/rmse/Rmse.md index 904c4d72..45c7972a 100644 --- a/lib/errors/rmse/Rmse.md +++ b/lib/errors/rmse/Rmse.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (RMSE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/rmsle/Rmsle.md b/lib/errors/rmsle/Rmsle.md index be0a42b5..c6b2ccb5 100644 --- a/lib/errors/rmsle/Rmsle.md +++ b/lib/errors/rmsle/Rmsle.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (RMSLE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Root Mean Squared Logarithmic Error is the square root of MSLE, providing an error metric in log-scale units. - Parameterized by `period`. - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "RMSLE: because sometimes your errors need to be measured in decades, not dollars." diff --git a/lib/errors/rse/Rse.md b/lib/errors/rse/Rse.md index 82c0d7a0..04c097dd 100644 --- a/lib/errors/rse/Rse.md +++ b/lib/errors/rse/Rse.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (Rse) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/rsquared/Rsquared.md b/lib/errors/rsquared/Rsquared.md index b7cbb693..e03e638e 100644 --- a/lib/errors/rsquared/Rsquared.md +++ b/lib/errors/rsquared/Rsquared.md @@ -3,17 +3,17 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | -| **Outputs** | Single series (R) | -| **Output range** | $\geq 0$ | +| **Outputs** | Single series (R²) | +| **Output range** | $(-\infty, 1]$ | | **Warmup** | `period` bars | ### TL;DR - The Coefficient of Determination (R²) measures the proportion of variance in the actual values that is predictable from the predicted values. - Parameterized by `period`. -- Output range: $\geq 0$. +- Output range: $(-\infty, 1]$. - Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/errors/smape/Smape.md b/lib/errors/smape/Smape.md index 7880f283..84d88e21 100644 --- a/lib/errors/smape/Smape.md +++ b/lib/errors/smape/Smape.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (SMAPE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Symmetric Mean Absolute Percentage Error addresses a fundamental asymmetry in MAPE: the fact that over-predictions and under-predictions of the sam... - Parameterized by `period`. - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "MAPE punishes based on who's right; SMAPE punishes based on how different they are." diff --git a/lib/errors/theilu/TheilU.md b/lib/errors/theilu/TheilU.md index de47b848..fce391b5 100644 --- a/lib/errors/theilu/TheilU.md +++ b/lib/errors/theilu/TheilU.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (TheilU) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/tukeybiweight/TukeyBiweight.md b/lib/errors/tukeybiweight/TukeyBiweight.md index e390b192..351e322c 100644 --- a/lib/errors/tukeybiweight/TukeyBiweight.md +++ b/lib/errors/tukeybiweight/TukeyBiweight.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | -| **Parameters** | `period`, `c` (default DefaultC) | -| **Outputs** | Single series (UNKNOWN) | +| **Inputs** | Actual vs Predicted (dual input) | +| **Parameters** | `period`, `c` (default 4.685) | +| **Outputs** | Single series (TukeyBiweight) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Tukey's Biweight (also called Bisquare) is a redescending M-estimator that completely ignores errors beyond a threshold. - Parameterized by `period`, `c` (default defaultc). - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "When outliers need to be silenced, not just quieted." diff --git a/lib/errors/wmape/Wmape.md b/lib/errors/wmape/Wmape.md index 2dd10a86..4c9b6546 100644 --- a/lib/errors/wmape/Wmape.md +++ b/lib/errors/wmape/Wmape.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (Wmape) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/wrmse/Wrmse.md b/lib/errors/wrmse/Wrmse.md index 1981e738..f67f22eb 100644 --- a/lib/errors/wrmse/Wrmse.md +++ b/lib/errors/wrmse/Wrmse.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (Wrmse) | | **Output range** | $\geq 0$ | diff --git a/lib/filters/agc/Agc.md b/lib/filters/agc/Agc.md index 3895e088..249382f8 100644 --- a/lib/filters/agc/Agc.md +++ b/lib/filters/agc/Agc.md @@ -6,15 +6,15 @@ | **Inputs** | Source (close) | | **Parameters** | `decay` (default 0.991) | | **Outputs** | Single series (AGC) | -| **Output range** | Tracks input | +| **Output range** | [-1, +1] (normalized) | | **Warmup** | `1` bars | ### TL;DR - The Automatic Gain Control normalizes any oscillating signal to the \[-1, +1\] range through exponential peak tracking. - Parameterized by `decay` (default 0.991). -- Output range: Tracks input. -- Requires `1` bars of warmup before first valid output (IsHot = true). +- Output range: [-1, +1] (normalized amplitude). +- Requires `1` bar of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The purpose of the AGC is to normalize the amplitude of any indicator to unity." — John F. Ehlers, TASC January 2015 @@ -86,26 +86,25 @@ Peak initializes to $10^{-10}$ (tiny positive) to avoid division by zero on the ### Operation Count (Streaming Mode) -AGC (Adaptive Gain Control) applies a slow EMA to estimate signal level, then scales the signal by the inverse of that level. O(1) per bar. +AGC uses exponential peak decay with a ratchet-up mechanism, then divides the signal by the tracked peak. O(1) per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Level EMA (FMA) | 1 | ~4 cy | ~4 cy | -| Gain = 1 / level (division) | 1 | ~10 cy | ~10 cy | -| Output multiply | 1 | ~3 cy | ~3 cy | -| **Total** | **3** | — | **~17 cycles** | +| Peak decay (multiply) | 1 | ~3 cy | ~3 cy | +| Abs + compare (ratchet) | 1 | ~2 cy | ~2 cy | +| Normalize (division) | 1 | ~10 cy | ~10 cy | +| **Total** | **3** | — | **~15 cycles** | -O(1) per bar. The division dominates; precomputing gain incrementally saves it but adds state. ~17 cycles/bar. +O(1) per bar. The division dominates. ~15 cycles/bar. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Level EMA recursion | No | Sequential IIR dependency | -| Gain division | No | Depends on current EMA | -| Output multiply | N/A | Single scalar multiply | +| Peak decay + ratchet | No | Data-dependent branching (max of decay vs abs) | +| Normalize (division) | No | Depends on current peak | -Fully recursive. Batch throughput: ~17 cy/bar. +Fully sequential due to data-dependent peak tracking. Batch throughput: ~15 cy/bar. | Metric | Value | |---|---| diff --git a/lib/filters/alaguerre/ALaguerre.md b/lib/filters/alaguerre/ALaguerre.md index 54e76edf..4c1b9365 100644 --- a/lib/filters/alaguerre/ALaguerre.md +++ b/lib/filters/alaguerre/ALaguerre.md @@ -7,7 +7,7 @@ | **Parameters** | `length` (default 20), `medianLength` (default 5) | | **Outputs** | Single series (ALaguerre) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `max(4, length)` bars | | **Signature** | [alaguerre_signature](alaguerre_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - The Adaptive Laguerre Filter extends Ehlers' four-element all-pass cascade by replacing the fixed damping factor with a per-bar adaptive alpha deri... - Parameterized by `length` (default 20), `medianlength` (default 5). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `max(4, length)` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The best filter is one that knows when to listen closely and when to smooth aggressively." -- John F. Ehlers (paraphrased) @@ -111,24 +111,28 @@ The filter requires $\max(4, N)$ bars before producing reliable output. The firs ### Operation Count (Streaming Mode) -Laguerre filter uses 4 cascaded Laguerre stages L0..L3, each an O(1) gamma-parameterized FMA, plus a final weighted combination. +Adaptive Laguerre combines HH/LL tracking error normalization (O(N) scan), median smoothing (O(M log M) sort), and 4-stage Laguerre cascade (O(1)). | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | +| Tracking error computation | 1 | ~3 cy | ~3 cy | +| HH/LL scan over N-element buffer | N | ~2 cy | ~40 cy (N=20) | +| Median via insertion sort (M elements) | M log M | ~3 cy | ~25 cy (M=5) | | Laguerre state update x4 (each: 2 FMA) | 8 | ~4 cy | ~32 cy | -| Weighted output combination (3 adds) | 3 | ~2 cy | ~6 cy | -| **Total** | **11** | — | **~38 cycles** | +| Weighted output combination | 3 | ~2 cy | ~6 cy | +| **Total** | — | — | **~106 cycles** (N=20, M=5) | -O(1) per bar. Four recursive stages with precomputed gamma constant. ~38 cycles/bar. +O(N + M log M) per bar due to HH/LL scan and median sort. Dominated by the lookback scan for large N. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Laguerre stage recursion | No | Each stage `L[k][n]` depends on `L[k-1][n]` and `L[k][n-1]` | -| Weighted combination | No | Only 4 terms; SIMD overhead not worthwhile | +| HH/LL scan | Partial | Min/max over contiguous buffer amenable to `Vector` | +| Median sort | No | Comparison-based sort, data-dependent | +| Laguerre cascade | No | Each stage depends on previous stage and previous bar | -Cascaded IIR stages cannot be vectorized. Batch throughput: ~38 cy/bar. +Adaptive overhead limits SIMD gains. Batch throughput: ~106 cy/bar (N=20, M=5). | Metric | Value | Notes | |--------|-------|-------| diff --git a/lib/filters/baxterking/BaxterKing.md b/lib/filters/baxterking/BaxterKing.md index 170588b6..95db8030 100644 --- a/lib/filters/baxterking/BaxterKing.md +++ b/lib/filters/baxterking/BaxterKing.md @@ -6,16 +6,16 @@ | **Inputs** | Source (close) | | **Parameters** | `pLow` (default 6), `pHigh` (default 32), `k` (default 12) | | **Outputs** | Single series (BaxterKing) | -| **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Output range** | Oscillates around zero | +| **Warmup** | `2K+1` bars (default 25) | | **Signature** | [baxterking_signature](baxterking_signature.md) | ### TL;DR - The **Baxter-King Band-Pass Filter** is a symmetric finite impulse response (FIR) filter that approximates the ideal spectral band-pass by truncati... - Parameterized by `plow` (default 6), `phigh` (default 32), `k` (default 12). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero (extracts cyclical component). +- Requires `2K+1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The business cycle is whatever remains after you strip away the trend and the noise. Baxter and King figured out the stripping." diff --git a/lib/filters/bessel/Bessel.md b/lib/filters/bessel/Bessel.md index 4d250783..a8692548 100644 --- a/lib/filters/bessel/Bessel.md +++ b/lib/filters/bessel/Bessel.md @@ -7,7 +7,7 @@ | **Parameters** | `length` | | **Outputs** | Single series (Bessel) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `length` bars | | **Signature** | [bessel_signature](bessel_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - The Bessel Filter is a 2nd-order low-pass IIR filter designed to preserve the **shape** and **timing** of price moves. - Parameterized by `length`. - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `length` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > When you care more about *when* the market turns than how aggressively you can torture the noise, you reach for a Bessel. diff --git a/lib/filters/bpf/Bpf.md b/lib/filters/bpf/Bpf.md index e9f90d08..e254ff34 100644 --- a/lib/filters/bpf/Bpf.md +++ b/lib/filters/bpf/Bpf.md @@ -6,7 +6,7 @@ | **Inputs** | Source (close) | | **Parameters** | `lowerPeriod`, `upperPeriod` | | **Outputs** | Single series (BPF) | -| **Output range** | Tracks input | +| **Output range** | Oscillates around zero | | **Warmup** | `Math.Max(lowerPeriod, upperPeriod)` bars | | **Signature** | [bpf_signature](bpf_signature.md) | @@ -14,7 +14,7 @@ - The **BPF** (BandPass Filter) is a second-order IIR architecture designed to surgically excise specific frequency components from a time series. - Parameterized by `lowerperiod`, `upperperiod`. -- Output range: Tracks input. +- Output range: Oscillates around zero (bandpass extracts cyclic component). - Requires `Math.Max(lowerPeriod, upperPeriod)` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/filters/cfitz/Cfitz.md b/lib/filters/cfitz/Cfitz.md index 70f04c1d..0546d61d 100644 --- a/lib/filters/cfitz/Cfitz.md +++ b/lib/filters/cfitz/Cfitz.md @@ -6,7 +6,7 @@ | **Inputs** | Source (close) | | **Parameters** | `pLow` (default 6), `pHigh` (default 32) | | **Outputs** | Single series (Cfitz) | -| **Output range** | Tracks input | +| **Output range** | Oscillates around zero | | **Warmup** | `2` bars | | **Signature** | [cfitz_signature](cfitz_signature.md) | @@ -14,7 +14,7 @@ - The **Christiano-Fitzgerald Band-Pass Filter** is an asymmetric full-sample filter that approximates the ideal spectral band-pass by using time-var... - Parameterized by `plow` (default 6), `phigh` (default 32). -- Output range: Tracks input. +- Output range: Oscillates around zero (extracts cyclical component). - Requires `2` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/filters/edcf/Edcf.md b/lib/filters/edcf/Edcf.md index 6c49cd23..8ada382f 100644 --- a/lib/filters/edcf/Edcf.md +++ b/lib/filters/edcf/Edcf.md @@ -7,7 +7,7 @@ | **Parameters** | `length` (default 15) | | **Outputs** | Single series (Edcf) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `length` bars | | **Signature** | [edcf_signature](edcf_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - The **Ehlers Distance Coefficient Filter (EDCF)** is a nonlinear adaptive FIR filter created by John F. - Parameterized by `length` (default 15). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `length` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. ## Overview diff --git a/lib/filters/gauss/Gauss.md b/lib/filters/gauss/Gauss.md index 4a4082cd..a8513e03 100644 --- a/lib/filters/gauss/Gauss.md +++ b/lib/filters/gauss/Gauss.md @@ -14,7 +14,7 @@ - Gauss (Gaussian Filter) is a smoothing filter that applies a Gaussian kernel to time series data. - Parameterized by `sigma` (default 1.0). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `2⌈3σ⌉+1` bars of warmup before first valid output (IsHot = true). Default: **7 bars** (σ=1.0). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "SMA smears data like cheap paint. Gaussian filtering respects the signal's soul." diff --git a/lib/filters/hann/Hann.md b/lib/filters/hann/Hann.md index 50e5023e..a135dc14 100644 --- a/lib/filters/hann/Hann.md +++ b/lib/filters/hann/Hann.md @@ -15,7 +15,7 @@ - Hann (Hann Filter) is a Finite Impulse Response (FIR) smoothing filter that applies a Hann window to time series data. - Parameterized by `length`. - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `length` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The Hanning window whispers where the Boxcar screams. Smoothness is not just an aesthetic; it's a mathematical necessity." diff --git a/lib/filters/hp/Hp.md b/lib/filters/hp/Hp.md index a07fcdf8..1083d8ab 100644 --- a/lib/filters/hp/Hp.md +++ b/lib/filters/hp/Hp.md @@ -14,7 +14,7 @@ - The Hodrick-Prescott (HP) filter is a widely used tool in macroeconomics for separating the cyclical component of a time series from raw data. - Parameterized by `lambda` (default 1600.0). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `⌈2√λ⌉` bars of warmup before first valid output (IsHot = true). Default: **~80 bars** (λ=1600). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Trends are not lines; they are curves that we simplify for our sanity, often at the cost of reality." diff --git a/lib/filters/hpf/Hpf.md b/lib/filters/hpf/Hpf.md index 6f22de54..d090ddad 100644 --- a/lib/filters/hpf/Hpf.md +++ b/lib/filters/hpf/Hpf.md @@ -14,8 +14,8 @@ - The 2-Pole Highpass Filter (HPF) is designed to separate high-frequency components (like cycles and noise) from the underlying trend. - Parameterized by `length` (default 40). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero (detrended signal). +- Requires `length` bars of warmup before first valid output (IsHot = true). Default: **40 bars**. - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Noise is just signal you haven't figured out how to filter yet. Or maybe, it's the only signal that matters." diff --git a/lib/filters/rmed/Rmed.md b/lib/filters/rmed/Rmed.md index f86e12c4..1c7294ec 100644 --- a/lib/filters/rmed/Rmed.md +++ b/lib/filters/rmed/Rmed.md @@ -7,7 +7,7 @@ | **Parameters** | `period` (default 12) | | **Outputs** | Single series (Rmed) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | 5 bars (MedianWindow) | | **Signature** | [rmed_signature](rmed_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - RMED applies exponential smoothing to a 5-bar running median, creating a nonlinear IIR filter that rejects impulsive spike noise while providing sm... - Parameterized by `period` (default 12). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires **5 bars** of warmup (MedianWindow) before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "John Ehlers combined two tools that rarely meet: the median (nonlinear, spike-resistant) and the EMA (smooth, recursive). The median kills the spikes, the EMA smooths the survivors. Together they produce a filter that is both resistant and smooth." diff --git a/lib/filters/roofing/Roofing.md b/lib/filters/roofing/Roofing.md index 5c29790a..f93e5654 100644 --- a/lib/filters/roofing/Roofing.md +++ b/lib/filters/roofing/Roofing.md @@ -6,15 +6,15 @@ | **Inputs** | Source (close) | | **Parameters** | `hpLength` (default 48), `ssLength` (default 10) | | **Outputs** | Single series (ROOFING) | -| **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Output range** | Oscillates around zero | +| **Warmup** | `hpLength` bars (default 48) | ### TL;DR - The **Roofing Filter** is John Ehlers' bandpass architecture designed specifically for oscillator construction. - Parameterized by `hplength` (default 48), `sslength` (default 10). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero (bandpass behavior). +- Requires `hpLength` bars of warmup before first valid output (IsHot = true). Default: **48 bars**. - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The trend is your friend until it overwhelms the signal. The noise is your enemy until you mistake it for alpha." diff --git a/lib/filters/spbf/Spbf.md b/lib/filters/spbf/Spbf.md index c13c117c..f315aa73 100644 --- a/lib/filters/spbf/Spbf.md +++ b/lib/filters/spbf/Spbf.md @@ -6,15 +6,15 @@ | **Inputs** | Source (close) | | **Parameters** | `shortPeriod` (default 40), `longPeriod` (default 60), `rmsPeriod` (default 50) | | **Outputs** | Single series (SPBF) | -| **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Output range** | Oscillates around zero | +| **Warmup** | `max(longPeriod, rmsPeriod)` bars (default 60) | ### TL;DR - The **Super Passband Filter** is John Ehlers' wide-band bandpass constructed by differencing two z-transformed EMAs with Ehlers-style smoothing ($\... - Parameterized by `shortperiod` (default 40), `longperiod` (default 60), `rmsperiod` (default 50). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero. +- Requires `max(longPeriod, rmsPeriod)` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Two EMAs walk into a frequency domain. The difference between them is the only thing worth trading." diff --git a/lib/filters/ssf2/ssf2.pine b/lib/filters/ssf2/ssf2.pine index 4080f479..e49d3b55 100644 --- a/lib/filters/ssf2/ssf2.pine +++ b/lib/filters/ssf2/ssf2.pine @@ -26,7 +26,7 @@ ssf2(series float src, simple int length) => float ssrc = nz(src, src[1]) float src1 = nz(src[1], ssrc) float src2 = nz(src[2], src1) - ssf_internal := c1 * ssrc + c2 * nz(ssf_internal[1], src1) + c3 * nz(ssf_internal[2], src2) + ssf_internal := c1 * (ssrc + src1) * 0.5 + c2 * nz(ssf_internal[1], src1) + c3 * nz(ssf_internal[2], src2) ssf_internal // ---------- Main loop ---------- diff --git a/lib/filters/usf/Usf.md b/lib/filters/usf/Usf.md index f2ebc01e..d76a31e4 100644 --- a/lib/filters/usf/Usf.md +++ b/lib/filters/usf/Usf.md @@ -62,25 +62,24 @@ Where: ### Operation Count (Streaming Mode) -Universal Smooth Filter: adaptive-weight FIR that adjusts taps based on signal characteristics. O(N) per bar. +Ehlers Ultimate Smoother Filter (USF): 2-pole IIR low-pass filter with high-pass subtraction for zero-lag smoothing. Five FMA operations per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Adaptive weight derivation | N | ~8 cy | ~240 cy (N=30) | -| Weighted sum FMA | N | ~5 cy | ~150 cy | -| Normalization | 1 | ~3 cy | ~3 cy | -| **Total (N=30)** | **2N+1** | — | **~393 cycles** | +| Input combination (3 taps) | 3 | ~4 cy | ~12 cy | +| Feedback (2 taps) | 2 | ~4 cy | ~8 cy | +| State update | 4 | ~1 cy | ~4 cy | +| **Total** | **9** | — | **~24 cycles** | -O(N) per bar. Adaptive weights computed per bar (no precomputation) because they depend on current signal level. ~393 cycles for N=30. +O(1) per bar. Coefficients derived from period parameter; precomputed. ~24 cycles/bar. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Adaptive weight computation | Partial | Independent per element; vectorizable if no data dependency | -| Weighted sum FMA | Yes | Standard dot product | +| USF recursion | No | y[n] depends on y[n-1] and y[n-2] | -SIMD potential: ~100 cy for N=30 if adaptive weights vectorized. +Batch throughput: ~24 cy/bar. | Metric | Score | Notes | | :--- | :--- | :--- | diff --git a/lib/filters/voss/Voss.md b/lib/filters/voss/Voss.md index 2684025f..004c5478 100644 --- a/lib/filters/voss/Voss.md +++ b/lib/filters/voss/Voss.md @@ -6,7 +6,7 @@ | **Inputs** | Source (close) | | **Parameters** | `period` (default 20), `predict` (default 3), `bandwidth` (default 0.25) | | **Outputs** | Single series (VOSS) | -| **Output range** | Tracks input | +| **Output range** | Oscillates around zero | | **Warmup** | `period` bars | | **Signature** | [voss_signature](voss_signature.md) | @@ -86,25 +86,26 @@ The Voss predictor stage is an IIR filter with `Order` feedback taps, each weigh ### Operation Count (Streaming Mode) -Voss-McCartney 1/f noise filter: octave-cascade of N random sources, each updated probabilistically. O(N) per bar worst-case, O(1) amortized. +Ehlers Voss Predictive Filter: two-stage pipeline — 2-pole bandpass filter (Stage 1) + weighted feedback predictor (Stage 2). O(Order) per bar where Order = 3 × Predict. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Bit-scan (which octave to update) | 1 | ~3 cy | ~3 cy | -| RNG sample + accumulate | 1 | ~8 cy | ~8 cy | -| Running sum update | 1 | ~2 cy | ~2 cy | -| **Total (amortized)** | **3** | — | **~13 cycles** | +| BPF: diff + 3-term FMA | 3 | ~4 cy | ~12 cy | +| Voss: weighted sum loop | Order | ~5 cy | ~45 cy (Order=9) | +| Voss: gain × filt − sumC | 2 | ~4 cy | ~8 cy | +| State update (shifts) | 4 | ~1 cy | ~4 cy | +| **Total (Order=9)** | **Order+9** | — | **~69 cycles** | -O(1) amortized per bar. Each bar updates exactly 1 octave source on average. ~13 cycles/bar amortized. +O(Order) per bar. Order = 3 × Predict; at defaults (Predict=3), Order=9. ~69 cycles/bar. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Octave update scheduling | No | Bit-count branching; data-dependent | -| RNG generation | Partial | SIMD RNG (e.g., xoshiro SIMD) available but niche | +| BPF recursion | No | y[n] depends on y[n-1] and y[n-2] | +| Voss weighted sum | Partial | Inner loop vectorizable but short (Order=9) | -Amortized O(1) makes SIMD gains minimal. Batch throughput: ~13 cy/bar. +Batch throughput: ~69 cy/bar at defaults. | Metric | Value | |--------|-------| diff --git a/lib/filters/wavelet/Wavelet.md b/lib/filters/wavelet/Wavelet.md index abec9960..567b3c91 100644 --- a/lib/filters/wavelet/Wavelet.md +++ b/lib/filters/wavelet/Wavelet.md @@ -7,14 +7,14 @@ | **Parameters** | `levels` (default 4), `threshMult` (default 1.0) | | **Outputs** | Single series (Wavelet) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `2^levels` bars (default 16) | ### TL;DR - The Wavelet Denoising Filter applies an *à trous* (with holes) Haar wavelet decomposition with soft thresholding to remove high-frequency noise fro... - Parameterized by `levels` (default 4), `threshmult` (default 1.0). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `2^levels` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The wavelet transform is to the Fourier transform what a microscope is to a telescope: same math, different scale." diff --git a/lib/forecasts/afirma/Afirma.md b/lib/forecasts/afirma/Afirma.md index 3bf9d084..f0270ecd 100644 --- a/lib/forecasts/afirma/Afirma.md +++ b/lib/forecasts/afirma/Afirma.md @@ -137,18 +137,18 @@ $$ ### Operation Count (Streaming Mode) -AFIRMA chains AR model estimation with IIR/FIR filtering — O(p) per bar where p = AR order. +Windowed FIR convolution — O(P) per bar where P = period (window length). | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| AR coefficient estimation (p terms) | p | 4 cy | ~4p cy | -| FIR forward pass (p multiplies) | p | 1 cy | ~p cy | -| IIR feedback pass (p multiplies) | p | 1 cy | ~p cy | -| Output computation via FMA | 1 | 1 cy | ~1 cy | -| NaN guard + state update | 1 | 2 cy | ~2 cy | -| **Total (p=8)** | **O(p)** | — | **~49 cy** | +| RingBuffer write | 1 | ~2 cy | ~2 cy | +| FIR convolution (P FMA ops) | P | ~1 cy | ~P cy | +| Weight normalization | 1 | ~1 cy | ~1 cy | +| LS regression (if enabled) | n | ~2 cy | ~2n cy | +| NaN guard + state update | 1 | ~2 cy | ~2 cy | +| **Total (P=20)** | **O(P)** | — | **~25 cy** | -O(p) per bar where p = AR order. AR coefficient estimation dominates; FMA-fused filter passes are cheap. Streaming mode maintains p state variables. +O(P) per bar where P = window length. FMA-fused dot product dominates; LS regression adds O(n) where n = min(⌊(P−1)/2⌋, 50). | Metric | Value | Notes | | :--- | :---: | :--- | diff --git a/lib/forecasts/afirma/afirma.pine b/lib/forecasts/afirma/afirma.pine index 4fe0443d..79fb661f 100644 --- a/lib/forecasts/afirma/afirma.pine +++ b/lib/forecasts/afirma/afirma.pine @@ -7,7 +7,7 @@ indicator("Autoregressive FIR Moving Average (AFIRMA)", "AFIRMA", overlay=true) //@param source Series to calculate AFIRMA from //@param period Lookback period - window size //@param windowType Window function type (1:Hanning, 2:Hamming, 3:Blackman, 4:Blackman-Harris) -//@param leastSquares Apply least squares cubic polynomial fitting for autoregressive prediction +//@param leastSquares Apply least squares linear regression fitting for autoregressive prediction //@returns AFIRMA value, calculates from first bar using available data //@optimized Uses windowing functions with O(n) complexity; least squares adds O(n) for polynomial fitting afirma(series float src, simple int period, simple int windowType=4, simple bool leastSquares=false) => @@ -111,7 +111,7 @@ afirma(series float src, simple int period, simple int windowType=4, simple bool i_period = input.int(20, "Period", minval=1) i_source = input.source(close, "Source") i_windowType = input.int(4, "Window Function", minval=1, maxval=4, tooltip="1:Hanning, 2:Hamming, 3:Blackman, 4:Blackman-Harris") -i_leastSquares = input.bool(false, "Least Squares Method", tooltip="Enable cubic polynomial fitting for autoregressive prediction") +i_leastSquares = input.bool(false, "Least Squares Method", tooltip="Enable linear regression fitting for autoregressive prediction") // Calculation afirma_value = afirma(i_source, i_period, i_windowType, i_leastSquares) diff --git a/lib/momentum/asi/Asi.md b/lib/momentum/asi/Asi.md index d00bf7eb..c8deb56e 100644 --- a/lib/momentum/asi/Asi.md +++ b/lib/momentum/asi/Asi.md @@ -7,7 +7,7 @@ | **Parameters** | `limitMove` (default 3.0) | | **Outputs** | Single series (Asi) | | **Output range** | Varies (see docs) | -| **Warmup** | `> 2` bars | +| **Warmup** | 2 bars | ### TL;DR diff --git a/lib/momentum/bop/Bop.md b/lib/momentum/bop/Bop.md index 4a1a7e50..eda343b2 100644 --- a/lib/momentum/bop/Bop.md +++ b/lib/momentum/bop/Bop.md @@ -7,7 +7,7 @@ | **Parameters** | None | | **Outputs** | Single series (BOP) | | **Output range** | Varies (see docs) | -| **Warmup** | `> 0` bars | +| **Warmup** | 0 bars (always hot) | ### TL;DR diff --git a/lib/momentum/cci/Cci.md b/lib/momentum/cci/Cci.md index 105e0c44..492fb1b0 100644 --- a/lib/momentum/cci/Cci.md +++ b/lib/momentum/cci/Cci.md @@ -4,17 +4,17 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `period` (default DefaultPeriod) | +| **Parameters** | `period` (default 20) | | **Outputs** | Single series (CCI) | | **Output range** | Varies (see docs) | -| **Warmup** | `> period` bars | +| **Warmup** | `period` bars | ### TL;DR - The Commodity Channel Index (CCI) is a versatile momentum-based oscillator developed by Donald Lambert in 1980. -- Parameterized by `period` (default defaultperiod). +- Parameterized by `period` (default 20). - Output range: Varies (see docs). -- Requires `> period` bars of warmup before first valid output (IsHot = true). +- Requires `period` bars (20 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. ## Overview diff --git a/lib/momentum/cfb/Cfb.md b/lib/momentum/cfb/Cfb.md index 74ab8fba..daac5087 100644 --- a/lib/momentum/cfb/Cfb.md +++ b/lib/momentum/cfb/Cfb.md @@ -7,14 +7,14 @@ | **Parameters** | int[]? lengths = null | | **Outputs** | Single series (CFB) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `maxLen` bars (192 default) | ### TL;DR - The Composite Fractal Behavior index measures trend duration by analyzing fractal efficiency across 96 simultaneous lookback periods (2 to 192 bars... - Parameterized by int[]? lengths = null. - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `maxLen` bars (192 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Mark Jurik's CFB is not a momentum indicator. It is a stopwatch for chaos." diff --git a/lib/momentum/cmo/Cmo.md b/lib/momentum/cmo/Cmo.md index 50334c43..7a956d9b 100644 --- a/lib/momentum/cmo/Cmo.md +++ b/lib/momentum/cmo/Cmo.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | Source (close) | -| **Parameters** | `period` (default DefaultPeriod) | +| **Parameters** | `period` (default 14) | | **Outputs** | Single series (Cmo) | | **Output range** | $-100$ to $+100$ | | **Warmup** | `period + 1` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Chande Momentum Oscillator (CMO) is a momentum indicator developed by Tushar Chande. -- Parameterized by `period` (default defaultperiod). +- Parameterized by `period` (default 14). - Output range: $-100$ to $+100$. - Requires `period + 1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/momentum/macd/Macd.md b/lib/momentum/macd/Macd.md index fa919674..0a3d3c84 100644 --- a/lib/momentum/macd/Macd.md +++ b/lib/momentum/macd/Macd.md @@ -7,14 +7,14 @@ | **Parameters** | `fastPeriod` (default 12), `slowPeriod` (default 26), `signalPeriod` (default 9) | | **Outputs** | Multiple series (Signal, Histogram) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `Max(fast, slow) + signal - 2` bars (33 default) | ### TL;DR - The Moving Average Convergence Divergence measures momentum through the relationship between two exponential moving averages. - Parameterized by `fastperiod` (default 12), `slowperiod` (default 26), `signalperiod` (default 9). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `Max(fast, slow) + signal - 2` bars (33 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The trend is your friend, until it bends." — Ed Seykota diff --git a/lib/momentum/ppo/Ppo.md b/lib/momentum/ppo/Ppo.md index 9a513615..3dcd61c8 100644 --- a/lib/momentum/ppo/Ppo.md +++ b/lib/momentum/ppo/Ppo.md @@ -4,17 +4,17 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | Source (close) | -| **Parameters** | `fastPeriod` (default DefaultFastPeriod), `slowPeriod` (default DefaultSlowPeriod), `signalPeriod` (default DefaultSignalPeriod) | +| **Parameters** | `fastPeriod` (default 12), `slowPeriod` (default 26), `signalPeriod` (default 9) | | **Outputs** | Multiple series (Signal, Histogram) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `slowPeriod + signalPeriod` bars (35 default) | ### TL;DR - PPO (Percentage Price Oscillator) measures the percentage difference between a fast EMA and a slow EMA. -- Parameterized by `fastperiod` (default defaultfastperiod), `slowperiod` (default defaultslowperiod), `signalperiod` (default defaultsignalperiod). +- Parameterized by `fastPeriod` (default 12), `slowPeriod` (default 26), `signalPeriod` (default 9). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `slowPeriod + signalPeriod` bars (35 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "MACD told you the spread in dollars. PPO tells you the spread in percent. One of those actually works across instruments." diff --git a/lib/momentum/rsx/Rsx.md b/lib/momentum/rsx/Rsx.md index 353d56e4..633850ce 100644 --- a/lib/momentum/rsx/Rsx.md +++ b/lib/momentum/rsx/Rsx.md @@ -1,4 +1,4 @@ -# RSX: Relative Strength Quality Index +# RSX: Relative Strength Xtra (Jurik RSX) | Property | Value | | ---------------- | -------------------------------- | diff --git a/lib/momentum/tsi/Tsi.md b/lib/momentum/tsi/Tsi.md index 319e6132..2f3125a3 100644 --- a/lib/momentum/tsi/Tsi.md +++ b/lib/momentum/tsi/Tsi.md @@ -4,17 +4,17 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | Source (close) | -| **Parameters** | `longPeriod` (default DefaultLongPeriod), `shortPeriod` (default DefaultShortPeriod), `signalPeriod` (default DefaultSignalPeriod) | +| **Parameters** | `longPeriod` (default 25), `shortPeriod` (default 13), `signalPeriod` (default 13) | | **Outputs** | Single series (Tsi) | -| **Output range** | $-1$ to $+1$ | -| **Warmup** | 1 bar | +| **Output range** | $-100$ to $+100$ | +| **Warmup** | `longPeriod + shortPeriod + signalPeriod` bars (51 default) | ### TL;DR - The True Strength Index (TSI) is a momentum oscillator developed by William Blau that uses double-smoothed exponential moving averages of price mom... -- Parameterized by `longperiod` (default defaultlongperiod), `shortperiod` (default defaultshortperiod), `signalperiod` (default defaultsignalperiod). -- Output range: $-1$ to $+1$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Parameterized by `longPeriod` (default 25), `shortPeriod` (default 13), `signalPeriod` (default 13). +- Output range: $-100$ to $+100$. +- Requires `longPeriod + shortPeriod + signalPeriod` bars (51 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. The True Strength Index (TSI) is a momentum oscillator developed by William Blau that uses double-smoothed exponential moving averages of price momentum to reduce noise and identify trend strength and direction. diff --git a/lib/numerics/accel/accel.pine b/lib/numerics/accel/accel.pine index 0ca4db72..af73962e 100644 --- a/lib/numerics/accel/accel.pine +++ b/lib/numerics/accel/accel.pine @@ -1,83 +1,28 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Acceleration (Slope of Slope) (ACCEL)", "ACCEL", overlay=false, precision=8) +indicator("Second Derivative / Acceleration (ACCEL)", "ACCEL", overlay=false, precision=8) -//@function Calculates acceleration (slope of slope) -//@param src Source series to calculate slope from -//@param len Lookback period for calculation -//@returns acceleration -accel(series float src, simple int len1) => - if len1 <= 1 - runtime.error("Length 1 for slope calculation must be greater than 1") - var float sumX1 = 0.0, var float sumY1 = 0.0, var float sumXY1 = 0.0, var float sumX21 = 0.0 - var int validCount1 = 0 - var array x_values1 = array.new_float(len1) - var array y_values1 = array.new_float(len1) - var int head1 = 0 - var int internal_time_counter1 = 0 - if internal_time_counter1 >= len1 - float oldX1 = array.get(x_values1, head1) - float oldY1 = array.get(y_values1, head1) - if not na(oldY1) - sumX1 := sumX1 - oldX1, sumY1 := sumY1 - oldY1 - sumXY1 := sumXY1 - oldX1 * oldY1, sumX21 := sumX21 - oldX1 * oldX1 - validCount1 := validCount1 - 1 - float currentX1 = internal_time_counter1 - float currentY1 = src - array.set(x_values1, head1, currentX1) - array.set(y_values1, head1, currentY1) - if not na(currentY1) - sumX1 := sumX1 + currentX1, sumY1 := sumY1 + currentY1 - sumXY1 := sumXY1 + currentX1 * currentY1, sumX21 := sumX21 + currentX1 * currentX1 - validCount1 := validCount1 + 1 - head1 := (head1 + 1) % len1 - internal_time_counter1 := internal_time_counter1 + 1 - float current_slope = na - if validCount1 >= 2 - float n1 = validCount1 - float divisor1 = n1 * sumX21 - sumX1 * sumX1 - if divisor1 != 0.0 - current_slope := (n1 * sumXY1 - sumX1 * sumY1) / divisor1 - var float sumX2 = 0.0, var float sumY2 = 0.0, var float sumXY2 = 0.0, var float sumX22 = 0.0 - var int validCount2 = 0 - var array x_values2 = array.new_float(len1) - var array y_values2 = array.new_float(len1) - var int head2 = 0 - var int internal_time_counter2 = 0 - if internal_time_counter2 >= len1 - float oldX2 = array.get(x_values2, head2) - float oldY2 = array.get(y_values2, head2) - if not na(oldY2) - sumX2 := sumX2 - oldX2, sumY2 := sumY2 - oldY2 - sumXY2 := sumXY2 - oldX2 * oldY2, sumX22 := sumX22 - oldX2 * oldX2 - validCount2 := validCount2 - 1 - float currentX2 = internal_time_counter2 - float currentY2 = current_slope - array.set(x_values2, head2, currentX2) - array.set(y_values2, head2, currentY2) - if not na(currentY2) - sumX2 := sumX2 + currentX2, sumY2 := sumY2 + currentY2 - sumXY2 := sumXY2 + currentX2 * currentY2, sumX22 := sumX22 + currentX2 * currentX2 - validCount2 := validCount2 + 1 - head2 := (head2 + 1) % len1 - internal_time_counter2 := internal_time_counter2 + 1 - float calculatedAccel = na - if validCount2 >= 2 - float n2 = validCount2 - float divisor2 = n2 * sumX22 - sumX2 * sumX2 - if divisor2 != 0.0 - calculatedAccel := (n2 * sumXY2 - sumX2 * sumY2) / divisor2 - calculatedAccel +//@function Calculates the second finite difference (acceleration): Accel = V[t] - 2*V[t-1] + V[t-2] +//@param src Source series +//@returns Second difference. Returns 0.0 until 3 bars are available. +//@optimized Uses direct history access and FMA-equivalent for zero-allocation streaming. +accel(series float src) => + float v0 = src + float v1 = src[1] + float v2 = src[2] + if na(v1) or na(v2) + 0.0 + else + v0 - 2.0 * v1 + v2 // ---------- Main loop ---------- // Inputs -i_period = input.int(14, "Period", minval=2) i_source = input.source(close, "Source") // Calculation -a = accel(i_source, i_period) +a = accel(i_source) // Plot plot(a, "Accel", color=color.yellow, linewidth=2) diff --git a/lib/numerics/change/change.pine b/lib/numerics/change/change.pine index bd184319..72de920d 100644 --- a/lib/numerics/change/change.pine +++ b/lib/numerics/change/change.pine @@ -27,4 +27,4 @@ i_length = input.int(1, "Length", minval = 1) result = change(i_source, i_length) // Plot -plot(result, "Change %", color.blue, color=color.yellow, linewidth=2) +plot(result, "Change %", color=color.yellow, linewidth=2) diff --git a/lib/numerics/cwt/Cwt.md b/lib/numerics/cwt/Cwt.md index 1a59b34f..7749e021 100644 --- a/lib/numerics/cwt/Cwt.md +++ b/lib/numerics/cwt/Cwt.md @@ -7,14 +7,14 @@ | **Parameters** | `scale` (default 10.0), `omega0` (default 6.0) | | **Outputs** | Single series (Cwt) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | windowSize (2K+1) bars, where K = round(3 × scale) | ### TL;DR - CWT computes the magnitude of the Continuous Wavelet Transform at a specified scale using the Morlet wavelet, providing a time-frequency decomposit... - Parameterized by `scale` (default 10.0), `omega0` (default 6.0). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires windowSize (2K+1) bars of warmup before first valid output (IsHot = true), where K = round(3 × scale). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. CWT computes the magnitude of the Continuous Wavelet Transform at a specified scale using the Morlet wavelet, providing a time-frequency decomposition that measures the energy content of a specific frequency band at each point in time. Unlike Fourier analysis which loses time localization, the wavelet transform maintains both time and frequency information simultaneously. The output is a non-negative magnitude series where peaks indicate strong presence of the target frequency (determined by the scale parameter) and troughs indicate absence of that frequency component. diff --git a/lib/numerics/exptrans/exptrans.pine b/lib/numerics/exptrans/exptrans.pine index 875926e9..391f1711 100644 --- a/lib/numerics/exptrans/exptrans.pine +++ b/lib/numerics/exptrans/exptrans.pine @@ -9,16 +9,17 @@ indicator("Exponential Transformation (EXP)", "Exptrans", overlay=false) //@optimized for performance and dirty data expT(series float source) => if na(source) - runtime.error("Parameter 'source' cannot be na.") - math.exp(source) + na + else + math.exp(source) // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") +i_source = input.source(close, "Source") // Calculation transformedSource = expT(i_source) // Plot -plot(transformedSource, "Exponential Transformation", color=color.yellow, linewidth=2) +plot(transformedSource, "Exptrans", color=color.yellow, linewidth=2) diff --git a/lib/numerics/fft/Fft.cs b/lib/numerics/fft/Fft.cs index 88f9fd72..b53def4f 100644 --- a/lib/numerics/fft/Fft.cs +++ b/lib/numerics/fft/Fft.cs @@ -1,8 +1,11 @@ // FFT: Fast Fourier Transform — Dominant Cycle Detector -// Estimates the dominant cycle period in bars using a DFT on a windowed price buffer. -// Algorithm: Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013. -// Hanning-windowed DFT across bins [minBin..maxBin], with parabolic interpolation -// for sub-bin period estimation. Output: dominant cycle period in bars (clamped). +// Estimates the dominant cycle period in bars using a radix-2 Cooley-Tukey FFT +// on a Hanning-windowed price buffer, with parabolic interpolation for sub-bin +// period estimation. Output: dominant cycle period in bars (clamped). +// +// Algorithm: Cooley, J.W. & Tukey, J.W. (1965). "An Algorithm for the Machine +// Calculation of Complex Fourier Series." Mathematics of Computation, 19(90). +// Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013 (application context). using System.Buffers; using System.Runtime.CompilerServices; @@ -12,15 +15,16 @@ namespace QuanTAlib; /// /// FFT: Fast Fourier Transform Dominant Cycle Detector -/// Computes the dominant cycle period using a Hanning-windowed DFT -/// over a rolling price buffer, with parabolic interpolation refinement. +/// Computes the dominant cycle period using a Hanning-windowed radix-2 +/// Cooley-Tukey FFT over a rolling price buffer, with parabolic interpolation. /// /// /// Key properties: /// - Output: dominant cycle period in bars, clamped to [minPeriod, maxPeriod] -/// - windowSize must be 32, 64, or 128 +/// - windowSize must be 32, 64, or 128 (power of 2 for radix-2) /// - WarmupPeriod = windowSize bars -/// - No allocation in Update (RingBuffer + precomputed Hanning weights) +/// - True O(N log N) radix-2 FFT with bit-reversal permutation +/// - Pre-allocated work arrays for zero-allocation streaming /// - Parabolic interpolation on peak bin for sub-bin accuracy /// [SkipLocalsInit] @@ -31,8 +35,10 @@ public sealed class Fft : AbstractBase private readonly int _maxPeriod; private readonly int _minBin; private readonly int _maxBin; - private readonly double _twoPiOverN; private readonly double[] _hanning; + private readonly int[] _bitRev; + private readonly double[] _workRe; + private readonly double[] _workIm; private readonly RingBuffer _buffer; [StructLayout(LayoutKind.Auto)] @@ -44,7 +50,7 @@ public sealed class Fft : AbstractBase /// /// Initializes a new Fft indicator. /// - /// DFT window size in bars. Must be 32, 64, or 128. Default 64. + /// FFT window size in bars. Must be 32, 64, or 128. Default 64. /// Minimum detectable cycle period. Must be >= 2. Default 4. /// Maximum detectable cycle period. Must be <= windowSize/2. Default 32. public Fft(int windowSize = 64, int minPeriod = 4, int maxPeriod = 32) @@ -67,19 +73,31 @@ public sealed class Fft : AbstractBase _windowSize = windowSize; _minPeriod = minPeriod; _maxPeriod = maxPeriod; - _twoPiOverN = 2.0 * Math.PI / windowSize; + int log2N = Log2(windowSize); - // bin k corresponds to period N/k; k=minBin → period=N/minBin=maxPeriod, k=maxBin → period=N/maxBin=minPeriod + // bin k corresponds to period N/k _minBin = Math.Max(1, windowSize / maxPeriod); _maxBin = Math.Min(windowSize / 2, windowSize / minPeriod); - // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N), n=0..N-1 + // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N) + double twoPiOverN = 2.0 * Math.PI / windowSize; _hanning = new double[windowSize]; for (int n = 0; n < windowSize; n++) { - _hanning[n] = 0.5 - 0.5 * Math.Cos(_twoPiOverN * n); + _hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); } + // Precompute bit-reversal permutation table + _bitRev = new int[windowSize]; + for (int i = 0; i < windowSize; i++) + { + _bitRev[i] = BitReverse(i, log2N); + } + + // Pre-allocate work arrays (zero allocation in hot path) + _workRe = new double[windowSize]; + _workIm = new double[windowSize]; + _buffer = new RingBuffer(windowSize); Name = $"Fft({windowSize},{minPeriod},{maxPeriod})"; WarmupPeriod = windowSize; @@ -90,10 +108,6 @@ public sealed class Fft : AbstractBase /// /// Initializes a new Fft indicator with source for event-based chaining. /// - /// Source indicator for chaining - /// DFT window size. Must be 32, 64, or 128. Default 64. - /// Minimum detectable period. Must be >= 2. Default 4. - /// Maximum detectable period. Must be <= windowSize/2. Default 32. public Fft(ITValuePublisher source, int windowSize = 64, int minPeriod = 4, int maxPeriod = 32) : this(windowSize, minPeriod, maxPeriod) { @@ -103,59 +117,155 @@ public sealed class Fft : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); + /// + /// Computes floor(log2(n)) for powers of 2. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Log2(int n) + { + int p = 0; + int x = n; + while (x > 1) + { + x >>= 1; + p++; + } + return p; + } + + /// + /// Reverses the bits of x using 'bits' bit-width. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitReverse(int x, int bits) + { + int r = 0; + for (int i = 0; i < bits; i++) + { + r = (r << 1) | (x & 1); + x >>= 1; + } + return r; + } + + /// + /// In-place iterative radix-2 Cooley-Tukey FFT. + /// + /// Real part array (modified in-place) + /// Imaginary part array (modified in-place) + /// Array length (must be power of 2) + /// Pre-computed bit-reversal table + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void FftInPlace(double[] re, double[] im, int n, int[] bitRev) + { + // Bit-reversal permutation + for (int i = 0; i < n; i++) + { + int j = bitRev[i]; + if (j > i) + { + (re[i], re[j]) = (re[j], re[i]); + (im[i], im[j]) = (im[j], im[i]); + } + } + + // Cooley-Tukey butterfly stages + int len = 2; + while (len <= n) + { + int half = len >> 1; + double angStep = -2.0 * Math.PI / len; + + for (int start = 0; start < n; start += len) + { + for (int k = 0; k < half; k++) + { + double angle = angStep * k; + double wr = Math.Cos(angle); + double wi = Math.Sin(angle); + + int i0 = start + k; + int i1 = i0 + half; + + double ur = re[i0]; + double ui = im[i0]; + double vr = re[i1]; + double vi = im[i1]; + + // Twiddle: t = w * v + double tr = Math.FusedMultiplyAdd(vr, wr, -(vi * wi)); + double ti = Math.FusedMultiplyAdd(vr, wi, vi * wr); + + re[i0] = ur + tr; + im[i0] = ui + ti; + re[i1] = ur - tr; + im[i1] = ui - ti; + } + } + + len <<= 1; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ComputeDominantPeriod() { var span = _buffer.GetSpan(); int n = _windowSize; - double maxMag = 0.0; - int peakBin = _minBin; - double magBefore = 0.0; - double magAtPeak = 0.0; - double magAfter = 0.0; + + // Fill work arrays: windowed data (oldest→newest), imag=0 + for (int i = 0; i < n; i++) + { + _workRe[i] = span[i] * _hanning[i]; + _workIm[i] = 0.0; + } + + // Radix-2 FFT in-place + FftInPlace(_workRe, _workIm, n, _bitRev); + + // Find peak magnitude in [minBin..maxBin] + double bestMag = -1.0; + int bestK = _minBin; for (int k = _minBin; k <= _maxBin; k++) { - double omegaK = _twoPiOverN * k; - double re = 0.0; - double im = 0.0; - - for (int idx = 0; idx < n; idx++) + double mag = Math.FusedMultiplyAdd(_workRe[k], _workRe[k], _workIm[k] * _workIm[k]); + if (mag > bestMag) { - // span[0]=oldest, span[n-1]=newest - // n=0 in DFT = current (newest): map DFT-n to span index (n-1-dftN) - // span[n-1-dftN]: dftN=0 → span[n-1] (newest), dftN=n-1 → span[0] (oldest) - double val = span[n - 1 - idx]; - double xw = val * _hanning[idx]; - double angle = omegaK * idx; - double cosA = Math.Cos(angle); - double sinA = Math.Sin(angle); - re = Math.FusedMultiplyAdd(xw, cosA, re); - im = Math.FusedMultiplyAdd(xw, -sinA, im); - } - - double mag = Math.FusedMultiplyAdd(re, re, im * im); - - if (mag > maxMag) - { - magBefore = magAtPeak; - magAfter = 0.0; - maxMag = mag; - magAtPeak = mag; - peakBin = k; - } - else if (peakBin > 0 && magAfter == 0.0) - { - magAfter = mag; + bestMag = mag; + bestK = k; } } - // Parabolic interpolation for sub-bin refinement - double denom = magBefore + 2.0 * maxMag + magAfter; - double shift = (denom > 0.0) ? (magBefore - magAfter) / denom : 0.0; - double dominantPeriod = (double)_windowSize / (peakBin + shift); + // Neighbor magnitudes for parabolic interpolation + double a, b, c; + b = bestMag; + + if (bestK > _minBin) + { + a = Math.FusedMultiplyAdd(_workRe[bestK - 1], _workRe[bestK - 1], + _workIm[bestK - 1] * _workIm[bestK - 1]); + } + else + { + a = b; + } + + if (bestK < _maxBin) + { + c = Math.FusedMultiplyAdd(_workRe[bestK + 1], _workRe[bestK + 1], + _workIm[bestK + 1] * _workIm[bestK + 1]); + } + else + { + c = b; + } + + // Parabolic interpolation: shift = 0.5*(a-c)/(a - 2b + c) + double denom = a - 2.0 * b + c; + double shift = Math.Abs(denom) > 0.0 ? 0.5 * (a - c) / denom : 0.0; + double dominantPeriod = (double)_windowSize / (bestK + shift); - // Clamp to [minPeriod, maxPeriod] return Math.Clamp(dominantPeriod, _minPeriod, _maxPeriod); } @@ -215,11 +325,6 @@ public sealed class Fft : AbstractBase /// /// Primes the indicator with historical values. /// - /// - /// Synthetic timestamps are generated by subtracting step × source.Length - /// from . For deterministic or replay-safe pipelines - /// use directly with explicit timestamps. - /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { TimeSpan interval = step ?? TimeSpan.FromSeconds(1); @@ -239,8 +344,8 @@ public sealed class Fft : AbstractBase } /// - /// Computes dominant cycle period over a span of values using a sliding Hanning-windowed DFT. - /// Uses stackalloc for Hanning weights when windowSize <= 64, otherwise ArrayPool. + /// Computes dominant cycle period over a span using sliding Hanning-windowed radix-2 FFT. + /// Uses stackalloc for work arrays when windowSize <= 64, otherwise ArrayPool. /// public static void Batch( ReadOnlySpan src, Span output, @@ -271,15 +376,24 @@ public sealed class Fft : AbstractBase throw new ArgumentException($"maxPeriod must be <= windowSize/2", nameof(maxPeriod)); } + int log2N = Log2(windowSize); double twoPiOverN = 2.0 * Math.PI / windowSize; int minBin = Math.Max(1, windowSize / maxPeriod); int maxBin = Math.Min(windowSize / 2, windowSize / minPeriod); double defaultPeriod = (minPeriod + maxPeriod) * 0.5; double lastValid = defaultPeriod; + // Precompute Hanning window and bit-reversal table const int StackallocThreshold = 64; - double[]? rentedW = null; + double[]? rentedH = null; + double[]? rentedRe = null; + double[]? rentedIm = null; + int[]? rentedBr = null; + scoped Span hanning; + double[] workRe; + double[] workIm; + int[] bitRev; if (windowSize <= StackallocThreshold) { @@ -287,15 +401,24 @@ public sealed class Fft : AbstractBase } else { - rentedW = ArrayPool.Shared.Rent(windowSize); - hanning = rentedW.AsSpan(0, windowSize); + rentedH = ArrayPool.Shared.Rent(windowSize); + hanning = rentedH.AsSpan(0, windowSize); } + // FFT work arrays (must be double[] for FftInPlace) + rentedRe = ArrayPool.Shared.Rent(windowSize); + rentedIm = ArrayPool.Shared.Rent(windowSize); + rentedBr = ArrayPool.Shared.Rent(windowSize); + workRe = rentedRe; + workIm = rentedIm; + bitRev = rentedBr; + try { for (int n = 0; n < windowSize; n++) { hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); + bitRev[n] = BitReverse(n, log2N); } for (int i = 0; i < src.Length; i++) @@ -313,52 +436,49 @@ public sealed class Fft : AbstractBase continue; } - double maxMag = 0.0; - int peakBin = minBin; - double magBefore = 0.0; - double magAtPeak = 0.0; - double magAfter = 0.0; + // Fill work arrays with windowed data + for (int n = 0; n < windowSize; n++) + { + double v = src[i - windowSize + 1 + n]; + if (!double.IsFinite(v)) + { + v = lastValid; + } + workRe[n] = v * hanning[n]; + workIm[n] = 0.0; + } + + // Radix-2 FFT + FftInPlace(workRe, workIm, windowSize, bitRev); + + // Find peak magnitude + double bestMag = -1.0; + int bestK = minBin; for (int k = minBin; k <= maxBin; k++) { - double omegaK = twoPiOverN * k; - double re = 0.0; - double im = 0.0; - - for (int dftN = 0; dftN < windowSize; dftN++) + double mag = Math.FusedMultiplyAdd(workRe[k], workRe[k], workIm[k] * workIm[k]); + if (mag > bestMag) { - // dftN=0 → newest (src[i]), dftN=windowSize-1 → oldest (src[start]) - double v = src[i - dftN]; - if (!double.IsFinite(v)) - { - v = lastValid; - } - - double xw = v * hanning[dftN]; - double angle = omegaK * dftN; - re = Math.FusedMultiplyAdd(xw, Math.Cos(angle), re); - im = Math.FusedMultiplyAdd(xw, -Math.Sin(angle), im); - } - - double mag = Math.FusedMultiplyAdd(re, re, im * im); - - if (mag > maxMag) - { - magBefore = magAtPeak; - magAfter = 0.0; - maxMag = mag; - magAtPeak = mag; - peakBin = k; - } - else if (peakBin > 0 && magAfter == 0.0) - { - magAfter = mag; + bestMag = mag; + bestK = k; } } - double denom = magBefore + 2.0 * maxMag + magAfter; - double shift = (denom > 0.0) ? (magBefore - magAfter) / denom : 0.0; - double dominant = (double)windowSize / (peakBin + shift); + // Neighbor magnitudes for parabolic interpolation + double a = bestK > minBin + ? Math.FusedMultiplyAdd(workRe[bestK - 1], workRe[bestK - 1], + workIm[bestK - 1] * workIm[bestK - 1]) + : bestMag; + + double c = bestK < maxBin + ? Math.FusedMultiplyAdd(workRe[bestK + 1], workRe[bestK + 1], + workIm[bestK + 1] * workIm[bestK + 1]) + : bestMag; + + double denom = a - 2.0 * bestMag + c; + double shift = Math.Abs(denom) > 0.0 ? 0.5 * (a - c) / denom : 0.0; + double dominant = (double)windowSize / (bestK + shift); double clamped = Math.Clamp(dominant, minPeriod, maxPeriod); lastValid = clamped; output[i] = clamped; @@ -366,10 +486,13 @@ public sealed class Fft : AbstractBase } finally { - if (rentedW != null) + if (rentedH != null) { - ArrayPool.Shared.Return(rentedW); + ArrayPool.Shared.Return(rentedH); } + ArrayPool.Shared.Return(rentedRe); + ArrayPool.Shared.Return(rentedIm); + ArrayPool.Shared.Return(rentedBr); } } diff --git a/lib/numerics/fft/Fft.md b/lib/numerics/fft/Fft.md index 2cf7b0b3..798d835f 100644 --- a/lib/numerics/fft/Fft.md +++ b/lib/numerics/fft/Fft.md @@ -6,46 +6,51 @@ | **Inputs** | Source (close) | | **Parameters** | `windowSize` (default 64), `minPeriod` (default 4), `maxPeriod` (default 32) | | **Outputs** | Single series (Fft) | -| **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Output range** | [minPeriod, maxPeriod] | +| **Warmup** | windowSize bars | ### TL;DR -- The FFT indicator computes the dominant cycle period in a price series using a Discrete Fourier Transform with a Hanning window. -- Parameterized by `windowsize` (default 64), `minperiod` (default 4), `maxperiod` (default 32). -- Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). -- Validated against TA-Lib, Skender, and Tulip reference implementations where available. +- The FFT indicator computes the dominant cycle period in a price series using a radix-2 Cooley-Tukey Fast Fourier Transform with a Hanning window. +- Parameterized by `windowSize` (default 64), `minPeriod` (default 4), `maxPeriod` (default 32). +- Output range: [minPeriod, maxPeriod] bars. +- Requires windowSize bars of warmup before first valid output (IsHot = true). +- True $O(N \log N)$ radix-2 FFT with bit-reversal permutation and Cooley-Tukey butterflies. -The FFT indicator computes the dominant cycle period in a price series using a Discrete Fourier Transform with a Hanning window. Rather than outputting frequency-domain magnitudes, it returns the estimated dominant cycle period in bars, making it directly usable as an adaptive period input for other indicators. The implementation uses a brute-force DFT over a constrained frequency band (not a radix-2 FFT), with parabolic interpolation on the magnitude spectrum to achieve sub-bin frequency resolution. With window sizes of 32, 64, or 128 and $O(N \cdot N/2)$ complexity per bar, the indicator trades computational cost for precise cycle detection within user-specified period bounds. +The FFT indicator computes the dominant cycle period in a price series using a true radix-2 Cooley-Tukey Fast Fourier Transform with a Hanning window. Rather than outputting frequency-domain magnitudes, it returns the estimated dominant cycle period in bars, making it directly usable as an adaptive period input for other indicators. The implementation uses an in-place iterative radix-2 FFT with bit-reversal permutation and Cooley-Tukey butterfly operations, achieving $O(N \log N)$ complexity. Parabolic interpolation on the magnitude spectrum provides sub-bin frequency resolution. With window sizes restricted to powers of two (32, 64, or 128), the indicator achieves precise cycle detection within user-specified period bounds with pre-allocated work arrays for zero-allocation streaming. ## Historical Context -The Fourier transform, formalized by Joseph Fourier (1822), decomposes any periodic signal into sinusoidal components. The Fast Fourier Transform algorithm (Cooley and Tukey, 1965) reduced the DFT from $O(N^2)$ to $O(N \log N)$, enabling real-time spectral analysis. However, for the small window sizes used in financial cycle detection (32-128 samples), the asymptotic advantage of FFT over DFT is minimal, and the DFT avoids the power-of-two length constraint. +The Fourier transform, formalized by Joseph Fourier (1822), decomposes any periodic signal into sinusoidal components. The Fast Fourier Transform algorithm, published by James Cooley and John Tukey in 1965, reduced the DFT from $O(N^2)$ to $O(N \log N)$ by recursively decomposing the DFT into smaller sub-problems using the "butterfly" operation pattern. The radix-2 variant requires power-of-two input lengths and uses bit-reversal permutation followed by iterative butterfly stages. -John Ehlers pioneered the application of spectral analysis to financial markets in the 1990s and 2000s, using DFT-based cycle measurement to create adaptive indicators. His work demonstrated that financial time series contain quasi-periodic cycles with time-varying periods, typically in the 6-40 bar range. The dominant cycle period, extracted via spectral peak detection, can drive adaptive moving averages (MAMA, FAMA), adaptive RSI, and other indicators that benefit from knowing the current market rhythm. +John Ehlers pioneered the application of spectral analysis to financial markets in the 1990s and 2000s, using FFT-based cycle measurement to create adaptive indicators. His work demonstrated that financial time series contain quasi-periodic cycles with time-varying periods, typically in the 6-40 bar range. The dominant cycle period, extracted via spectral peak detection, can drive adaptive moving averages (MAMA, FAMA), adaptive RSI, and other indicators that benefit from knowing the current market rhythm. The Hanning window (also called Hann window, after Julius von Hann) is applied to reduce spectral leakage. Without windowing, the sharp truncation of a finite data segment creates artificial high-frequency components that contaminate the spectrum. The Hanning window tapers the data to zero at both ends, suppressing sidelobes at the cost of slightly wider main lobes (reduced frequency resolution). ## Architecture and Physics -The computation pipeline has four stages: +The computation pipeline has five stages: -**Stage 1: Windowed DFT** computes the real and imaginary components of the Fourier coefficients for frequency bins $k$ ranging from `minBin` to `maxBin`: +**Stage 1: Windowing** applies the Hanning window to the rolling price buffer: -$$X[k] = \sum_{n=0}^{N-1} x[n] \cdot w[n] \cdot e^{-j 2\pi k n / N}$$ +$$x_w[n] = x[n] \cdot w[n], \quad w[n] = 0.5 - 0.5\cos\!\left(\frac{2\pi n}{N}\right)$$ -where $w[n] = 0.5 - 0.5\cos(2\pi n/N)$ is the Hanning window. Only bins corresponding to periods in `[minPeriod, maxPeriod]` are evaluated, reducing computation. +**Stage 2: Bit-reversal permutation** reorders the windowed data according to the bit-reversed indices, preparing for in-place butterfly computation. The permutation table is pre-computed in the constructor. -**Stage 2: Power spectrum peak** finds the bin $k^*$ with maximum squared magnitude $|X[k]|^2 = \text{Re}^2 + \text{Im}^2$. During the search, the magnitudes of the bins adjacent to the peak (one before, one after) are captured for interpolation. +**Stage 3: Cooley-Tukey butterflies** perform $\log_2(N)$ stages of butterfly operations. Each stage $s$ processes pairs of elements separated by $2^{s-1}$ positions, combining them with twiddle factors: -**Stage 3: Parabolic interpolation** refines the peak location using a three-point parabola fit on the magnitudes at bins $k^*-1$, $k^*$, $k^*+1$: +$$\begin{aligned} +X[i_0] &\leftarrow X[i_0] + W_N^k \cdot X[i_1] \\ +X[i_1] &\leftarrow X[i_0] - W_N^k \cdot X[i_1] +\end{aligned}$$ -$$\delta = \frac{M_{k^*-1} - M_{k^*+1}}{M_{k^*-1} + 2 M_{k^*} + M_{k^*+1}}$$ +where $W_N^k = e^{-j2\pi k/N}$ is the twiddle factor. -The refined dominant period is $N / (k^* + \delta)$. +**Stage 4: Peak detection with parabolic interpolation** finds the bin $k^*$ with maximum squared magnitude in `[minBin, maxBin]`, then refines using a three-point parabolic fit: -**Stage 4: Clamping** ensures the output stays within `[minPeriod, maxPeriod]`. +$$\delta = \frac{0.5 \cdot (P[k^*-1] - P[k^*+1])}{P[k^*-1] - 2P[k^*] + P[k^*+1]}$$ + +**Stage 5: Period extraction and clamping** converts the refined bin index to period: $T = N / (k^* + \delta)$, clamped to `[minPeriod, maxPeriod]`. **Window size trade-offs**: $N = 32$ gives coarse resolution (period bins spaced ~1 bar apart) but fast response; $N = 128$ gives fine resolution (~0.25 bar spacing) but sluggish adaptation. The default $N = 64$ balances resolution and responsiveness. @@ -55,6 +60,12 @@ The **Discrete Fourier Transform** for $N$ samples: $$X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j 2\pi k n / N}, \quad k = 0, 1, \ldots, N-1$$ +**Radix-2 Cooley-Tukey decomposition** splits the DFT into even and odd indexed sub-problems: + +$$X[k] = \sum_{r=0}^{N/2-1} x[2r] \cdot W_{N/2}^{kr} + W_N^k \sum_{r=0}^{N/2-1} x[2r+1] \cdot W_{N/2}^{kr}$$ + +This recursion, applied iteratively with bit-reversal permutation, achieves $O(N \log N)$ complexity. + **Hanning window**: $$w[n] = 0.5 - 0.5\cos\!\left(\frac{2\pi n}{N}\right)$$ @@ -69,7 +80,7 @@ $$k_{\min} = \max\!\left(1,\; \left\lfloor\frac{N}{T_{\max}}\right\rfloor\right) **Parabolic interpolation** for sub-bin precision: -$$\hat{k} = k^* + \frac{P[k^*-1] - P[k^*+1]}{P[k^*-1] + 2P[k^*] + P[k^*+1]}$$ +$$\hat{k} = k^* + \frac{0.5 \cdot (P[k^*-1] - P[k^*+1])}{P[k^*-1] - 2P[k^*] + P[k^*+1]}$$ $$T_{\text{dominant}} = \frac{N}{\hat{k}}$$ @@ -78,28 +89,33 @@ $$T_{\text{dominant}} = \frac{N}{\hat{k}}$$ ``` FFT(source, windowSize, minPeriod, maxPeriod): N = windowSize - twoPiOverN = 2 * pi / N - minBin = max(1, N / maxPeriod) - maxBin = min(N/2, N / minPeriod) + // Stage 1: Apply Hanning window + for n = 0 to N-1: + workRe[n] = source[n] * hanning[n] + workIm[n] = 0 - maxMag = 0; peakBin = 0 - for k = minBin to maxBin: - re = 0; im = 0 - for n = 0 to N-1: - w = 0.5 - 0.5 * cos(twoPiOverN * n) // Hanning - xw = source[n] * w - angle = twoPiOverN * k * n - re += xw * cos(angle) - im -= xw * sin(angle) - mag = re*re + im*im - if mag > maxMag: - track neighbor magnitudes - maxMag = mag; peakBin = k + // Stage 2: Bit-reversal permutation + for i = 0 to N-1: + j = bitReverse(i) + if j > i: swap(workRe[i], workRe[j]) - // Parabolic interpolation - shift = (magBefore - magAfter) / (magBefore + 2*maxMag + magAfter) - dominantPeriod = N / (peakBin + shift) - return clamp(dominantPeriod, minPeriod, maxPeriod) + // Stage 3: Cooley-Tukey butterflies + len = 2 + while len <= N: + half = len / 2 + angStep = -2π / len + for start = 0 to N-1 step len: + for k = 0 to half-1: + w = exp(j * angStep * k) + butterfly(workRe, workIm, start+k, start+k+half, w) + len *= 2 + + // Stage 4: Peak detection + interpolation + peakBin = argmax |X[k]|² for k in [minBin..maxBin] + shift = 0.5*(P[k-1] - P[k+1]) / (P[k-1] - 2*P[k] + P[k+1]) + + // Stage 5: Period extraction + return clamp(N / (peakBin + shift), minPeriod, maxPeriod) ``` @@ -107,34 +123,37 @@ FFT(source, windowSize, minPeriod, maxPeriod): ### Operation Count (Streaming Mode) -FFT (DFT dominant cycle detector) evaluates B frequency bins, each requiring N multiply-accumulates — O(N*B) per bar. +FFT (radix-2 Cooley-Tukey) performs N/2 butterflies per stage across log₂(N) stages — O(N log N) per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | | Hanning window multiply | N | 2 cy | ~2N cy | -| DFT inner loop (B bins * N samples) | B*N | 4 cy | ~4*N*B cy | -| cos/sin evaluation (precomputed table) | 2*B*N | 0 cy | ~0 cy | -| Magnitude comparison + peak track | B | 2 cy | ~2B cy | -| Parabolic interpolation (3 points) | 1 | 5 cy | ~5 cy | -| **Total (N=64, B=10)** | **O(N*B)** | — | **~2617 cy** | +| Bit-reversal permutation | N | 1 cy | ~N cy | +| Butterfly operations (log₂N stages × N/2) | N/2 × log₂N | 8 cy | ~4N·log₂N cy | +| cos/sin per butterfly | N/2 × log₂N | 14 cy | ~7N·log₂N cy | +| Magnitude search (B bins) | B | 4 cy | ~4B cy | +| Parabolic interpolation | 1 | 10 cy | ~10 cy | +| **Total (N=64, B=10)** | **O(N log N)** | — | **~4362 cy** | -O(N*B) per bar where B = active frequency bins. Precomputed sin/cos tables eliminate transcendental cost. Suitable for 1-minute+ timeframes; not tick-data hot paths. +O(N log N) per bar. Pre-allocated work arrays ensure zero allocation in the hot path. Twiddle factor computation dominates; pre-computing sin/cos tables would reduce to ~2500 cy. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | | Hanning window application | Yes | Vector multiply with precomputed weights | -| DFT inner dot product | Yes | FMA with sin/cos table lookup | -| Magnitude squared | Yes | Vector FMA (re^2 + im^2) | +| Bit-reversal permutation | No | Random access pattern; scalar only | +| Butterfly multiply-add | Yes | Complex FMA operations on paired elements | +| Magnitude squared | Yes | Vector FMA (re² + im²) | | Peak search | Partial | Max reduction; SIMD-friendly | -Strong batch SIMD: inner dot products are FMA-vectorizable. AVX2 processes 4 complex outputs per 2 cycles. Expected 3-4× speedup for N=64. +Moderate SIMD potential: butterfly FMA operations are vectorizable within each stage. Bit-reversal permutation is inherently scalar. Expected 2× speedup over scalar for N=64. ## Resources -- Cooley, J.W. & Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." Mathematics of Computation, 1965. +- Cooley, J.W. & Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." *Mathematics of Computation*, 1965. - Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013. - Ehlers, J.F. "Rocket Science for Traders." Wiley, 2001. -- Harris, F.J. "On the Use of Windows for Harmonic Analysis with the Discrete Fourier Transform." Proc. IEEE, 1978. +- Harris, F.J. "On the Use of Windows for Harmonic Analysis with the Discrete Fourier Transform." *Proc. IEEE*, 1978. - Oppenheim, A.V. & Schafer, R.W. "Discrete-Time Signal Processing." 3rd edition, Pearson, 2010. +- PineScript reference: [`fft.pine`](fft.pine) diff --git a/lib/numerics/ifft/Ifft.cs b/lib/numerics/ifft/Ifft.cs index c7c448fd..32e9d75e 100644 --- a/lib/numerics/ifft/Ifft.cs +++ b/lib/numerics/ifft/Ifft.cs @@ -1,7 +1,10 @@ -// IFFT: Inverse FFT Spectral Low-Pass Filter -// Reconstructs a filtered price signal by summing the DC component and -// the first H harmonics of the Hanning-windowed DFT. Output overlays on price. -// More harmonics → less smoothing; fewer harmonics → smoother output. +// IFFT: Inverse Fast Fourier Transform — Spectral Low-Pass Filter +// Reconstructs a filtered price signal by performing a forward radix-2 FFT, +// zeroing frequency bins above numHarmonics, then applying an inverse FFT. +// Output overlays on price. More harmonics → less smoothing; fewer → smoother. +// +// Algorithm: Cooley, J.W. & Tukey, J.W. (1965). Forward FFT → spectral +// truncation → inverse FFT reconstruction. using System.Buffers; using System.Runtime.CompilerServices; @@ -11,16 +14,18 @@ namespace QuanTAlib; /// /// IFFT: Inverse FFT Spectral Low-Pass Filter -/// Reconstructs a filtered price value from the DC component plus -/// the first numHarmonics frequency bins of the Hanning-windowed DFT. +/// Reconstructs a filtered price value by performing a forward radix-2 FFT, +/// zeroing bins above numHarmonics (preserving conjugate symmetry), +/// then applying an inverse FFT to reconstruct the time-domain signal. /// /// /// Key properties: /// - Output: reconstructed price (spectral low-pass filtered), overlays on price chart -/// - windowSize must be 32, 64, or 128 +/// - windowSize must be 32, 64, or 128 (power of 2 for radix-2) /// - numHarmonics clamped to [1, windowSize/2] /// - WarmupPeriod = windowSize bars -/// - No allocation in Update (RingBuffer + precomputed Hanning weights) +/// - True O(N log N) radix-2 FFT/IFFT with bit-reversal permutation +/// - Pre-allocated work arrays for zero-allocation streaming /// - Increasing harmonics increases detail (less smoothing) /// [SkipLocalsInit] @@ -28,9 +33,11 @@ public sealed class Ifft : AbstractBase { private readonly int _windowSize; private readonly int _numHarmonics; - private readonly double _twoPiOverN; private readonly double _invN; private readonly double[] _hanning; + private readonly int[] _bitRev; + private readonly double[] _workRe; + private readonly double[] _workIm; private readonly RingBuffer _buffer; [StructLayout(LayoutKind.Auto)] @@ -42,8 +49,8 @@ public sealed class Ifft : AbstractBase /// /// Initializes a new Ifft indicator. /// - /// DFT window size in bars. Must be 32, 64, or 128. Default 64. - /// Number of harmonics to reconstruct. Must be >= 1. Default 5. + /// FFT window size in bars. Must be 32, 64, or 128. Default 64. + /// Number of harmonics to preserve. Must be >= 1. Default 5. public Ifft(int windowSize = 64, int numHarmonics = 5) { if (windowSize != 32 && windowSize != 64 && windowSize != 128) @@ -58,16 +65,28 @@ public sealed class Ifft : AbstractBase _windowSize = windowSize; _numHarmonics = Math.Min(numHarmonics, windowSize / 2); - _twoPiOverN = 2.0 * Math.PI / windowSize; + int log2N = Log2(windowSize); _invN = 1.0 / windowSize; - // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N), n=0..N-1 + // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N) + double twoPiOverN = 2.0 * Math.PI / windowSize; _hanning = new double[windowSize]; for (int n = 0; n < windowSize; n++) { - _hanning[n] = 0.5 - 0.5 * Math.Cos(_twoPiOverN * n); + _hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); } + // Precompute bit-reversal permutation table + _bitRev = new int[windowSize]; + for (int i = 0; i < windowSize; i++) + { + _bitRev[i] = BitReverse(i, log2N); + } + + // Pre-allocate work arrays (zero allocation in hot path) + _workRe = new double[windowSize]; + _workIm = new double[windowSize]; + _buffer = new RingBuffer(windowSize); Name = $"Ifft({windowSize},{numHarmonics})"; WarmupPeriod = windowSize; @@ -78,9 +97,6 @@ public sealed class Ifft : AbstractBase /// /// Initializes a new Ifft indicator with source for event-based chaining. /// - /// Source indicator for chaining - /// DFT window size. Must be 32, 64, or 128. Default 64. - /// Number of harmonics to reconstruct. Must be >= 1. Default 5. public Ifft(ITValuePublisher source, int windowSize = 64, int numHarmonics = 5) : this(windowSize, numHarmonics) { @@ -90,42 +106,99 @@ public sealed class Ifft : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Log2(int n) + { + int p = 0; + int x = n; + while (x > 1) + { + x >>= 1; + p++; + } + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitReverse(int x, int bits) + { + int r = 0; + for (int i = 0; i < bits; i++) + { + r = (r << 1) | (x & 1); + x >>= 1; + } + return r; + } + + /// + /// Applies spectral truncation: zeroes frequency bins outside the + /// preserved range [0..numHarmonics] and their conjugate mirrors + /// [N-numHarmonics..N-1], ensuring real-valued IFFT output. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SpectralTruncate(double[] re, double[] im, int n, int numHarmonics) + { + // Keep bins 0..numHarmonics and N-numHarmonics..N-1 (conjugate symmetry) + // Zero everything in between: bins numHarmonics+1..N-numHarmonics-1 + int startZero = numHarmonics + 1; + int endZero = n - numHarmonics; // exclusive + + for (int k = startZero; k < endZero; k++) + { + re[k] = 0.0; + im[k] = 0.0; + } + } + + /// + /// Computes inverse FFT in-place using the conjugate method: + /// IFFT(X) = (1/N) * conj(FFT(conj(X))) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void IfftInPlace(double[] re, double[] im, int n, int[] bitRev, double invN) + { + // Conjugate input + for (int i = 0; i < n; i++) + { + im[i] = -im[i]; + } + + // Forward FFT + Fft.FftInPlace(re, im, n, bitRev); + + // Conjugate output and scale by 1/N + for (int i = 0; i < n; i++) + { + re[i] *= invN; + im[i] = -im[i] * invN; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ComputeIfft() { var span = _buffer.GetSpan(); int n = _windowSize; - // DC component (k=0): sum of windowed values / N - double dcRe = 0.0; - for (int idx = 0; idx < n; idx++) + // Fill work arrays: windowed data (oldest→newest), imag=0 + for (int i = 0; i < n; i++) { - // span[0]=oldest, span[n-1]=newest - // dftN=0→newest, dftN=n-1→oldest → span index = n-1-dftN - double val = span[n - 1 - idx]; - dcRe = Math.FusedMultiplyAdd(val, _hanning[idx], dcRe); + _workRe[i] = span[i] * _hanning[i]; + _workIm[i] = 0.0; } - double result = dcRe * _invN; + // Forward FFT + Fft.FftInPlace(_workRe, _workIm, n, _bitRev); - // Harmonics k=1..H: add 2*re/N at time n=0 (reconstruction at current bar) - for (int k = 1; k <= _numHarmonics; k++) - { - double omegaK = _twoPiOverN * k; - double re = 0.0; + // Spectral truncation: zero bins above numHarmonics + SpectralTruncate(_workRe, _workIm, n, _numHarmonics); - for (int idx = 0; idx < n; idx++) - { - double val = span[n - 1 - idx]; - double xw = val * _hanning[idx]; - double angle = omegaK * idx; - re = Math.FusedMultiplyAdd(xw, Math.Cos(angle), re); - } + // Inverse FFT to reconstruct filtered time-domain signal + IfftInPlace(_workRe, _workIm, n, _bitRev, _invN); - result = Math.FusedMultiplyAdd(2.0 * _invN, re, result); - } - - return result; + // Return the newest sample (last position in the array) + return _workRe[n - 1]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -200,8 +273,8 @@ public sealed class Ifft : AbstractBase } /// - /// Computes IFFT reconstruction over a span of values using a sliding Hanning-windowed DFT. - /// Uses stackalloc for Hanning weights when windowSize <= 64, otherwise ArrayPool. + /// Computes IFFT reconstruction over a span using sliding Hanning-windowed + /// radix-2 FFT → spectral truncation → inverse FFT. /// public static void Batch( ReadOnlySpan src, Span output, @@ -228,12 +301,14 @@ public sealed class Ifft : AbstractBase } int clampedHarmonics = Math.Min(numHarmonics, windowSize / 2); + int log2N = Log2(windowSize); double twoPiOverN = 2.0 * Math.PI / windowSize; double invN = 1.0 / windowSize; double lastValid = 0.0; + // Precompute Hanning window const int StackallocThreshold = 64; - double[]? rentedW = null; + double[]? rentedH = null; scoped Span hanning; if (windowSize <= StackallocThreshold) @@ -242,15 +317,21 @@ public sealed class Ifft : AbstractBase } else { - rentedW = ArrayPool.Shared.Rent(windowSize); - hanning = rentedW.AsSpan(0, windowSize); + rentedH = ArrayPool.Shared.Rent(windowSize); + hanning = rentedH.AsSpan(0, windowSize); } + // FFT work arrays and bit-reversal table + double[] workRe = ArrayPool.Shared.Rent(windowSize); + double[] workIm = ArrayPool.Shared.Rent(windowSize); + int[] bitRev = ArrayPool.Shared.Rent(windowSize); + try { for (int n = 0; n < windowSize; n++) { hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); + bitRev[n] = BitReverse(n, log2N); } for (int i = 0; i < src.Length; i++) @@ -268,52 +349,42 @@ public sealed class Ifft : AbstractBase continue; } - // DC component - double dcRe = 0.0; - for (int dftN = 0; dftN < windowSize; dftN++) + // Fill work arrays with windowed data (oldest→newest) + for (int n = 0; n < windowSize; n++) { - double v = src[i - dftN]; + double v = src[i - windowSize + 1 + n]; if (!double.IsFinite(v)) { v = lastValid; } - - dcRe = Math.FusedMultiplyAdd(v, hanning[dftN], dcRe); + workRe[n] = v * hanning[n]; + workIm[n] = 0.0; } - double result = dcRe * invN; + // Forward FFT + Fft.FftInPlace(workRe, workIm, windowSize, bitRev); - // Harmonics - for (int k = 1; k <= clampedHarmonics; k++) - { - double omegaK = twoPiOverN * k; - double re = 0.0; + // Spectral truncation + SpectralTruncate(workRe, workIm, windowSize, clampedHarmonics); - for (int dftN = 0; dftN < windowSize; dftN++) - { - double v = src[i - dftN]; - if (!double.IsFinite(v)) - { - v = lastValid; - } - - double xw = v * hanning[dftN]; - re = Math.FusedMultiplyAdd(xw, Math.Cos(omegaK * dftN), re); - } - - result = Math.FusedMultiplyAdd(2.0 * invN, re, result); - } + // Inverse FFT + IfftInPlace(workRe, workIm, windowSize, bitRev, invN); + // Extract newest sample + double result = workRe[windowSize - 1]; lastValid = result; output[i] = result; } } finally { - if (rentedW != null) + if (rentedH != null) { - ArrayPool.Shared.Return(rentedW); + ArrayPool.Shared.Return(rentedH); } + ArrayPool.Shared.Return(workRe); + ArrayPool.Shared.Return(workIm); + ArrayPool.Shared.Return(bitRev); } } diff --git a/lib/numerics/ifft/Ifft.md b/lib/numerics/ifft/Ifft.md index 279707ec..1464ffb9 100644 --- a/lib/numerics/ifft/Ifft.md +++ b/lib/numerics/ifft/Ifft.md @@ -1,4 +1,4 @@ -# IFFT: Inverse Fast Fourier Transform (Spectral Filter) +# IFFT: Inverse Fast Fourier Transform (Spectral Low-Pass Filter) | Property | Value | | ---------------- | -------------------------------- | @@ -6,64 +6,68 @@ | **Inputs** | Source (close) | | **Parameters** | `windowSize` (default 64), `numHarmonics` (default 5) | | **Outputs** | Single series (Ifft) | -| **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Output range** | Varies (overlays on price) | +| **Warmup** | windowSize bars | ### TL;DR -- The Inverse FFT indicator reconstructs a smoothed version of the price series by performing a forward DFT, retaining only the lowest-frequency harm... -- Parameterized by `windowsize` (default 64), `numharmonics` (default 5). -- Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). -- Validated against TA-Lib, Skender, and Tulip reference implementations where available. +- The IFFT indicator reconstructs a smoothed version of the price series using a true forward FFT → spectral truncation → inverse FFT pipeline. +- Parameterized by `windowSize` (default 64), `numHarmonics` (default 5). +- Output range: Varies (overlays on price chart). +- Requires windowSize bars of warmup before first valid output (IsHot = true). +- True $O(N \log N)$ radix-2 FFT/IFFT with bit-reversal permutation and Cooley-Tukey butterflies. -The Inverse FFT indicator reconstructs a smoothed version of the price series by performing a forward DFT, retaining only the lowest-frequency harmonics, and synthesizing the output via inverse transform. The result is a spectral low-pass filter that preserves the dominant cyclical components while discarding high-frequency noise. By controlling the number of retained harmonics $H$, the user adjusts the smoothness/responsiveness trade-off: $H = 1$ yields a near-sinusoidal trend, while $H = N/2$ reproduces the original (windowed) signal. The indicator overlays on price and provides a frequency-domain alternative to conventional moving averages. +The IFFT indicator reconstructs a smoothed version of the price series by performing a true radix-2 forward FFT, zeroing frequency bins above the specified number of harmonics (spectral truncation), then applying a true inverse FFT to reconstruct the filtered time-domain signal. The result is a spectral low-pass filter that preserves the dominant cyclical components while discarding high-frequency noise. By controlling the number of retained harmonics $H$, the user adjusts the smoothness/responsiveness trade-off: $H = 1$ yields a near-sinusoidal trend, while $H = N/2$ reproduces the original (windowed) signal. The indicator overlays on price and provides a frequency-domain alternative to conventional moving averages. ## Historical Context -Spectral filtering via Fourier decomposition dates to Joseph Fourier's 1822 work on heat conduction, where he showed that any periodic function can be represented as a sum of sinusoids. The idea of reconstructing a signal from a subset of its Fourier coefficients is foundational to signal compression (JPEG, MP3) and has been applied to financial time series since the 1970s. +Spectral filtering via Fourier decomposition dates to Joseph Fourier's 1822 work on heat conduction, where he showed that any periodic function can be represented as a sum of sinusoids. The Cooley-Tukey FFT algorithm (1965) made real-time spectral analysis practical by reducing complexity from $O(N^2)$ to $O(N \log N)$. -John Ehlers brought spectral methods to mainstream technical analysis through his books on cycle analytics. His approach typically uses the DFT to identify the dominant cycle, then constructs adaptive filters tuned to that cycle. The IFFT indicator takes the complementary approach: rather than extracting a single cycle period, it reconstructs the signal from the $H$ lowest-frequency components, producing a multi-harmonic trend estimate. +John Ehlers brought spectral methods to mainstream technical analysis through his books on cycle analytics. The IFFT indicator implements the classic spectral filtering paradigm: forward FFT to decompose into frequency components, selective retention of low-frequency bins, and inverse FFT to reconstruct the filtered signal. -The Hanning window applied before the forward DFT reduces spectral leakage, ensuring that the retained harmonics accurately represent the true low-frequency content rather than artifacts of the window boundary. The inverse step only uses the real part of the synthesis (cosine terms), since the output must be a real-valued price estimate. The factor of 2 in the inverse accounts for the conjugate symmetry of real-valued DFT coefficients. +The Hanning window applied before the forward FFT reduces spectral leakage, ensuring that the retained harmonics accurately represent the true low-frequency content rather than artifacts of the window boundary. Conjugate symmetry is preserved during spectral truncation to guarantee real-valued reconstruction. ## Architecture and Physics -The computation has three stages executed per bar: +The computation has four stages executed per bar: -**Stage 1: DC component** computes the windowed mean of the source over the window. This is the zero-frequency (average level) component: +**Stage 1: Forward FFT** applies a Hanning window to the rolling price buffer, then performs an in-place radix-2 Cooley-Tukey FFT with bit-reversal permutation: -$$\text{DC} = \frac{1}{N}\sum_{n=0}^{N-1} x[n] \cdot w[n]$$ +$$X[k] = \text{FFT}\!\left(x[n] \cdot w[n]\right)$$ -**Stage 2: Forward DFT for harmonics $k = 1$ to $H$** computes the real and imaginary Fourier coefficients for each retained harmonic. The Hanning window $w[n] = 0.5 - 0.5\cos(2\pi n/N)$ is applied to every sample. +where $w[n] = 0.5 - 0.5\cos(2\pi n/N)$. -**Stage 3: Inverse synthesis** reconstructs the current bar's value by summing the DC component plus twice the real part of each harmonic evaluated at $n = 0$ (the current bar): +**Stage 2: Spectral truncation** zeroes frequency bins outside the preserved range, keeping bins $k = 0, 1, \ldots, H$ and their conjugate mirrors $k = N-H, \ldots, N-1$: -$$\hat{x}[0] = \frac{\text{DC}_{\text{Re}}}{N} + \sum_{k=1}^{H} \frac{2 \cdot \text{Re}(X[k])}{N}$$ +$$\tilde{X}[k] = \begin{cases} X[k] & \text{if } k \le H \text{ or } k \ge N-H \\ 0 & \text{otherwise} \end{cases}$$ -The factor $2/N$ accounts for: (1) the $1/N$ normalization of the inverse DFT, and (2) the factor of 2 from collapsing the conjugate-symmetric negative frequencies. +This preserves conjugate symmetry ($\tilde{X}[N-k] = \tilde{X}[k]^*$), ensuring the inverse FFT produces real-valued output. -**Complexity**: The forward DFT for $H$ harmonics costs $O(N \cdot H)$ multiply-adds per bar. With $N = 64$ and $H = 5$ (defaults), this is ~320 multiply-adds per bar. The inverse synthesis at $n = 0$ reduces to just summing the real components, costing $O(H)$. +**Stage 3: Inverse FFT** reconstructs the filtered time-domain signal using the conjugate method: -**Smoothness control**: Fewer harmonics produce smoother output but introduce more lag and lose detail. The relationship between harmonics and equivalent moving average length is roughly: $H$ harmonics approximate the smoothness of an $N/(2H)$-period moving average, but with better frequency selectivity (sharper cutoff). +$$\hat{x}[n] = \frac{1}{N} \cdot \overline{\text{FFT}\!\left(\overline{\tilde{X}[k]}\right)}$$ + +This reuses the forward FFT algorithm by conjugating inputs, applying FFT, conjugating outputs, and scaling by $1/N$. + +**Stage 4: Sample extraction** returns the value at position $N-1$ (the newest bar in the window). + +**Complexity**: Two FFT passes of $O(N \log N)$ each, plus $O(N)$ for windowing and spectral truncation. Total: $O(N \log N)$ per bar. + +**Smoothness control**: Fewer harmonics produce smoother output but introduce more lag. The relationship between harmonics and equivalent moving average length is roughly: $H$ harmonics approximate the smoothness of an $N/(2H)$-period moving average, with better frequency selectivity (sharper cutoff). ## Mathematical Foundation -The **forward DFT** with Hanning window: +The **forward FFT** with Hanning window (radix-2 Cooley-Tukey): -$$X[k] = \sum_{n=0}^{N-1} x[n] \cdot w[n] \cdot e^{-j 2\pi k n / N}$$ +$$X[k] = \text{FFT}_N\!\left(x[n] \cdot w[n]\right), \quad k = 0, 1, \ldots, N-1$$ -where $w[n] = 0.5 - 0.5\cos(2\pi n / N)$. +**Spectral truncation** (ideal low-pass in frequency domain): -The **inverse DFT** evaluated at the current bar ($n = 0$): +$$\tilde{X}[k] = X[k] \cdot H_{\text{LP}}[k], \quad H_{\text{LP}}[k] = \begin{cases} 1 & k \le H \text{ or } k \ge N-H \\ 0 & \text{otherwise} \end{cases}$$ -$$\hat{x}[0] = \frac{1}{N}\sum_{k=0}^{N-1} X[k] \cdot e^{j 2\pi k \cdot 0 / N} = \frac{1}{N}\sum_{k=0}^{N-1} X[k]$$ +**Inverse FFT** via conjugation: -Since $e^{j \cdot 0} = 1$, the inverse at $n = 0$ is simply the sum of all retained coefficients divided by $N$. - -For a real-valued signal, $X[N-k] = X[k]^*$, so: - -$$\hat{x}[0] = \frac{X[0]}{N} + \frac{2}{N}\sum_{k=1}^{H} \text{Re}(X[k])$$ +$$\hat{x}[n] = \frac{1}{N} \cdot \overline{\text{FFT}_N\!\left(\overline{\tilde{X}[k]}\right)}$$ **Parseval's theorem** relates the energy retained: @@ -75,27 +79,27 @@ $$\frac{\sum_{k=0}^{H} |X[k]|^2}{\sum_{k=0}^{N/2} |X[k]|^2} = \text{fraction of IFFT(source, windowSize, numHarmonics): N = windowSize H = min(numHarmonics, N/2) - twoPiOverN = 2 * pi / N - // DC component (k=0) - dcRe = 0 + // Stage 1: Window + Forward FFT for n = 0 to N-1: - w = 0.5 - 0.5 * cos(twoPiOverN * n) - dcRe += source[n] * w - result = dcRe / N + workRe[n] = source[n] * hanning[n] + workIm[n] = 0 + FFT_InPlace(workRe, workIm, N) - // Harmonics k=1..H - for k = 1 to H: - re = 0; im = 0 - for n = 0 to N-1: - w = 0.5 - 0.5 * cos(twoPiOverN * n) - xw = source[n] * w - angle = twoPiOverN * k * n - re += xw * cos(angle) - im -= xw * sin(angle) - result += 2 * re / N // inverse at n=0 + // Stage 2: Spectral truncation + for k = H+1 to N-H-1: + workRe[k] = 0 + workIm[k] = 0 - return result + // Stage 3: Inverse FFT (via conjugation) + for i = 0 to N-1: workIm[i] = -workIm[i] + FFT_InPlace(workRe, workIm, N) + for i = 0 to N-1: + workRe[i] /= N + workIm[i] = -workIm[i] / N + + // Stage 4: Extract newest sample + return workRe[N-1] ``` @@ -103,32 +107,37 @@ IFFT(source, windowSize, numHarmonics): ### Operation Count (Streaming Mode) -IFFT (Inverse DFT reconstruction) sums B frequency components back into the time domain — O(N*B) per bar. +IFFT performs two radix-2 FFT passes (forward + inverse) plus spectral truncation — O(N log N) per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Complex multiply-accumulate (N * B) | N*B | 4 cy | ~4*N*B cy | -| cos/sin table lookup (precomputed) | 2*N*B | 0 cy | ~0 cy | -| Division by N for normalization | N | 1 cy | ~N cy | -| NaN guard + state update | 1 | 2 cy | ~2 cy | -| **Total (N=64, B=10)** | **O(N*B)** | — | **~2626 cy** | +| Hanning window multiply | N | 2 cy | ~2N cy | +| Forward FFT (N/2 × log₂N butterflies) | N/2 × log₂N | 8 cy | ~4N·log₂N cy | +| Spectral truncation | N-2H | 1 cy | ~(N-2H) cy | +| Conjugation (2×) | 2N | 1 cy | ~2N cy | +| Inverse FFT (N/2 × log₂N butterflies) | N/2 × log₂N | 8 cy | ~4N·log₂N cy | +| Scale by 1/N | N | 1 cy | ~N cy | +| **Total (N=64, H=5)** | **O(N log N)** | — | **~3254 cy** | -Same complexity as forward FFT. Precomputed trig tables allow the inner loop to reduce to 4 FMAs per bin. Paired with FFT for frequency-domain filtering. +Two FFT passes dominate cost. Pre-allocated work arrays ensure zero allocation in the hot path. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Complex MAC (re*cos - im*sin) | Yes | FMA with precomputed table | -| Normalization | Yes | Vector divide by N | -| Output time-domain signal | Yes | Full SIMD reconstruction | +| Hanning window application | Yes | Vector multiply with precomputed weights | +| FFT butterfly operations | Yes | Complex FMA on paired elements | +| Spectral truncation (zeroing) | Yes | Vector zero-fill | +| IFFT butterfly operations | Yes | Same as forward FFT | +| Scale by 1/N | Yes | Vector multiply by constant | -Same SIMD profile as FFT forward pass. 3-4× batch speedup expected over scalar using Vector FMA. +Good SIMD potential: both FFT passes are vectorizable. Expected 2× speedup over scalar for N=64. ## Resources +- Cooley, J.W. & Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." *Mathematics of Computation*, 1965. - Fourier, J.B.J. "Theorie Analytique de la Chaleur." Firmin Didot, 1822. - Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013. - Oppenheim, A.V. & Schafer, R.W. "Discrete-Time Signal Processing." 3rd edition, Pearson, 2010. - Bloomfield, P. "Fourier Analysis of Time Series: An Introduction." 2nd edition, Wiley, 2000. -- Priestley, M.B. "Spectral Analysis and Time Series." Academic Press, 1981. +- PineScript reference: [`ifft.pine`](ifft.pine) diff --git a/lib/numerics/jerk/jerk.pine b/lib/numerics/jerk/jerk.pine index 70855a90..e922d661 100644 --- a/lib/numerics/jerk/jerk.pine +++ b/lib/numerics/jerk/jerk.pine @@ -1,112 +1,29 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Acceleration, Slope of Slope (JERK)", "JERK", overlay=false, precision=8) +indicator("Third Derivative / Jerk (JERK)", "JERK", overlay=false, precision=8) -//@function Calculates jerk (slope of slope of slope) -//@param src Source series to calculate slope from -//@param len Lookback period for calculation -//@returns jerk -jerk(series float src, simple int len1) => - if len1 <= 1 - runtime.error("Length 1 for first slope calculation must be greater than 1") - var float sumX1 = 0.0, var float sumY1 = 0.0, var float sumXY1 = 0.0, var float sumX21 = 0.0 - var int validCount1 = 0 - var array x_values1 = array.new_float(len1) - var array y_values1 = array.new_float(len1) - var int head1 = 0 - var int internal_time_counter1 = 0 - if internal_time_counter1 >= len1 - float oldX1 = array.get(x_values1, head1) - float oldY1 = array.get(y_values1, head1) - if not na(oldY1) - sumX1 := sumX1 - oldX1, sumY1 := sumY1 - oldY1 - sumXY1 := sumXY1 - oldX1 * oldY1, sumX21 := sumX21 - oldX1 * oldX1 - validCount1 := validCount1 - 1 - float currentX1 = internal_time_counter1 - float currentY1 = src - array.set(x_values1, head1, currentX1) - array.set(y_values1, head1, currentY1) - if not na(currentY1) - sumX1 := sumX1 + currentX1, sumY1 := sumY1 + currentY1 - sumXY1 := sumXY1 + currentX1 * currentY1, sumX21 := sumX21 + currentX1 * currentX1 - validCount1 := validCount1 + 1 - head1 := (head1 + 1) % len1 - internal_time_counter1 := internal_time_counter1 + 1 - float current_slope1 = na - if validCount1 >= 2 - float n1 = validCount1 - float divisor1 = n1 * sumX21 - sumX1 * sumX1 - if divisor1 != 0.0 - current_slope1 := (n1 * sumXY1 - sumX1 * sumY1) / divisor1 - var float sumX2 = 0.0, var float sumY2 = 0.0, var float sumXY2 = 0.0, var float sumX22 = 0.0 - var int validCount2 = 0 - var array x_values2 = array.new_float(len1) - var array y_values2 = array.new_float(len1) - var int head2 = 0 - var int internal_time_counter2 = 0 - if internal_time_counter2 >= len1 - float oldX2 = array.get(x_values2, head2) - float oldY2 = array.get(y_values2, head2) - if not na(oldY2) - sumX2 := sumX2 - oldX2, sumY2 := sumY2 - oldY2 - sumXY2 := sumXY2 - oldX2 * oldY2, sumX22 := sumX22 - oldX2 * oldX2 - validCount2 := validCount2 - 1 - float currentX2 = internal_time_counter2 - float currentY2 = current_slope1 - array.set(x_values2, head2, currentX2) - array.set(y_values2, head2, currentY2) - if not na(currentY2) - sumX2 := sumX2 + currentX2, sumY2 := sumY2 + currentY2 - sumXY2 := sumXY2 + currentX2 * currentY2, sumX22 := sumX22 + currentX2 * currentX2 - validCount2 := validCount2 + 1 - head2 := (head2 + 1) % len1 - internal_time_counter2 := internal_time_counter2 + 1 - float current_accel = na - if validCount2 >= 2 - float n2 = validCount2 - float divisor2 = n2 * sumX22 - sumX2 * sumX2 - if divisor2 != 0.0 - current_accel := (n2 * sumXY2 - sumX2 * sumY2) / divisor2 - var float sumX3 = 0.0, var float sumY3 = 0.0, var float sumXY3 = 0.0, var float sumX23 = 0.0 - var int validCount3 = 0 - var array x_values3 = array.new_float(len1) - var array y_values3 = array.new_float(len1) - var int head3 = 0 - var int internal_time_counter3 = 0 - if internal_time_counter3 >= len1 - float oldX3 = array.get(x_values3, head3) - float oldY3 = array.get(y_values3, head3) - if not na(oldY3) - sumX3 := sumX3 - oldX3, sumY3 := sumY3 - oldY3 - sumXY3 := sumXY3 - oldX3 * oldY3, sumX23 := sumX23 - oldX3 * oldX3 - validCount3 := validCount3 - 1 - float currentX3 = internal_time_counter3 - float currentY3 = current_accel - array.set(x_values3, head3, currentX3) - array.set(y_values3, head3, currentY3) - if not na(currentY3) - sumX3 := sumX3 + currentX3, sumY3 := sumY3 + currentY3 - sumXY3 := sumXY3 + currentX3 * currentY3, sumX23 := sumX23 + currentX3 * currentX3 - validCount3 := validCount3 + 1 - head3 := (head3 + 1) % len1 - internal_time_counter3 := internal_time_counter3 + 1 - float calculatedjerk = na - if validCount3 >= 2 - float n3 = validCount3 - float divisor3 = n3 * sumX23 - sumX3 * sumX3 - if divisor3 != 0.0 - calculatedjerk := (n3 * sumXY3 - sumX3 * sumY3) / divisor3 - calculatedjerk +//@function Calculates the third finite difference (jerk): Jerk = V[t] - 3*V[t-1] + 3*V[t-2] - V[t-3] +//@param src Source series +//@returns Third difference. Returns 0.0 until 4 bars are available. +//@optimized Uses direct history access and binomial coefficients [1,-3,3,-1]. +jerk(series float src) => + float v0 = src + float v1 = src[1] + float v2 = src[2] + float v3 = src[3] + if na(v1) or na(v2) or na(v3) + 0.0 + else + v0 - 3.0 * v1 + 3.0 * v2 - v3 - // ---------- Main loop ---------- +// ---------- Main loop ---------- // Inputs -i_period = input.int(14, "Period", minval=2) i_source = input.source(close, "Source") // Calculation -a = jerk(i_source, i_period) +j = jerk(i_source) // Plot -plot(a, "jerk", color=color.yellow, linewidth=2) +plot(j, "Jerk", color=color.yellow, linewidth=2) diff --git a/lib/numerics/lineartrans/lineartrans.pine b/lib/numerics/lineartrans/lineartrans.pine index f1502ad5..c1b840e7 100644 --- a/lib/numerics/lineartrans/lineartrans.pine +++ b/lib/numerics/lineartrans/lineartrans.pine @@ -1,46 +1,29 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Linear Transformation (LINEAR)", "Lineartrans", overlay=false) - -//@function Applies a linear transformation (y = a*(x - sma) + sma + b) relative to the source's SMA, calculated internally. -//@param source series float The input series to transform. -//@param period simple int The lookback period for the internal SMA calculation. -//@param a float The scaling factor (slope). -//@param b float The offset (intercept). -//@returns series float The linearly transformed series relative to its internally calculated SMA. -//@optimized for performance and dirty data -linear(series float source, float a, float b) => - if na(source) or na(a) or na(b) - runtime.error("Parameters 'source', 'a', 'b' cannot be na and 'period' must be > 0.") - var int p = 200 - var array buffer = array.new_float(p, na) - var int head = 0 - var float sum = 0.0 - var int valid_count = 0 - float oldest = array.get(buffer, head) - if not na(oldest) - sum -= oldest - valid_count -= 1 - if not na(source) - sum += source - valid_count += 1 - array.set(buffer, head, source) - head := (head + 1) % p - smaValue = nz(sum / valid_count, source) - a * (source - smaValue) + smaValue + b +indicator("Linear Scaling Transformer (LINEARTRANS)", "LINEARTRANS", overlay=true, precision=8) +//@function Applies a simple affine (linear) transformation: y = slope * x + intercept +//@param src Source series to transform +//@param a Slope (scaling factor). Default 1.0. +//@param b Intercept (offset). Default 0.0. +//@returns Linearly transformed value: a * src + b +//@optimized Single FMA operation per bar — O(1) with zero allocations. +lineartrans(series float src, float a, float b) => + if na(src) + na + else + a * src + b // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") -i_smaPeriod = input.int(200, "SMA Period", minval=1) -i_a = input.float(2.0, "Scale (a)") -i_b = input.float(20.0, "Offset (b)") +i_source = input.source(close, "Source") +i_slope = input.float(1.0, "Slope (a)") +i_intercept = input.float(0.0, "Intercept (b)") // Calculation -transformedSource = linear(i_source, i_a, i_b) +result = lineartrans(i_source, i_slope, i_intercept) // Plot -plot(transformedSource, "Linear Transformation", color=color.yellow, linewidth=2) +plot(result, "Lineartrans", color=color.yellow, linewidth=2) diff --git a/lib/numerics/logtrans/logtrans.pine b/lib/numerics/logtrans/logtrans.pine index 21551c8b..b300dd83 100644 --- a/lib/numerics/logtrans/logtrans.pine +++ b/lib/numerics/logtrans/logtrans.pine @@ -9,8 +9,8 @@ indicator("Logarithmic Transformation (LOG)", "Logtrans", overlay=false) //@optimized for performance and dirty data logT(series float source) => if na(source) - runtime.error("Parameter 'source' cannot be na.") - if source <= 0 + na + else if source <= 0 na else math.log(source) @@ -18,10 +18,10 @@ logT(series float source) => // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") +i_source = input.source(close, "Source") // Calculation transformedSource = logT(i_source) // Plot -plot(transformedSource, "Log Transformation", color=color.green, color=color.yellow, linewidth=2) +plot(transformedSource, "Logtrans", color=color.yellow, linewidth=2) diff --git a/lib/numerics/slope/slope.pine b/lib/numerics/slope/slope.pine index 9fa4ded9..b93dbb5e 100644 --- a/lib/numerics/slope/slope.pine +++ b/lib/numerics/slope/slope.pine @@ -1,61 +1,26 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Slope, Linear Regression (SLOPE)", "SLOPE", overlay=false, precision=8) +indicator("First Derivative / Velocity (SLOPE)", "SLOPE", overlay=false, precision=8) -//@function Calculates slope (linear regression) -//@param src Source series to calculate slope from -//@param len Lookback period for calculation -//@returns Slope value properly calculated -slope(series float src, simple int len) => - if len <= 1 - runtime.error("Length must be greater than 1") - var float sumX = 0.0 - var float sumY = 0.0 - var float sumXY = 0.0 - var float sumX2 = 0.0 - var int validCount = 0 - var array x_values = array.new_float(len) - var array y_values = array.new_float(len) - var int head = 0 - var int internal_time_counter = 0 - if internal_time_counter >= len - float oldX = array.get(x_values, head) - float oldY = array.get(y_values, head) - if not na(oldY) - sumX := sumX - oldX - sumY := sumY - oldY - sumXY := sumXY - oldX * oldY - sumX2 := sumX2 - oldX * oldX - validCount := validCount - 1 - float currentX = internal_time_counter - float currentY = src - array.set(x_values, head, currentX) - array.set(y_values, head, currentY) - if not na(currentY) - sumX := sumX + currentX - sumY := sumY + currentY - sumXY := sumXY + currentX * currentY - sumX2 := sumX2 + currentX * currentX - validCount := validCount + 1 - head := (head + 1) % len - internal_time_counter := internal_time_counter + 1 - float calculatedSlope = na - if validCount >= 2 - float n = validCount - float divisor = n * sumX2 - sumX * sumX - if divisor != 0.0 - calculatedSlope := (n * sumXY - sumX * sumY) / divisor - calculatedSlope +//@function Calculates the first finite difference (velocity): Slope = V[t] - V[t-1] +//@param src Source series +//@returns First difference of consecutive values. Returns 0.0 on first bar. +//@optimized Uses direct history access for zero-allocation streaming. +slope(series float src) => + float prev = src[1] + if na(prev) + 0.0 + else + src - prev // ---------- Main loop ---------- // Inputs -i_period = input.int(14, "Period", minval=2) i_source = input.source(close, "Source") // Calculation -s = slope(i_source, i_period) +s = slope(i_source) // Plot plot(s, "Slope", color=color.yellow, linewidth=2) diff --git a/lib/numerics/sqrttrans/sqrttrans.pine b/lib/numerics/sqrttrans/sqrttrans.pine index 11f95146..61be281d 100644 --- a/lib/numerics/sqrttrans/sqrttrans.pine +++ b/lib/numerics/sqrttrans/sqrttrans.pine @@ -9,8 +9,8 @@ indicator("Square Root Transformation (SQRT)", "Sqrttrans", overlay=false) //@optimized for performance and dirty data sqrtT(series float source) => if na(source) - runtime.error("Parameter 'source' cannot be na.") - if source < 0 + na + else if source < 0 na else math.sqrt(source) @@ -18,10 +18,10 @@ sqrtT(series float source) => // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") +i_source = input.source(close, "Source") // Calculation transformedSource = sqrtT(i_source) // Plot -plot(transformedSource, "Square Root Transformation", color=color.yellow, linewidth=2) +plot(transformedSource, "Sqrttrans", color=color.yellow, linewidth=2) diff --git a/lib/oscillators/ac/Ac.cs b/lib/oscillators/ac/Ac.cs index 0cd219d7..07bf68b6 100644 --- a/lib/oscillators/ac/Ac.cs +++ b/lib/oscillators/ac/Ac.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; namespace QuanTAlib; /// -/// AC: Acceleration Oscillator +/// AC: Accelerator Oscillator /// /// /// Bill Williams' Acceleration Oscillator measures the acceleration or deceleration diff --git a/lib/oscillators/ac/Ac.md b/lib/oscillators/ac/Ac.md index 560c108e..34b5033b 100644 --- a/lib/oscillators/ac/Ac.md +++ b/lib/oscillators/ac/Ac.md @@ -1,4 +1,4 @@ -# AC: Acceleration Oscillator +# AC: Accelerator Oscillator > "Momentum tells you which way the wind is blowing. Acceleration tells you whether the wind is picking up." -- Bill Williams, paraphrased diff --git a/lib/oscillators/bbb/bbb.pine b/lib/oscillators/bbb/bbb.pine index dd3da7c0..656d196e 100644 --- a/lib/oscillators/bbb/bbb.pine +++ b/lib/oscillators/bbb/bbb.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Bollinger %B", "BBB", overlay=false) +indicator("Bollinger %B (BBB)", "BBB", overlay=false) //@function Calculates Bollinger Bands %B oscillator //@param source Series to calculate %B from diff --git a/lib/oscillators/cfo/cfo.pine b/lib/oscillators/cfo/cfo.pine index b7079fcd..ed03f205 100644 --- a/lib/oscillators/cfo/cfo.pine +++ b/lib/oscillators/cfo/cfo.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Chande Forecast Oscillator", "CFO", overlay=false) +indicator("Chande Forecast Oscillator (CFO)", "CFO", overlay=false) //@function Chande Forecast Oscillator - measures percentage difference between price and forecasted price //@param source Price data to analyze diff --git a/lib/oscillators/coppock/Coppock.md b/lib/oscillators/coppock/Coppock.md index eac6a7b5..2af0a8c5 100644 --- a/lib/oscillators/coppock/Coppock.md +++ b/lib/oscillators/coppock/Coppock.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Oscillator | | **Inputs** | Source (close) | -| **Parameters** | `longRoc` (default DefaultLongRoc), `shortRoc` (default DefaultShortRoc), `wmaPeriod` (default DefaultWmaPeriod) | +| **Parameters** | `longRoc` (default 14), `shortRoc` (default 11), `wmaPeriod` (default 10) | | **Outputs** | Single series (Coppock) | | **Output range** | Varies (see docs) | | **Warmup** | 1 bar | @@ -12,7 +12,7 @@ ### TL;DR - The Coppock Curve is a long-term momentum oscillator that applies a Weighted Moving Average to the sum of two Rate of Change calculations at differ... -- Parameterized by `longroc` (default defaultlongroc), `shortroc` (default defaultshortroc), `wmaperiod` (default defaultwmaperiod). +- Parameterized by `longRoc` (default 14), `shortRoc` (default 11), `wmaPeriod` (default 10). - Output range: Varies (see docs). - Requires 1 bar of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/oscillators/fisher04/fisher04.pine b/lib/oscillators/fisher04/fisher04.pine index 6ae96387..9fe6cd30 100644 --- a/lib/oscillators/fisher04/fisher04.pine +++ b/lib/oscillators/fisher04/fisher04.pine @@ -1,3 +1,6 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 // Fisher04: Ehlers Fisher Transform (2004 Cybernetic Analysis) // Source: John Ehlers, "Cybernetic Analysis for Stocks and Futures", Wiley, 2004, Chapter 1 // @@ -7,9 +10,7 @@ // Clamp threshold: 0.9999 vs 0.99→0.999 // Fisher multiplier: 0.25 vs 0.5 // Fisher IIR: 0.5 (same) - -//@version=6 -indicator("Fisher04 - Ehlers 2004 Cybernetic Analysis", shorttitle="Fisher04", overlay=false) +indicator("Ehlers Fisher Transform 2004 (FISHER04)", "FISHER04", overlay=false) length = input.int(10, "Length", minval=1) diff --git a/lib/oscillators/gator/gator.pine b/lib/oscillators/gator/gator.pine index a22fa9ca..002168ce 100644 --- a/lib/oscillators/gator/gator.pine +++ b/lib/oscillators/gator/gator.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Williams Gator Oscillator", "GATOR", overlay=false) +indicator("Williams Gator Oscillator (GATOR)", "GATOR", overlay=false) //@function Calculates Williams Gator Oscillator from Alligator lines //@param source Series to calculate from diff --git a/lib/oscillators/inertia/inertia.pine b/lib/oscillators/inertia/inertia.pine index 55e15f99..c4276e09 100644 --- a/lib/oscillators/inertia/inertia.pine +++ b/lib/oscillators/inertia/inertia.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Inertia", "INERTIA", overlay=false) +indicator("Inertia Oscillator (INERTIA)", "INERTIA", overlay=false) //@function Calculates Inertia oscillator measuring trend strength based on distance from linear regression //@param source Source series to calculate Inertia for diff --git a/lib/oscillators/kdj/kdj.pine b/lib/oscillators/kdj/kdj.pine index 12f9782b..e8f73c1c 100644 --- a/lib/oscillators/kdj/kdj.pine +++ b/lib/oscillators/kdj/kdj.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("KDJ", "KDJ", overlay=false) +indicator("KDJ Oscillator (KDJ)", "KDJ", overlay=false) //@function Calculates KDJ (K, D, J) lines - enhanced Stochastic Oscillator //@param high Series of high prices diff --git a/lib/oscillators/kst/Kst.md b/lib/oscillators/kst/Kst.md index 5a406f04..d1e4c00c 100644 --- a/lib/oscillators/kst/Kst.md +++ b/lib/oscillators/kst/Kst.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Oscillator | | **Inputs** | Source (close) | -| **Parameters** | `r1` (default DefaultR1), `r2` (default DefaultR2), `r3` (default DefaultR3), `r4` (default DefaultR4), `s1` (default DefaultS1), `s2` (default DefaultS2), `s3` (default DefaultS3), `s4` (default DefaultS4), `sigPeriod` (default DefaultSigPeriod) | +| **Parameters** | `r1` (default 10), `r2` (default 15), `r3` (default 20), `r4` (default 30), `s1` (default 10), `s2` (default 10), `s3` (default 10), `s4` (default 15), `sigPeriod` (default 9) | | **Outputs** | Multiple series (KstValue, Signal) | | **Output range** | Varies (see docs) | | **Warmup** | `Math.Max(Math.Max(r1, r2), Math.Max(r3, r4)) @@ -14,7 +14,7 @@ ### TL;DR - The Know Sure Thing is a multi-timeframe momentum oscillator that computes four Rate of Change values at progressively longer lookback periods, smo... -- Parameterized by `r1` (default defaultr1), `r2` (default defaultr2), `r3` (default defaultr3), `r4` (default defaultr4), `s1` (default defaults1), `s2` (default defaults2), `s3` (default defaults3), `s4` (default defaults4), `sigperiod` (default defaultsigperiod). +- Parameterized by `r1` (default 10), `r2` (default 15), `r3` (default 20), `r4` (default 30), `s1` (default 10), `s2` (default 10), `s3` (default 10), `s4` (default 15), `sigPeriod` (default 9). - Output range: Varies (see docs). - Requires `Math.Max(Math.Max(r1, r2), Math.Max(r3, r4)) + Math.Max(Math.Max(s1, s2), Math.Max(s3, s4)) diff --git a/lib/oscillators/lrsi/lrsi.pine b/lib/oscillators/lrsi/lrsi.pine index cfebd220..8f290f93 100644 --- a/lib/oscillators/lrsi/lrsi.pine +++ b/lib/oscillators/lrsi/lrsi.pine @@ -1,11 +1,12 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj //@version=6 // LRSI: Laguerre RSI // John Ehlers, "Cybernetic Analysis for Stocks and Futures" (2004) // A modified RSI that uses a 4-element Laguerre filter as its core moving average. // The gamma parameter controls the damping of the filter stages, trading // responsiveness against smoothness. Output is dimensionless [0, 1]. - -indicator("LRSI: Laguerre RSI", shorttitle="LRSI", overlay=false) +indicator("Laguerre RSI (LRSI)", "LRSI", overlay=false) gamma = input.float(0.5, "Gamma", minval=0.0, maxval=1.0, step=0.01, tooltip="Damping factor [0,1]. Lower = more responsive; higher = smoother.") diff --git a/lib/oscillators/marketfi/marketfi.pine b/lib/oscillators/marketfi/marketfi.pine index 03e86a55..b6b79f33 100644 --- a/lib/oscillators/marketfi/marketfi.pine +++ b/lib/oscillators/marketfi/marketfi.pine @@ -1,5 +1,7 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj //@version=6 -indicator("MARKETFI: Market Facilitation Index", shorttitle="MARKETFI", overlay=false) +indicator("Market Facilitation Index (MARKETFI)", "MARKETFI", overlay=false) // Bill Williams' Market Facilitation Index (BW MFI) // Measures price movement efficiency per unit of volume. diff --git a/lib/oscillators/pgo/pgo.pine b/lib/oscillators/pgo/pgo.pine index f3f44589..058d1847 100644 --- a/lib/oscillators/pgo/pgo.pine +++ b/lib/oscillators/pgo/pgo.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Pretty Good Oscillator", "PGO", overlay=false) +indicator("Pretty Good Oscillator (PGO)", "PGO", overlay=false) //@function Calculate Pretty Good Oscillator (PGO) //@param source Price data to analyze diff --git a/lib/oscillators/qqe/Qqe.md b/lib/oscillators/qqe/Qqe.md index 0cc3b84d..4696397a 100644 --- a/lib/oscillators/qqe/Qqe.md +++ b/lib/oscillators/qqe/Qqe.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Oscillator | | **Inputs** | Source (close) | -| **Parameters** | `rsiPeriod` (default DefaultRsiPeriod), `smoothFactor` (default DefaultSmoothFactor), `qqeFactor` (default DefaultQqeFactor) | +| **Parameters** | `rsiPeriod` (default 14), `smoothFactor` (default 5), `qqeFactor` (default 4.236) | | **Outputs** | Single series (Qqe) | | **Output range** | Varies (see docs) | | **Warmup** | `rsiPeriod + smoothFactor + darPeriod * 2` bars | @@ -12,7 +12,7 @@ ### TL;DR - Quantitative Qualitative Estimation applies a multi-stage smoothing pipeline to RSI and then constructs dynamic volatility-based trailing bands aro... -- Parameterized by `rsiperiod` (default defaultrsiperiod), `smoothfactor` (default defaultsmoothfactor), `qqefactor` (default defaultqqefactor). +- Parameterized by `rsiPeriod` (default 14), `smoothFactor` (default 5), `qqeFactor` (default 4.236). - Output range: Varies (see docs). - Requires `rsiPeriod + smoothFactor + darPeriod * 2` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/oscillators/reflex/reflex.pine b/lib/oscillators/reflex/reflex.pine index 78467943..09683cc1 100644 --- a/lib/oscillators/reflex/reflex.pine +++ b/lib/oscillators/reflex/reflex.pine @@ -1,7 +1,5 @@ -// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 -// https://mozilla.org/MPL/2.0/ -// © QuanTAlib - +// Licensed under the Apache License, Version 2.0 +// © mihakralj //@version=6 indicator("Ehlers Reflex Indicator (REFLEX)", "REFLEX", overlay = false) diff --git a/lib/oscillators/stoch/stoch.pine b/lib/oscillators/stoch/stoch.pine index 06c22b47..a64735e0 100644 --- a/lib/oscillators/stoch/stoch.pine +++ b/lib/oscillators/stoch/stoch.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Stochastic Oscillator (STOCH)", "Stoch", overlay=false) +indicator("Stochastic Oscillator (STOCH)", "STOCH", overlay=false) //@function Calculates the Stochastic Oscillator (%K and %D). %K = 100 * (close - lowest_low(kLength)) / (highest_high(kLength) - lowest_low(kLength)). %D = SMA(%K, dPeriod). Uses efficient deque implementation for min/max and buffer-based SMA. //@param kLength `simple int` The lookback period for calculating highest high and lowest low. diff --git a/lib/oscillators/stochrsi/stochrsi.pine b/lib/oscillators/stochrsi/stochrsi.pine index 0ffb9d65..d2fcae36 100644 --- a/lib/oscillators/stochrsi/stochrsi.pine +++ b/lib/oscillators/stochrsi/stochrsi.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Stochastic RSI (STOCHRSI)", "StochRSI", overlay=false) +indicator("Stochastic RSI (STOCHRSI)", "STOCHRSI", overlay=false) //@function Calculates Stochastic RSI oscillator //@param source Source series to calculate STOCHRSI for diff --git a/lib/oscillators/trendflex/trendflex.pine b/lib/oscillators/trendflex/trendflex.pine index 776c65e3..4c0d0ca4 100644 --- a/lib/oscillators/trendflex/trendflex.pine +++ b/lib/oscillators/trendflex/trendflex.pine @@ -2,7 +2,7 @@ // © mihakralj //@version=6 // Indicator algorithm (C) 2013 John F. Ehlers -indicator(" Ehlers Trendflex Indicator (TRENDFLEX)", "TRENDFLEX", overlay=false) +indicator("Ehlers Trendflex (TRENDFLEX)", "TRENDFLEX", overlay=false) //@function Calculates Ehlers Trendflex using SuperSmoother pre-filtering and cumulative slope with RMS normalization //@param source Series to calculate Trendflex from diff --git a/lib/oscillators/trix/trix.pine b/lib/oscillators/trix/trix.pine index 5c95e51d..f28e47b5 100644 --- a/lib/oscillators/trix/trix.pine +++ b/lib/oscillators/trix/trix.pine @@ -1,7 +1,7 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("TRIX", "TRIX", overlay=false) +indicator("Triple Exponential Average (TRIX)", "TRIX", overlay=false) //@function Calculates TRIX oscillator with compensation //@param source Series to calculate TRIX from diff --git a/lib/oscillators/ttm_wave/ttm_wave.pine b/lib/oscillators/ttm_wave/ttm_wave.pine new file mode 100644 index 00000000..4b0e6307 --- /dev/null +++ b/lib/oscillators/ttm_wave/ttm_wave.pine @@ -0,0 +1,67 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("TTM Wave (TTM_WAVE)", "TTM_WAVE", overlay=false) + +//@function EMA — standard exponential moving average for MACD computation. +//@param src Source series +//@param length EMA period +//@returns EMA value +ema_calc(series float src, simple int length) => + float alpha = 2.0 / (length + 1) + var float result = 0.0 + result := bar_index == 0 ? src : alpha * src + (1 - alpha) * result + result + +//@function MACD histogram — (fast_ema - slow_ema) - signal_ema(fast_ema - slow_ema) +// This is the second derivative of momentum: acceleration of the spread. +//@param src Source series +//@param fast Fast EMA period +//@param slow Slow EMA period +//@param signal Signal EMA period (same as slow for TTM Wave) +//@returns MACD histogram value +macd_hist(series float src, simple int fast, simple int slow, simple int signal) => + float fast_ema = ema_calc(src, fast) + float slow_ema = ema_calc(src, slow) + float macd_line = fast_ema - slow_ema + float signal_line = ema_calc(macd_line, signal) + macd_line - signal_line + +//@function TTM Wave — six parallel MACD histogram channels at Fibonacci periods. +// Wave A (short-term): channels 1 (8,34,34) and 2 (8,55,55) +// Wave B (medium-term): channels 3 (8,89,89) and 4 (8,144,144) +// Wave C (long-term): channels 5 (8,233,233) and 6 (8,377,377) +// All channels share fast period 8. Signal period equals slow period. +//@param src Source series (default: close) +//@returns [waveA1, waveA2, waveB1, waveB2, waveC1, waveC2] +//@reference John Carter, "Mastering the Trade" (2005, 2012) +//@optimized Six independent EMA cascades, O(1) per bar per channel +ttm_wave(series float src) => + float waveA1 = macd_hist(src, 8, 34, 34) + float waveA2 = macd_hist(src, 8, 55, 55) + float waveB1 = macd_hist(src, 8, 89, 89) + float waveB2 = macd_hist(src, 8, 144, 144) + float waveC1 = macd_hist(src, 8, 233, 233) + float waveC2 = macd_hist(src, 8, 377, 377) + [waveA1, waveA2, waveB1, waveB2, waveC1, waveC2] + +// ── Inputs ── +float i_src = input.source(close, "Source") + +// ── Calculation ── +[waveA1, waveA2, waveB1, waveB2, waveC1, waveC2] = ttm_wave(i_src) + +// ── Plot (thinkorswim color convention) ── +// Wave A: yellow/green (short-term momentum) +plot(waveA1, "Wave A1", color=color.yellow, style=plot.style_histogram, linewidth=2) +plot(waveA2, "Wave A2", color=color.lime, style=plot.style_histogram, linewidth=2) + +// Wave B: magenta/pink (medium-term momentum) +plot(waveB1, "Wave B1", color=color.fuchsia, style=plot.style_histogram, linewidth=2) +plot(waveB2, "Wave B2", color=color.new(color.fuchsia, 40), style=plot.style_histogram, linewidth=2) + +// Wave C: red/orange (long-term momentum) +plot(waveC1, "Wave C1", color=color.red, style=plot.style_histogram, linewidth=2) +plot(waveC2, "Wave C2", color=color.orange, style=plot.style_histogram, linewidth=2) + +hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted) diff --git a/lib/reversals/chandelier/Chandelier.md b/lib/reversals/chandelier/Chandelier.md index 813c14c5..f7497dad 100644 --- a/lib/reversals/chandelier/Chandelier.md +++ b/lib/reversals/chandelier/Chandelier.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `period` (default DefaultPeriod), `multiplier` (default DefaultMultiplier) | +| **Parameters** | `period` (default 22), `multiplier` (default 3.0) | | **Outputs** | Single series (Chandelier) | | **Output range** | Varies (see docs) | | **Warmup** | `period + 1` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Chandelier Exit computes ATR-based trailing stop levels that hang from the highest high (for longs) or rise from the lowest low (for shorts) ov... -- Parameterized by `period` (default defaultperiod), `multiplier` (default defaultmultiplier). +- Parameterized by `period` (default 22), `multiplier` (default 3.0). - Output range: Varies (see docs). - Requires `period + 1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/reversals/chandelier/chandelier.pine b/lib/reversals/chandelier/chandelier.pine new file mode 100644 index 00000000..c5c411ac --- /dev/null +++ b/lib/reversals/chandelier/chandelier.pine @@ -0,0 +1,45 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Chandelier Exit (CHANDELIER)", "CHANDELIER", overlay=true) + +//@function Chandelier Exit — ATR-based trailing stops that hang from the highest high +// (ExitLong) or rise from the lowest low (ExitShort) over a lookback period. +// Uses SMA-seeded Wilder's RMA for ATR, matching Skender/TradingView convention. +//@param period Lookback for ATR and rolling HH/LL (default 22) +//@param multiplier ATR scaling factor (default 3.0) +//@returns [exitLong, exitShort] — two overlay stop levels +//@reference Charles Le Beau; Alexander Elder, "Come Into My Trading Room" (2002) +//@optimized O(1) per bar using ta.rma, ta.highest, ta.lowest +chandelier(simple int period = 22, simple float multiplier = 3.0) => + if period < 1 + runtime.error("Period must be >= 1") + if multiplier <= 0 + runtime.error("Multiplier must be > 0") + + // True Range + float tr = na(close[1]) ? high - low : math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) + + // Wilder's ATR (RMA = EMA with alpha = 1/period, SMA-seeded) + float atr = ta.rma(tr, period) + + // Rolling extremes over the lookback window + float hh = ta.highest(high, period) + float ll = ta.lowest(low, period) + + // Chandelier exits + float exit_long = hh - multiplier * atr + float exit_short = ll + multiplier * atr + + [exit_long, exit_short] + +// ── Inputs ── +int i_period = input.int(22, "Period", minval=1) +float i_multiplier = input.float(3.0, "Multiplier", minval=0.01, step=0.1) + +// ── Calculation ── +[exit_long, exit_short] = chandelier(i_period, i_multiplier) + +// ── Plot ── +plot(exit_long, "Exit Long", color=color.green, linewidth=2) +plot(exit_short, "Exit Short", color=color.red, linewidth=2) diff --git a/lib/reversals/ckstop/Ckstop.md b/lib/reversals/ckstop/Ckstop.md index d428d67e..7a078b29 100644 --- a/lib/reversals/ckstop/Ckstop.md +++ b/lib/reversals/ckstop/Ckstop.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `atrPeriod` (default DefaultAtrPeriod), `multiplier` (default DefaultMultiplier), `stopPeriod` (default DefaultStopPeriod) | +| **Parameters** | `atrPeriod` (default 10), `multiplier` (default 1.0), `stopPeriod` (default 9) | | **Outputs** | Single series (Ckstop) | | **Output range** | Varies (see docs) | | **Warmup** | `atrPeriod + stopPeriod` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Chande Kroll Stop computes adaptive trailing stop levels using ATR-smoothed volatility envelopes around rolling extremes. -- Parameterized by `atrperiod` (default defaultatrperiod), `multiplier` (default defaultmultiplier), `stopperiod` (default defaultstopperiod). +- Parameterized by `atrPeriod` (default 10), `multiplier` (default 1.0), `stopPeriod` (default 9). - Output range: Varies (see docs). - Requires `atrPeriod + stopPeriod` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/reversals/ckstop/ckstop.pine b/lib/reversals/ckstop/ckstop.pine new file mode 100644 index 00000000..219f9919 --- /dev/null +++ b/lib/reversals/ckstop/ckstop.pine @@ -0,0 +1,52 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Chande Kroll Stop (CKSTOP)", "CKSTOP", overlay=true) + +//@function Chande Kroll Stop — adaptive trailing stop using ATR-smoothed volatility +// envelopes around rolling extremes, then smoothed through a second window. +// Stage 1: first_high_stop = HH(p) - m*ATR, first_low_stop = LL(p) + m*ATR +// Stage 2: StopShort = highest(first_high_stop, x), StopLong = lowest(first_low_stop, x) +//@param atr_period ATR and first-stop extreme lookback (default 10) +//@param multiplier ATR scaling factor (default 1.0) +//@param stop_period Second smoothing window (default 9) +//@returns [stop_long, stop_short] — two overlay stop levels +//@reference Chande & Kroll, "The New Technical Trader" (1994) +//@optimized O(1) per bar using ta.rma, ta.highest, ta.lowest +ckstop(simple int atr_period = 10, simple float multiplier = 1.0, simple int stop_period = 9) => + if atr_period < 1 + runtime.error("ATR period must be >= 1") + if multiplier <= 0 + runtime.error("Multiplier must be > 0") + if stop_period < 1 + runtime.error("Stop period must be >= 1") + + // True Range + float tr = na(close[1]) ? high - low : math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) + + // ATR via Wilder's RMA + float atr = ta.rma(tr, atr_period) + + // Stage 1: First stops (volatility envelope) + float hh = ta.highest(high, atr_period) + float ll = ta.lowest(low, atr_period) + float first_high_stop = hh - multiplier * atr + float first_low_stop = ll + multiplier * atr + + // Stage 2: Smoothed stops over stop_period + float stop_short = ta.highest(first_high_stop, stop_period) + float stop_long = ta.lowest(first_low_stop, stop_period) + + [stop_long, stop_short] + +// ── Inputs ── +int i_atr_period = input.int(10, "ATR Period", minval=1) +float i_multiplier = input.float(1.0, "Multiplier", minval=0.01, step=0.1) +int i_stop_period = input.int(9, "Stop Period", minval=1) + +// ── Calculation ── +[stop_long, stop_short] = ckstop(i_atr_period, i_multiplier, i_stop_period) + +// ── Plot ── +plot(stop_long, "Stop Long", color=color.green, linewidth=2) +plot(stop_short, "Stop Short", color=color.red, linewidth=2) diff --git a/lib/reversals/fractals/Fractals.cs b/lib/reversals/fractals/Fractals.cs index cf9b365b..5c281d4c 100644 --- a/lib/reversals/fractals/Fractals.cs +++ b/lib/reversals/fractals/Fractals.cs @@ -1,6 +1,6 @@ // FRACTALS: Williams Fractals // Five-bar pattern identifying local highs (up fractals) and local lows (down fractals). -// Created by Larry Williams (1995, "Trading Chaos"). +// Created by Bill Williams (1995, "Trading Chaos"). using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/lib/reversals/psar/Psar.md b/lib/reversals/psar/Psar.md index c401bc82..c16a17db 100644 --- a/lib/reversals/psar/Psar.md +++ b/lib/reversals/psar/Psar.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `afStart` (default DefaultAfStart), `afIncrement` (default DefaultAfIncrement), `afMax` (default DefaultAfMax) | +| **Parameters** | `afStart` (default 0.02), `afIncrement` (default 0.02), `afMax` (default 0.20) | | **Outputs** | Single series (Psar) | | **Output range** | Varies (see docs) | | **Warmup** | `1` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Parabolic Stop And Reverse (PSAR) is a trend-following overlay indicator created by J. -- Parameterized by `afstart` (default defaultafstart), `afincrement` (default defaultafincrement), `afmax` (default defaultafmax). +- Parameterized by `afStart` (default 0.02), `afIncrement` (default 0.02), `afMax` (default 0.20). - Output range: Varies (see docs). - Requires `1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/reversals/sarext/sarext.pine b/lib/reversals/sarext/sarext.pine new file mode 100644 index 00000000..61c06366 --- /dev/null +++ b/lib/reversals/sarext/sarext.pine @@ -0,0 +1,124 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Parabolic SAR Extended (SAREXT)", "SAREXT", overlay=true) + +//@function Parabolic SAR Extended — asymmetric PSAR with separate long/short AF parameters, +// auto-detect or forced initial direction, and an offset applied on reversal. +// Output is sign-encoded: positive SAR = long mode, negative SAR = short mode. +//@param start_value Initial direction: >0 force long, <0 force short, 0 auto-detect from DM +//@param offset_on_reverse Gap added to SAR on reversal (default 0) +//@param af_init_long Initial AF for long positions (default 0.02) +//@param af_long AF increment per new EP in long (default 0.02) +//@param af_max_long Maximum AF for long (default 0.20) +//@param af_init_short Initial AF for short positions (default 0.02) +//@param af_short AF increment per new EP in short (default 0.02) +//@param af_max_short Maximum AF for short (default 0.20) +//@returns Sign-encoded SAR value (positive = long, negative = short) +//@reference J. Welles Wilder Jr., "New Concepts in Technical Trading Systems" (1978) +//@optimized O(1) per bar, state-machine with asymmetric acceleration factors +sarext(simple float start_value = 0.0, simple float offset_on_reverse = 0.0, + simple float af_init_long = 0.02, simple float af_long = 0.02, simple float af_max_long = 0.20, + simple float af_init_short = 0.02, simple float af_short = 0.02, simple float af_max_short = 0.20) => + if af_init_long <= 0 or af_long <= 0 or af_max_long <= af_init_long + runtime.error("Long AF parameters invalid") + if af_init_short <= 0 or af_short <= 0 or af_max_short <= af_init_short + runtime.error("Short AF parameters invalid") + if offset_on_reverse < 0 + runtime.error("Offset must be >= 0") + + var bool is_long = true + var float sar = low + var float ep = high + var float af = af_init_long + + if bar_index == 0 + // Bar 0: collect first bar data + is_long := true + sar := high + ep := low + af := af_init_short + float(na) + else if bar_index == 1 + // Bar 1: determine initial direction + if start_value > 0 + is_long := true + sar := math.min(low[1], low) + ep := math.max(high[1], high) + af := af_init_long + else if start_value < 0 + is_long := false + sar := math.max(high[1], high) + ep := math.min(low[1], low) + af := af_init_short + else + // Auto-detect from DM + float plus_dm = high - high[1] + float minus_dm = low[1] - low + if plus_dm > minus_dm and plus_dm > 0 + is_long := true + sar := math.min(low[1], low) + ep := math.max(high[1], high) + af := af_init_long + else + is_long := false + sar := math.max(high[1], high) + ep := math.min(low[1], low) + af := af_init_short + is_long ? sar : -sar + else + // Bar 2+: standard SAR state machine with asymmetric AF + float new_sar = sar + af * (ep - sar) + if is_long + new_sar := math.min(new_sar, low[1]) + if bar_index > 1 + new_sar := math.min(new_sar, low[2]) + if low <= new_sar + // Reverse to short + is_long := false + new_sar := ep + offset_on_reverse + ep := low + af := af_init_short + else + if high > ep + ep := high + af := math.min(af + af_long, af_max_long) + else + new_sar := math.max(new_sar, high[1]) + if bar_index > 1 + new_sar := math.max(new_sar, high[2]) + if high >= new_sar + // Reverse to long + is_long := true + new_sar := ep - offset_on_reverse + ep := high + af := af_init_long + else + if low < ep + ep := low + af := math.min(af + af_short, af_max_short) + sar := new_sar + is_long ? sar : -sar + +// ── Inputs ── +float i_start = input.float(0.0, "Start Value (0=auto)") +float i_offset = input.float(0.0, "Offset on Reverse", minval=0) +float i_af_init_long = input.float(0.02, "AF Init Long", minval=0.001, step=0.001) +float i_af_long = input.float(0.02, "AF Long", minval=0.001, step=0.001) +float i_af_max_long = input.float(0.20, "AF Max Long", minval=0.01, step=0.01) +float i_af_init_short = input.float(0.02, "AF Init Short", minval=0.001, step=0.001) +float i_af_short = input.float(0.02, "AF Short", minval=0.001, step=0.001) +float i_af_max_short = input.float(0.20, "AF Max Short", minval=0.01, step=0.01) + +// ── Calculation ── +float result = sarext(i_start, i_offset, i_af_init_long, i_af_long, i_af_max_long, + i_af_init_short, i_af_short, i_af_max_short) + +float sar_val = math.abs(nz(result)) +bool is_long = nz(result) > 0 +float sar_above = not is_long ? sar_val : na +float sar_below = is_long ? sar_val : na + +// ── Plot ── +plot(sar_above, "SAREXT Above", color=color.red, style=plot.style_linebr, linewidth=2) +plot(sar_below, "SAREXT Below", color=color.green, style=plot.style_linebr, linewidth=2) diff --git a/lib/reversals/swings/Swings.md b/lib/reversals/swings/Swings.md index dba38ea7..4656793c 100644 --- a/lib/reversals/swings/Swings.md +++ b/lib/reversals/swings/Swings.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `lookback` (default DefaultLookback) | +| **Parameters** | `lookback` (default 5) | | **Outputs** | Single series (Swings) | | **Output range** | Varies (see docs) | | **Warmup** | 1 bar | @@ -12,7 +12,7 @@ ### TL;DR - Swing High/Low detection identifies local price extremes using a configurable lookback window. -- Parameterized by `lookback` (default defaultlookback). +- Parameterized by `lookback` (default 5). - Output range: Varies (see docs). - Requires 1 bar of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/statistics/acf/acf.pine b/lib/statistics/acf/acf.pine new file mode 100644 index 00000000..c9fbac2c --- /dev/null +++ b/lib/statistics/acf/acf.pine @@ -0,0 +1,87 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Autocorrelation Function (ACF)", "ACF", overlay=false) + +//@function Calculates autocorrelation at a specified lag using a circular buffer +//@param src series float Input data series +//@param len simple int Lookback period for calculation +//@param lag simple int Lag order for autocorrelation (default 1) +//@returns float Autocorrelation coefficient between -1 and 1 +//@optimized for performance using running sums and biased estimator (divide by n) +acf(series float src, simple int len, simple int lag) => + if len <= 0 + runtime.error("Period must be greater than 0") + if lag < 1 + runtime.error("Lag must be at least 1") + if len <= lag + 1 + runtime.error("Period must be greater than lag + 1") + var int p = math.max(1, len) + var array buffer = array.new_float(p, na) + var int head = 0, var int count = 0 + float oldest = array.get(buffer, head) + if not na(oldest) + count -= 1 + if not na(src) + array.set(buffer, head, src) + count += 1 + else + array.set(buffer, head, na) + head := (head + 1) % p + if count <= lag + na + else + // Calculate mean + float sum = 0.0 + int validN = 0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + sum += val + validN += 1 + if validN <= lag + na + else + float mean = sum / validN + + // Calculate variance (population): Σ(x - mean)² / n + float variance = 0.0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + float diff = val - mean + variance += diff * diff + variance /= validN + + if variance <= 0 + 0.0 + else + // Calculate autocovariance at lag k (biased: divide by n) + // Need to iterate in temporal order through the circular buffer + int startIdx = count < p ? 0 : head + float autocovariance = 0.0 + for t = lag to count - 1 + int currentIdx = (startIdx + t) % p + int laggedIdx = (startIdx + t - lag) % p + float xt = array.get(buffer, currentIdx) + float xtk = array.get(buffer, laggedIdx) + if not na(xt) and not na(xtk) + autocovariance += (xt - mean) * (xtk - mean) + autocovariance /= validN + + // ACF = γ_k / γ_0, clamped to [-1, 1] + float result = autocovariance / variance + math.max(-1.0, math.min(1.0, result)) + +// ---------- Main loop ---------- + +// Inputs +i_source = input.source(close, "Source") +i_period = input.int(20, "Period", minval=3) +i_lag = input.int(1, "Lag", minval=1) + +// Calculation +acf_value = acf(i_source, i_period, i_lag) + +// Plot +plot(acf_value, "ACF", color=color.yellow, linewidth=2) diff --git a/lib/statistics/pacf/pacf.pine b/lib/statistics/pacf/pacf.pine new file mode 100644 index 00000000..fbffe7f8 --- /dev/null +++ b/lib/statistics/pacf/pacf.pine @@ -0,0 +1,132 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Partial Autocorrelation Function (PACF)", "PACF", overlay=false) + +//@function Calculates partial autocorrelation at a specified lag using Durbin-Levinson recursion +//@param src series float Input data series +//@param len simple int Lookback period for calculation +//@param lag simple int Lag order for partial autocorrelation (default 1) +//@returns float Partial autocorrelation coefficient between -1 and 1 +//@optimized using Durbin-Levinson recursion to remove shorter-lag effects +pacf(series float src, simple int len, simple int lag) => + if len <= 0 + runtime.error("Period must be greater than 0") + if lag < 1 + runtime.error("Lag must be at least 1") + if len <= lag + 1 + runtime.error("Period must be greater than lag + 1") + var int p = math.max(1, len) + var array buffer = array.new_float(p, na) + var int head = 0, var int count = 0 + float oldest = array.get(buffer, head) + if not na(oldest) + count -= 1 + if not na(src) + array.set(buffer, head, src) + count += 1 + else + array.set(buffer, head, na) + head := (head + 1) % p + if count <= lag + na + else + // Calculate mean + float sum = 0.0 + int validN = 0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + sum += val + validN += 1 + if validN <= lag + na + else + float mean = sum / validN + + // Calculate variance (population): Σ(x - mean)² / n + float variance = 0.0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + float diff = val - mean + variance += diff * diff + variance /= validN + + if variance <= 0 + 0.0 + else + // Calculate ACF for lags 0 to target lag + int startIdx = count < p ? 0 : head + array acfValues = array.new_float(lag + 1, 0.0) + array.set(acfValues, 0, 1.0) + + for k = 1 to lag + float autocovariance = 0.0 + for t = k to count - 1 + int currentIdx = (startIdx + t) % p + int laggedIdx = (startIdx + t - k) % p + float xt = array.get(buffer, currentIdx) + float xtk = array.get(buffer, laggedIdx) + if not na(xt) and not na(xtk) + autocovariance += (xt - mean) * (xtk - mean) + autocovariance /= validN + array.set(acfValues, k, autocovariance / variance) + + // Durbin-Levinson recursion + float pacfResult = na + if lag == 1 + // PACF at lag 1 equals ACF at lag 1 + pacfResult := array.get(acfValues, 1) + else + array phi = array.new_float(lag + 1, 0.0) + array phiPrev = array.new_float(lag + 1, 0.0) + + // Initialize: φ_11 = r_1 + array.set(phi, 1, array.get(acfValues, 1)) + + // Iterate for k = 2 to target lag + for k = 2 to lag + // Copy phi to phiPrev + for j = 0 to lag + array.set(phiPrev, j, array.get(phi, j)) + + // numerator = r_k - Σ(φ_{k-1,j} * r_{k-j}) for j=1..k-1 + float numerator = array.get(acfValues, k) + for j = 1 to k - 1 + numerator -= array.get(phiPrev, j) * array.get(acfValues, k - j) + + // denominator = 1 - Σ(φ_{k-1,j} * r_j) for j=1..k-1 + float denominator = 1.0 + for j = 1 to k - 1 + denominator -= array.get(phiPrev, j) * array.get(acfValues, j) + + if math.abs(denominator) < 1e-15 + pacfResult := 0.0 + break + + // φ_kk = numerator / denominator + array.set(phi, k, numerator / denominator) + + // Update: φ_kj = φ_{k-1,j} - φ_kk * φ_{k-1,k-j} + for j = 1 to k - 1 + array.set(phi, j, array.get(phiPrev, j) - array.get(phi, k) * array.get(phiPrev, k - j)) + + if na(pacfResult) + pacfResult := array.get(phi, lag) + + // Clamp to [-1, 1] + math.max(-1.0, math.min(1.0, pacfResult)) + +// ---------- Main loop ---------- + +// Inputs +i_source = input.source(close, "Source") +i_period = input.int(20, "Period", minval=3) +i_lag = input.int(1, "Lag", minval=1) + +// Calculation +pacf_value = pacf(i_source, i_period, i_lag) + +// Plot +plot(pacf_value, "PACF", color=color.yellow, linewidth=2) diff --git a/lib/trends_FIR/pwma/pwma.pine b/lib/trends_FIR/pwma/pwma.pine index c5c02180..38d3e536 100644 --- a/lib/trends_FIR/pwma/pwma.pine +++ b/lib/trends_FIR/pwma/pwma.pine @@ -1,38 +1,26 @@ // Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Pascal Weighted Moving Average (PWMA)", "PWMA", overlay=true) +indicator("Parabolic Weighted Moving Average (PWMA)", "PWMA", overlay=true) -//@function Calculates PWMA using Pascal's triangle coefficients as weights with compensator +//@function Calculates PWMA using i² (parabolic) weights with compensator //@param source Series to calculate PWMA from //@param period Lookback period - FIR window size //@returns PWMA value, calculates from first bar using available data -//@optimized Uses Pascal's triangle weighting with O(n) complexity per bar due to lookback loop +//@optimized Uses parabolic weighting w[i]=i² with O(n) complexity per bar due to lookback loop pwma(series float source, simple int period) => if period <= 0 runtime.error("Period must be greater than 0") int p = math.min(bar_index + 1, period) - var array weights = array.new_float(1, 1.0) - var int last_p = 1 - if last_p != p - weights := array.new_float(p, 0.0) - array.set(weights, 0, 1.0) - if p > 1 - float prev_weight = 1.0 - for i = 1 to p - 1 - float curr_weight = prev_weight * (p - i) / i - array.set(weights, i, curr_weight) - prev_weight := curr_weight - last_p := p - float sum = 0.0 + float psum = 0.0 float weight_sum = 0.0 for i = 0 to p - 1 float price = source[i] if not na(price) - float w = array.get(weights, i) - sum += price * w + float w = float((p - i) * (p - i)) + psum += price * w weight_sum += w - nz(sum / weight_sum, source) + nz(psum / weight_sum, source) // ---------- Main loop ---------- diff --git a/lib/trends_IIR/adxvma/adxvma.pine b/lib/trends_IIR/adxvma/adxvma.pine index 76b7dd29..24536606 100644 --- a/lib/trends_IIR/adxvma/adxvma.pine +++ b/lib/trends_IIR/adxvma/adxvma.pine @@ -9,8 +9,6 @@ indicator("ADX Variable Moving Average (ADXVMA)", "ADXVMA", overlay=true) //@returns ADXVMA value that adapts smoothing based on trend strength measured by ADX //@optimized O(1) per bar using Wilder's RMA with warmup compensation for all smoothed components adxvma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 1.0 / float(period) float beta = 1.0 - alpha float EPSILON = 1e-10 diff --git a/lib/trends_IIR/ahrens/ahrens.pine b/lib/trends_IIR/ahrens/ahrens.pine index 2ee0a553..ed44b744 100644 --- a/lib/trends_IIR/ahrens/ahrens.pine +++ b/lib/trends_IIR/ahrens/ahrens.pine @@ -11,8 +11,6 @@ indicator("Ahrens Moving Average (AHRENS)", "AHRENS", overlay=true) //@reference Richard D. Ahrens, "Build A Better Moving Average" (Stocks & Commodities V.31:11, October 2013) //@optimized O(1) per bar via circular buffer for lagged MA state; O(period) memory for the ring buffer ahrens(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") // Circular buffer to store past ahma values for period-bar lookback var array buffer = array.new_float(period, na) diff --git a/lib/trends_IIR/coral/coral.pine b/lib/trends_IIR/coral/coral.pine index f1e140e0..65575634 100644 --- a/lib/trends_IIR/coral/coral.pine +++ b/lib/trends_IIR/coral/coral.pine @@ -20,8 +20,6 @@ indicator("Coral Filter (CORAL)", "CORAL", overlay=true) // bfr = -cd³*i6 + c3*i5 + c4*i4 + c5*i3 // Note: c3 + c4 + c5 + (-cd³) = 1 (unity DC gain). coral(series float source, simple int period, simple float cd = 0.4) => - if period <= 0 - runtime.error("Period must be greater than 0") float di = (period - 1.0) / 2.0 + 1.0 float c1 = 2.0 / (di + 1.0) float c2 = 1.0 - c1 diff --git a/lib/trends_IIR/decay/Decay.cs b/lib/trends_IIR/decay/Decay.cs index 75efb753..2efdeef3 100644 --- a/lib/trends_IIR/decay/Decay.cs +++ b/lib/trends_IIR/decay/Decay.cs @@ -18,9 +18,9 @@ namespace QuanTAlib; [SkipLocalsInit] public sealed class Decay : AbstractBase { - private readonly int _period; private readonly double _scale; private int _count; + [StructLayout(LayoutKind.Auto)] private record struct State(double LastValid, double LastOutput); private State _state, _p_state; private int _p_count; @@ -40,7 +40,6 @@ public sealed class Decay : AbstractBase throw new ArgumentException("Period must be >= 1", nameof(period)); } - _period = period; _scale = 1.0 / period; Name = $"Decay({period})"; WarmupPeriod = 1; diff --git a/lib/trends_IIR/decay/decay.pine b/lib/trends_IIR/decay/decay.pine index b0c9a354..653512ef 100644 --- a/lib/trends_IIR/decay/decay.pine +++ b/lib/trends_IIR/decay/decay.pine @@ -8,8 +8,6 @@ indicator("Linear Decay (DECAY)", "DECAY", overlay=true) //@param length Decay period //@returns Decayed value that tracks peaks and descends linearly decay(series float source, simple int length) => - if length <= 0 - runtime.error("Length must be greater than 0") var float prev = na float scale = 1.0 / length float result = na diff --git a/lib/trends_IIR/decycler/decycler.pine b/lib/trends_IIR/decycler/decycler.pine index 08ae0de8..f870cf62 100644 --- a/lib/trends_IIR/decycler/decycler.pine +++ b/lib/trends_IIR/decycler/decycler.pine @@ -10,8 +10,6 @@ indicator("Ehlers Decycler (DECYCLER)", "DECYCLER", overlay=true) //@returns Decycler value (source minus high-pass filtered component) //@optimized Uses 2-pole Butterworth HP with O(1) complexity per bar decycler(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be positive") float src = na(source) ? 0.0 : source diff --git a/lib/trends_IIR/dema/dema.pine b/lib/trends_IIR/dema/dema.pine index c1a61611..d6097901 100644 --- a/lib/trends_IIR/dema/dema.pine +++ b/lib/trends_IIR/dema/dema.pine @@ -10,8 +10,6 @@ indicator("Double Exponential Moving Average (DEMA)", "DEMA", overlay=true) //@returns DEMA value from first bar with proper compensation //@optimized Uses exponential warmup compensator on both EMA stages for O(1) complexity dema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a var bool warmup = true diff --git a/lib/trends_IIR/edecay/Edecay.Quantower.Tests.cs b/lib/trends_IIR/edecay/Edecay.Quantower.Tests.cs new file mode 100644 index 00000000..78f0ad8a --- /dev/null +++ b/lib/trends_IIR/edecay/Edecay.Quantower.Tests.cs @@ -0,0 +1,210 @@ +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib.Tests; + +public class EdecayIndicatorTests +{ + [Fact] + public void EdecayIndicator_Constructor_SetsDefaults() + { + var indicator = new EdecayIndicator(); + + Assert.Equal(5, indicator.Period); + Assert.Equal(SourceType.Close, indicator.Source); + Assert.True(indicator.ShowColdValues); + Assert.Equal("EDECAY - Exponential Decay", indicator.Name); + Assert.False(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + } + + [Fact] + public void EdecayIndicator_MinHistoryDepths_EqualsZero() + { + var indicator = new EdecayIndicator { Period = 20 }; + + Assert.Equal(0, EdecayIndicator.MinHistoryDepths); + Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths); + } + + [Fact] + public void EdecayIndicator_ShortName_IncludesPeriodAndSource() + { + var indicator = new EdecayIndicator { Period = 15 }; + + Assert.Contains("EDECAY", indicator.ShortName, StringComparison.Ordinal); + Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal); + } + + [Fact] + public void EdecayIndicator_Initialize_CreatesLineSeries() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void EdecayIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + + Assert.Equal(1, indicator.LinesSeries[0].Count); + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0))); + } + + [Fact] + public void EdecayIndicator_ProcessUpdate_NewBar_ComputesValue() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106); + + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); + + Assert.Equal(2, indicator.LinesSeries[0].Count); + } + + [Fact] + public void EdecayIndicator_ProcessUpdate_NewTick_ProcessesWithoutError() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick)); + + Assert.Equal(2, indicator.LinesSeries[0].Count); + } + + [Fact] + public void EdecayIndicator_MultipleUpdates_ProducesCorrectSequence() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 20; i++) + { + indicator.HistoricalData.AddBar( + now.AddMinutes(i), + 100 + i * 2, + 105 + i * 2, + 95 + i * 2, + 102 + i * 2); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + Assert.Equal(20, indicator.LinesSeries[0].Count); + + for (int i = 0; i < 20; i++) + { + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i))); + } + } + + [Fact] + public void EdecayIndicator_DifferentSourceTypes_Work() + { + var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 }; + + foreach (var source in sources) + { + var indicator = new EdecayIndicator { Period = 5, Source = source }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)), + $"Source {source} should produce finite value"); + } + } + + [Fact] + public void EdecayIndicator_Period_CanBeChanged() + { + var indicator = new EdecayIndicator { Period = 10 }; + Assert.Equal(10, indicator.Period); + + indicator.Period = 20; + Assert.Equal(20, indicator.Period); + Assert.Equal(0, EdecayIndicator.MinHistoryDepths); + } + + [Fact] + public void EdecayIndicator_Uptrend_OutputFollowsPrice() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + double price = 100 + i * 5; + indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + // In uptrend, edecay output should equal close price (input > decayed) + double lastValue = indicator.LinesSeries[0].GetValue(0); + Assert.Equal(145, lastValue, 1); // last close = 100 + 9*5 = 145 + } + + [Fact] + public void EdecayIndicator_FlatPrices_OutputEqualsInput() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 5; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + double lastValue = indicator.LinesSeries[0].GetValue(0); + Assert.Equal(100, lastValue, 1); + } + + [Fact] + public void EdecayIndicator_DifferentPeriods_Work() + { + var periods = new[] { 1, 5, 10, 20 }; + + foreach (var period in periods) + { + var indicator = new EdecayIndicator { Period = period }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 102 + i, 98 + i, 101 + i); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + Assert.Equal(10, indicator.LinesSeries[0].Count); + } + } +} diff --git a/lib/trends_IIR/edecay/Edecay.Quantower.cs b/lib/trends_IIR/edecay/Edecay.Quantower.cs new file mode 100644 index 00000000..1420ef18 --- /dev/null +++ b/lib/trends_IIR/edecay/Edecay.Quantower.cs @@ -0,0 +1,60 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +/// +/// EDECAY (Exponential Decay) Quantower indicator. +/// Tracks peaks and decays exponentially at a rate of (period-1)/period per bar. +/// Formula: output = max(input, prev_output * (period-1)/period) +/// +[SkipLocalsInit] +public class EdecayIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)] + public int Period { get; set; } = 5; + + [IndicatorExtensions.DataSourceInput] + public SourceType Source { get; set; } = SourceType.Close; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Edecay _edecay = null!; + protected LineSeries Series; + protected string SourceName = null!; + private Func _priceSelector = null!; + + public static int MinHistoryDepths => 0; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public override string ShortName => $"EDECAY {Period}:{SourceName}"; + + public EdecayIndicator() + { + OnBackGround = true; + SeparateWindow = false; + SourceName = Source.ToString(); + Name = "EDECAY - Exponential Decay"; + Description = "Exponential Decay: output = max(input, prev_output * (period-1)/period)"; + Series = new LineSeries(name: $"EDECAY {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid); + AddLineSeries(Series); + } + + protected override void OnInit() + { + _edecay = new Edecay(Period); + SourceName = Source.ToString(); + _priceSelector = Source.GetPriceSelector(); + base.OnInit(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnUpdate(UpdateArgs args) + { + var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; + TValue result = _edecay.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar()); + Series.SetValue(result.Value, _edecay.IsHot, ShowColdValues); + } +} diff --git a/lib/trends_IIR/edecay/Edecay.Tests.cs b/lib/trends_IIR/edecay/Edecay.Tests.cs new file mode 100644 index 00000000..687d0b6d --- /dev/null +++ b/lib/trends_IIR/edecay/Edecay.Tests.cs @@ -0,0 +1,449 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class EdecayTests +{ + private readonly TSeries _gbm; + private const int TestPeriod = 5; + private const int DataPoints = 100; + + public EdecayTests() + { + var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.5, seed: 42); + var bars = gbm.Fetch(DataPoints, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + _gbm = bars.Close; + } + + #region Constructor Tests + + [Fact] + public void Constructor_WithValidPeriod_SetsProperties() + { + var edecay = new Edecay(TestPeriod); + Assert.Equal($"Edecay({TestPeriod})", edecay.Name); + Assert.Equal(1, edecay.WarmupPeriod); + } + + [Fact] + public void Constructor_WithZeroPeriod_ThrowsArgumentException() + { + var ex = Assert.Throws(() => new Edecay(0)); + Assert.Equal("period", ex.ParamName); + } + + [Fact] + public void Constructor_WithNegativePeriod_ThrowsArgumentException() + { + var ex = Assert.Throws(() => new Edecay(-1)); + Assert.Equal("period", ex.ParamName); + } + + [Fact] + public void Constructor_WithSource_SubscribesToEvents() + { + var source = new TSeries(DataPoints); + var edecay = new Edecay(source, TestPeriod); + Assert.NotNull(edecay); + } + + #endregion + + #region Basic Calculation Tests + + [Fact] + public void Update_FirstBar_ReturnsInputValue() + { + var edecay = new Edecay(TestPeriod); + var tv = edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + Assert.Equal(100.0, tv.Value); + } + + [Fact] + public void Update_DecayingValues_OutputDecaysExponentially() + { + var edecay = new Edecay(5); // scale = 4/5 = 0.8 + var time = DateTime.UtcNow; + + // First bar at 1.0 + edecay.Update(new TValue(time, 1.0), true); + + // Next bars at 0.0 — output should decay by ×0.8 per bar + var tv1 = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + Assert.Equal(0.8, tv1.Value, 10); // 1.0 * 0.8 + + var tv2 = edecay.Update(new TValue(time.AddSeconds(2), 0.0), true); + Assert.Equal(0.64, tv2.Value, 10); // 0.8 * 0.8 + + var tv3 = edecay.Update(new TValue(time.AddSeconds(3), 0.0), true); + Assert.Equal(0.512, tv3.Value, 10); // 0.64 * 0.8 + } + + [Fact] + public void Update_RisingInput_FollowsInput() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 100.0), true); + var tv = edecay.Update(new TValue(time.AddSeconds(1), 105.0), true); + Assert.Equal(105.0, tv.Value, 10); // input > decayed, so follows input + } + + [Fact] + public void Last_IsAccessible() + { + var edecay = new Edecay(TestPeriod); + edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + Assert.Equal(100.0, edecay.Last.Value, 10); + } + + [Fact] + public void IsHot_ReturnsFalseBeforeFirstBar() + { + var edecay = new Edecay(TestPeriod); + Assert.False(edecay.IsHot); + } + + [Fact] + public void IsHot_ReturnsTrueAfterFirstBar() + { + var edecay = new Edecay(TestPeriod); + edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + Assert.True(edecay.IsHot); + } + + [Fact] + public void Name_IsAccessible() + { + var edecay = new Edecay(TestPeriod); + Assert.Equal($"Edecay({TestPeriod})", edecay.Name); + } + + #endregion + + #region State Management Tests + + [Fact] + public void Update_WithIsNewTrue_AdvancesState() + { + var edecay = new Edecay(TestPeriod); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 100.0), true); + edecay.Update(new TValue(time.AddSeconds(1), 105.0), true); + edecay.Update(new TValue(time.AddSeconds(2), 110.0), true); + + Assert.NotEqual(default, edecay.Last); + } + + [Fact] + public void Update_WithIsNewFalse_UpdatesCurrentState() + { + var edecay = new Edecay(5); // scale = 0.8 + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var first = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + + // Correct same bar with different value + var corrected = edecay.Update(new TValue(time.AddSeconds(1), 0.5), false); + + // first: max(0.0, 1.0*0.8)=0.8 + Assert.Equal(0.8, first.Value, 10); + // corrected: max(0.5, 1.0*0.8)=0.8 + Assert.Equal(0.8, corrected.Value, 10); + } + + [Fact] + public void Update_IterativeCorrections_RestoresPreviousState() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var baseline = edecay.Update(new TValue(time.AddSeconds(1), 0.5), true); + + // Make several corrections + edecay.Update(new TValue(time.AddSeconds(1), 0.9), false); + edecay.Update(new TValue(time.AddSeconds(1), 0.1), false); + var restored = edecay.Update(new TValue(time.AddSeconds(1), 0.5), false); + + Assert.Equal(baseline.Value, restored.Value, 10); + } + + [Fact] + public void Reset_ClearsStateAndLastValidTracking() + { + var edecay = new Edecay(TestPeriod); + var time = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + edecay.Update(new TValue(time.AddSeconds(i), 100.0 + i)); + } + + edecay.Reset(); + + Assert.Equal(default, edecay.Last); + Assert.False(edecay.IsHot); + } + + #endregion + + #region Robustness Tests + + [Fact] + public void Update_WithNaN_UsesLastValidValue() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var afterNaN = edecay.Update(new TValue(time.AddSeconds(1), double.NaN), true); + + Assert.True(double.IsFinite(afterNaN.Value)); + // NaN uses last valid (1.0), so max(1.0, 1.0*0.8)=1.0 + Assert.Equal(1.0, afterNaN.Value, 10); + } + + [Fact] + public void Update_WithInfinity_UsesLastValidValue() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var afterInf = edecay.Update(new TValue(time.AddSeconds(1), double.PositiveInfinity), true); + + Assert.True(double.IsFinite(afterInf.Value)); + } + + [Fact] + public void Update_BatchNaN_HandlesSafely() + { + var edecay = new Edecay(TestPeriod); + var time = DateTime.UtcNow; + + for (int i = 0; i < 20; i++) + { + var value = i % 3 == 0 ? double.NaN : 100.0 + i; + var tv = edecay.Update(new TValue(time.AddSeconds(i), value), true); + Assert.True(double.IsFinite(tv.Value)); + } + } + + #endregion + + #region Consistency Tests (All 4 modes must match) + + [Fact] + public void AllModes_ProduceSameResults() + { + // Mode 1: Batch via TSeries + var batchResult = Edecay.Batch(_gbm, TestPeriod); + + // Mode 2: Streaming + var streamingEdecay = new Edecay(TestPeriod); + var streamingResult = new TSeries(DataPoints); + for (int i = 0; i < _gbm.Count; i++) + { + var tv = streamingEdecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true); + streamingResult.Add(tv, true); + } + + // Mode 3: Span-based + Span spanOutput = stackalloc double[DataPoints]; + Edecay.Batch(_gbm.Values, spanOutput, TestPeriod); + + // Mode 4: Event-driven + var eventEdecay = new Edecay(TestPeriod); + var eventResult = new TSeries(DataPoints); + eventEdecay.Pub += (object? _, in TValueEventArgs e) => eventResult.Add(e.Value, e.IsNew); + for (int i = 0; i < _gbm.Count; i++) + { + eventEdecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true); + } + + int compareCount = Math.Min(100, DataPoints); + for (int i = DataPoints - compareCount; i < DataPoints; i++) + { + Assert.Equal(batchResult[i].Value, streamingResult[i].Value, 10); + Assert.Equal(batchResult[i].Value, spanOutput[i], 10); + Assert.Equal(batchResult[i].Value, eventResult[i].Value, 10); + } + } + + #endregion + + #region Span API Tests + + [Fact] + public void Calculate_Span_ValidatesEmptySource() + { + var ex = Assert.Throws(() => + { + ReadOnlySpan empty = []; + Span output = stackalloc double[1]; + Edecay.Batch(empty, output, TestPeriod); + }); + Assert.Equal("source", ex.ParamName); + } + + [Fact] + public void Calculate_Span_ValidatesOutputLength() + { + var ex = Assert.Throws(() => + { + ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; + Span output = stackalloc double[3]; // too short + Edecay.Batch(source, output, TestPeriod); + }); + Assert.Equal("output", ex.ParamName); + } + + [Fact] + public void Calculate_Span_ValidatesPeriod() + { + var ex = Assert.Throws(() => + { + ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; + Span output = stackalloc double[5]; + Edecay.Batch(source, output, 0); + }); + Assert.Equal("period", ex.ParamName); + } + + [Fact] + public void Calculate_Span_MatchesTSeries() + { + var batchResult = Edecay.Batch(_gbm, TestPeriod); + + Span spanOutput = stackalloc double[DataPoints]; + Edecay.Batch(_gbm.Values, spanOutput, TestPeriod); + + for (int i = 0; i < DataPoints; i++) + { + Assert.Equal(batchResult[i].Value, spanOutput[i], 10); + } + } + + [Fact] + public void Calculate_Span_LargeData_NoStackOverflow() + { + int largeSize = 10000; + double[] source = new double[largeSize]; + double[] output = new double[largeSize]; + + for (int i = 0; i < largeSize; i++) + { + source[i] = 100.0 + i * 0.1; + } + + Edecay.Batch(source, output, TestPeriod); + + Assert.Equal(largeSize, output.Length); + } + + #endregion + + #region Chainability Tests + + [Fact] + public void Pub_FiresOnUpdate() + { + var edecay = new Edecay(TestPeriod); + bool eventFired = false; + + edecay.Pub += (object? _, in TValueEventArgs e) => eventFired = true; + edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + + Assert.True(eventFired); + } + + [Fact] + public void EventBasedChaining_Works() + { + var source = new TSeries(10); + var edecay = new Edecay(source, 2); + var results = new List(); + + edecay.Pub += (object? _, in TValueEventArgs e) => results.Add(e.Value.Value); + + for (int i = 0; i < 10; i++) + { + source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), true); + } + + Assert.Equal(10, results.Count); + } + + #endregion + + #region Edecay-Specific Tests + + [Fact] + public void Edecay_Period1_DecaysToZero() + { + var edecay = new Edecay(1); // scale = 0/1 = 0.0 + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 5.0), true); + var tv = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + // max(0.0, 5.0*0.0) = 0.0 + Assert.Equal(0.0, tv.Value, 10); + } + + [Fact] + public void Edecay_ConstantInput_OutputEqualsInput() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + for (int i = 0; i < 20; i++) + { + var tv = edecay.Update(new TValue(time.AddSeconds(i), 100.0), true); + Assert.Equal(100.0, tv.Value, 10); + } + } + + [Fact] + public void Edecay_OutputNeverBelowInput() + { + var edecay = new Edecay(10); + var time = DateTime.UtcNow; + var rng = new Random(42); + + for (int i = 0; i < 100; i++) + { + double input = rng.NextDouble() * 200; + var tv = edecay.Update(new TValue(time.AddSeconds(i), input), true); + Assert.True(tv.Value >= input || Math.Abs(tv.Value - input) < 1e-10, + $"Output {tv.Value} should be >= input {input}"); + } + } + + [Fact] + public void Edecay_DiffersFromLinearDecay() + { + var edecay = new Edecay(5); // scale = 0.8 + var decay = new Decay(5); // scale = 0.2 + var time = DateTime.UtcNow; + + // Start both at 100 + edecay.Update(new TValue(time, 100.0), true); + decay.Update(new TValue(time, 100.0), true); + + // Feed 0.0 and compare + var e1 = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + var d1 = decay.Update(new TValue(time.AddSeconds(1), 0.0), true); + + // Edecay: max(0, 100*0.8) = 80 + // Decay: max(0, 100-0.2) = 99.8 + Assert.Equal(80.0, e1.Value, 10); + Assert.Equal(99.8, d1.Value, 10); + Assert.NotEqual(e1.Value, d1.Value); + } + + #endregion +} diff --git a/lib/trends_IIR/edecay/Edecay.Validation.Tests.cs b/lib/trends_IIR/edecay/Edecay.Validation.Tests.cs new file mode 100644 index 00000000..38bae1a2 --- /dev/null +++ b/lib/trends_IIR/edecay/Edecay.Validation.Tests.cs @@ -0,0 +1,260 @@ +using Xunit; +using Xunit.Abstractions; + +namespace QuanTAlib.Tests; + +/// +/// Validation tests for EDECAY (Exponential Decay) against the Tulip Indicators algorithm. +/// The Tulip .NET binding does not expose decay/edecay directly, so validation +/// uses manual computation of the Tulip ti_edecay algorithm: +/// output[0] = input[0] +/// output[i] = max(input[i], output[i-1] * (period-1)/period) +/// +public sealed class EdecayValidationTests(ITestOutputHelper output) : IDisposable +{ + private readonly ValidationTestData _testData = new(); + private readonly ITestOutputHelper _output = output; + private bool _disposed; + + private const int TestPeriod = 5; + private const double TulipTolerance = 1e-9; + + public void Dispose() + { + Dispose(disposing: true); + } + + private void Dispose(bool disposing) + { + if (_disposed) { return; } + _disposed = true; + if (disposing) { _testData?.Dispose(); } + } + + /// + /// Reference implementation of Tulip ti_edecay for validation. + /// + private static double[] TulipEdecay(double[] input, int period) + { + double[] output = new double[input.Length]; + double scale = (period - 1.0) / period; + output[0] = input[0]; + for (int i = 1; i < input.Length; i++) + { + double d = output[i - 1] * scale; + output[i] = input[i] > d ? input[i] : d; + } + return output; + } + + #region Tulip Algorithm Validation + + [Fact] + public void Edecay_MatchesTulipEdecay_Batch() + { + double[] input = _testData.RawData.ToArray(); + + var quantResult = Edecay.Batch(_testData.Data, TestPeriod); + double[] tulipResult = TulipEdecay(input, TestPeriod); + + int count = quantResult.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance, + $"Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}"); + } + + _output.WriteLine("Edecay Batch validated successfully against Tulip edecay algorithm"); + } + + [Fact] + public void Edecay_MatchesTulipEdecay_Streaming() + { + double[] input = _testData.RawData.ToArray(); + + var edecay = new Edecay(TestPeriod); + var streamingResults = new List(); + + foreach (var item in _testData.Data) + { + streamingResults.Add(edecay.Update(item).Value); + } + + double[] tulipResult = TulipEdecay(input, TestPeriod); + + int count = streamingResults.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(streamingResults[i] - tulipResult[i]) <= TulipTolerance, + $"Mismatch at index {i}: QuanTAlib={streamingResults[i]:G17}, Tulip={tulipResult[i]:G17}"); + } + + _output.WriteLine("Edecay Streaming validated successfully against Tulip edecay algorithm"); + } + + [Fact] + public void Edecay_MatchesTulipEdecay_Span() + { + double[] input = _testData.RawData.ToArray(); + + var quantOutput = new double[input.Length]; + Edecay.Batch(new ReadOnlySpan(input), quantOutput, TestPeriod); + + double[] tulipResult = TulipEdecay(input, TestPeriod); + + int count = quantOutput.Length; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(quantOutput[i] - tulipResult[i]) <= TulipTolerance, + $"Mismatch at index {i}: QuanTAlib={quantOutput[i]:G17}, Tulip={tulipResult[i]:G17}"); + } + + _output.WriteLine("Edecay Span validated successfully against Tulip edecay algorithm"); + } + + #endregion + + #region Different Periods + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + [InlineData(20)] + [InlineData(50)] + public void Edecay_MatchesTulipEdecay_DifferentPeriods(int period) + { + double[] input = _testData.RawData.ToArray(); + + var quantResult = Edecay.Batch(_testData.Data, period); + double[] tulipResult = TulipEdecay(input, period); + + int count = quantResult.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance, + $"Period={period}, Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}"); + } + } + + #endregion + + #region Edge Cases + + [Fact] + public void Edecay_HandlesConstantValues() + { + var constantData = new TSeries(100); + for (int i = 0; i < 100; i++) + { + constantData.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0), true); + } + + var result = Edecay.Batch(constantData, TestPeriod); + + // Constant input: output always equals input since input >= decayed + for (int i = 0; i < 100; i++) + { + Assert.Equal(100.0, result[i].Value, TulipTolerance); + } + } + + [Fact] + public void Edecay_HandlesExponentiallyDecreasing() + { + double[] input = new double[20]; + for (int i = 0; i < 20; i++) + { + input[i] = 100.0 * Math.Pow(0.9, i); + } + + var quantOutput = new double[20]; + Edecay.Batch(input, quantOutput, TestPeriod); + + double[] tulipResult = TulipEdecay(input, TestPeriod); + + for (int i = 0; i < 20; i++) + { + Assert.Equal(tulipResult[i], quantOutput[i], TulipTolerance); + } + } + + [Fact] + public void Batch_MatchesStreaming_IdenticalResults() + { + var batchResult = Edecay.Batch(_testData.Data, TestPeriod); + + var edecay = new Edecay(TestPeriod); + var streamingResults = new List(); + foreach (var item in _testData.Data) + { + streamingResults.Add(edecay.Update(item).Value); + } + + int count = _testData.Data.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + for (int i = start; i < count; i++) + { + Assert.Equal(batchResult[i].Value, streamingResults[i], ValidationHelper.DefaultTolerance); + } + _output.WriteLine("Edecay Batch vs Streaming consistency validated"); + } + + [Fact] + public void Edecay_OutputAlwaysGreaterOrEqualInput() + { + double[] input = _testData.RawData.ToArray(); + var quantOutput = new double[input.Length]; + Edecay.Batch(input, quantOutput, TestPeriod); + + for (int i = 0; i < input.Length; i++) + { + Assert.True(quantOutput[i] >= input[i] - 1e-15, + $"Output {quantOutput[i]} must be >= input {input[i]} at index {i}"); + } + } + + [Fact] + public void Edecay_DecayIsMultiplicative() + { + // With period=5, scale = 4/5 = 0.8 + // After a spike, each subsequent bar without new highs should multiply by 0.8 + double[] input = [100.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + double[] tulipResult = TulipEdecay(input, TestPeriod); + + // output[0] = 100.0 + // output[1] = max(0, 100 * 0.8) = 80.0 + // output[2] = max(0, 80 * 0.8) = 64.0 + // output[3] = max(0, 64 * 0.8) = 51.2 + // output[4] = max(0, 51.2 * 0.8) = 40.96 + // output[5] = max(0, 40.96 * 0.8) = 32.768 + Assert.Equal(100.0, tulipResult[0], TulipTolerance); + Assert.Equal(80.0, tulipResult[1], TulipTolerance); + Assert.Equal(64.0, tulipResult[2], TulipTolerance); + Assert.Equal(51.2, tulipResult[3], TulipTolerance); + Assert.Equal(40.96, tulipResult[4], TulipTolerance); + Assert.Equal(32.768, tulipResult[5], TulipTolerance); + + var quantOutput = new double[6]; + Edecay.Batch(input, quantOutput, TestPeriod); + + for (int i = 0; i < 6; i++) + { + Assert.Equal(tulipResult[i], quantOutput[i], TulipTolerance); + } + } + + #endregion +} diff --git a/lib/trends_IIR/edecay/Edecay.cs b/lib/trends_IIR/edecay/Edecay.cs index b011f44a..ffd92b48 100644 --- a/lib/trends_IIR/edecay/Edecay.cs +++ b/lib/trends_IIR/edecay/Edecay.cs @@ -18,9 +18,9 @@ namespace QuanTAlib; [SkipLocalsInit] public sealed class Edecay : AbstractBase { - private readonly int _period; private readonly double _scale; private int _count; + [StructLayout(LayoutKind.Auto)] private record struct State(double LastValid, double LastOutput); private State _state, _p_state; private int _p_count; @@ -40,7 +40,6 @@ public sealed class Edecay : AbstractBase throw new ArgumentException("Period must be >= 1", nameof(period)); } - _period = period; _scale = (period - 1.0) / period; Name = $"Edecay({period})"; WarmupPeriod = 1; diff --git a/lib/trends_IIR/edecay/Edecay.md b/lib/trends_IIR/edecay/Edecay.md new file mode 100644 index 00000000..6dedcc71 --- /dev/null +++ b/lib/trends_IIR/edecay/Edecay.md @@ -0,0 +1,165 @@ +# EDECAY: Exponential Decay + +| Property | Value | +| ---------------- | -------------------------------- | +| **Category** | Trends (IIR) | +| **Inputs** | Source (close) | +| **Parameters** | `period` (default 5) | +| **Outputs** | Single series (Edecay) | +| **Output range** | Same as input (overlay) | +| **Warmup** | `1` bar | + +### TL;DR + +- EDECAY (Exponential Decay) tracks the maximum of the current input and the previous output multiplied by a decay factor of `(period-1)/period`. +- Parameterized by `period` (default 5). +- Output range: Same as input — this is an overlay indicator. +- Requires `1` bar of warmup before first valid output (IsHot = true). +- Validated against Tulip Indicators `ti_edecay` reference algorithm. + +> "A ratchet that only moves down gradually: price can push it up instantly, but gravity pulls it back at an exponential pace — faster when far from zero, slower as it approaches." + +EDECAY implements the exponential decaying function. When price is above the decayed level, output snaps to price. When price falls below, the output decays exponentially by multiplying by `(period-1)/period` per bar, creating a ceiling that gradually descends. Unlike linear DECAY which subtracts a fixed amount, EDECAY's multiplicative factor produces a proportional decay rate. + +## Historical Context + +The exponential decay indicator originates from the Tulip Indicators library, a high-performance C library of technical indicators. It provides a peak-tracking mechanism where the tracked level decays at a proportional rate. The indicator is useful for: + +- **Trailing stops**: The decaying level acts as a trailing stop that descends proportionally. +- **Peak detection**: Identifies when price last reached a new high relative to the decay rate. +- **Signal filtering**: Removes noise by requiring price to exceed the decayed level to register as significant. + +## Architecture & Physics + +### 1. Pure IIR (No Buffer) + +The indicator requires no history buffer — only the previous output value is needed: + +$$ +\text{state} = \{y_{t-1}\} +$$ + +This makes it O(1) in both time and space. + +### 2. Exponential Decay Calculation + +$$ +y_t = \max(x_t, \; y_{t-1} \cdot \frac{p-1}{p}) +$$ + +where: +- $x_t$ = current input value +- $y_{t-1}$ = previous output value +- $p$ = period parameter +- $\frac{p-1}{p}$ = multiplicative decay factor per bar + +### 3. First Bar Initialization + +$$ +y_0 = x_0 +$$ + +The first bar simply passes through the input value. + +### 4. State Management + +The indicator uses state rollback for bar correction: + +``` +if isNew: + save current state as previous +else: + restore previous state +``` + +## Mathematical Foundation + +### Core Formula + +$$ +y_t = \max(x_t, \; y_{t-1} \cdot s) +$$ + +where $s = \frac{p-1}{p}$ is the multiplicative decay factor. + +### Decay Behavior + +After a peak at value $v$, with no new inputs exceeding the decayed level, the output follows: + +$$ +y_{t+k} = v \cdot s^k = v \cdot \left(\frac{p-1}{p}\right)^k +$$ + +The output asymptotically approaches zero but never reaches it ($v > 0$). + +### Comparison with Linear Decay + +| Property | DECAY (Linear) | EDECAY (Exponential) | +|----------|----------------|---------------------| +| Formula | $y - \frac{1}{p}$ | $y \cdot \frac{p-1}{p}$ | +| Decay rate | Constant absolute | Proportional to current value | +| Reaches zero | Yes, in finite time | No, asymptotic approach | +| Scale-invariant | No | Yes | + +### Properties + +| Property | Value | +|----------|-------| +| Lookback | 0 | +| Output ≥ Input | Always (by construction) | +| Decay rate | Proportional $\frac{p-1}{p}$ | +| Monotonic when decaying | Yes (strictly decreasing) | +| Scale-invariant | Yes | + +## Performance Profile + +### Operation Count (Streaming Mode) + +| Operation | Count | Notes | +| :--- | :---: | :--- | +| MUL | 1 | prev_output × scale | +| MAX/CMP | 1 | max(input, decayed) | +| State copy | 1 | rollback support | +| **Total** | **~3 ops** | Extremely lightweight | + +### Batch Mode (Span-based) + +| Operation | Complexity | Notes | +| :--- | :---: | :--- | +| Per-element | O(1) | Mul + compare | +| Total | O(n) | Linear scan | +| Memory | O(1) | No additional allocation | + +### Quality Metrics + +| Metric | Score | Notes | +| :--- | :---: | :--- | +| **Accuracy** | 10/10 | Exact arithmetic, no approximation | +| **Timeliness** | 10/10 | Zero lag on upward moves | +| **Smoothness** | 3/10 | Exponential curve smoother than linear staircase | +| **Simplicity** | 10/10 | Single multiplication + compare | + +## Validation + +| Library | Status | Notes | +| :--- | :---: | :--- | +| **Tulip** | ✅ | Manual ti_edecay algorithm matches exactly | + +## Common Pitfalls + +1. **Not a moving average**: Edecay is a peak-tracking/envelope indicator, not a smoothing filter. It only descends when price is below the decayed level. + +2. **Proportional decay rate**: Unlike linear DECAY, EDECAY decays proportionally. For a stock at $100 with period=5, the first bar decays by $20; for a stock at $10, it decays by $2. This makes EDECAY scale-invariant. + +3. **Period interpretation**: Period=5 means `scale = 4/5 = 0.8`, so each bar retains 80% of the previous value. After 5 bars, approximately 32.8% of the peak value remains. + +4. **First bar**: The first bar always equals the input — there is no warmup period in the traditional sense. + +5. **Asymmetric behavior**: Upward moves are instant (output = input), but downward moves are rate-limited to multiplication by `(period-1)/period` per bar. + +6. **Never reaches zero**: Unlike linear DECAY, exponential decay asymptotically approaches zero but never reaches it (assuming positive values). + +## References + +- Tulip Indicators Library: https://tulipindicators.org/edecay +- Kegel, L. "Tulip Indicators" — Open-source C library of technical indicators. diff --git a/lib/trends_IIR/edecay/edecay.pine b/lib/trends_IIR/edecay/edecay.pine new file mode 100644 index 00000000..33f3aacb --- /dev/null +++ b/lib/trends_IIR/edecay/edecay.pine @@ -0,0 +1,33 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Exponential Decay (EDECAY)", "EDECAY", overlay=true) + +//@function Calculates exponential decay: output = max(input, prev_output * (period-1)/period) +//@param source Source price series +//@param length Decay period +//@returns Decayed value that tracks peaks and descends exponentially +//@optimized Uses multiplicative decay factor for O(1) complexity per bar +edecay(series float source, simple int length) => + var float prev = na + float scale = (length - 1.0) / length + float result = na + if na(prev) + result := source + else + float d = prev * scale + result := source > d ? source : d + prev := result + result + +// ---------- Main loop ---------- + +// Inputs +i_source = input.source(close, "Source") +i_length = input.int(5, "Length", minval=1) + +// Calculate Edecay +float edecay_val = edecay(i_source, i_length) + +// Plot +plot(edecay_val, "Edecay", color=color.yellow, linewidth=2) diff --git a/lib/trends_IIR/ema/ema.pine b/lib/trends_IIR/ema/ema.pine index c59ceff9..9cebaca4 100644 --- a/lib/trends_IIR/ema/ema.pine +++ b/lib/trends_IIR/ema/ema.pine @@ -10,8 +10,6 @@ indicator("Exponential Moving Average (EMA)", "EMA", overlay=true) //@returns EMA value from first bar with proper compensation //@optimized Uses exponential warmup compensator for O(1) complexity and valid output from bar 1 ema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a var bool warmup = true diff --git a/lib/trends_IIR/gdema/gdema.pine b/lib/trends_IIR/gdema/gdema.pine index b56049bd..dc01a2fd 100644 --- a/lib/trends_IIR/gdema/gdema.pine +++ b/lib/trends_IIR/gdema/gdema.pine @@ -14,8 +14,6 @@ indicator("Generalized Double Exponential Moving Average (GDEMA)", "GDEMA", over //@reference Patrick G. Mulloy, "Smoothing Data with Faster Moving Averages" (TASC, Feb 1994) //@optimized O(1) per bar; two cascaded EMA states with shared warmup compensator gdema(series float source, simple int period, simple float vfactor) => - if period <= 0 - runtime.error("Period must be greater than 0") float a = 2.0 / (period + 1) float beta = 1.0 - a diff --git a/lib/trends_IIR/holt/holt.pine b/lib/trends_IIR/holt/holt.pine index aa7e912e..61af00c2 100644 --- a/lib/trends_IIR/holt/holt.pine +++ b/lib/trends_IIR/holt/holt.pine @@ -15,8 +15,6 @@ indicator("Holt Exponential Moving Average (HOLT)", "HOLT", overlay=true) // When gamma=0, degenerates to standard EMA (no trend correction). // When gamma=alpha, provides balanced level/trend tracking. holt(series float source, simple int period, simple float gamma=0) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 2.0 / (period + 1) float g = gamma > 0 ? gamma : alpha var float level = na diff --git a/lib/trends_IIR/kama/kama.pine b/lib/trends_IIR/kama/kama.pine index b5fdf482..374d5049 100644 --- a/lib/trends_IIR/kama/kama.pine +++ b/lib/trends_IIR/kama/kama.pine @@ -11,12 +11,6 @@ indicator("Kaufman's Adaptive Moving Average (KAMA)", "KAMA", overlay=true) //@returns KAMA value with efficiency ratio-based adaptive smoothing //@optimized Uses efficiency ratio calculation for O(n) complexity per bar due to lookback sum kama(series float source, simple int period, simple float fast_alpha=0.666667, simple float slow_alpha=0.0645) => - if period <= 0 - runtime.error("Period must be greater than 0") - if fast_alpha <= 0 or slow_alpha <= 0 - runtime.error("Alpha values must be greater than 0") - if fast_alpha <= slow_alpha - runtime.error("Fast alpha must be greater than slow alpha") var float kama_state = na float current_kama = na if not na(source) diff --git a/lib/trends_IIR/lema/lema.pine b/lib/trends_IIR/lema/lema.pine index 226da8b8..68b8181a 100644 --- a/lib/trends_IIR/lema/lema.pine +++ b/lib/trends_IIR/lema/lema.pine @@ -16,9 +16,6 @@ indicator("Leader EMA (LEMA)", "LEMA", overlay=true) // Stocks & Commodities, 26(7), 30-37. //@optimized O(1) per bar — two IIR state variables with warmup compensation lema(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - float price = nz(source) float alpha = 2.0 / (period + 1) float beta = 1.0 - alpha diff --git a/lib/trends_IIR/ltma/ltma.pine b/lib/trends_IIR/ltma/ltma.pine index 0ca4ac38..460f9a80 100644 --- a/lib/trends_IIR/ltma/ltma.pine +++ b/lib/trends_IIR/ltma/ltma.pine @@ -12,8 +12,6 @@ indicator("Linear Trend Moving Average (LTMA)", "LTMA", overlay=true) // EMA1 lags by τ = (1−α)/α bars; EMA1−EMA2 ≈ slope·τ; result = EMA1 + (EMA1−EMA2). // Initializing both EMAs to source on bar 1 gives zero warmup bias with no compensator needed. ltma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 2.0 / (period + 1) float beta = 1.0 - alpha diff --git a/lib/trends_IIR/mama/mama.pine b/lib/trends_IIR/mama/mama.pine index 4e700a0e..982cfaf1 100644 --- a/lib/trends_IIR/mama/mama.pine +++ b/lib/trends_IIR/mama/mama.pine @@ -10,8 +10,6 @@ indicator("Ehlers MESA Adaptive Moving Average (MAMA)", "MAMA", overlay=true) //@returns [mama, fama] array containing MAMA and FAMA values //@optimized Uses Hilbert Transform phase detection for O(1) complexity per bar mama(series float source, float fastLimit=0.5, float slowLimit=0.05) => - if fastLimit < slowLimit or fastLimit <= 0 or slowLimit < 0 - runtime.error("MAMA: fastLimit must be > slowLimit > 0") var float mama_val = na var float fama_val = na var float period = 0.0 diff --git a/lib/trends_IIR/mavp/mavp.pine b/lib/trends_IIR/mavp/mavp.pine index a7dc6668..32417919 100644 --- a/lib/trends_IIR/mavp/mavp.pine +++ b/lib/trends_IIR/mavp/mavp.pine @@ -11,10 +11,6 @@ indicator("Moving Average Variable Period (MAVP)", "MAVP", overlay=true) //@returns EMA value with variable alpha = 2/(period+1), compensated warmup //@optimized Uses adaptive warmup compensator that tracks cumulative (1-alpha) product for O(1) per bar mavp(series float source, series float period, simple int min_period, simple int max_period) => - if min_period < 1 - runtime.error("min_period must be >= 1") - if max_period < min_period - runtime.error("max_period must be >= min_period") var float ema = 0.0 var float e = 1.0 var bool warmup = true diff --git a/lib/trends_IIR/mcnma/mcnma.pine b/lib/trends_IIR/mcnma/mcnma.pine index 6f7c3e7f..635f4914 100644 --- a/lib/trends_IIR/mcnma/mcnma.pine +++ b/lib/trends_IIR/mcnma/mcnma.pine @@ -13,9 +13,6 @@ indicator("MCNMA - McNicholl EMA", "MCNMA", overlay=true) //@param period Lookback period (must be > 0) //@returns McNicholl EMA value from bar 1 mcnma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - float src = nz(source) float alpha = 2.0 / (period + 1) float beta = 1.0 - alpha diff --git a/lib/trends_IIR/mgdi/mgdi.pine b/lib/trends_IIR/mgdi/mgdi.pine index 688ee5dd..2367e3a1 100644 --- a/lib/trends_IIR/mgdi/mgdi.pine +++ b/lib/trends_IIR/mgdi/mgdi.pine @@ -10,10 +10,6 @@ indicator("McGinley Dynamic Indicator (MGDI)", "MGDI", overlay=true) //@returns MGDI value that tracks price movements more closely than EMAs //@optimized Uses adaptive smoothing with O(1) complexity after initialization mgdi(series float source, simple int period, simple float factor=0.6) => - if period <= 0 - runtime.error("Period must be greater than 0") - if factor <= 0 - runtime.error("Factor must be greater than 0") var float mgdi_val = na if not na(source) if na(mgdi_val) diff --git a/lib/trends_IIR/mma/mma.pine b/lib/trends_IIR/mma/mma.pine index 115c00a3..31b848e3 100644 --- a/lib/trends_IIR/mma/mma.pine +++ b/lib/trends_IIR/mma/mma.pine @@ -9,8 +9,6 @@ indicator("Modified Moving Average (MMA)", "MMA", overlay=true) //@returns MMA value, combines SMA with weighted component for balanced smoothing //@optimized Uses circular buffer for O(1) sum updates, O(n) for weighted component mma(series float source, simple int period) => - if period < 2 - runtime.error("Period must be at least 2") var array buffer = array.new_float(math.min(math.max(2, period), 4000), na) var int head = 0 var float sum = 0.0 diff --git a/lib/trends_IIR/nma/nma.pine b/lib/trends_IIR/nma/nma.pine index 6f707d4b..15287f53 100644 --- a/lib/trends_IIR/nma/nma.pine +++ b/lib/trends_IIR/nma/nma.pine @@ -21,9 +21,6 @@ indicator("NMA - Natural Moving Average", "NMA", overlay=true) // @param period Lookback window for volatility analysis (must be > 0) // @returns NMA value nma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - float src = nz(source) // Step 1: scaled natural log of price diff --git a/lib/trends_IIR/rema/rema.pine b/lib/trends_IIR/rema/rema.pine index fb2948c4..15e6554b 100644 --- a/lib/trends_IIR/rema/rema.pine +++ b/lib/trends_IIR/rema/rema.pine @@ -10,10 +10,6 @@ indicator("Regularized EMA (REMA)", "REMA", overlay=true) //@returns REMA value, calculates from first bar using available data //@optimized Uses regularization term to reduce noise for O(1) complexity rema(series float source, simple int period, simple float lambda=0.5) => - if period <= 0 - runtime.error("Period must be greater than 0") - if lambda < 0.0 or lambda > 1.0 - runtime.error("Lambda must be between 0 and 1") float alpha = 2.0 / (period + 1.0) var float rema_val = na var float prev_rema = na diff --git a/lib/trends_IIR/rgma/rgma.pine b/lib/trends_IIR/rgma/rgma.pine index 724812b3..d32282aa 100644 --- a/lib/trends_IIR/rgma/rgma.pine +++ b/lib/trends_IIR/rgma/rgma.pine @@ -10,10 +10,6 @@ indicator("Recursive Gaussian Moving Average (RGMA)", "RGMA", overlay=true) //@returns RGMA value with gaussian-like smoothing properties using recursive calculation //@optimized Uses cascaded exponential filters for O(1) complexity per bar rgma(series float source, simple int period, simple int passes=3) => - if period <= 0 - runtime.error("Period must be greater than 0") - if passes <= 0 - runtime.error("Passes must be greater than 0") simple float alpha = 2.0 / (period / math.sqrt(passes) + 1.0) var array filters = array.new_float(passes, na) float result = na diff --git a/lib/trends_IIR/rma/rma.pine b/lib/trends_IIR/rma/rma.pine index 7195ee1e..07f20959 100644 --- a/lib/trends_IIR/rma/rma.pine +++ b/lib/trends_IIR/rma/rma.pine @@ -9,8 +9,6 @@ indicator("Wilder's Moving Average (RMA)", "RMA", overlay=true) //@returns RMA value from first bar with proper compensation for early values //@optimized Uses exponential warmup compensator with Wilder's alpha (1/period) for O(1) complexity rma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be provided") float a = 1.0 / float(period) float beta = 1.0 - a var bool warmup = true diff --git a/lib/trends_IIR/t3/t3.pine b/lib/trends_IIR/t3/t3.pine index 1c56589b..2b4e7575 100644 --- a/lib/trends_IIR/t3/t3.pine +++ b/lib/trends_IIR/t3/t3.pine @@ -10,8 +10,6 @@ indicator("Tillson T3 Moving Average (T3)", "T3", overlay=true) //@returns T3 value with optimized coefficients //@optimized Uses six cascaded EMAs with precomputed coefficients for O(1) complexity t3(series float src, simple int period, simple float v) => - if period <= 0 - runtime.error("T3 period must be > 0") float a = 2.0 / (period + 1) float v2 = v * v float v3 = v2 * v diff --git a/lib/trends_IIR/tema/tema.pine b/lib/trends_IIR/tema/tema.pine index 0345fdb8..fd3202ec 100644 --- a/lib/trends_IIR/tema/tema.pine +++ b/lib/trends_IIR/tema/tema.pine @@ -11,8 +11,6 @@ indicator("Triple Exponential Moving Average (TEMA)", "TEMA", overlay=true) //@returns TEMA value from first bar with proper compensation //@optimized Uses exponential warmup compensator on all three EMA stages for O(1) complexity tema(series float source, simple int period=0, simple float alpha=0.0, simple bool corrected=false) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a1 = alpha > 0 ? alpha : (period > 0 ? 2.0 / (period + 1) : 0.1) float r = math.pow(1.0 / a1, 1.0 / 3.0) float a2 = corrected ? a1 * r : a1 diff --git a/lib/trends_IIR/vama/vama.pine b/lib/trends_IIR/vama/vama.pine index 7f106519..74c52eaa 100644 --- a/lib/trends_IIR/vama/vama.pine +++ b/lib/trends_IIR/vama/vama.pine @@ -13,14 +13,6 @@ indicator("Volatility Adjusted Moving Average (VAMA)", "VAMA", overlay=true) //@returns VAMA value //@optimized Uses RMA compensator for ATR and circular buffer for O(1) sum updates vama(series float source, simple int base_length, simple int short_atr_period=10, simple int long_atr_period=50, simple int min_length=5, simple int max_length=100) => - if base_length <= 0 - runtime.error("Base length must be greater than 0") - if short_atr_period <= 0 or long_atr_period <= 0 - runtime.error("ATR periods must be greater than 0") - if min_length <= 0 or max_length <= 0 - runtime.error("Min and max length must be greater than 0") - if min_length > max_length - runtime.error("Min length must be less than or equal to max length") var float prevClose = na float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/trends_IIR/vidya/vidya.pine b/lib/trends_IIR/vidya/vidya.pine index 65eeba52..d35ff113 100644 --- a/lib/trends_IIR/vidya/vidya.pine +++ b/lib/trends_IIR/vidya/vidya.pine @@ -10,8 +10,6 @@ indicator("Variable Index Dynamic Average (VIDYA)", "VIDYA", overlay=true) //@returns VIDYA value that adapts to market volatility //@optimized Uses volatility index calculation with O(n) complexity per bar due to lookback loops vidya(series float source, simple int period, simple int std_period=0) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 2.0 / (period + 1.0) var float vidya = na if not na(source) diff --git a/lib/trends_IIR/yzvama/yzvama.pine b/lib/trends_IIR/yzvama/yzvama.pine index b7f0a9f4..e685f393 100644 --- a/lib/trends_IIR/yzvama/yzvama.pine +++ b/lib/trends_IIR/yzvama/yzvama.pine @@ -13,14 +13,6 @@ indicator("Yang-Zhang Volatility Adjusted Moving Average (YZVAMA)", "YZVAMA", ov //@returns YZVAMA value //@optimized Uses RMA compensators for YZV and circular buffers for O(1) sum updates yzvama(series float source, simple int yzv_short_period=3, simple int yzv_long_period=50, simple int percentile_lookback=100, simple int min_length=5, simple int max_length=100) => - if yzv_short_period <= 0 or yzv_long_period <= 0 - runtime.error("All periods must be greater than 0") - if min_length <= 0 or max_length <= 0 - runtime.error("Min and max length must be greater than 0") - if min_length > max_length - runtime.error("Min length must be less than or equal to max length") - if percentile_lookback <= 0 - runtime.error("Percentile lookback must be greater than 0") var float prev_close = na float o = open float h = high diff --git a/lib/trends_IIR/zldema/zldema.pine b/lib/trends_IIR/zldema/zldema.pine index 4d004dfb..cd4795e5 100644 --- a/lib/trends_IIR/zldema/zldema.pine +++ b/lib/trends_IIR/zldema/zldema.pine @@ -10,8 +10,6 @@ indicator("Zero-Lag Double EMA (ZLDEMA)", "ZLDEMA", overlay=true) //@returns ZLDEMA value with zero-lag effect applied //@optimized Uses lag compensation buffer and exponential warmup compensator on both EMA stages for O(1) complexity zldema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a simple int lag = math.max(1, math.round((period - 1) / 2)) diff --git a/lib/trends_IIR/zlema/zlema.pine b/lib/trends_IIR/zlema/zlema.pine index e7464a5d..552158ee 100644 --- a/lib/trends_IIR/zlema/zlema.pine +++ b/lib/trends_IIR/zlema/zlema.pine @@ -10,8 +10,6 @@ indicator("Zero-Lag EMA (ZLEMA)", "ZLEMA", overlay=true) //@returns ZLEMA value with zero-lag effect applied //@optimized Uses lag compensation buffer and exponential warmup compensator for O(1) complexity zlema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a simple int lag = math.max(1, math.round((period - 1) / 2)) diff --git a/lib/trends_IIR/zltema/zltema.pine b/lib/trends_IIR/zltema/zltema.pine index 7d23438e..6f71b054 100644 --- a/lib/trends_IIR/zltema/zltema.pine +++ b/lib/trends_IIR/zltema/zltema.pine @@ -10,13 +10,8 @@ indicator("Zero-Lag Triple EMA (ZLTEMA)", "ZLTEMA", overlay=true) //@returns ZLTEMA value with zero-lag effect applied //@optimized Uses lag compensation buffer and exponential warmup compensator on all three EMA stages for O(1) complexity zltema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") - float a1 = alpha > 0 ? alpha : 2.0 / (period + 1) - float beta1 = 1.0 - a1 - float r = math.pow(1.0 / a1, 1.0 / 3.0) - float a2 = a1 * r - float a3 = a2 * r + float a = alpha > 0 ? alpha : 2.0 / (period + 1) + float beta = 1.0 - a simple int lag = math.max(1, math.round((period - 1) / 2)) var bool warmup = true var float e = 1.0 @@ -37,21 +32,21 @@ zltema(series float source, simple int period=0, simple float alpha=0) => array.push(priceBuffer, source) float laggedPrice = nz(array.get(priceBuffer, 0), source) float signal = 2 * source - laggedPrice - ema1_raw := a1 * (signal - ema1_raw) + ema1_raw + ema1_raw := a * (signal - ema1_raw) + ema1_raw if warmup - e *= beta1 + e *= beta float c = 1.0 / (1.0 - e) ema1 := c * ema1_raw - ema2_raw := a2 * (ema1 - ema2_raw) + ema2_raw + ema2_raw := a * (ema1 - ema2_raw) + ema2_raw ema2 := c * ema2_raw - ema3_raw := a3 * (ema2 - ema3_raw) + ema3_raw + ema3_raw := a * (ema2 - ema3_raw) + ema3_raw ema3 := c * ema3_raw warmup := e > 1e-10 else ema1 := ema1_raw - ema2_raw := a2 * (ema1 - ema2_raw) + ema2_raw + ema2_raw := a * (ema1 - ema2_raw) + ema2_raw ema2 := ema2_raw - ema3_raw := a3 * (ema2 - ema3_raw) + ema3_raw + ema3_raw := a * (ema2 - ema3_raw) + ema3_raw ema3 := ema3_raw 3 * ema1 - 3 * ema2 + ema3 else diff --git a/lib/volatility/adr/adr.pine b/lib/volatility/adr/adr.pine index e3ee7dc3..8b0e62ae 100644 --- a/lib/volatility/adr/adr.pine +++ b/lib/volatility/adr/adr.pine @@ -9,10 +9,6 @@ indicator("Average Daily Range (ADR)", "ADR", overlay=false) //@returns float ADR value //@optimized for performance and dirty data adr(simple int length, simple int method = 1) => - if length <= 0 - runtime.error("Length must be greater than 0") - if method < 1 or method > 3 - runtime.error("Method must be 1 (SMA), 2 (EMA), or 3 (WMA)") var int p = math.max(1, length) var int head = 0 var int count = 0 diff --git a/lib/volatility/atr/atr.pine b/lib/volatility/atr/atr.pine index b9c87a9a..35857dfb 100644 --- a/lib/volatility/atr/atr.pine +++ b/lib/volatility/atr/atr.pine @@ -8,8 +8,6 @@ indicator("Average True Range (ATR)", "ATR", overlay=false) //@returns The ATR value. //@optimized Beta precomputation for RMA warmup compensation atr(simple int length) => - if length <= 0 - runtime.error("Period must be greater than 0") var float prevClose = close float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/volatility/atrn/atrn.pine b/lib/volatility/atrn/atrn.pine index 23b1faf5..104286c6 100644 --- a/lib/volatility/atrn/atrn.pine +++ b/lib/volatility/atrn/atrn.pine @@ -8,8 +8,6 @@ indicator("Average True Range Normalized (ATRN)", "ATRN", overlay=false, format= //@returns The ATRN value, normalized relative to its maximum over the longer period. //@optimized Beta precomputation for RMA warmup compensation atrn(simple int length) => - if length <= 0 - runtime.error("Period must be greater than 0") var float prevClose = close float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/volatility/bbw/bbw.pine b/lib/volatility/bbw/bbw.pine index 6d3fafa7..c75b4aa6 100644 --- a/lib/volatility/bbw/bbw.pine +++ b/lib/volatility/bbw/bbw.pine @@ -10,8 +10,6 @@ indicator("Bollinger Band Width (BBW)", "BBW", overlay=false) //@returns BBW value representing the width between Bollinger Bands //@optimized for performance and dirty data bbw(series float source, simple int period, simple float multiplier) => - if period <= 0 or multiplier <= 0.0 - runtime.error("Period and multiplier must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array buffer = array.new_float(p, na) var float sum = 0.0, var float sumSq = 0.0 @@ -28,7 +26,7 @@ bbw(series float source, simple int period, simple float multiplier) => head := (head + 1) % p float basis = nz(sum / count, source) float dev = count > 1 ? multiplier * math.sqrt(math.max(0.0, sumSq / count - basis * basis)) : 0.0 - 2 * dev + basis != 0.0 ? 2 * dev / basis : 0.0 // ---------- Main loop ---------- diff --git a/lib/volatility/bbwn/bbwn.pine b/lib/volatility/bbwn/bbwn.pine index 363a267e..b89585d8 100644 --- a/lib/volatility/bbwn/bbwn.pine +++ b/lib/volatility/bbwn/bbwn.pine @@ -11,8 +11,6 @@ indicator("Bollinger Band Width Normalized (BBWN)", "BBWN", overlay=false) //@returns BBWN value representing current BBW normalized to [0,1] range //@optimized for performance and dirty data bbwn(series float source, simple int period, simple float multiplier, simple int lookback) => - if period <= 0 or multiplier <= 0.0 or lookback <= 0 - runtime.error("Period, multiplier, and lookback must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array buffer = array.new_float(p, na) var float sum = 0.0, var float sumSq = 0.0 @@ -29,7 +27,7 @@ bbwn(series float source, simple int period, simple float multiplier, simple int head := (head + 1) % p float basis = nz(sum / count, source) float dev = count > 1 ? multiplier * math.sqrt(math.max(0.0, sumSq / count - basis * basis)) : 0.0 - float bbw = 2 * dev + float bbw = basis != 0.0 ? 2 * dev / basis : 0.0 var int l = math.max(1, lookback), var int hist_head = 0, var int hist_count = 0 var array hist_buffer = array.new_float(l, na) var float min_val = bbw, var float max_val = bbw diff --git a/lib/volatility/bbwp/bbwp.pine b/lib/volatility/bbwp/bbwp.pine index 53b8c7b5..65d646f2 100644 --- a/lib/volatility/bbwp/bbwp.pine +++ b/lib/volatility/bbwp/bbwp.pine @@ -11,8 +11,6 @@ indicator("Bollinger Band Width Percentile (BBWP)", "BBWP", overlay=false, forma //@returns BBWP value representing current BBW percentile in historical range //@optimized for performance and dirty data bbwp(series float source, simple int period, simple float multiplier, simple int lookback) => - if period <= 0 or multiplier <= 0.0 or lookback <= 0 - runtime.error("Period, multiplier, and lookback must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array buffer = array.new_float(p, na) var float sum = 0.0, var float sumSq = 0.0 @@ -29,7 +27,7 @@ bbwp(series float source, simple int period, simple float multiplier, simple int head := (head + 1) % p float basis = nz(sum / count, source) float dev = count > 1 ? multiplier * math.sqrt(math.max(0.0, sumSq / count - basis * basis)) : 0.0 - float bbw = 2 * dev + float bbw = basis != 0.0 ? 2 * dev / basis : 0.0 var int l = math.max(1, lookback), var int hist_head = 0, var int hist_count = 0 var array hist_buffer = array.new_float(l, na) float hist_oldest = array.get(hist_buffer, hist_head) diff --git a/lib/volatility/ccv/ccv.pine b/lib/volatility/ccv/ccv.pine index 553fcb1a..a3550d36 100644 --- a/lib/volatility/ccv/ccv.pine +++ b/lib/volatility/ccv/ccv.pine @@ -9,10 +9,6 @@ indicator("Close-to-Close Volatility (CCV)", "CCV", overlay=false) //@returns float Volatility value //@optimized Beta precomputation for RMA warmup compensation ccv(simple int length, simple int method) => - if length <= 0 - runtime.error("Length must be greater than 0") - if method < 1 or method > 3 - runtime.error("Method must be 1 (SMA), 2 (EMA), or 3 (WMA)") var int p = math.max(1, length) var int head = 0 var int count = 0 diff --git a/lib/volatility/cv/cv.pine b/lib/volatility/cv/cv.pine index c1f842ce..590139db 100644 --- a/lib/volatility/cv/cv.pine +++ b/lib/volatility/cv/cv.pine @@ -10,14 +10,6 @@ indicator("Conditional Volatility (CV)", "CV", overlay=false) //@returns float Conditional volatility value //@optimized for performance and efficient variance updating cv(simple int length, simple float alpha, simple float beta) => - if length <= 0 - runtime.error("Length must be greater than 0") - if alpha <= 0.0 or alpha >= 1.0 - runtime.error("Alpha must be between 0 and 1") - if beta <= 0.0 or beta >= 1.0 - runtime.error("Beta must be between 0 and 1") - if alpha + beta >= 1.0 - runtime.error("Alpha + Beta must be less than 1 for stationarity") var float omega = 0.0 var float longRunVar = 0.0 var float prevVariance = 0.0 diff --git a/lib/volatility/cvi/cvi.pine b/lib/volatility/cvi/cvi.pine index 66598555..ec45e1ef 100644 --- a/lib/volatility/cvi/cvi.pine +++ b/lib/volatility/cvi/cvi.pine @@ -9,8 +9,6 @@ indicator("Chaikin's Volatility (CVI)", "CVI", overlay=false) //@returns float Volatility value measuring change in trading ranges //@optimized for performance using efficient range ROC calculation cvi(simple int roc_length, simple int smooth_length) => - if roc_length <= 0 or smooth_length <= 0 - runtime.error("Lengths must be greater than 0") var float prevEma = 0.0 hlRange = high - low alpha = 2.0 / (smooth_length + 1) diff --git a/lib/volatility/etherm/etherm.pine b/lib/volatility/etherm/etherm.pine index 0345cda6..3e6f7bce 100644 --- a/lib/volatility/etherm/etherm.pine +++ b/lib/volatility/etherm/etherm.pine @@ -8,9 +8,6 @@ indicator("Elder's Thermometer", "ETHERM", overlay=false) //@returns [thermometer, signal] The raw thermometer value and EMA signal line //@optimized Beta precomputation for EMA warmup compensation etherm(simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - // Step 1: Calculate raw thermometer value // Temperature = max of upward high protrusion and downward low protrusion // Only outward extensions count; contractions clamp to zero diff --git a/lib/volatility/ewma/ewma.pine b/lib/volatility/ewma/ewma.pine index fd709a11..f3079650 100644 --- a/lib/volatility/ewma/ewma.pine +++ b/lib/volatility/ewma/ewma.pine @@ -11,10 +11,6 @@ indicator("Exponential Weighted MA Volatility", "EWMA Volty", overlay=false) //@returns float The EWMA Volatility value. //@optimized for performance and dirty data ewmaVolty(series float src, simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float logReturn = nz(math.log(src / src[1]),0.0) float squaredReturn = logReturn * logReturn var float raw_rma_sq_ret = 0.0, var float e_rma = 1.0 diff --git a/lib/volatility/gkv/gkv.pine b/lib/volatility/gkv/gkv.pine index 9da61a35..ec1b7217 100644 --- a/lib/volatility/gkv/gkv.pine +++ b/lib/volatility/gkv/gkv.pine @@ -10,10 +10,6 @@ indicator("Garman-Klass Volatility (GKV)", "GKV", overlay=false) //@returns float The Garman-Klass Volatility value. //@optimized for performance and dirty data gkv(simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float lnH = math.log(high), float lnL = math.log(low), float lnO = math.log(open), float lnC = math.log(close) float C_2LN2_1 = 0.3862941611 // 2 * math.log(2) - 1 float term1 = 0.5 * math.pow(lnH - lnL, 2) diff --git a/lib/volatility/hlv/hlv.pine b/lib/volatility/hlv/hlv.pine index b3e838e0..c772e21c 100644 --- a/lib/volatility/hlv/hlv.pine +++ b/lib/volatility/hlv/hlv.pine @@ -10,10 +10,6 @@ indicator("High-Low Volatility (HLV)", "HLV", overlay=false) //@returns float The High-Low Volatility value. //@optimized for performance and dirty data hlv(simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float lnH = math.log(high), float lnL = math.log(low) float C_4LN2_INV = 0.3606737602 // 1.0 / (4.0 * math.log(2.0)) float parkinsonEstimator = C_4LN2_INV * math.pow(lnH - lnL, 2) diff --git a/lib/volatility/hv/hv.pine b/lib/volatility/hv/hv.pine index 2ff26172..bd08d6f4 100644 --- a/lib/volatility/hv/hv.pine +++ b/lib/volatility/hv/hv.pine @@ -11,10 +11,6 @@ indicator("Historical Volatility (HV)", "HV", overlay=false) //@returns float The Historical Volatility value. //@optimized for performance and dirty data hv(series float src_price, simple int length_hv, simple bool annualize = true, simple int annualPeriods = 252) => - if length_hv <= 1 - runtime.error("Length for HV must be greater than 1") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") var array _buffer_hv = array.new_float(length_hv, na) var int _head_idx_hv = 0 var int _current_fill_count_hv = 0 diff --git a/lib/volatility/massi/massi.pine b/lib/volatility/massi/massi.pine index cbe72986..960b471a 100644 --- a/lib/volatility/massi/massi.pine +++ b/lib/volatility/massi/massi.pine @@ -9,8 +9,6 @@ indicator("Mass Index (MASSI)", "MASSI", overlay=false) //@returns float Mass Index value //@optimized for performance and dirty data massi(simple int ema_length, simple int sum_length) => - if ema_length <= 0 or sum_length <= 0 - runtime.error("Periods must be greater than 0") float a = 2.0 / (ema_length + 1) float beta = 1.0 - a var bool warmup = true, var float e = 1.0 diff --git a/lib/volatility/natr/natr.pine b/lib/volatility/natr/natr.pine index d0dd537b..5f0bf603 100644 --- a/lib/volatility/natr/natr.pine +++ b/lib/volatility/natr/natr.pine @@ -8,8 +8,6 @@ indicator("Normalized Average True Range", "NATR", overlay=false, format=format. //@returns The NATR value as a percentage of close price. //@optimized Beta precomputation for RMA warmup compensation natr(simple int length) => - if length <= 0 - runtime.error("Period must be greater than 0") float prevClose = nz(close[1], close) float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/volatility/rsv/rsv.pine b/lib/volatility/rsv/rsv.pine index 36509075..d6d544dc 100644 --- a/lib/volatility/rsv/rsv.pine +++ b/lib/volatility/rsv/rsv.pine @@ -9,10 +9,6 @@ indicator("Rogers-Satchell Volatility (RSV)", "RSV", overlay=false) //@param annualPeriods Number of periods in a year for annualization. Default is 252 for daily data. //@returns float The Rogers-Satchell Volatility value. rsv(simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float h = math.max(high, 0.0000001) float l = math.max(low, 0.0000001) float o = math.max(open, 0.0000001) diff --git a/lib/volatility/rv/rv.pine b/lib/volatility/rv/rv.pine index f5a1573b..d74227eb 100644 --- a/lib/volatility/rv/rv.pine +++ b/lib/volatility/rv/rv.pine @@ -10,10 +10,6 @@ indicator("Realized Volatility (RV)", "RV", overlay=false) //@param annualPeriods Number of periods (of the main chart's timeframe) in a year for annualization. Default is 252 (assuming daily chart). //@returns float The Realized Volatility value. rv(simple int length = 20, simple string intradayTimeframe = "5", simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") intraday_closes_arr = request.security_lower_tf(syminfo.tickerid, intradayTimeframe, close) float sum_sq_log_returns = 0.0 if array.size(intraday_closes_arr) > 1 diff --git a/lib/volatility/rvi/Rvi.Validation.Tests.cs b/lib/volatility/rvi/Rvi.Validation.Tests.cs index 44a7043a..d2389229 100644 --- a/lib/volatility/rvi/Rvi.Validation.Tests.cs +++ b/lib/volatility/rvi/Rvi.Validation.Tests.cs @@ -258,28 +258,34 @@ public class RviValidationTests } /// - /// Validates TBar update uses only Close price. + /// Validates TBar update uses High and Low channels (revised 1995 algorithm), + /// producing a different result than single-price Close-only input. /// [Fact] - public void Rvi_TBar_UsesOnlyClose() + public void Rvi_TBar_UsesDualChannel_HighLow() { var bars = GenerateTestData(50); - // Using TBar + // Using TBar (revised: high + low dual-channel) var rviBar = new Rvi(10, 14); for (int i = 0; i < bars.Count; i++) { rviBar.Update(bars[i]); } - // Using just Close prices + // Using just Close prices (single-channel) var rviClose = new Rvi(10, 14); for (int i = 0; i < bars.Count; i++) { rviClose.Update(new TValue(bars[i].Time, bars[i].Close)); } - Assert.Equal(rviClose.Last.Value, rviBar.Last.Value, 10); + // TBar uses High/Low channels → different from Close-only + Assert.NotEqual(rviClose.Last.Value, rviBar.Last.Value); + + // Both should still be in valid range + Assert.True(rviBar.Last.Value >= 0 && rviBar.Last.Value <= 100); + Assert.True(rviClose.Last.Value >= 0 && rviClose.Last.Value <= 100); } // === Parameter Sensitivity === diff --git a/lib/volatility/rvi/Rvi.cs b/lib/volatility/rvi/Rvi.cs index 5b767fdb..313fc248 100644 --- a/lib/volatility/rvi/Rvi.cs +++ b/lib/volatility/rvi/Rvi.cs @@ -55,9 +55,13 @@ public sealed class Rvi : AbstractBase public Rvi(int stdevLength = 10, int rmaLength = 14) { if (stdevLength < 2) + { throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength)); + } if (rmaLength < 1) + { throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength)); + } _stdevLength = stdevLength; _rmaLength = rmaLength; @@ -101,7 +105,9 @@ public sealed class Rvi : AbstractBase public TSeries Update(TBarSeries source) { if (source.Count == 0) + { return []; + } int len = source.Count; var t = new List(len); @@ -126,7 +132,9 @@ public sealed class Rvi : AbstractBase // Sync internal state for (int i = 0; i < len; i++) + { Update(source[i], isNew: true); + } return new TSeries(t, v); } @@ -134,7 +142,9 @@ public sealed class Rvi : AbstractBase public override TSeries Update(TSeries source) { if (source.Count == 0) + { return []; + } int len = source.Count; var t = new List(len); @@ -150,7 +160,9 @@ public sealed class Rvi : AbstractBase source.Times.CopyTo(tSpan); for (int i = 0; i < len; i++) + { Update(new TValue(source.Times[i], source.Values[i]), isNew: true); + } return new TSeries(t, v); } @@ -188,9 +200,13 @@ public sealed class Rvi : AbstractBase double rviValue = (rviHi + rviLo) * 0.5; if (!double.IsFinite(rviValue)) + { rviValue = _lastValue; + } else + { _lastValue = rviValue; + } Last = new TValue(timeTicks, rviValue); PubEvent(Last, isNew); @@ -244,9 +260,13 @@ public sealed class Rvi : AbstractBase double upStdVal = 0.0; double downStdVal = 0.0; if (priceChange > 0) + { upStdVal = currentStdDev; + } else if (priceChange < 0) + { downStdVal = currentStdDev; + } double rawRmaUp = Math.FusedMultiplyAdd(s.RawRmaUp, _rmaLength - 1, upStdVal) / _rmaLength; double eUp = (1 - _alpha) * s.EUp; @@ -266,7 +286,9 @@ public sealed class Rvi : AbstractBase public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { for (int i = 0; i < source.Length; i++) + { Update(new TValue(DateTime.UtcNow, source[i]), isNew: true); + } } public override void Reset() @@ -288,9 +310,13 @@ public sealed class Rvi : AbstractBase public static TSeries Batch(TSeries source, int stdevLength = 10, int rmaLength = 14) { if (stdevLength < 2) + { throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength)); + } if (rmaLength < 1) + { throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength)); + } int len = source.Count; var t = new List(len); @@ -323,11 +349,17 @@ public sealed class Rvi : AbstractBase int rmaLength = 14) { if (stdevLength < 2) + { throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength)); + } if (rmaLength < 1) + { throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength)); + } if (output.Length < prices.Length) + { throw new ArgumentException("Output span must be at least as long as prices span", nameof(output)); + } // Single-price: feed same data to both channels, average = original BatchDual(prices, prices, output, stdevLength, rmaLength); @@ -344,15 +376,23 @@ public sealed class Rvi : AbstractBase int rmaLength = 14) { if (stdevLength < 2) + { throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength)); + } if (rmaLength < 1) + { throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength)); + } int len = highs.Length; if (len == 0) + { return; + } if (output.Length < len) + { throw new ArgumentException("Output span must be at least as long as input span", nameof(output)); + } // Allocate temp buffers for each channel's RVI output Span rviHi = len <= 256 ? stackalloc double[len] : new double[len]; @@ -363,7 +403,9 @@ public sealed class Rvi : AbstractBase // Average for (int i = 0; i < len; i++) + { output[i] = (rviHi[i] + rviLo[i]) * 0.5; + } } /// @@ -377,7 +419,9 @@ public sealed class Rvi : AbstractBase { int len = prices.Length; if (len == 0) + { return; + } double alpha = 1.0 / rmaLength; @@ -407,7 +451,9 @@ public sealed class Rvi : AbstractBase } if (count < stdevLength) + { count++; + } else { double oldest = priceBuffer[head]; @@ -434,7 +480,9 @@ public sealed class Rvi : AbstractBase prevPrice = price; if (count < stdevLength) + { count++; + } else { double oldest = priceBuffer[head]; @@ -458,9 +506,13 @@ public sealed class Rvi : AbstractBase double upStdVal = 0.0; double downStdVal = 0.0; if (priceChange > 0) + { upStdVal = currentStdDev; + } else if (priceChange < 0) + { downStdVal = currentStdDev; + } rawRmaUp = Math.FusedMultiplyAdd(rawRmaUp, rmaLength - 1, upStdVal) / rmaLength; eUp = (1 - alpha) * eUp; @@ -474,9 +526,13 @@ public sealed class Rvi : AbstractBase double rviValue = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0; if (!double.IsFinite(rviValue)) + { rviValue = lastValue; + } else + { lastValue = rviValue; + } output[i] = rviValue; } @@ -488,4 +544,4 @@ public sealed class Rvi : AbstractBase TSeries results = indicator.Update(source); return (results, indicator); } -} \ No newline at end of file +} diff --git a/lib/volatility/rvi/rvi.pine b/lib/volatility/rvi/rvi.pine index 007d299f..c7af6486 100644 --- a/lib/volatility/rvi/rvi.pine +++ b/lib/volatility/rvi/rvi.pine @@ -9,10 +9,6 @@ indicator("Relative Volatility Index (RVI)", shorttitle="RVI", overlay=false) //@param rmaLength The lookback period for Wilder's smoothing (RMA) of the upward and downward standard deviations. Default is 14. //@returns float The Relative Volatility Index value. rvi(series float src = close, simple int stdevLength = 10, simple int rmaLength = 14) => - if stdevLength <= 1 - runtime.error("Standard Deviation Length must be greater than 1") - if rmaLength <= 0 - runtime.error("RMA Length must be greater than 0") float currentStdDev = 0.0 var array buffer_stddev = array.new_float(stdevLength, na) // p_stddev simplified var int head_stddev = 0, var int count_stddev = 0 diff --git a/lib/volatility/ui/ui.pine b/lib/volatility/ui/ui.pine index 9e97a3b6..70e96ac1 100644 --- a/lib/volatility/ui/ui.pine +++ b/lib/volatility/ui/ui.pine @@ -8,8 +8,6 @@ indicator("Ulcer Index (UI)", shorttitle="UI", format=format.price, precision=2, //@param period The lookback period. Default is 14. //@returns float The Ulcer Index value. ui(series float src, int period) => - if period <= 0 - runtime.error("Period must be greater than 0") var deque = array.new_int(0) var src_buffer = array.new_float(period, na) var int current_index = 0 diff --git a/lib/volatility/vov/vov.pine b/lib/volatility/vov/vov.pine index d276e0b7..b854047a 100644 --- a/lib/volatility/vov/vov.pine +++ b/lib/volatility/vov/vov.pine @@ -9,8 +9,6 @@ indicator("Volatility of Volatility (VOV)", shorttitle="VOV", format=format.pric //@param vovPeriod The lookback period for calculating the standard deviation of the volatility series. Default is 10. //@returns float The VOV value. vov(series float src, int volatilityPeriod, int vovPeriod) => - if volatilityPeriod <= 0 or vovPeriod <= 0 - runtime.error("Periods must be greater than 0") var int p1 = 0 var array buffer1 = array.new_float(0) var int head1 = 0, var int count1 = 0 diff --git a/lib/volatility/vr/vr.pine b/lib/volatility/vr/vr.pine index 086c7d21..a8933984 100644 --- a/lib/volatility/vr/vr.pine +++ b/lib/volatility/vr/vr.pine @@ -9,8 +9,6 @@ indicator("Volatility Ratio (VR)", shorttitle="VR", format=format.price, precisi //@param atrPeriod The lookback period for ATR. Must be > 0. //@returns float The Volatility Ratio value for the current bar. vr(int atrPeriod) => - if atrPeriod <= 0 - runtime.error("ATR Period must be greater than 0") var float EPSILON_ATR = 1e-10 var float raw_atr = 0.0 var float e_compensator = 1.0 diff --git a/lib/volatility/yzv/yzv.pine b/lib/volatility/yzv/yzv.pine index ce673443..fc4652e6 100644 --- a/lib/volatility/yzv/yzv.pine +++ b/lib/volatility/yzv/yzv.pine @@ -8,9 +8,6 @@ indicator("Yang-Zhang Volatility (YZV)", shorttitle="YZV", overlay=false) //@returns Yang-Zhang Volatility value for the current bar //@optimized Uses bias-corrected RMA with OHLC prices for O(1) complexity per bar yzv(int length) => - if length <= 0 - runtime.error("Length must be greater than 0 for YZV calculation.") - float(na) o=open,h=high,l=low,c=close,pc=na(close[1])?open:close[1] ro=math.log(o/pc),rc=math.log(c/o),rh=math.log(h/o),rl=math.log(l/o) s_o_sq=ro*ro,s_c_sq=rc*rc diff --git a/lib/volume/adosc/adosc.pine b/lib/volume/adosc/adosc.pine index d86f0e1b..b4c4a1f0 100644 --- a/lib/volume/adosc/adosc.pine +++ b/lib/volume/adosc/adosc.pine @@ -9,8 +9,6 @@ indicator("Chaikin A/D Oscillator (ADOSC)", "ADOSC", overlay=false) //@returns (float) The ADOSC value for the current bar (difference between short and long EMAs of ADL) adosc(simple int shortPeriod, simple int longPeriod) => float EPSILON = 1e-10 - if shortPeriod <= 0 or longPeriod <= 0 - runtime.error("Periods must be greater than 0") short_alpha = 2.0 / (shortPeriod + 1) long_alpha = 2.0 / (longPeriod + 1) one_minus_long_alpha = 1.0 - long_alpha @@ -36,4 +34,4 @@ longPeriod = input.int(10, "Long Period", minval=1) osc = adosc(shortPeriod, longPeriod) // ---------- Plotting ---------- -plot(osc, "ADOSC", color.new(color.yellow, 0, color=color.yellow, linewidth=2), linewidth=2) +plot(osc, "ADOSC", color=color.yellow, linewidth=2) diff --git a/lib/volume/aobv/aobv.pine b/lib/volume/aobv/aobv.pine index 77c9a387..5e6c2728 100644 --- a/lib/volume/aobv/aobv.pine +++ b/lib/volume/aobv/aobv.pine @@ -25,7 +25,7 @@ aobv(src, vol) => resArr = array.new_float(2, na) for i = 0 to array.size(periods) - 1 period = array.get(periods, i) - alpha = 2.0 / math.max(period, 1) + alpha = 2.0 / (period + 1) beta = array.get(betaArr, i) if beta == 0.0 beta := 1.0 - alpha @@ -50,8 +50,8 @@ aobv(src, vol) => [array.get(resArr, 0), array.get(resArr, 1)] // ---------- Inputs ---------- -src = input(close, "Source") -vol = input(volume, "Volume") +src = input.source(close, "Source") +vol = input.source(volume, "Volume") // ---------- Calculations ---------- [aobvFast, aobvSlow] = aobv(src, vol) diff --git a/lib/volume/cmf/cmf.pine b/lib/volume/cmf/cmf.pine index 2303c053..9d1b834f 100644 --- a/lib/volume/cmf/cmf.pine +++ b/lib/volume/cmf/cmf.pine @@ -11,10 +11,6 @@ indicator("Chaikin Money Flow (CMF)", "CMF", overlay=false) //@param src_vol The volume (default: built-in volume) //@returns float CMF value between -1 and 1 cmf(len = 20, src_high = high, src_low = low, src_close = close, src_vol = volume) => - // Validate parameters - if len < 1 - runtime.error("Length must be >= 1") - // Calculate Money Flow Multiplier float mfm = 0.0 if not na(src_high) and not na(src_low) and not na(src_close) diff --git a/lib/volume/efi/efi.pine b/lib/volume/efi/efi.pine index 1fda4818..3132507e 100644 --- a/lib/volume/efi/efi.pine +++ b/lib/volume/efi/efi.pine @@ -9,8 +9,6 @@ indicator("Elder's Force Index (EFI)", "EFI", overlay=false) //@param src_vol The volume (default: built-in volume) //@returns float The smoothed Force Index value efi(len = 13, src = close, src_vol = volume) => - if len < 1 - runtime.error("Length must be >= 1") var float prev_src = src float raw_force = (nz(src) - nz(prev_src)) * nz(src_vol) prev_src := src diff --git a/lib/volume/eom/eom.pine b/lib/volume/eom/eom.pine index bdc67c86..7dadd84b 100644 --- a/lib/volume/eom/eom.pine +++ b/lib/volume/eom/eom.pine @@ -9,8 +9,6 @@ indicator("Ease of Movement (EOM)", "EOM", overlay=false) //@returns float Ease of Movement value //@optimized for performance and dirty data eom(i_smoothing, i_vol_scale, i_high=high, i_low=low, i_volume=volume) => - if i_smoothing < 1 or i_vol_scale <= 0 - runtime.error("Smoothing or Volume scale out or range") var float oldMidPoint = na midPoint = (i_high + i_low) * 0.5 midPointChange = midPoint - nz(oldMidPoint, midPoint) diff --git a/lib/volume/evwma/evwma.pine b/lib/volume/evwma/evwma.pine index 6240c5c5..f2950306 100644 --- a/lib/volume/evwma/evwma.pine +++ b/lib/volume/evwma/evwma.pine @@ -10,8 +10,6 @@ indicator("Elastic Volume Weighted Moving Average (EVWMA)", "EVWMA", overlay=tru //@returns EVWMA value where high-volume bars get more weight (faster response) //@optimized O(1) per bar via circular buffer for running volume sum evwma(series float src, series float vol, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array vol_buffer = array.new_float(p, 0.0) var float sum_vol = 0.0 diff --git a/lib/volume/iii/iii.pine b/lib/volume/iii/iii.pine index 7dc3d522..c2ba6680 100644 --- a/lib/volume/iii/iii.pine +++ b/lib/volume/iii/iii.pine @@ -28,7 +28,7 @@ iii(simple int period, simple bool cumulative=false, series float h=high, series valid_count := not na(raw_iii) ? valid_count + 1 : valid_count array.set(buffer, head, raw_iii) head := (head + 1) % period - smoothed_iii = sum / period + smoothed_iii = valid_count > 0 ? sum / valid_count : 0.0 cumulative_value := cumulative_value + raw_iii cumulative ? cumulative_value : smoothed_iii diff --git a/lib/volume/mfi/mfi.pine b/lib/volume/mfi/mfi.pine index f56e7ef9..8c858072 100644 --- a/lib/volume/mfi/mfi.pine +++ b/lib/volume/mfi/mfi.pine @@ -12,8 +12,6 @@ indicator("Money Flow Index (MFI)", "MFI", overlay=false) //@returns float The MFI value (0-100) //@optimized Uses circular buffers for O(1) performance with proper NA handling mfi(simple int len, series float src_high=high, series float src_low=low, series float src_close=close, series float src_vol=volume) => - if len < 1 - runtime.error("Invalid parameter: len must be >= 1") float typical_price = (src_high + src_low + src_close) / 3.0 float raw_money_flow = typical_price * nz(src_vol, 0.0) float prev_typical_price = nz(typical_price[1], typical_price) diff --git a/lib/volume/pvd/pvd.pine b/lib/volume/pvd/pvd.pine index 662e0ef3..4c0cb18f 100644 --- a/lib/volume/pvd/pvd.pine +++ b/lib/volume/pvd/pvd.pine @@ -12,8 +12,6 @@ indicator("Price Volume Divergence (PVD)", "PVD", overlay=false) //@returns Smoothed divergence value //@optimized for performance and dirty data pvd(simple int price_period, simple int volume_period, simple int smoothing_period, series float c=close, series float vol=volume ) => - if smoothing_period <= 0 - runtime.error("Smoothing period must be greater than 0") float close_price = nz(c, close) float volume_val = math.max(nz(vol, 0.0), 1.0) float prev_close = bar_index < price_period ? close_price[math.max(bar_index, 1)] : close_price[price_period] diff --git a/lib/volume/pvo/pvo.pine b/lib/volume/pvo/pvo.pine index 34b9204b..35813488 100644 --- a/lib/volume/pvo/pvo.pine +++ b/lib/volume/pvo/pvo.pine @@ -11,10 +11,6 @@ indicator("Percentage Volume Oscillator (PVO)", "PVO", overlay=false) //@returns tuple with [pvo, signal, histogram] values //@optimized Beta precomputation for EMA warmup compensation pvo(series float vol, simple int fast_period, simple int slow_period, simple int signal_period) => - if fast_period <= 0 or slow_period <= 0 or signal_period <= 0 - runtime.error("All periods must be greater than 0") - if fast_period >= slow_period - runtime.error("Fast period must be less than slow period") float vol_val = nz(vol, 0.0) float fast_alpha = 2.0 / (fast_period + 1) float slow_alpha = 2.0 / (slow_period + 1) diff --git a/lib/volume/pvr/pvr.pine b/lib/volume/pvr/pvr.pine index 774473f8..e39231d3 100644 --- a/lib/volume/pvr/pvr.pine +++ b/lib/volume/pvr/pvr.pine @@ -22,4 +22,4 @@ i_price_source = input.source(close, "Price Source") pvr_value = pvr(i_price_source) // Plot -plot(pvr_value, "PVR", color=color.yellow, linewidth=2, plot.style_stepline) +plot(pvr_value, "PVR", color=color.yellow, linewidth=2, style=plot.style_stepline) diff --git a/lib/volume/vo/vo.pine b/lib/volume/vo/vo.pine index b6d04e2d..fbf8eb09 100644 --- a/lib/volume/vo/vo.pine +++ b/lib/volume/vo/vo.pine @@ -11,8 +11,6 @@ indicator("Volume Oscillator (VO)", "VO", overlay=false) //@returns Volume Oscillator value //@optimized for performance and dirty data vo(simple int short_period, simple int long_period, simple int signal_period, series float vol=volume) => - if short_period >= long_period - runtime.error("Short period must be less than long period") volume_val = math.max(nz(vol, 0.0), 1.0) var p_short = short_period var buffer_short = array.new_float(p_short, na) diff --git a/lib/volume/vroc/vroc.pine b/lib/volume/vroc/vroc.pine index dc37d68b..bf6163d7 100644 --- a/lib/volume/vroc/vroc.pine +++ b/lib/volume/vroc/vroc.pine @@ -10,9 +10,6 @@ indicator("Volume Rate of Change (VROC)", "VROC", overlay=false) //@returns Volume Rate of Change value //@optimized for performance and dirty data vroc(simple int period, simple bool calc_type, series float vol = volume) => - if period <= 0 - runtime.error("Period must be greater than 0") - float current_volume = vol float historical_volume = vol[period] if na(current_volume) or na(historical_volume) diff --git a/lib/volume/vwad/vwad.pine b/lib/volume/vwad/vwad.pine index 4892faa9..234ca35e 100644 --- a/lib/volume/vwad/vwad.pine +++ b/lib/volume/vwad/vwad.pine @@ -12,8 +12,6 @@ indicator("Volume Weighted Accumulation/Distribution (VWAD)", "VWAD", overlay=fa //@returns VWAD value representing volume-weighted accumulation/distribution //@optimized for performance and dirty data vwad(simple int period, series float src_high = high, series float src_low = low, series float src_close = close, series float src_vol = volume) => - if period <= 0 - runtime.error("Period must be greater than 0") var int p = math.max(1, period), var int head = 0 var array vol_buffer = array.new_float(p, na) var float sum_vol = 0.0 diff --git a/lib/volume/vwma/vwma.pine b/lib/volume/vwma/vwma.pine index 6ad3be68..a2aedf59 100644 --- a/lib/volume/vwma/vwma.pine +++ b/lib/volume/vwma/vwma.pine @@ -10,8 +10,6 @@ indicator("Volume Weighted Moving Average (VWMA)", "VWMA", overlay=true) //@returns VWMA value representing volume-weighted moving average //@optimized for performance and dirty data vwma(series float src, series float vol, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array price_buffer = array.new_float(p, na) var array vol_buffer = array.new_float(p, na)