diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml
index 1e4afae9..fe858258 100644
--- a/.github/workflows/Publish.yml
+++ b/.github/workflows/Publish.yml
@@ -107,6 +107,17 @@ jobs:
path: .sarif/resharper.sarif
retention-days: 7
+ - name: Upload Roslyn SARIF artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: sarif-roslyn
+ path: |
+ .sarif/*.sarif
+ !.sarif/resharper.sarif
+ retention-days: 7
+ if-no-files-found: warn
+
# ==============================================================================
# 2) Snyk Security Scan -> SARIF artifact
# ==============================================================================
@@ -438,20 +449,8 @@ jobs:
path: coverage-merged/
retention-days: 7
- - name: List Roslyn SARIF files
- if: always()
- run: |
- echo "Roslyn SARIF files generated (per-project, SARIF 2.1):"
- ls -la .sarif/*.sarif 2>/dev/null || echo "No SARIF files found"
-
- - name: Upload Roslyn SARIF Artifacts
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: sarif-roslyn
- path: .sarif/*.sarif
- retention-days: 7
- if-no-files-found: warn
+ # Roslyn SARIF artifacts are uploaded by ReSharper_Analysis job
+ # (which builds without SonarScanner wrapper that can suppress ErrorLog output)
# ==============================================================================
# 5) GitHub Code Scanning Upload (SARIF → Security tab)
diff --git a/LICENSE b/LICENSE
index 261eeb9e..1795092c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
- Apache License
+Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -48,7 +48,7 @@
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
+ submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
@@ -60,7 +60,7 @@
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
+ on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
@@ -106,7 +106,7 @@
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
+ within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
@@ -175,18 +175,7 @@
END OF TERMS AND CONDITIONS
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
+ Copyright 2024 Miha Kralj
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -198,4 +187,4 @@
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
- limitations under the License.
+ limitations under the License.
\ No newline at end of file
diff --git a/_sidebar.md b/_sidebar.md
index fabfa20d..29b2ef40 100644
--- a/_sidebar.md
+++ b/_sidebar.md
@@ -197,11 +197,15 @@
* [CHOP - Choppiness Index](/lib/dynamics/chop/Chop.md)
* [DMX - Jurik Directional Movement Index](/lib/dynamics/dmx/Dmx.md)
* [DX - Directional Movement Index](/lib/dynamics/dx/Dx.md)
+ * [MINUS_DI - Minus Directional Indicator](/lib/dynamics/minus_di/MinusDi.md)
+ * [MINUS_DM - Minus Directional Movement](/lib/dynamics/minus_dm/MinusDm.md)
* [GHLA - Gann High-Low Activator](/lib/dynamics/ghla/Ghla.md)
* [HT_TRENDMODE - Ehlers Hilbert Transform Trend vs Cycle Mode](/lib/dynamics/ht_trendmode/HtTrendmode.md)
* [ICHIMOKU - Ichimoku Cloud](/lib/dynamics/ichimoku/Ichimoku.md)
* [IMPULSE - Elder Impulse System](/lib/dynamics/impulse/Impulse.md)
* [PFE - Polarized Fractal Efficiency](/lib/dynamics/pfe/Pfe.md)
+ * [PLUS_DI - Plus Directional Indicator](/lib/dynamics/plus_di/PlusDi.md)
+ * [PLUS_DM - Plus Directional Movement](/lib/dynamics/plus_dm/PlusDm.md)
* [QSTICK - Qstick Indicator](/lib/dynamics/qstick/Qstick.md)
* [RAVI - Chande Range Action Verification Index](/lib/dynamics/ravi/Ravi.md)
* [SUPER - SuperTrend](/lib/dynamics/super/Super.md)
@@ -369,12 +373,14 @@
* [FFT - Fast Fourier Transform](/lib/numerics/fft/Fft.md)
* [GAMMADIST - Gamma Distribution](/lib/numerics/gammadist/Gammadist.md)
* [HIGHEST - Rolling Maximum](/lib/numerics/highest/Highest.md)
+ * [MAXINDEX - Index of Highest Value](/lib/numerics/maxindex/Maxindex.md)
* [IFFT - Inverse Fast Fourier Transform](/lib/numerics/ifft/Ifft.md)
* [JERK - Jerk](/lib/numerics/jerk/Jerk.md)
* [LINEARTRANS - Linear Transform](/lib/numerics/lineartrans/Lineartrans.md)
* [LOGNORMDIST - Log-Normal Distribution](/lib/numerics/lognormdist/Lognormdist.md)
* [LOGTRANS - Logarithmic Transform](/lib/numerics/logtrans/Logtrans.md)
* [LOWEST - Rolling Minimum](/lib/numerics/lowest/Lowest.md)
+ * [MININDEX - Index of Lowest Value](/lib/numerics/minindex/Minindex.md)
* [NORMDIST - Normal Distribution](/lib/numerics/normdist/Normdist.md)
* [NORMALIZE - Min-Max Normalization](/lib/numerics/normalize/Normalize.md)
* [POISSONDIST - Poisson Distribution](/lib/numerics/poissondist/Poissondist.md)
@@ -447,6 +453,7 @@
* [PIVOTFIB - Fibonacci Pivot Points](/lib/reversals/pivotfib/Pivotfib.md)
* [PIVOTWOOD - Woodie's Pivot Points](/lib/reversals/pivotwood/Pivotwood.md)
* [PSAR - Parabolic Stop And Reverse](/lib/reversals/psar/Psar.md)
+ * [SAREXT - Parabolic SAR Extended](/lib/reversals/sarext/Sarext.md)
* [SWINGS - Swing High/Low Detection](/lib/reversals/swings/Swings.md)
* [TTM_SCALPER - TTM Scalper Alert](/lib/reversals/ttm_scalper/TtmScalper.md)
diff --git a/docs/indicators.md b/docs/indicators.md
index a936de60..2e388c36 100644
--- a/docs/indicators.md
+++ b/docs/indicators.md
@@ -194,12 +194,16 @@ Indicators measuring trend strength, regime, and directional movement quality.
| [**CHOP**](../lib/dynamics/chop/Chop.md) | Choppiness Index | ATR sum vs range; trending vs choppy |
| [**DMX**](../lib/dynamics/dmx/Dmx.md) | Jurik DMX | Enhanced directional movement |
| [**DX**](../lib/dynamics/dx/Dx.md) | Directional Movement Index | Raw directional strength |
+| [**MINUS_DI**](../lib/dynamics/minusdi/MinusDi.md) | Minus Directional Indicator | Downward DI (0-100) |
+| [**MINUS_DM**](../lib/dynamics/minusdm/MinusDm.md) | Minus Directional Movement | Smoothed downward DM |
| [**GHLA**](../lib/dynamics/ghla/Ghla.md) | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover |
| [**HT_TRENDMODE**](../lib/dynamics/ht_trendmode/HtTrendmode.md) | Ehlers Hilbert Transform Trend vs Cycle Mode | Cycle vs trend regime detection |
| [**ICHIMOKU**](../lib/dynamics/ichimoku/Ichimoku.md) | Ichimoku Cloud | Multi-component trend system |
| [**IMPULSE**](../lib/dynamics/impulse/Impulse.md) | Elder Impulse System | EMA + MACD-H trend/momentum fusion |
| [**QSTICK**](../lib/dynamics/qstick/Qstick.md) | Qstick | Average close-open difference |
| [**PFE**](../lib/dynamics/pfe/Pfe.md) | Polarized Fractal Efficiency | Fractal path efficiency as trend strength |
+| [**PLUS_DI**](../lib/dynamics/plusdi/PlusDi.md) | Plus Directional Indicator | Upward DI (0-100) |
+| [**PLUS_DM**](../lib/dynamics/plusdm/PlusDm.md) | Plus Directional Movement | Smoothed upward DM |
| [**RAVI**](../lib/dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dual-SMA divergence as trend strength |
| [**SUPER**](../lib/dynamics/super/Super.md) | SuperTrend | ATR-based trend bands |
| [**TTM_SQUEEZE**](../lib/dynamics/ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | BB inside KC squeeze with momentum |
@@ -429,14 +433,18 @@ Mathematical transformations and derivative indicators. Building blocks for anal
| [**WEIBULLDIST**](../lib/numerics/weibulldist/Weibulldist.md) | Weibull Distribution (CDF) | Rolling Weibull CDF — two-parameter closed-form probability transform via pow + exp |
| [**CHANGE**](../lib/numerics/change/Change.md) | Percentage Change | Relative price movement |
| [**CWT**](../lib/numerics/cwt/Cwt.md) | Continuous Wavelet Transform | Morlet CWT magnitude at a specified scale — time-frequency decomposition |
+| [**DECAY**](../lib/numerics/decay/Decay.md) | Linear Decay | Peak envelope with linear degradation |
| [**DWT**](../lib/numerics/dwt/Dwt.md) | Discrete Wavelet Transform | À trous Haar stationary DWT — multi-resolution approximation + detail, WarmupPeriod = 2^levels |
+| [**EDECAY**](../lib/numerics/edecay/Edecay.md) | Exponential Decay | Peak envelope with exponential degradation |
| [**EXPTRANS**](../lib/numerics/exptrans/Exptrans.md) | Exponential Transform | e^x for log-space reversal |
| [**HIGHEST**](../lib/numerics/highest/Highest.md) | Rolling Maximum | O(1) via monotonic deque |
+| [**MAXINDEX**](../lib/numerics/maxindex/Maxindex.md) | Rolling Maximum Index | Position of max in window |
| [**JERK**](../lib/numerics/jerk/Jerk.md) | Jerk (3rd Derivative) | Change in acceleration |
| [**LINEARTRANS**](../lib/numerics/lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling |
| [**LOGNORMDIST**](../lib/numerics/lognormdist/Lognormdist.md) | Log-Normal Distribution | Log-normal CDF via min-max normalization |
| [**LOGTRANS**](../lib/numerics/logtrans/Logtrans.md) | Logarithmic Transform | Natural log for percentage analysis |
| [**LOWEST**](../lib/numerics/lowest/Lowest.md) | Rolling Minimum | O(1) via monotonic deque |
+| [**MININDEX**](../lib/numerics/minindex/Minindex.md) | Rolling Minimum Index | Position of min in window |
| [**NORMALIZE**](../lib/numerics/normalize/Normalize.md) | Min-Max Normalization | Scale to [0,1] via rolling min/max |
| [**RELU**](../lib/numerics/relu/Relu.md) | Rectified Linear Unit | max(0, x) activation |
| [**SIGMOID**](../lib/numerics/sigmoid/Sigmoid.md) | Logistic Function | 1/(1+e^-x) bounded [0,1] |
@@ -492,6 +500,7 @@ Reversal indicators identify potential turning points, compute adaptive stop lev
| [**PIVOTFIB**](../lib/reversals/pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Fibonacci pivots: 7 levels (PP, R1-R3, S1-S3); ratios 0.382/0.618/1.000 applied to range |
| [**PIVOTWOOD**](../lib/reversals/pivotwood/Pivotwood.md) | Woodie's Pivot Points | Close-weighted pivots: 7 levels (PP, R1-R3, S1-S3); PP = (H+L+2C)/4 biased toward close |
| [**PSAR**](../lib/reversals/psar/Psar.md) | Parabolic Stop And Reverse | Accelerating trailing stop; SAR dots flip on reversal; Welles Wilder (1978) |
+| [**SAREXT**](../lib/reversals/sarext/Sarext.md) | Parabolic SAR Extended | Asymmetric AF PSAR; sign-encoded output |
| [**SWINGS**](../lib/reversals/swings/Swings.md) | Swing High/Low Detection | Configurable-lookback pattern detector; dual SwingHigh/SwingLow with persistent levels |
| [**TTM_SCALPER**](../lib/reversals/ttm_scalper/TtmScalper.md) | TTM Scalper Alert | 3-bar pivot high/low detection for scalping entries; John Carter |
diff --git a/lib/_index.md b/lib/_index.md
index 6d6c824d..6a24a347 100644
--- a/lib/_index.md
+++ b/lib/_index.md
@@ -59,7 +59,7 @@
| [BAXTERKING](filters/baxterking/BaxterKing.md) | Baxter-King Band-Pass Filter | Filters |
| [BBANDS](channels/bbands/Bbands.md) | Bollinger Bands | Channels |
| [BBB](oscillators/bbb/Bbb.md) | Bollinger %B | Oscillators |
-| [BBI](oscillators/bbi/Bbi.md) | Bulls Bears Index | Oscillators |
+| [BBI](oscillators/bbi/Bbi.md) | Bulls Bears Index | Oscillators |
| [BBS](oscillators/bbs/Bbs.md) | Bollinger Band Squeeze | Oscillators |
| [BBW](volatility/bbw/Bbw.md) | Bollinger Band Width | Volatility |
| [BBWN](volatility/bbwn/Bbwn.md) | BB Width Normalized | Volatility |
@@ -72,7 +72,7 @@
| [BINOMDIST](numerics/binomdist/Binomdist.md) | Binomial Distribution | Numerics |
| [BLMA](trends_FIR/blma/Blma.md) | Blackman MA | Trends (FIR) |
| [BOP](momentum/bop/Bop.md) | Balance of Power | Momentum |
-| [BPF](filters/bpf/Bpf.md) | BandPass Filter | Filters |
+| [BPF](filters/bpf/Bpf.md) | BandPass Filter | Filters |
| [BRAR](oscillators/brar/Brar.md) | Bull-Bear Power Ratio | Oscillators |
| [BUTTER2](filters/butter2/Butter2.md) | Ehlers 2-Pole Butterworth Filter | Filters |
| [BUTTER3](filters/butter3/Butter3.md) | Ehlers 3-Pole Butterworth Filter | Filters |
@@ -114,9 +114,9 @@
| [DEMA](trends_IIR/dema/Dema.md) | Double Exponential MA | Trends (IIR) |
| [DMX](dynamics/dmx/Dmx.md) | Jurik Directional Movement Index | Dynamics |
| [DOSC](oscillators/dosc/Dosc.md) | Derivative Oscillator | Oscillators |
-| [DPO](oscillators/dpo/Dpo.md) | Detrended Price Oscillator | Oscillators |
-| [DYMOI](oscillators/dymoi/Dymoi.md) | Dynamic Momentum Index | Oscillators |
-| [DSMA](trends_IIR/dsma/Dsma.md) | Deviation-Scaled MA | Trends (IIR) |
+| [DPO](oscillators/dpo/Dpo.md) | Detrended Price Oscillator | Oscillators |
+| [DYMOI](oscillators/dymoi/Dymoi.md) | Dynamic Momentum Index | Oscillators |
+| [DSMA](trends_IIR/dsma/Dsma.md) | Deviation-Scaled MA | Trends (IIR) |
| [DSP](cycles/dsp/Dsp.md) | Ehlers Detrended Synthetic Price | Cycles |
| [DWMA](trends_FIR/dwma/Dwma.md) | Double Weighted MA | Trends (FIR) |
| [DWT](numerics/dwt/Dwt.md) | Discrete Wavelet Transform | Numerics |
@@ -130,7 +130,7 @@
| [ENTROPY](statistics/entropy/Entropy.md) | Shannon Entropy | Statistics |
| [EOM](volume/eom/Eom.md) | Ease of Movement | Volume |
| [ER](oscillators/er/Er.md) | Efficiency Ratio | Oscillators |
-| [ERI](oscillators/eri/Eri.md) | Elder Ray Index | Oscillators |
+| [ERI](oscillators/eri/Eri.md) | Elder Ray Index | Oscillators |
| [ETHERM](volatility/etherm/Etherm.md) | Elder's Thermometer | Volatility |
| [EVWMA](volume/evwma/Evwma.md) | Elastic Volume Weighted MA | Volume |
| [EWMA](volatility/ewma/Ewma.md) | EWMA Volatility | Volatility |
@@ -148,7 +148,7 @@
| [GAMMADIST](numerics/gammadist/Gammadist.md) | Gamma Distribution | Numerics |
| [GATOR](oscillators/gator/Gator.md) | Williams Gator Oscillator | Oscillators |
| [GAUSS](filters/gauss/Gauss.md) | Gaussian Filter | Filters |
-| [GDEMA](trends_IIR/gdema/Gdema.md) | Generalized DEMA | Trends (IIR) |
+| [GDEMA](trends_IIR/gdema/Gdema.md) | Generalized DEMA | Trends (IIR) |
| [GEOMEAN](statistics/geomean/Geomean.md) | Geometric Mean | Statistics |
| [GHLA](dynamics/ghla/Ghla.md) | Gann High-Low Activator | Dynamics |
| [GKV](volatility/gkv/Gkv.md) | Garman-Klass Volatility | Volatility |
@@ -169,11 +169,11 @@
| [HP](filters/hp/Hp.md) | Hodrick-Prescott | Filters |
| [HPF](filters/hpf/Hpf.md) | Ehlers Highpass Filter | Filters |
| [HTIT](trends_IIR/htit/Htit.md) | Ehlers Hilbert Transform Instantaneous Trend (aka HT_TRENDLINE) | Trends (IIR) |
-| [HT_DCPERIOD](cycles/ht_dcperiod/Htdcperiod.md) | Ehlers HT Dominant Cycle Period | Cycles |
-| [HT_DCPHASE](cycles/ht_dcphase/Htdcphase.md) | Ehlers HT Dominant Cycle Phase | Cycles |
-| [HT_PHASOR](cycles/ht_phasor/Htphasor.md) | Ehlers HT Phasor Components | Cycles |
-| [HT_SINE](cycles/ht_sine/Htsine.md) | Ehlers HT SineWave | Cycles |
-| [HT_TRENDMODE](dynamics/ht_trendmode/Httrendmode.md) | Ehlers HT Trend vs Cycle | Dynamics |
+| [HT_DCPERIOD](cycles/ht_dcperiod/Htdcperiod.md) | Ehlers HT Dominant Cycle Period | Cycles |
+| [HT_DCPHASE](cycles/ht_dcphase/Htdcphase.md) | Ehlers HT Dominant Cycle Phase | Cycles |
+| [HT_PHASOR](cycles/ht_phasor/Htphasor.md) | Ehlers HT Phasor Components | Cycles |
+| [HT_SINE](cycles/ht_sine/Htsine.md) | Ehlers HT SineWave | Cycles |
+| [HT_TRENDMODE](dynamics/ht_trendmode/Httrendmode.md) | Ehlers HT Trend vs Cycle | Dynamics |
| [HUBER](errors/huber/Huber.md) | Huber Loss | Errors |
| [HURST](statistics/hurst/Hurst.md) | Hurst Exponent | Statistics |
| [HV](volatility/hv/Hv.md) | Historical Volatility | Volatility |
@@ -227,7 +227,7 @@
| [MASE](errors/mase/Mase.md) | Mean Absolute Scaled Error | Errors |
| [MASSI](volatility/massi/Massi.md) | Mass Index | Volatility |
| [MAVP](trends_IIR/mavp/Mavp.md) | Moving Average Variable Period | Trends (IIR) |
-| [MCNMA](trends_IIR/mcnma/Mcnma.md) | McNicholl EMA | Trends (IIR) |
+| [MCNMA](trends_IIR/mcnma/Mcnma.md) | McNicholl EMA | Trends (IIR) |
| [MDAE](errors/mdae/Mdae.md) | Median Absolute Error | Errors |
| [MDAPE](errors/mdape/Mdape.md) | Median Absolute % Error | Errors |
| [ME](errors/me/Me.md) | Mean Error | Errors |
@@ -256,7 +256,7 @@
| [NOTCH](filters/notch/Notch.md) | Notch Filter | Filters |
| [NVI](volume/nvi/Nvi.md) | Negative Volume Index | Volume |
| [NW](filters/nw/Nw.md) | Nadaraya-Watson Kernel Regression | Filters |
-| [NYQMA](trends_FIR/nyqma/Nyqma.md) | Nyquist MA | Trends (FIR) |
+| [NYQMA](trends_FIR/nyqma/Nyqma.md) | Nyquist MA | Trends (FIR) |
| [OBV](volume/obv/Obv.md) | On Balance Volume | Volume |
| [ONEEURO](filters/oneeuro/OneEuro.md) | One Euro Filter | Filters |
| [PACF](statistics/pacf/Pacf.md) | Partial Autocorrelation Function | Statistics |
@@ -274,7 +274,7 @@
| [PMA](trends_FIR/pma/Pma.md) | Predictive Moving Average | Trends (FIR) |
| [PMO](momentum/pmo/Pmo.md) | Price Momentum Oscillator | Momentum |
| [POISSONDIST](numerics/poissondist/Poissondist.md) | Poisson Distribution | Numerics |
-| [POLYFIT](statistics/polyfit/Polyfit.md) | Polynomial Fitting | Statistics |
+| [POLYFIT](statistics/polyfit/Polyfit.md) | Polynomial Fitting | Statistics |
| [PPO](momentum/ppo/Ppo.md) | Percentage Price Oscillator | Momentum |
| [PRS](momentum/prs/Prs.md) | Price Relative Strength | Momentum |
| [PSAR](reversals/psar/Psar.md) | Parabolic Stop And Reverse | Reversals |
@@ -294,7 +294,7 @@
| [QUANTILELOSS](errors/quantileloss/QuantileLoss.md) | Quantile Loss | Errors |
| [RAE](errors/rae/Rae.md) | Relative Absolute Error | Errors |
| [RAIN](trends_FIR/rain/Rain.md) | Rainbow MA | Trends (FIR) |
-| [RAVI](dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dynamics |
+| [RAVI](dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dynamics |
| [REFLEX](oscillators/reflex/Reflex.md) | Ehlers Reflex Indicator | Oscillators |
| [REGCHANNEL](channels/regchannel/Regchannel.md) | Regression Channels | Channels |
| [RELU](numerics/relu/Relu.md) | Rectified Linear Unit | Numerics |
diff --git a/lib/channels/aberr/aberr.pine b/lib/channels/aberr/aberr.pine
index 468f909d..87c0bad7 100644
--- a/lib/channels/aberr/aberr.pine
+++ b/lib/channels/aberr/aberr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Aberration (ABERR)", "ABERR", overlay=true)
diff --git a/lib/channels/accbands/accbands.pine b/lib/channels/accbands/accbands.pine
index 3cf0f7a1..f85326d7 100644
--- a/lib/channels/accbands/accbands.pine
+++ b/lib/channels/accbands/accbands.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Acceleration Bands (ACCBANDS)", "ACCBANDS", overlay=true)
diff --git a/lib/channels/apchannel/apchannel.md b/lib/channels/apchannel/apchannel.md
index 4886da06..b58a86c2 100644
--- a/lib/channels/apchannel/apchannel.md
+++ b/lib/channels/apchannel/apchannel.md
@@ -4,17 +4,17 @@
| ---------------- | -------------------------------- |
| **Category** | Channel |
| **Inputs** | OHLCV bar (TBar) |
-| **Parameters** | None |
-| **Outputs** | Single series (Apchannel) |
+| **Parameters** | `alpha` (default 0.2) |
+| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
-| **Warmup** | 1 bar |
+| **Warmup** | `⌈3/alpha⌉` bars (default 15) |
### TL;DR
- APCHANNEL applies exponential smoothing independently to price highs and lows, creating a dynamic envelope that "remembers" significant extremes wh...
-- No configurable parameters; computation is stateless per bar.
+- Parameterized by `alpha` (default 0.2).
- Output range: Tracks input.
-- Requires 1 bar of warmup before first valid output (IsHot = true).
+- Requires `⌈3/alpha⌉` bars (default 15) of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
APCHANNEL applies exponential smoothing independently to price highs and lows, creating a dynamic envelope that "remembers" significant extremes while gradually fading their influence over time. Unlike rigid Donchian channels that drop price extremes abruptly when they exit the lookback window (the "cliff effect"), APCHANNEL decays them smoothly through leaky integration. The result is a channel with continuously sloping boundaries that responds to volatility without the discontinuous jumps that plague fixed-window approaches. The algorithm is $O(1)$ per bar with only two state variables and no buffers.
diff --git a/lib/channels/apchannel/apchannel.pine b/lib/channels/apchannel/apchannel.pine
index cc0ca753..a9f67394 100644
--- a/lib/channels/apchannel/apchannel.pine
+++ b/lib/channels/apchannel/apchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Adaptive Price Channel (APCHANNEL)", "APCHANNEL", overlay=true)
diff --git a/lib/channels/apz/apz.pine b/lib/channels/apz/apz.pine
index 418d9514..0b2f79bb 100644
--- a/lib/channels/apz/apz.pine
+++ b/lib/channels/apz/apz.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Adaptive Price Zone", "APZ", overlay=true)
diff --git a/lib/channels/atrbands/atrbands.pine b/lib/channels/atrbands/atrbands.pine
index 127f8afd..515872c0 100644
--- a/lib/channels/atrbands/atrbands.pine
+++ b/lib/channels/atrbands/atrbands.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("ATR Bands (ATRBANDS)", "ATRBANDS", overlay=true)
diff --git a/lib/channels/bbands/bbands.pine b/lib/channels/bbands/bbands.pine
index cc86e738..80b3c52d 100644
--- a/lib/channels/bbands/bbands.pine
+++ b/lib/channels/bbands/bbands.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bollinger Bands (BBANDS)", "BBANDS", overlay=true)
diff --git a/lib/channels/dchannel/dchannel.pine b/lib/channels/dchannel/dchannel.pine
index 6dc69281..2da080b0 100644
--- a/lib/channels/dchannel/dchannel.pine
+++ b/lib/channels/dchannel/dchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Donchian Channels (DCHANNEL)", "DCHANNEL", overlay=true)
diff --git a/lib/channels/decaychannel/decaychannel.pine b/lib/channels/decaychannel/decaychannel.pine
index 1ac60957..2020db47 100644
--- a/lib/channels/decaychannel/decaychannel.pine
+++ b/lib/channels/decaychannel/decaychannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Decay Min-Max Channel (DECAYCHANNEL)", "DECAYCHANNEL", overlay=true)
diff --git a/lib/channels/fcb/fcb.pine b/lib/channels/fcb/fcb.pine
index 0f394dd1..a369e02f 100644
--- a/lib/channels/fcb/fcb.pine
+++ b/lib/channels/fcb/fcb.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Fractal Chaos Bands (FCB)", "FCB", overlay=true)
diff --git a/lib/channels/jbands/Jbands.pine b/lib/channels/jbands/Jbands.pine
index 04807578..d146c277 100644
--- a/lib/channels/jbands/Jbands.pine
+++ b/lib/channels/jbands/Jbands.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Jurik Adaptive Envelope Bands", "JBANDS", overlay=true)
diff --git a/lib/channels/jbands/jbands.md b/lib/channels/jbands/jbands.md
index c72e18e3..534e13b5 100644
--- a/lib/channels/jbands/jbands.md
+++ b/lib/channels/jbands/jbands.md
@@ -7,14 +7,14 @@
| **Parameters** | `period`, `phase` (default 0) |
| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
-| **Warmup** | 1 bar |
+| **Warmup** | `⌈20 + 80 × period^0.36⌉` bars |
### TL;DR
- JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new pri...
- Parameterized by `period`, `phase` (default 0).
- Output range: Tracks input.
-- Requires 1 bar of warmup before first valid output (IsHot = true).
+- Requires `⌈20 + 80 × period^0.36⌉` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new price extremes and decay exponentially during consolidation. Unlike standard volatility bands (Bollinger, Keltner) which maintain symmetric width around a center line, JBANDS feature "snap-and-decay" hysteresis: expansion is instantaneous (plasticity), contraction is gradual (elasticity). The decay rate is dynamically modulated by a two-stage volatility estimator — a 10-bar SMA feeding a 128-bar trimmed mean — making the bands tight during quiet markets and expansive during trends. The center line is the full JMA: a 2-pole IIR filter with phase control and adaptive alpha.
diff --git a/lib/channels/kchannel/kchannel.pine b/lib/channels/kchannel/kchannel.pine
index 1d05b214..96262305 100644
--- a/lib/channels/kchannel/kchannel.pine
+++ b/lib/channels/kchannel/kchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Keltner Channel (KCHANNEL)", "KCHANNEL", overlay=true)
diff --git a/lib/channels/maenv/maenv.pine b/lib/channels/maenv/maenv.pine
index 54575383..a0ab3939 100644
--- a/lib/channels/maenv/maenv.pine
+++ b/lib/channels/maenv/maenv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("MA Envelope (MAE)", "MAE", overlay=true)
diff --git a/lib/channels/mmchannel/mmchannel.pine b/lib/channels/mmchannel/mmchannel.pine
index 2c84b785..28fea159 100644
--- a/lib/channels/mmchannel/mmchannel.pine
+++ b/lib/channels/mmchannel/mmchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Min-Max Channel (MMCHANNEL)", "MMCHANNEL", overlay=true)
diff --git a/lib/channels/pchannel/pchannel.pine b/lib/channels/pchannel/pchannel.pine
index 159cc821..3c04be73 100644
--- a/lib/channels/pchannel/pchannel.pine
+++ b/lib/channels/pchannel/pchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Channel (PCHANNEL)", "PCHANNEL", overlay=true)
diff --git a/lib/channels/regchannel/Regchannel.cs b/lib/channels/regchannel/Regchannel.cs
index 7181bdc5..73a6d8f3 100644
--- a/lib/channels/regchannel/Regchannel.cs
+++ b/lib/channels/regchannel/Regchannel.cs
@@ -1,3 +1,4 @@
+using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -206,7 +207,7 @@ public sealed class Regchannel : ITValuePublisher
for (int i = 0; i < count; i++)
{
sumY += values[i];
- sumXY += i * values[i];
+ sumXY = Math.FusedMultiplyAdd((double)i, values[i], sumXY);
}
double n = count;
@@ -325,7 +326,11 @@ public sealed class Regchannel : ITValuePublisher
///
/// Batch calculation using spans.
+ /// O(period) per bar: sums recomputed from circular buffer each bar for numerical
+ /// consistency with the streaming path, plus closed-form residual variance:
+ /// sumResiduals² = sumY² − intercept·sumY − slope·sumXY, eliminating a second O(period) pass.
///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(
ReadOnlySpan source,
Span middle,
@@ -360,121 +365,124 @@ public sealed class Regchannel : ITValuePublisher
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double denomFull = period * sumX2Full - sumXFull * sumXFull;
- // Track last valid value for NaN substitution
- double lastValid = double.NaN;
-
- for (int i = 0; i < len; i++)
+ // Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences.
+ const int StackAllocThreshold = 256;
+ double[]? rentedWindow = null;
+ scoped Span window;
+ if (period <= StackAllocThreshold)
{
- // Get valid value with last-valid substitution
- double currentValue = source[i];
- if (double.IsFinite(currentValue))
- {
- lastValid = currentValue;
- }
- else
- {
- currentValue = lastValid;
- }
+ window = stackalloc double[period];
+ }
+ else
+ {
+ rentedWindow = ArrayPool.Shared.Rent(period);
+ window = rentedWindow.AsSpan(0, period);
+ }
- // If still NaN (no valid value seen yet), output NaN
- if (!double.IsFinite(currentValue))
+ try
+ {
+ window.Clear();
+
+ double lastValid = double.NaN;
+ int head = 0, count = 0;
+
+ for (int i = 0; i < len; i++)
{
- middle[i] = double.NaN;
- upper[i] = double.NaN;
- lower[i] = double.NaN;
- continue;
- }
-
- int count = Math.Min(i + 1, period);
- int start = i - count + 1;
-
- if (count <= 1)
- {
- middle[i] = currentValue;
- upper[i] = currentValue;
- lower[i] = currentValue;
- continue;
- }
-
- // Calculate sums for linear regression with NaN handling
- double sumY = 0;
- double sumXY = 0;
- double lastValidInWindow = double.NaN;
-
- for (int j = 0; j < count; j++)
- {
- double rawY = source[start + j];
- double y;
- if (double.IsFinite(rawY))
+ // NaN substitution
+ double y = source[i];
+ if (double.IsFinite(y))
{
- lastValidInWindow = rawY;
- y = rawY;
+ lastValid = y;
}
else
{
- y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
+ y = lastValid;
}
- sumY += y;
- sumXY += j * y;
- }
- double n = count;
- double sx, denom;
-
- if (count < period)
- {
- sx = 0.5 * n * (n - 1);
- double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
- denom = n * sx2 - sx * sx;
- }
- else
- {
- sx = sumXFull;
- denom = denomFull;
- }
-
- double slope, intercept, regression;
-
- if (Math.Abs(denom) < 1e-10)
- {
- slope = 0;
- intercept = sumY / n;
- regression = intercept;
- }
- else
- {
- slope = (n * sumXY - sx * sumY) / denom;
- intercept = (sumY - slope * sx) / n;
- regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
- }
-
- // Calculate standard deviation of residuals with NaN handling
- double sumResiduals2 = 0;
- lastValidInWindow = double.NaN;
- for (int j = 0; j < count; j++)
- {
- double rawY = source[start + j];
- double y;
- if (double.IsFinite(rawY))
+ // No valid value seen yet
+ if (!double.IsFinite(y))
{
- lastValidInWindow = rawY;
- y = rawY;
+ middle[i] = double.NaN;
+ upper[i] = double.NaN;
+ lower[i] = double.NaN;
+ continue;
+ }
+
+ // Store in circular buffer
+ window[head] = y;
+ head = (head + 1) % period;
+ if (count < period)
+ {
+ count++;
+ }
+
+ if (count <= 1)
+ {
+ middle[i] = y;
+ upper[i] = y;
+ lower[i] = y;
+ continue;
+ }
+
+ // Recompute sums fresh from circular buffer (oldest-to-newest).
+ // This matches the streaming Update() path numerically.
+ double sumY = 0, sumXY = 0, sumY2 = 0;
+ int oldestIdx = (head - count + period) % period;
+ for (int k = 0; k < count; k++)
+ {
+ double wk = window[(oldestIdx + k) % period];
+ sumY += wk;
+ sumXY = Math.FusedMultiplyAdd((double)k, wk, sumXY);
+ sumY2 = Math.FusedMultiplyAdd(wk, wk, sumY2);
+ }
+
+ double n = count;
+ double sx, denom;
+
+ if (count < period)
+ {
+ sx = 0.5 * n * (n - 1);
+ double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
+ denom = n * sx2 - sx * sx;
}
else
{
- y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
+ sx = sumXFull;
+ denom = denomFull;
}
- double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
- double residual = y - predicted;
- sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
+
+ double slope, intercept, regression;
+
+ if (Math.Abs(denom) < 1e-10)
+ {
+ slope = 0;
+ intercept = sumY / n;
+ regression = intercept;
+ }
+ else
+ {
+ slope = (n * sumXY - sx * sumY) / denom;
+ intercept = (sumY - slope * sx) / n;
+ regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
+ }
+
+ // Closed-form residual variance (normal-equation identity):
+ // sumResiduals² = sumY² − intercept·sumY − slope·sumXY
+ double sumResiduals2 = Math.Max(0.0, sumY2 - intercept * sumY - slope * sumXY);
+ double stdDev = Math.Sqrt(sumResiduals2 / n);
+ double band = multiplier * stdDev;
+
+ middle[i] = regression;
+ upper[i] = regression + band;
+ lower[i] = regression - band;
+ }
+ }
+ finally
+ {
+ if (rentedWindow != null)
+ {
+ ArrayPool.Shared.Return(rentedWindow);
}
-
- double stdDev = Math.Sqrt(sumResiduals2 / n);
- double band = multiplier * stdDev;
-
- middle[i] = regression;
- upper[i] = regression + band;
- lower[i] = regression - band;
}
}
diff --git a/lib/channels/regchannel/regchannel.pine b/lib/channels/regchannel/regchannel.pine
index d02ac098..b456dd22 100644
--- a/lib/channels/regchannel/regchannel.pine
+++ b/lib/channels/regchannel/regchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Regression Channels (REGCHANNEL)", "REGCHANNEL", overlay=true)
diff --git a/lib/channels/sdchannel/Sdchannel.cs b/lib/channels/sdchannel/Sdchannel.cs
index 2fcb9ba7..90f8d23d 100644
--- a/lib/channels/sdchannel/Sdchannel.cs
+++ b/lib/channels/sdchannel/Sdchannel.cs
@@ -1,3 +1,4 @@
+using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -325,7 +326,11 @@ public sealed class Sdchannel : ITValuePublisher
///
/// Batch calculation using spans.
+ /// O(period) per bar: sums and residuals recomputed from circular buffer each bar for numerical
+ /// consistency with the streaming path. Uses the same plain arithmetic as Update() for sumXY
+ /// and the same FMA residual loop to ensure streaming/batch agreement within 1e-9.
///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(
ReadOnlySpan source,
Span middle,
@@ -360,75 +365,130 @@ public sealed class Sdchannel : ITValuePublisher
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double denomFull = period * sumX2Full - sumXFull * sumXFull;
- for (int i = 0; i < len; i++)
+ // Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences.
+ const int StackAllocThreshold = 256;
+ double[]? rentedWindow = null;
+ scoped Span window;
+ if (period <= StackAllocThreshold)
{
- int count = Math.Min(i + 1, period);
- int start = i - count + 1;
+ window = stackalloc double[period];
+ }
+ else
+ {
+ rentedWindow = ArrayPool.Shared.Rent(period);
+ window = rentedWindow.AsSpan(0, period);
+ }
- if (count <= 1)
+ try
+ {
+ window.Clear();
+
+ double lastValid = double.NaN;
+ int head = 0, count = 0;
+
+ for (int i = 0; i < len; i++)
{
- middle[i] = source[i];
- upper[i] = source[i];
- lower[i] = source[i];
- continue;
+ // NaN substitution matching GetValid() in streaming Update()
+ double y = source[i];
+ if (double.IsFinite(y))
+ {
+ lastValid = y;
+ }
+ else
+ {
+ y = lastValid;
+ }
+
+ // No valid value seen yet
+ if (!double.IsFinite(y))
+ {
+ middle[i] = double.NaN;
+ upper[i] = double.NaN;
+ lower[i] = double.NaN;
+ continue;
+ }
+
+ // Store in circular buffer
+ window[head] = y;
+ head = (head + 1) % period;
+ if (count < period)
+ {
+ count++;
+ }
+
+ if (count <= 1)
+ {
+ middle[i] = y;
+ upper[i] = y;
+ lower[i] = y;
+ continue;
+ }
+
+ // Recompute sums fresh from circular buffer (oldest-to-newest).
+ // Plain arithmetic for sumXY matches streaming Update() path numerically.
+ double sumY = 0, sumXY = 0;
+ int oldestIdx = (head - count + period) % period;
+ for (int k = 0; k < count; k++)
+ {
+ double wk = window[(oldestIdx + k) % period];
+ sumY += wk;
+ sumXY += (double)k * wk;
+ }
+
+ double n = count;
+ double sx, denom;
+
+ if (count < period)
+ {
+ sx = 0.5 * n * (n - 1);
+ double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
+ denom = n * sx2 - sx * sx;
+ }
+ else
+ {
+ sx = sumXFull;
+ denom = denomFull;
+ }
+
+ double slope, intercept, regression;
+
+ if (Math.Abs(denom) < 1e-10)
+ {
+ slope = 0;
+ intercept = sumY / n;
+ regression = intercept;
+ }
+ else
+ {
+ slope = (n * sumXY - sx * sumY) / denom;
+ intercept = (sumY - slope * sx) / n;
+ regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
+ }
+
+ // Explicit residual loop matching streaming Update() path numerically.
+ double sumResiduals2 = 0;
+ for (int k = 0; k < count; k++)
+ {
+ double wk = window[(oldestIdx + k) % period];
+ double predicted = Math.FusedMultiplyAdd(slope, k, intercept);
+ double residual = wk - predicted;
+ sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
+ }
+
+ double stdDev = Math.Sqrt(sumResiduals2 / n);
+ double band = multiplier * stdDev;
+
+ middle[i] = regression;
+ upper[i] = regression + band;
+ lower[i] = regression - band;
}
-
- // Calculate sums for linear regression
- double sumY = 0;
- double sumXY = 0;
-
- for (int j = 0; j < count; j++)
+ }
+ finally
+ {
+ if (rentedWindow != null)
{
- double y = source[start + j];
- sumY += y;
- sumXY += j * y;
+ ArrayPool.Shared.Return(rentedWindow);
}
-
- double n = count;
- double sx, denom;
-
- if (count < period)
- {
- sx = 0.5 * n * (n - 1);
- double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
- denom = n * sx2 - sx * sx;
- }
- else
- {
- sx = sumXFull;
- denom = denomFull;
- }
-
- double slope, intercept, regression;
-
- if (Math.Abs(denom) < 1e-10)
- {
- slope = 0;
- intercept = sumY / n;
- regression = intercept;
- }
- else
- {
- slope = (n * sumXY - sx * sumY) / denom;
- intercept = (sumY - slope * sx) / n;
- regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
- }
-
- // Calculate standard deviation of residuals
- double sumResiduals2 = 0;
- for (int j = 0; j < count; j++)
- {
- double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
- double residual = source[start + j] - predicted;
- sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
- }
-
- double stdDev = Math.Sqrt(sumResiduals2 / n);
- double band = multiplier * stdDev;
-
- middle[i] = regression;
- upper[i] = regression + band;
- lower[i] = regression - band;
}
}
@@ -464,7 +524,9 @@ public sealed class Sdchannel : ITValuePublisher
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Sdchannel Indicator) Calculate(TSeries source, int period = 20, double multiplier = 2.0)
{
- var indicator = new Sdchannel(source, period, multiplier);
+ // Use parameterless constructor to avoid double-processing: new Sdchannel(source, ...) calls Prime(source),
+ // then Update(source) would call Prime again.
+ var indicator = new Sdchannel(period, multiplier);
var results = indicator.Update(source);
return (results, indicator);
}
diff --git a/lib/channels/sdchannel/sdchannel.pine b/lib/channels/sdchannel/sdchannel.pine
index eb76dcda..c4739026 100644
--- a/lib/channels/sdchannel/sdchannel.pine
+++ b/lib/channels/sdchannel/sdchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Standard Deviation Channel (SDCHANNEL)", "SDCHANNEL", overlay=true)
diff --git a/lib/channels/starchannel/starchannel.pine b/lib/channels/starchannel/starchannel.pine
index 9ce0d868..a7a9539a 100644
--- a/lib/channels/starchannel/starchannel.pine
+++ b/lib/channels/starchannel/starchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Stoller Average Range Channel (STARCHANNEL)", "STARCHANNEL", overlay=true)
diff --git a/lib/channels/stbands/stbands.pine b/lib/channels/stbands/stbands.pine
index d45c62bf..5f90fd2f 100644
--- a/lib/channels/stbands/stbands.pine
+++ b/lib/channels/stbands/stbands.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Super Trend Bands (STBANDS)", "STBANDS", overlay=true)
diff --git a/lib/channels/ubands/ubands.pine b/lib/channels/ubands/ubands.pine
index e8cc4c5f..2346196d 100644
--- a/lib/channels/ubands/ubands.pine
+++ b/lib/channels/ubands/ubands.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Ultimate Bands logic based on work by John F. Ehlers (c) 2024
diff --git a/lib/channels/uchannel/uchannel.md b/lib/channels/uchannel/uchannel.md
index c82672b9..cda324df 100644
--- a/lib/channels/uchannel/uchannel.md
+++ b/lib/channels/uchannel/uchannel.md
@@ -7,14 +7,14 @@
| **Parameters** | `strPeriod` (default DefaultStrPeriod), `centerPeriod` (default DefaultCenterPeriod), `multiplier` (default DefaultMultiplier) |
| **Outputs** | Multiple series (Upper, Middle, Lower, STR) |
| **Output range** | Tracks input |
-| **Warmup** | 1 bar |
+| **Warmup** | `Math.Max(strPeriod, centerPeriod)` bars |
### TL;DR
- Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band widt...
- Parameterized by `strperiod` (default defaultstrperiod), `centerperiod` (default defaultcenterperiod), `multiplier` (default defaultmultiplier).
- Output range: Tracks input.
-- Requires 1 bar of warmup before first valid output (IsHot = true).
+- Requires `Math.Max(strPeriod, centerPeriod)` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band width, creating a channel where both the trend estimate and the volatility measure share the same low-lag, zero-overshoot filter characteristics. Unlike UBANDS which uses RMS of price residuals, UCHANNEL uses Smoothed True Range (STR) for band width, making it responsive to gap-inclusive volatility. Separate period parameters allow independent tuning of centerline smoothness and band-width responsiveness.
diff --git a/lib/channels/uchannel/uchannel.pine b/lib/channels/uchannel/uchannel.pine
index 9a04fa8e..780e23ee 100644
--- a/lib/channels/uchannel/uchannel.pine
+++ b/lib/channels/uchannel/uchannel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Ultimate Channel logic based on work by John F. Ehlers (c) 2024
diff --git a/lib/channels/vwapbands/vwapbands.pine b/lib/channels/vwapbands/vwapbands.pine
index 1b8f2446..2da9fd51 100644
--- a/lib/channels/vwapbands/vwapbands.pine
+++ b/lib/channels/vwapbands/vwapbands.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("VWAP Bands (VWAPBANDS)", "VWAPBANDS", overlay=true)
diff --git a/lib/channels/vwapsd/vwapsd.pine b/lib/channels/vwapsd/vwapsd.pine
index 00d6110c..09c10547 100644
--- a/lib/channels/vwapsd/vwapsd.pine
+++ b/lib/channels/vwapsd/vwapsd.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("VWAP with Standard Deviation Bands", "VWAPSD", overlay=true)
diff --git a/lib/core/ringbuffer/RingBuffer.Tests.cs b/lib/core/ringbuffer/RingBuffer.Tests.cs
index 27b7df8a..6795d2d6 100644
--- a/lib/core/ringbuffer/RingBuffer.Tests.cs
+++ b/lib/core/ringbuffer/RingBuffer.Tests.cs
@@ -12,7 +12,7 @@ public class RingBufferTests
Assert.Equal(0, buffer.Count);
Assert.False(buffer.IsFull);
Assert.Equal(0, buffer.Sum);
- Assert.Equal(0, buffer.Average);
+ Assert.True(double.IsNaN(buffer.Average));
}
[Fact]
@@ -192,7 +192,7 @@ public class RingBufferTests
Assert.Equal(0, buffer.Count);
Assert.Equal(0, buffer.Sum);
- Assert.Equal(0, buffer.Average);
+ Assert.True(double.IsNaN(buffer.Average));
Assert.False(buffer.IsFull);
}
@@ -453,11 +453,11 @@ public class RingBufferTests
}
[Fact]
- public void Average_EmptyBuffer_ReturnsZero()
+ public void Average_EmptyBuffer_ReturnsNaN()
{
var buffer = new RingBuffer(5);
- Assert.Equal(0, buffer.Average);
+ Assert.True(double.IsNaN(buffer.Average));
}
[Fact]
diff --git a/lib/core/ringbuffer/RingBuffer.cs b/lib/core/ringbuffer/RingBuffer.cs
index 0e85ee94..38a0e067 100644
--- a/lib/core/ringbuffer/RingBuffer.cs
+++ b/lib/core/ringbuffer/RingBuffer.cs
@@ -141,12 +141,12 @@ public sealed class RingBuffer : IEnumerable
///
/// Average of all elements in the buffer.
- /// Returns 0 if buffer is empty.
+ /// Returns if buffer is empty.
///
public double Average
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _count > 0 ? _sum / _count : 0;
+ get => _count > 0 ? _sum / _count : double.NaN;
}
///
diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs
index 0f2451a1..c84158ee 100644
--- a/lib/core/simd/SimdExtensions.cs
+++ b/lib/core/simd/SimdExtensions.cs
@@ -90,7 +90,7 @@ public static class SimdExtensions
for (int i = 0; i < span.Length; i++)
{
double diff = span[i] - mean;
- sumSquares += diff * diff;
+ sumSquares = Math.FusedMultiplyAdd(diff, diff, sumSquares);
}
return sumSquares / (span.Length - 1);
}
@@ -438,7 +438,7 @@ public static class SimdExtensions
for (; i < span.Length; i++)
{
double diff = span[i] - m;
- result += diff * diff;
+ result = Math.FusedMultiplyAdd(diff, diff, result);
}
return result / (span.Length - 1);
@@ -682,17 +682,17 @@ public static class SimdExtensions
// Unroll scalar loop with 4 accumulators to break dependency chains
for (; i <= len - 4; i += 4)
{
- s1 += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i);
- s2 += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1);
- s3 += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2);
- s4 += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3);
+ s1 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i), Unsafe.Add(ref br, i), s1);
+ s2 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 1), Unsafe.Add(ref br, i + 1), s2);
+ s3 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 2), Unsafe.Add(ref br, i + 2), s3);
+ s4 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 3), Unsafe.Add(ref br, i + 3), s4);
}
double s = s1 + s2 + s3 + s4;
for (; i < len; i++)
{
- s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i);
+ s = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i), Unsafe.Add(ref br, i), s);
}
return s;
}
diff --git a/lib/core/tseries/tseries.cs b/lib/core/tseries/tseries.cs
index 6ac28d7c..746dc8fe 100644
--- a/lib/core/tseries/tseries.cs
+++ b/lib/core/tseries/tseries.cs
@@ -213,6 +213,12 @@ public class TSeries : IReadOnlyList, ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
+ ///
+ /// Synthetic timestamps: Each element receives a fabricated timestamp starting at
+ /// (captured once at call time) and incrementing by one minute
+ /// per item. These timestamps do not reflect real market time. Use the typed overloads
+ /// (, ) when real timestamps matter.
+ ///
public void Add(IEnumerable values)
{
long t = DateTime.UtcNow.Ticks;
diff --git a/lib/cycles/ccor/Ccor.Validation.Tests.cs b/lib/cycles/ccor/Ccor.Validation.Tests.cs
index 0ca9cc84..32f98f77 100644
--- a/lib/cycles/ccor/Ccor.Validation.Tests.cs
+++ b/lib/cycles/ccor/Ccor.Validation.Tests.cs
@@ -364,5 +364,33 @@ public class CcorValidationTests
"Different thresholds should produce different market state classifications");
}
+ [Fact]
+ public void Ccor_Correction_Recomputes()
+ {
+ var ind = new Ccor(period: 20);
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ ind.Update(new TValue(t0.AddMinutes(i),
+ 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorPrice = 105.5;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with a dramatically different price — recompute must yield different result
+ ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original price — must exactly restore original result
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, Tolerance);
+ }
+
#endregion
}
diff --git a/lib/cycles/ccor/Ccor.cs b/lib/cycles/ccor/Ccor.cs
index d992dffc..14046cc2 100644
--- a/lib/cycles/ccor/Ccor.cs
+++ b/lib/cycles/ccor/Ccor.cs
@@ -231,7 +231,7 @@ public sealed class Ccor : AbstractBase
{
foreach (double value in source)
{
- Update(new TValue(DateTime.UtcNow, value));
+ Update(new TValue(DateTime.MinValue, value));
}
}
diff --git a/lib/cycles/ccor/ccor.pine b/lib/cycles/ccor/ccor.pine
index 12a02baa..ae3ca750 100644
--- a/lib/cycles/ccor/ccor.pine
+++ b/lib/cycles/ccor/ccor.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Correlation Cycle (CCOR)", "CCOR", overlay=false)
diff --git a/lib/cycles/ccyc/ccyc.pine b/lib/cycles/ccyc/ccyc.pine
index 21842d3d..8810086e 100644
--- a/lib/cycles/ccyc/ccyc.pine
+++ b/lib/cycles/ccyc/ccyc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Cyber Cycle (CCYC)", "CCYC", overlay=false)
diff --git a/lib/cycles/cg/Cg.cs b/lib/cycles/cg/Cg.cs
index 9652e94a..fb1e31d8 100644
--- a/lib/cycles/cg/Cg.cs
+++ b/lib/cycles/cg/Cg.cs
@@ -295,7 +295,7 @@ public sealed class Cg : AbstractBase
int idx = (effectiveStart + j) % period;
double price = buffer[idx];
int weight = j + 1; // 1-based weighting
- weightedSum += weight * price;
+ weightedSum = Math.FusedMultiplyAdd(weight, price, weightedSum);
sum += price;
}
diff --git a/lib/cycles/cg/cg.pine b/lib/cycles/cg/cg.pine
index dab426dd..2c52116f 100644
--- a/lib/cycles/cg/cg.pine
+++ b/lib/cycles/cg/cg.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Center of Gravity (CG)", "CG", overlay=false)
diff --git a/lib/cycles/dsp/dsp.pine b/lib/cycles/dsp/dsp.pine
index 88c07afb..981c7df1 100644
--- a/lib/cycles/dsp/dsp.pine
+++ b/lib/cycles/dsp/dsp.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Detrended Synthetic Price (DSP)", "DSP", overlay=false)
diff --git a/lib/cycles/eacp/Eacp.Validation.Tests.cs b/lib/cycles/eacp/Eacp.Validation.Tests.cs
index 0fb55c84..ad48adf9 100644
--- a/lib/cycles/eacp/Eacp.Validation.Tests.cs
+++ b/lib/cycles/eacp/Eacp.Validation.Tests.cs
@@ -418,5 +418,33 @@ public class EacpValidationTests
Assert.True(diff > 5, $"Should detect different cycles: {eacp1.DominantCycle} vs {eacp2.DominantCycle}");
}
+ [Fact]
+ public void Eacp_Correction_Recomputes()
+ {
+ var ind = new Eacp(8, 48, 3, true);
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ ind.Update(new TValue(t0.AddMinutes(i),
+ 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorPrice = 105.5;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with a dramatically different price — recompute must yield different result
+ ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original price — must exactly restore original result
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, Tolerance);
+ }
+
#endregion
}
diff --git a/lib/cycles/eacp/Eacp.cs b/lib/cycles/eacp/Eacp.cs
index 225b7003..78bf5f72 100644
--- a/lib/cycles/eacp/Eacp.cs
+++ b/lib/cycles/eacp/Eacp.cs
@@ -48,6 +48,7 @@ public sealed class Eacp : AbstractBase
private readonly double[] _corr;
private readonly double[] _power;
private readonly double[] _smooth;
+ private readonly double[] _p_smooth;
// State for filters and output
[StructLayout(LayoutKind.Auto)]
@@ -122,6 +123,7 @@ public sealed class Eacp : AbstractBase
_corr = new double[size];
_power = new double[size];
_smooth = new double[size];
+ _p_smooth = new double[size];
_filtHistory = new RingBuffer(size + maxPeriod);
Name = $"Eacp({minPeriod},{maxPeriod})";
@@ -156,11 +158,13 @@ public sealed class Eacp : AbstractBase
{
_ps = _s;
_filtHistory.Snapshot();
+ Array.Copy(_smooth, _p_smooth, _smooth.Length);
}
else
{
_s = _ps;
_filtHistory.Restore();
+ Array.Copy(_p_smooth, _smooth, _smooth.Length);
}
var s = _s;
@@ -407,6 +411,7 @@ public sealed class Eacp : AbstractBase
Array.Clear(_corr);
Array.Clear(_power);
Array.Clear(_smooth);
+ Array.Clear(_p_smooth);
Last = default;
}
@@ -414,7 +419,7 @@ public sealed class Eacp : AbstractBase
{
foreach (double value in source)
{
- Update(new TValue(DateTime.UtcNow, value));
+ Update(new TValue(DateTime.MinValue, value));
}
}
@@ -459,7 +464,7 @@ public sealed class Eacp : AbstractBase
var eacp = new Eacp(minPeriod, maxPeriod, avgLength, enhance);
for (int i = 0; i < len; i++)
{
- var result = eacp.Update(new TValue(DateTime.UtcNow, source[i]));
+ var result = eacp.Update(new TValue(DateTime.MinValue, source[i]));
output[i] = result.Value;
}
}
diff --git a/lib/cycles/eacp/eacp.pine b/lib/cycles/eacp/eacp.pine
index 0902232b..def587bc 100644
--- a/lib/cycles/eacp/eacp.pine
+++ b/lib/cycles/eacp/eacp.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Autocorrelation Periodogram (EACP)","EACP",overlay=false)
diff --git a/lib/cycles/ebsw/ebsw.md b/lib/cycles/ebsw/ebsw.md
index f2859512..65bead80 100644
--- a/lib/cycles/ebsw/ebsw.md
+++ b/lib/cycles/ebsw/ebsw.md
@@ -7,14 +7,14 @@
| **Parameters** | `hpLength` (default 40), `ssfLength` (default 10) |
| **Outputs** | Single series (Ebsw) |
| **Output range** | Varies (see docs) |
-| **Warmup** | 1 bar |
+| **Warmup** | `Math.Max(hpLength, ssfLength) + 3` bars (default 43) |
### TL;DR
- EBSW is a refined cycle oscillator that combines a high-pass filter (trend removal), a Super-Smoother filter (noise removal), and Automatic Gain Co...
- Parameterized by `hplength` (default 40), `ssflength` (default 10).
- Output range: Varies (see docs).
-- Requires 1 bar of warmup before first valid output (IsHot = true).
+- Requires `Math.Max(hpLength, ssfLength) + 3` bars (default 43) of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
EBSW is a refined cycle oscillator that combines a high-pass filter (trend removal), a Super-Smoother filter (noise removal), and Automatic Gain Control to produce a normalized $[-1, +1]$ output representing the current position within the dominant market cycle. Developed by John Ehlers as an improvement over the original Hilbert Transform SineWave, it provides cleaner turning point detection without requiring complex phase extraction mathematics.
diff --git a/lib/cycles/ebsw/ebsw.pine b/lib/cycles/ebsw/ebsw.pine
index fe7b2620..a7d005e1 100644
--- a/lib/cycles/ebsw/ebsw.pine
+++ b/lib/cycles/ebsw/ebsw.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Even Better Sinewave (EBSW)", "EBSW", overlay=false)
diff --git a/lib/cycles/homod/homod.md b/lib/cycles/homod/homod.md
index 7366c1cb..40800f5d 100644
--- a/lib/cycles/homod/homod.md
+++ b/lib/cycles/homod/homod.md
@@ -7,14 +7,14 @@
| **Parameters** | `minPeriod` (default 6.0), `maxPeriod` (default 50.0) |
| **Outputs** | Single series (Homod) |
| **Output range** | Varies (see docs) |
-| **Warmup** | 1 bar |
+| **Warmup** | `maxPeriod * 2` bars (default 100) |
### TL;DR
- HOMOD estimates the dominant cycle period of a market using homodyne mixing, a technique from radio engineering where a signal is multiplied by a d...
- Parameterized by `minperiod` (default 6.0), `maxperiod` (default 50.0).
- Output range: Varies (see docs).
-- Requires 1 bar of warmup before first valid output (IsHot = true).
+- Requires `maxPeriod * 2` bars (default 100) of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
HOMOD estimates the dominant cycle period of a market using homodyne mixing, a technique from radio engineering where a signal is multiplied by a delayed copy of itself to expose the angular phase change between samples. The output is a continuously varying period measurement (in bars) that tracks the market's instantaneous cycle length, enabling adaptive indicator tuning. Developed by John Ehlers, it offers better noise rejection and stability than the raw Hilbert Transform period estimator.
diff --git a/lib/cycles/homod/homod.pine b/lib/cycles/homod/homod.pine
index f1a40ae5..6c93b96a 100644
--- a/lib/cycles/homod/homod.pine
+++ b/lib/cycles/homod/homod.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Homodyne Discriminator (HOMOD)","HOMOD",overlay=false)
diff --git a/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs b/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs
index a0a50b67..a81c1c32 100644
--- a/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs
+++ b/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs
@@ -92,4 +92,32 @@ public sealed class HtDcperiodValidationTests : IDisposable
var q = new HtDcperiod();
Assert.Equal(talibLookback, q.WarmupPeriod);
}
+
+ [Fact]
+ public void HtDcperiod_Correction_Recomputes()
+ {
+ var ind = new HtDcperiod();
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ ind.Update(new TValue(t0.AddMinutes(i),
+ 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorPrice = 105.5;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with a dramatically different price — recompute must yield different result
+ ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original price — must exactly restore original result
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
diff --git a/lib/cycles/ht_dcperiod/ht_dcperiod.pine b/lib/cycles/ht_dcperiod/ht_dcperiod.pine
index 08a90716..03fc68d5 100644
--- a/lib/cycles/ht_dcperiod/ht_dcperiod.pine
+++ b/lib/cycles/ht_dcperiod/ht_dcperiod.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Hilbert Transform Dominant Cycle Period (HT_DCPERIOD)", "HT_DCPERIOD", overlay=false)
diff --git a/lib/cycles/ht_dcphase/HtDcphase.Tests.cs b/lib/cycles/ht_dcphase/HtDcphase.Tests.cs
index 71887d53..98f2b601 100644
--- a/lib/cycles/ht_dcphase/HtDcphase.Tests.cs
+++ b/lib/cycles/ht_dcphase/HtDcphase.Tests.cs
@@ -139,8 +139,8 @@ public class HtDcphaseTests
// First update (new bar)
var result1 = ht.Update(new TValue(now.AddMinutes(70), 105), isNew: true);
- // Same bar update
- var result2 = ht.Update(new TValue(now.AddMinutes(70), 106), isNew: false);
+ // Same bar update with the same price — must yield identical result
+ var result2 = ht.Update(new TValue(now.AddMinutes(70), 105), isNew: false);
Assert.Equal(result1.Value, result2.Value);
}
diff --git a/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs b/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs
index 2c50481b..1153b810 100644
--- a/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs
+++ b/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs
@@ -92,4 +92,32 @@ public sealed class HtDcphaseValidationTests : IDisposable
var q = new HtDcphase();
Assert.Equal(talibLookback, q.WarmupPeriod);
}
+
+ [Fact]
+ public void HtDcphase_Correction_Recomputes()
+ {
+ var ind = new HtDcphase();
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ ind.Update(new TValue(t0.AddMinutes(i),
+ 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorPrice = 105.5;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with a dramatically different price — recompute must yield different result
+ ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original price — must exactly restore original result
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
diff --git a/lib/cycles/ht_dcphase/HtDcphase.cs b/lib/cycles/ht_dcphase/HtDcphase.cs
index f2baf2b9..21f471ea 100644
--- a/lib/cycles/ht_dcphase/HtDcphase.cs
+++ b/lib/cycles/ht_dcphase/HtDcphase.cs
@@ -217,12 +217,10 @@ public sealed class HtDcphase : AbstractBase
}
else
{
- // Same-bar update: restore previous state and return cached result from Last
_state = _p_state;
Array.Copy(_p_circBuffer, _circBuffer, CIRC_BUFFER_SIZE);
Array.Copy(_p_smoothPrice, _smoothPrice, SMOOTH_PRICE_SIZE);
Array.Copy(_p_priceHistory, _priceHistory, PRICE_HISTORY_SIZE);
- return Last.Value;
}
var s = _state;
diff --git a/lib/cycles/ht_dcphase/ht_dcphase.pine b/lib/cycles/ht_dcphase/ht_dcphase.pine
index 5b2fb9b6..e5a4f00c 100644
--- a/lib/cycles/ht_dcphase/ht_dcphase.pine
+++ b/lib/cycles/ht_dcphase/ht_dcphase.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Hilbert Transform Dominant Cycle Phase (HT_DCPHASE)", "HT_DCPHASE", overlay=false)
diff --git a/lib/cycles/ht_phasor/phasor.pine b/lib/cycles/ht_phasor/phasor.pine
index 2efa687e..3dc0782c 100644
--- a/lib/cycles/ht_phasor/phasor.pine
+++ b/lib/cycles/ht_phasor/phasor.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Hilbert Transform Phasor Components (HT_PHASOR)", "HT_PHASOR", overlay=false)
diff --git a/lib/cycles/ht_sine/HtSine.Validation.Tests.cs b/lib/cycles/ht_sine/HtSine.Validation.Tests.cs
index 7db0a6d1..42f6255d 100644
--- a/lib/cycles/ht_sine/HtSine.Validation.Tests.cs
+++ b/lib/cycles/ht_sine/HtSine.Validation.Tests.cs
@@ -122,4 +122,34 @@ public sealed class HtSineValidationTests : IDisposable
Assert.Equal(talibLookback, htSine.WarmupPeriod);
}
+
+ [Fact]
+ public void HtSine_Correction_Recomputes()
+ {
+ var ind = new HtSine();
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ ind.Update(new TValue(t0.AddMinutes(i),
+ 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorPrice = 105.5;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorSine = ind.Last.Value;
+ double anchorLeadSine = ind.LeadSine;
+
+ // Correction with a dramatically different price — recompute must yield different results
+ ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
+ Assert.NotEqual(anchorSine, ind.Last.Value);
+
+ // Correction back to original price — must exactly restore original results
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorSine, ind.Last.Value, 1e-9);
+ Assert.Equal(anchorLeadSine, ind.LeadSine, 1e-9);
+ }
}
diff --git a/lib/cycles/ht_sine/HtSine.cs b/lib/cycles/ht_sine/HtSine.cs
index 74d51ee6..bd81f0c6 100644
--- a/lib/cycles/ht_sine/HtSine.cs
+++ b/lib/cycles/ht_sine/HtSine.cs
@@ -295,7 +295,6 @@ public sealed class HtSine : AbstractBase
Array.Copy(_circBuffer, _p_circBuffer, CIRC_BUFFER_SIZE);
Array.Copy(_smoothPrice, _p_smoothPrice, SMOOTH_PRICE_SIZE);
Array.Copy(_priceHistory, _p_priceHistory, PRICE_HISTORY_SIZE);
- _state.Today++;
}
else
{
@@ -305,6 +304,8 @@ public sealed class HtSine : AbstractBase
Array.Copy(_p_priceHistory, _priceHistory, PRICE_HISTORY_SIZE);
}
+ _state.Today++;
+
// Local copy of state for struct promotion (AGENTS.md §2.5)
var s = _state;
diff --git a/lib/cycles/ht_sine/ht_sine.pine b/lib/cycles/ht_sine/ht_sine.pine
index 8a686602..a9fe9ed3 100644
--- a/lib/cycles/ht_sine/ht_sine.pine
+++ b/lib/cycles/ht_sine/ht_sine.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Hilbert Transform SineWave (HT_SINE)", "HT_SINE", overlay=false)
diff --git a/lib/cycles/lunar/lunar.pine b/lib/cycles/lunar/lunar.pine
index a4427f05..52ff7132 100644
--- a/lib/cycles/lunar/lunar.pine
+++ b/lib/cycles/lunar/lunar.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Lunar Phase (LUNAR)", "LUNAR", overlay=false)
diff --git a/lib/cycles/solar/solar.pine b/lib/cycles/solar/solar.pine
index f0bab0c7..87f0d29a 100644
--- a/lib/cycles/solar/solar.pine
+++ b/lib/cycles/solar/solar.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Solar Cycle (SOLAR)", "SOLAR", overlay=false)
diff --git a/lib/cycles/ssfdsp/ssfdsp.pine b/lib/cycles/ssfdsp/ssfdsp.pine
index 457917d2..5031dbbe 100644
--- a/lib/cycles/ssfdsp/ssfdsp.pine
+++ b/lib/cycles/ssfdsp/ssfdsp.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers SSF Detrended Synthetic Price (SSFDSP)", "SSF-DSP", overlay=false)
diff --git a/lib/dynamics/_index.md b/lib/dynamics/_index.md
index a0bf8a0d..5eb2d6b7 100644
--- a/lib/dynamics/_index.md
+++ b/lib/dynamics/_index.md
@@ -17,6 +17,8 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu
| [CHOP](chop/Chop.md) | Choppiness Index | Trendiness measure. High values = choppy. Low = trending. |
| [DMX](dmx/Dmx.md) | Jurik DMX | Smoothed bipolar DMI using Jurik smoothing. Low noise. |
| [DX](dx/Dx.md) | Directional Movement Index | Raw directional strength. Unsmoothed ADX component. |
+| [MINUS_DI](minusdi/MinusDi.md) | Minus Directional Indicator | Downward directional movement as % of true range. 0-100. |
+| [MINUS_DM](minusdm/MinusDm.md) | Minus Directional Movement | Wilder-smoothed downward directional movement. Price units. |
| [HT_TRENDMODE](ht_trendmode/Httrendmode.md) | Ehlers Hilbert Transform Trend vs Cycle Mode | Ehlers Hilbert Transform. Binary trend/cycle mode detection. |
| [ICHIMOKU](ichimoku/Ichimoku.md) | Ichimoku Cloud | Five-line system. Cloud defines support/resistance zones. |
| [IMPULSE](impulse/Impulse.md) | Elder Impulse System | EMA + MACD histogram alignment. Color-coded trend/momentum filter. |
@@ -27,5 +29,7 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu
| [VORTEX](vortex/Vortex.md) | Vortex Indicator | VI+ and VI- measure positive/negative trend movement. |
| [GHLA](ghla/Ghla.md) | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover. |
| [PFE](pfe/Pfe.md) | Polarized Fractal Efficiency | Trend efficiency: straight-line / total path distance. |
+| [PLUS_DI](plusdi/PlusDi.md) | Plus Directional Indicator | Upward directional movement as % of true range. 0-100. |
+| [PLUS_DM](plusdm/PlusDm.md) | Plus Directional Movement | Wilder-smoothed upward directional movement. Price units. |
| [RAVI](ravi/Ravi.md) | Chande Range Action Verification Index | \|SMA(short) − SMA(long)\| / SMA(long) × 100. |
| [VHF](vhf/Vhf.md) | Vertical Horizontal Filter | Max-min range / sum of absolute changes. |
diff --git a/lib/dynamics/adx/Adx.Validation.Tests.cs b/lib/dynamics/adx/Adx.Validation.Tests.cs
index 36fdc7af..1f98c88c 100644
--- a/lib/dynamics/adx/Adx.Validation.Tests.cs
+++ b/lib/dynamics/adx/Adx.Validation.Tests.cs
@@ -91,6 +91,78 @@ public sealed class AdxValidationTests : IDisposable
ValidationHelper.VerifyData(results, tulipResults, lookback: offset);
}
+ [Fact]
+ public void DiPlus_MatchesTalib()
+ {
+ var adx = new Adx(14);
+ var diPlusResults = new List();
+
+ for (int i = 0; i < _data.Bars.Count; i++)
+ {
+ adx.Update(_data.Bars[i]);
+ diPlusResults.Add(adx.DiPlus.Value);
+ }
+
+ double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
+ double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
+ double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray();
+ double[] outReal = new double[_data.Bars.Count];
+
+ var retCode = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
+ Assert.Equal(TALib.Core.RetCode.Success, retCode);
+
+ int lookback = Functions.PlusDILookback(14);
+ ValidationHelper.VerifyData(diPlusResults, outReal, outRange, lookback);
+ }
+
+ [Fact]
+ public void DiMinus_MatchesTalib()
+ {
+ var adx = new Adx(14);
+ var diMinusResults = new List();
+
+ for (int i = 0; i < _data.Bars.Count; i++)
+ {
+ adx.Update(_data.Bars[i]);
+ diMinusResults.Add(adx.DiMinus.Value);
+ }
+
+ double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
+ double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
+ double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray();
+ double[] outReal = new double[_data.Bars.Count];
+
+ var retCode = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
+ Assert.Equal(TALib.Core.RetCode.Success, retCode);
+
+ int lookback = Functions.MinusDILookback(14);
+ ValidationHelper.VerifyData(diMinusResults, outReal, outRange, lookback);
+ }
+
+ [Fact]
+ public void MatchesSkender_DiValues()
+ {
+ var adx = new Adx(14);
+ var diPlusResults = new List();
+ var diMinusResults = new List();
+
+ for (int i = 0; i < _data.Bars.Count; i++)
+ {
+ adx.Update(_data.Bars[i]);
+ diPlusResults.Add(adx.DiPlus.Value);
+ diMinusResults.Add(adx.DiMinus.Value);
+ }
+
+ // Skender's GetAdx returns ADX with +DI and -DI values
+ var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList();
+
+ // Verify +DI
+ ValidationHelper.VerifyData(diPlusResults, skenderResults, x => x.Pdi);
+
+ // Verify -DI
+ ValidationHelper.VerifyData(diMinusResults, skenderResults, x => x.Mdi);
+ }
+
[Fact]
public void MatchesOoples()
{
diff --git a/lib/dynamics/adx/Adx.cs b/lib/dynamics/adx/Adx.cs
index 6c491489..957b6257 100644
--- a/lib/dynamics/adx/Adx.cs
+++ b/lib/dynamics/adx/Adx.cs
@@ -62,6 +62,16 @@ public sealed class Adx : ITValuePublisher
///
public TValue DiMinus { get; private set; }
+ ///
+ /// Current smoothed +DM value (RMA-smoothed raw plus directional movement, before TR normalization).
+ ///
+ public TValue DmPlus { get; private set; }
+
+ ///
+ /// Current smoothed -DM value (RMA-smoothed raw minus directional movement, before TR normalization).
+ ///
+ public TValue DmMinus { get; private set; }
+
///
/// True if the ADX has warmed up and is providing valid results.
///
@@ -116,6 +126,8 @@ public sealed class Adx : ITValuePublisher
Last = default;
DiPlus = default;
DiMinus = default;
+ DmPlus = default;
+ DmMinus = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -325,6 +337,8 @@ public sealed class Adx : ITValuePublisher
DiPlus = new TValue(input.Time, diPlus);
DiMinus = new TValue(input.Time, diMinus);
+ DmPlus = new TValue(input.Time, _samples >= _period ? _dmPlusSmooth : 0);
+ DmMinus = new TValue(input.Time, _samples >= _period ? _dmMinusSmooth : 0);
Last = new TValue(input.Time, finalAdx);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
diff --git a/lib/dynamics/adx/adx.pine b/lib/dynamics/adx/adx.pine
index c3c8db0b..12eac1ff 100644
--- a/lib/dynamics/adx/adx.pine
+++ b/lib/dynamics/adx/adx.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Average Directional Movement Index (ADX)", "ADX", overlay=false)
diff --git a/lib/dynamics/adxr/adxr.pine b/lib/dynamics/adxr/adxr.pine
index 5dbc3992..dbe3099f 100644
--- a/lib/dynamics/adxr/adxr.pine
+++ b/lib/dynamics/adxr/adxr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Average Directional Movement Index Rating (ADXR)", "ADXR", overlay=false)
diff --git a/lib/dynamics/alligator/alligator.pine b/lib/dynamics/alligator/alligator.pine
index f7e1be3a..5714b6cd 100644
--- a/lib/dynamics/alligator/alligator.pine
+++ b/lib/dynamics/alligator/alligator.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Williams Alligator", "ALLIGATOR", overlay=true)
diff --git a/lib/dynamics/amat/amat.pine b/lib/dynamics/amat/amat.pine
index 454814cd..48c67c73 100644
--- a/lib/dynamics/amat/amat.pine
+++ b/lib/dynamics/amat/amat.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Archer Moving Averages Trends (AMAT)", "AMAT", overlay=false)
diff --git a/lib/dynamics/aroon/aroon.pine b/lib/dynamics/aroon/aroon.pine
index b19b704a..992beae6 100644
--- a/lib/dynamics/aroon/aroon.pine
+++ b/lib/dynamics/aroon/aroon.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Aroon (AROON)", "AROON", overlay=false)
diff --git a/lib/dynamics/aroonosc/aroonosc.pine b/lib/dynamics/aroonosc/aroonosc.pine
index 01dfbdea..ff41b982 100644
--- a/lib/dynamics/aroonosc/aroonosc.pine
+++ b/lib/dynamics/aroonosc/aroonosc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Aroon Oscillator", "AROONOSC", overlay=false)
diff --git a/lib/dynamics/chop/chop.pine b/lib/dynamics/chop/chop.pine
index fb92f272..848a9175 100644
--- a/lib/dynamics/chop/chop.pine
+++ b/lib/dynamics/chop/chop.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Choppiness Index", "CHOP", overlay=false)
diff --git a/lib/dynamics/dmx/dmx.pine b/lib/dynamics/dmx/dmx.pine
index 1dbad938..fdbb89e5 100644
--- a/lib/dynamics/dmx/dmx.pine
+++ b/lib/dynamics/dmx/dmx.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Jurik Directional Movement Index (DMX)", "DMX", overlay=false)
diff --git a/lib/dynamics/dx/Dx.Validation.Tests.cs b/lib/dynamics/dx/Dx.Validation.Tests.cs
index 40711525..3130b97e 100644
--- a/lib/dynamics/dx/Dx.Validation.Tests.cs
+++ b/lib/dynamics/dx/Dx.Validation.Tests.cs
@@ -102,6 +102,52 @@ public sealed class DxValidationTests : IDisposable
ValidationHelper.VerifyData(diMinusResults, outReal, outRange, lookback);
}
+ [Fact]
+ public void DmPlus_MatchesTalib()
+ {
+ var dx = new Dx(14);
+ var dmPlusResults = new List();
+
+ for (int i = 0; i < _data.Bars.Count; i++)
+ {
+ dx.Update(_data.Bars[i]);
+ dmPlusResults.Add(dx.DmPlus.Value);
+ }
+
+ double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
+ double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
+ double[] outReal = new double[_data.Bars.Count];
+
+ var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
+ Assert.Equal(TALib.Core.RetCode.Success, retCode);
+
+ int lookback = Functions.PlusDMLookback(14);
+ ValidationHelper.VerifyData(dmPlusResults, outReal, outRange, lookback);
+ }
+
+ [Fact]
+ public void DmMinus_MatchesTalib()
+ {
+ var dx = new Dx(14);
+ var dmMinusResults = new List();
+
+ for (int i = 0; i < _data.Bars.Count; i++)
+ {
+ dx.Update(_data.Bars[i]);
+ dmMinusResults.Add(dx.DmMinus.Value);
+ }
+
+ double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
+ double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
+ double[] outReal = new double[_data.Bars.Count];
+
+ var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, 14);
+ Assert.Equal(TALib.Core.RetCode.Success, retCode);
+
+ int lookback = Functions.MinusDMLookback(14);
+ ValidationHelper.VerifyData(dmMinusResults, outReal, outRange, lookback);
+ }
+
[Fact]
public void MatchesSkender_DiValues()
{
diff --git a/lib/dynamics/dx/Dx.cs b/lib/dynamics/dx/Dx.cs
index db5c1beb..df8a675a 100644
--- a/lib/dynamics/dx/Dx.cs
+++ b/lib/dynamics/dx/Dx.cs
@@ -53,6 +53,18 @@ public sealed class Dx : ITValuePublisher
///
public TValue DiMinus { get; private set; }
+ ///
+ /// Current smoothed +DM value (Wilder-smoothed raw plus directional movement, before TR normalization).
+ /// Equivalent to TA-Lib PLUS_DM.
+ ///
+ public TValue DmPlus { get; private set; }
+
+ ///
+ /// Current smoothed -DM value (Wilder-smoothed raw minus directional movement, before TR normalization).
+ /// Equivalent to TA-Lib MINUS_DM.
+ ///
+ public TValue DmMinus { get; private set; }
+
///
/// True if the DX has warmed up and is providing valid results.
///
@@ -106,6 +118,8 @@ public sealed class Dx : ITValuePublisher
Last = default;
DiPlus = default;
DiMinus = default;
+ DmPlus = default;
+ DmMinus = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -276,6 +290,8 @@ public sealed class Dx : ITValuePublisher
DiPlus = new TValue(input.Time, diPlus);
DiMinus = new TValue(input.Time, diMinus);
+ DmPlus = new TValue(input.Time, _samples >= _period ? _dmPlusSmooth : 0);
+ DmMinus = new TValue(input.Time, _samples >= _period ? _dmMinusSmooth : 0);
Last = new TValue(input.Time, dx);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
diff --git a/lib/dynamics/dx/dx.pine b/lib/dynamics/dx/dx.pine
index 112b7036..d32b4ae4 100644
--- a/lib/dynamics/dx/dx.pine
+++ b/lib/dynamics/dx/dx.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Directional Movement Index (DX)", "DX", overlay=false)
diff --git a/lib/dynamics/ghla/ghla.pine b/lib/dynamics/ghla/ghla.pine
index 656264c9..1f44609c 100644
--- a/lib/dynamics/ghla/ghla.pine
+++ b/lib/dynamics/ghla/ghla.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Gann High-Low Activator", "GHLA", overlay=true)
diff --git a/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs b/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs
index cb83ae72..6283ff47 100644
--- a/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs
+++ b/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs
@@ -197,4 +197,32 @@ public sealed class HtTrendmodeValidationTests : IDisposable
Assert.Equal(results1[i], results2[i]);
}
}
+
+ [Fact]
+ public void HtTrendmode_Correction_Recomputes()
+ {
+ var ind = new HtTrendmode();
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ ind.Update(new TValue(t0.AddMinutes(i),
+ 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorPrice = 105.5;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorSmooth = ind.SmoothPeriod;
+
+ // Correction with a dramatically different price — SmoothPeriod must change
+ ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
+ Assert.NotEqual(anchorSmooth, ind.SmoothPeriod);
+
+ // Correction back to original price — must exactly restore original SmoothPeriod
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorSmooth, ind.SmoothPeriod, 1e-9);
+ }
}
diff --git a/lib/dynamics/ht_trendmode/ht_trendmode.pine b/lib/dynamics/ht_trendmode/ht_trendmode.pine
index 6c1e801d..954db3ed 100644
--- a/lib/dynamics/ht_trendmode/ht_trendmode.pine
+++ b/lib/dynamics/ht_trendmode/ht_trendmode.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Hilbert Transform Trend vs Cycle Mode (HT_TRENDMODE)", "HT_TRENDMODE", overlay=false)
diff --git a/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs b/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs
index 613efc76..4565387c 100644
--- a/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs
+++ b/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs
@@ -679,4 +679,32 @@ public sealed class IchimokuValidationTests : IDisposable
}
#endregion
+
+ [Fact]
+ public void Ichimoku_Correction_Recomputes()
+ {
+ var ind = new Ichimoku();
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ double p = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
+ ind.Update(new TBar(t0.AddMinutes(i), p, p + 2, p - 2, p, 1000), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorClose = 105.5;
+ ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: true);
+ double anchorTenkan = ind.Tenkan.Value;
+
+ // Correction with a dramatically different price — Tenkan must change
+ ind.Update(new TBar(anchorTime, anchorClose * 10, (anchorClose + 2) * 10, (anchorClose - 2) * 10, anchorClose * 10, 1000), isNew: false);
+ Assert.NotEqual(anchorTenkan, ind.Tenkan.Value);
+
+ // Correction back to original price — must exactly restore original Tenkan
+ ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: false);
+ Assert.Equal(anchorTenkan, ind.Tenkan.Value, 1e-9);
+ }
}
diff --git a/lib/dynamics/ichimoku/ichimoku.pine b/lib/dynamics/ichimoku/ichimoku.pine
index bd1debba..406782f1 100644
--- a/lib/dynamics/ichimoku/ichimoku.pine
+++ b/lib/dynamics/ichimoku/ichimoku.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ichimoku Cloud", "ICHIMOKU", overlay=true)
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/minusdi/MinusDi.cs b/lib/dynamics/minusdi/MinusDi.cs
new file mode 100644
index 00000000..cf01d35e
--- /dev/null
+++ b/lib/dynamics/minusdi/MinusDi.cs
@@ -0,0 +1,121 @@
+using System.Runtime.CompilerServices;
+
+namespace QuanTAlib;
+
+///
+/// MINUS_DI: Minus Directional Indicator (Wilder, 1978)
+///
+///
+/// Measures downward directional movement as a percentage of true range.
+/// Extracted from the DX calculation: -DI = Smoothed(-DM) / Smoothed(TR) × 100.
+/// Range: 0 to 100. Higher values indicate stronger downward movement.
+///
+[SkipLocalsInit]
+public sealed class MinusDi : ITValuePublisher
+{
+ private readonly Dx _dx;
+
+ /// Display name for the indicator.
+ public string Name { get; }
+
+ public event TValuePublishedHandler? Pub;
+
+ /// Current -DI value.
+ public TValue Last { get; private set; }
+
+ /// True when the indicator has warmed up.
+ public bool IsHot => _dx.IsHot;
+
+ /// Bars required for warmup.
+ public int WarmupPeriod => _dx.WarmupPeriod;
+
+ /// The period parameter.
+ public int Period => _dx.Period;
+
+ ///
+ /// Creates MinusDi with specified period.
+ ///
+ /// Wilder smoothing period (must be > 0)
+ public MinusDi(int period = 14)
+ {
+ if (period <= 0)
+ {
+ throw new ArgumentException("Period must be greater than 0", nameof(period));
+ }
+ _dx = new Dx(period);
+ Name = $"MinusDi({period})";
+ }
+
+ ///
+ /// Creates MinusDi and immediately processes the bar series.
+ ///
+ public MinusDi(TBarSeries source, int period = 14) : this(period)
+ {
+ var result = Batch(source, period);
+ Last = result[^1];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TBar input, bool isNew = true)
+ {
+ _dx.Update(input, isNew);
+ Last = _dx.DiMinus;
+ if (isNew)
+ {
+ Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
+ }
+ return Last;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TValue input, bool isNew = true)
+ {
+ // DI requires OHLC data — scalar update not meaningful
+ return Last;
+ }
+
+ public TSeries Update(TBarSeries source)
+ {
+ var result = new TSeries(source.Count);
+ foreach (var bar in source)
+ {
+ result.Add(Update(bar));
+ }
+ return result;
+ }
+
+ ///
+ /// Initializes the indicator state using the provided bar series history.
+ ///
+ public void Prime(TBarSeries source)
+ {
+ foreach (var bar in source)
+ {
+ Update(bar);
+ }
+ }
+
+ public void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ // Not applicable — DI requires OHLC bar data, not scalar values
+ }
+
+ public static TSeries Batch(TBarSeries source, int period = 14)
+ {
+ var indicator = new MinusDi(period);
+ return indicator.Update(source);
+ }
+
+ public static (TSeries Results, MinusDi Indicator) Calculate(TBarSeries source, int period = 14)
+ {
+ var indicator = new MinusDi(period);
+ return (indicator.Update(source), indicator);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Reset()
+ {
+ _dx.Reset();
+ Last = default;
+ }
+}
diff --git a/lib/dynamics/minusdi/MinusDi.md b/lib/dynamics/minusdi/MinusDi.md
new file mode 100644
index 00000000..9ad88342
--- /dev/null
+++ b/lib/dynamics/minusdi/MinusDi.md
@@ -0,0 +1,31 @@
+# MINUS_DI: Minus Directional Indicator
+
+### TL;DR
+Measures downward directional movement strength as a percentage (0-100).
+
+## Introduction
+The Minus Directional Indicator (-DI) measures the strength of downward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr.
+
+When -DI is rising, downward price pressure is increasing. When -DI crosses above +DI, it signals a potential bearish trend. The -DI line is commonly plotted alongside +DI to visualize directional balance.
+
+## Calculation
+-DI = Smoothed(-DM) / Smoothed(TR) × 100
+
+Where:
+- -DM (Minus Directional Movement) = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0
+- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|)
+- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input
+
+## Parameters
+| Parameter | Default | Range | Description |
+| :--- | :--- | :--- | :--- |
+| Period | 14 | 2-∞ | Wilder smoothing period |
+
+## Interpretation
+- **Rising -DI:** Strengthening downward movement
+- **-DI > +DI:** Bears dominate; potential downtrend
+- **-DI crossover above +DI:** Bearish signal
+- **High -DI (>40):** Strong downward momentum
+
+## References
+- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
diff --git a/lib/dynamics/minusdm/MinusDm.cs b/lib/dynamics/minusdm/MinusDm.cs
new file mode 100644
index 00000000..d8308b12
--- /dev/null
+++ b/lib/dynamics/minusdm/MinusDm.cs
@@ -0,0 +1,121 @@
+using System.Runtime.CompilerServices;
+
+namespace QuanTAlib;
+
+///
+/// MINUS_DM: Minus Directional Movement (Wilder, 1978)
+///
+///
+/// Wilder-smoothed downward directional movement in price units.
+/// Extracted from the DX calculation: Smoothed(-DM) using Wilder's method.
+/// Values ≥ 0 in price units. Higher values indicate stronger downward movement magnitude.
+///
+[SkipLocalsInit]
+public sealed class MinusDm : ITValuePublisher
+{
+ private readonly Dx _dx;
+
+ /// Display name for the indicator.
+ public string Name { get; }
+
+ public event TValuePublishedHandler? Pub;
+
+ /// Current smoothed -DM value.
+ public TValue Last { get; private set; }
+
+ /// True when the indicator has warmed up.
+ public bool IsHot => _dx.IsHot;
+
+ /// Bars required for warmup.
+ public int WarmupPeriod => _dx.WarmupPeriod;
+
+ /// The period parameter.
+ public int Period => _dx.Period;
+
+ ///
+ /// Creates MinusDm with specified period.
+ ///
+ /// Wilder smoothing period (must be > 0)
+ public MinusDm(int period = 14)
+ {
+ if (period <= 0)
+ {
+ throw new ArgumentException("Period must be greater than 0", nameof(period));
+ }
+ _dx = new Dx(period);
+ Name = $"MinusDm({period})";
+ }
+
+ ///
+ /// Creates MinusDm and immediately processes the bar series.
+ ///
+ public MinusDm(TBarSeries source, int period = 14) : this(period)
+ {
+ var result = Batch(source, period);
+ Last = result[^1];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TBar input, bool isNew = true)
+ {
+ _dx.Update(input, isNew);
+ Last = _dx.DmMinus;
+ if (isNew)
+ {
+ Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
+ }
+ return Last;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TValue input, bool isNew = true)
+ {
+ // DM requires OHLC data — scalar update not meaningful
+ return Last;
+ }
+
+ public TSeries Update(TBarSeries source)
+ {
+ var result = new TSeries(source.Count);
+ foreach (var bar in source)
+ {
+ result.Add(Update(bar));
+ }
+ return result;
+ }
+
+ ///
+ /// Initializes the indicator state using the provided bar series history.
+ ///
+ public void Prime(TBarSeries source)
+ {
+ foreach (var bar in source)
+ {
+ Update(bar);
+ }
+ }
+
+ public void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ // Not applicable — DM requires OHLC bar data, not scalar values
+ }
+
+ public static TSeries Batch(TBarSeries source, int period = 14)
+ {
+ var indicator = new MinusDm(period);
+ return indicator.Update(source);
+ }
+
+ public static (TSeries Results, MinusDm Indicator) Calculate(TBarSeries source, int period = 14)
+ {
+ var indicator = new MinusDm(period);
+ return (indicator.Update(source), indicator);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Reset()
+ {
+ _dx.Reset();
+ Last = default;
+ }
+}
diff --git a/lib/dynamics/minusdm/MinusDm.md b/lib/dynamics/minusdm/MinusDm.md
new file mode 100644
index 00000000..721dc068
--- /dev/null
+++ b/lib/dynamics/minusdm/MinusDm.md
@@ -0,0 +1,28 @@
+# MINUS_DM: Minus Directional Movement
+
+### TL;DR
+Wilder-smoothed downward directional movement in price units (≥0).
+
+## Introduction
+Minus Directional Movement (-DM) measures the magnitude of downward price movement, smoothed using Wilder's method. Unlike -DI which normalizes by true range to produce a percentage, -DM outputs raw smoothed values in price units.
+
+-DM captures when the previous bar's low exceeds the current bar's low by more than the current bar's high exceeds the previous bar's high. It is the raw building block of the Directional Movement System.
+
+## Calculation
+-DM = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0
+
+Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input
+
+## Parameters
+| Parameter | Default | Range | Description |
+| :--- | :--- | :--- | :--- |
+| Period | 14 | 2-∞ | Wilder smoothing period |
+
+## Interpretation
+- **Rising -DM:** Increasing downward price extension
+- **-DM > +DM:** Downward movement exceeds upward movement
+- **Zero -DM:** No downward directional movement on the bar
+- Values are in price units and scale with the instrument
+
+## References
+- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
diff --git a/lib/dynamics/pfe/pfe.pine b/lib/dynamics/pfe/pfe.pine
index 56dfbefd..0ee77dbc 100644
--- a/lib/dynamics/pfe/pfe.pine
+++ b/lib/dynamics/pfe/pfe.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("PFE: Polarized Fractal Efficiency", "PFE", overlay=false)
diff --git a/lib/dynamics/plusdi/PlusDi.cs b/lib/dynamics/plusdi/PlusDi.cs
new file mode 100644
index 00000000..72db2ded
--- /dev/null
+++ b/lib/dynamics/plusdi/PlusDi.cs
@@ -0,0 +1,121 @@
+using System.Runtime.CompilerServices;
+
+namespace QuanTAlib;
+
+///
+/// PLUS_DI: Plus Directional Indicator (Wilder, 1978)
+///
+///
+/// Measures upward directional movement as a percentage of true range.
+/// Extracted from the DX calculation: +DI = Smoothed(+DM) / Smoothed(TR) × 100.
+/// Range: 0 to 100. Higher values indicate stronger upward movement.
+///
+[SkipLocalsInit]
+public sealed class PlusDi : ITValuePublisher
+{
+ private readonly Dx _dx;
+
+ /// Display name for the indicator.
+ public string Name { get; }
+
+ public event TValuePublishedHandler? Pub;
+
+ /// Current +DI value.
+ public TValue Last { get; private set; }
+
+ /// True when the indicator has warmed up.
+ public bool IsHot => _dx.IsHot;
+
+ /// Bars required for warmup.
+ public int WarmupPeriod => _dx.WarmupPeriod;
+
+ /// The period parameter.
+ public int Period => _dx.Period;
+
+ ///
+ /// Creates PlusDi with specified period.
+ ///
+ /// Wilder smoothing period (must be > 0)
+ public PlusDi(int period = 14)
+ {
+ if (period <= 0)
+ {
+ throw new ArgumentException("Period must be greater than 0", nameof(period));
+ }
+ _dx = new Dx(period);
+ Name = $"PlusDi({period})";
+ }
+
+ ///
+ /// Creates PlusDi and immediately processes the bar series.
+ ///
+ public PlusDi(TBarSeries source, int period = 14) : this(period)
+ {
+ var result = Batch(source, period);
+ Last = result[^1];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TBar input, bool isNew = true)
+ {
+ _dx.Update(input, isNew);
+ Last = _dx.DiPlus;
+ if (isNew)
+ {
+ Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
+ }
+ return Last;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TValue input, bool isNew = true)
+ {
+ // DI requires OHLC data — scalar update not meaningful
+ return Last;
+ }
+
+ public TSeries Update(TBarSeries source)
+ {
+ var result = new TSeries(source.Count);
+ foreach (var bar in source)
+ {
+ result.Add(Update(bar));
+ }
+ return result;
+ }
+
+ ///
+ /// Initializes the indicator state using the provided bar series history.
+ ///
+ public void Prime(TBarSeries source)
+ {
+ foreach (var bar in source)
+ {
+ Update(bar);
+ }
+ }
+
+ public void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ // Not applicable — DI requires OHLC bar data, not scalar values
+ }
+
+ public static TSeries Batch(TBarSeries source, int period = 14)
+ {
+ var indicator = new PlusDi(period);
+ return indicator.Update(source);
+ }
+
+ public static (TSeries Results, PlusDi Indicator) Calculate(TBarSeries source, int period = 14)
+ {
+ var indicator = new PlusDi(period);
+ return (indicator.Update(source), indicator);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Reset()
+ {
+ _dx.Reset();
+ Last = default;
+ }
+}
diff --git a/lib/dynamics/plusdi/PlusDi.md b/lib/dynamics/plusdi/PlusDi.md
new file mode 100644
index 00000000..e3b79167
--- /dev/null
+++ b/lib/dynamics/plusdi/PlusDi.md
@@ -0,0 +1,31 @@
+# PLUS_DI: Plus Directional Indicator
+
+### TL;DR
+Measures upward directional movement strength as a percentage (0-100).
+
+## Introduction
+The Plus Directional Indicator (+DI) measures the strength of upward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr.
+
+When +DI is rising, upward price pressure is increasing. When +DI crosses above -DI, it signals a potential bullish trend. The +DI line is commonly plotted alongside -DI to visualize directional balance.
+
+## Calculation
++DI = Smoothed(+DM) / Smoothed(TR) × 100
+
+Where:
+- +DM (Plus Directional Movement) = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0
+- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|)
+- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input
+
+## Parameters
+| Parameter | Default | Range | Description |
+| :--- | :--- | :--- | :--- |
+| Period | 14 | 2-∞ | Wilder smoothing period |
+
+## Interpretation
+- **Rising +DI:** Strengthening upward movement
+- **+DI > -DI:** Bulls dominate; potential uptrend
+- **+DI crossover above -DI:** Bullish signal
+- **High +DI (>40):** Strong upward momentum
+
+## References
+- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
diff --git a/lib/dynamics/plusdm/PlusDm.cs b/lib/dynamics/plusdm/PlusDm.cs
new file mode 100644
index 00000000..0ed40180
--- /dev/null
+++ b/lib/dynamics/plusdm/PlusDm.cs
@@ -0,0 +1,121 @@
+using System.Runtime.CompilerServices;
+
+namespace QuanTAlib;
+
+///
+/// PLUS_DM: Plus Directional Movement (Wilder, 1978)
+///
+///
+/// Wilder-smoothed upward directional movement in price units.
+/// Extracted from the DX calculation: Smoothed(+DM) using Wilder's method.
+/// Values ≥ 0 in price units. Higher values indicate stronger upward movement magnitude.
+///
+[SkipLocalsInit]
+public sealed class PlusDm : ITValuePublisher
+{
+ private readonly Dx _dx;
+
+ /// Display name for the indicator.
+ public string Name { get; }
+
+ public event TValuePublishedHandler? Pub;
+
+ /// Current smoothed +DM value.
+ public TValue Last { get; private set; }
+
+ /// True when the indicator has warmed up.
+ public bool IsHot => _dx.IsHot;
+
+ /// Bars required for warmup.
+ public int WarmupPeriod => _dx.WarmupPeriod;
+
+ /// The period parameter.
+ public int Period => _dx.Period;
+
+ ///
+ /// Creates PlusDm with specified period.
+ ///
+ /// Wilder smoothing period (must be > 0)
+ public PlusDm(int period = 14)
+ {
+ if (period <= 0)
+ {
+ throw new ArgumentException("Period must be greater than 0", nameof(period));
+ }
+ _dx = new Dx(period);
+ Name = $"PlusDm({period})";
+ }
+
+ ///
+ /// Creates PlusDm and immediately processes the bar series.
+ ///
+ public PlusDm(TBarSeries source, int period = 14) : this(period)
+ {
+ var result = Batch(source, period);
+ Last = result[^1];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TBar input, bool isNew = true)
+ {
+ _dx.Update(input, isNew);
+ Last = _dx.DmPlus;
+ if (isNew)
+ {
+ Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
+ }
+ return Last;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TValue input, bool isNew = true)
+ {
+ // DM requires OHLC data — scalar update not meaningful
+ return Last;
+ }
+
+ public TSeries Update(TBarSeries source)
+ {
+ var result = new TSeries(source.Count);
+ foreach (var bar in source)
+ {
+ result.Add(Update(bar));
+ }
+ return result;
+ }
+
+ ///
+ /// Initializes the indicator state using the provided bar series history.
+ ///
+ public void Prime(TBarSeries source)
+ {
+ foreach (var bar in source)
+ {
+ Update(bar);
+ }
+ }
+
+ public void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ // Not applicable — DM requires OHLC bar data, not scalar values
+ }
+
+ public static TSeries Batch(TBarSeries source, int period = 14)
+ {
+ var indicator = new PlusDm(period);
+ return indicator.Update(source);
+ }
+
+ public static (TSeries Results, PlusDm Indicator) Calculate(TBarSeries source, int period = 14)
+ {
+ var indicator = new PlusDm(period);
+ return (indicator.Update(source), indicator);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Reset()
+ {
+ _dx.Reset();
+ Last = default;
+ }
+}
diff --git a/lib/dynamics/plusdm/PlusDm.md b/lib/dynamics/plusdm/PlusDm.md
new file mode 100644
index 00000000..fc035a08
--- /dev/null
+++ b/lib/dynamics/plusdm/PlusDm.md
@@ -0,0 +1,28 @@
+# PLUS_DM: Plus Directional Movement
+
+### TL;DR
+Wilder-smoothed upward directional movement in price units (≥0).
+
+## Introduction
+Plus Directional Movement (+DM) measures the magnitude of upward price movement, smoothed using Wilder's method. Unlike +DI which normalizes by true range to produce a percentage, +DM outputs raw smoothed values in price units.
+
++DM captures when the current bar's high exceeds the previous bar's high by more than the previous bar's low exceeds the current bar's low. It is the raw building block of the Directional Movement System.
+
+## Calculation
++DM = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0
+
+Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input
+
+## Parameters
+| Parameter | Default | Range | Description |
+| :--- | :--- | :--- | :--- |
+| Period | 14 | 2-∞ | Wilder smoothing period |
+
+## Interpretation
+- **Rising +DM:** Increasing upward price extension
+- **+DM > -DM:** Upward movement exceeds downward movement
+- **Zero +DM:** No upward directional movement on the bar
+- Values are in price units and scale with the instrument
+
+## References
+- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978)
diff --git a/lib/dynamics/qstick/qstick.pine b/lib/dynamics/qstick/qstick.pine
index ddb2e9b2..52735b6b 100644
--- a/lib/dynamics/qstick/qstick.pine
+++ b/lib/dynamics/qstick/qstick.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Qstick Indicator", "QSTICK", overlay=false)
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/dynamics/ravi/ravi.pine b/lib/dynamics/ravi/ravi.pine
index d2e5657e..1596f864 100644
--- a/lib/dynamics/ravi/ravi.pine
+++ b/lib/dynamics/ravi/ravi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("RAVI: Chande Range Action Verification Index", "RAVI", overlay=false)
diff --git a/lib/dynamics/super/super.pine b/lib/dynamics/super/super.pine
index e34ff8ed..c7f22934 100644
--- a/lib/dynamics/super/super.pine
+++ b/lib/dynamics/super/super.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("SuperTrend", "SUPER", overlay=true)
diff --git a/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs b/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs
index 9058adb4..ee397900 100644
--- a/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs
+++ b/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs
@@ -325,5 +325,33 @@ public class TtmSqueezeValidationTests
Assert.InRange(squeeze.ColorCode, 0, 3);
}
+ [Fact]
+ public void TtmSqueeze_Correction_Recomputes()
+ {
+ var ind = new TtmSqueeze();
+ var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
+
+ // Build state well past warmup
+ for (int i = 0; i < 100; i++)
+ {
+ double p = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
+ ind.Update(new TBar(t0.AddMinutes(i), p, p + 2, p - 2, p, 1000), isNew: true);
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddMinutes(100);
+ const double anchorClose = 105.5;
+ ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: true);
+ double anchorMomentum = ind.Momentum.Value;
+
+ // Correction with a dramatically different price — Momentum must change
+ ind.Update(new TBar(anchorTime, anchorClose * 10, (anchorClose + 2) * 10, (anchorClose - 2) * 10, anchorClose * 10, 1000), isNew: false);
+ Assert.NotEqual(anchorMomentum, ind.Momentum.Value);
+
+ // Correction back to original price — must exactly restore original Momentum
+ ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: false);
+ Assert.Equal(anchorMomentum, ind.Momentum.Value, 1e-9);
+ }
+
#endregion
}
diff --git a/lib/dynamics/ttm_squeeze/TtmSqueeze.cs b/lib/dynamics/ttm_squeeze/TtmSqueeze.cs
index 258b9fa0..65c4a73b 100644
--- a/lib/dynamics/ttm_squeeze/TtmSqueeze.cs
+++ b/lib/dynamics/ttm_squeeze/TtmSqueeze.cs
@@ -322,7 +322,7 @@ public sealed class TtmSqueeze : ITValuePublisher
_priceSum -= oldest;
_priceSumSquares -= oldest * oldest;
}
- _priceBuffer.Add(close, isNew);
+ _priceBuffer.Add(close);
_priceSum += close;
_priceSumSquares += close * close;
@@ -365,11 +365,11 @@ public sealed class TtmSqueeze : ITValuePublisher
_prevSqueezeOn = squeezeOn;
// === Donchian Midline ===
- _highBuffer.Add(high, isNew);
- _lowBuffer.Add(low, isNew);
+ _highBuffer.Add(high);
+ _lowBuffer.Add(low);
- double donchianHigh = GetMax(_highBuffer);
- double donchianLow = GetMin(_lowBuffer);
+ double donchianHigh = _highBuffer.Max();
+ double donchianLow = _lowBuffer.Min();
double donchianMid = (donchianHigh + donchianLow) / 2;
// === Momentum (Linear Regression) ===
@@ -383,7 +383,7 @@ public sealed class TtmSqueeze : ITValuePublisher
_momentumSumXY = _momentumSumXY + prevSumY - _momPeriod * oldest;
_momentumSumY -= oldest;
}
- _momentumBuffer.Add(deviation, isNew);
+ _momentumBuffer.Add(deviation);
_momentumSumY += deviation;
// Recalculate sumXY during warmup (non-O(1), but short duration)
@@ -535,6 +535,10 @@ public sealed class TtmSqueeze : ITValuePublisher
_saved_prevMomentum = _prevMomentum;
_saved_prevSqueezeOn = _prevSqueezeOn;
_saved_barCount = _barCount;
+ _priceBuffer.Snapshot();
+ _highBuffer.Snapshot();
+ _lowBuffer.Snapshot();
+ _momentumBuffer.Snapshot();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -552,43 +556,10 @@ public sealed class TtmSqueeze : ITValuePublisher
_prevMomentum = _saved_prevMomentum;
_prevSqueezeOn = _saved_prevSqueezeOn;
_barCount = _saved_barCount;
+ _priceBuffer.Restore();
+ _highBuffer.Restore();
+ _lowBuffer.Restore();
+ _momentumBuffer.Restore();
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static double GetMax(RingBuffer buffer)
- {
- if (buffer.Count == 0)
- {
- return 0;
- }
- var span = buffer.GetSpan();
- double max = span[0];
- for (int i = 1; i < span.Length; i++)
- {
- if (span[i] > max)
- {
- max = span[i];
- }
- }
- return max;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static double GetMin(RingBuffer buffer)
- {
- if (buffer.Count == 0)
- {
- return 0;
- }
- var span = buffer.GetSpan();
- double min = span[0];
- for (int i = 1; i < span.Length; i++)
- {
- if (span[i] < min)
- {
- min = span[i];
- }
- }
- return min;
- }
}
diff --git a/lib/dynamics/ttm_trend/TtmTrend.pine b/lib/dynamics/ttm_trend/TtmTrend.pine
index cca20600..c3f0ef23 100644
--- a/lib/dynamics/ttm_trend/TtmTrend.pine
+++ b/lib/dynamics/ttm_trend/TtmTrend.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("TTM Trend", "TTM_TREND", overlay=true)
diff --git a/lib/dynamics/vhf/Vhf.cs b/lib/dynamics/vhf/Vhf.cs
index feaef26d..83f267e2 100644
--- a/lib/dynamics/vhf/Vhf.cs
+++ b/lib/dynamics/vhf/Vhf.cs
@@ -412,22 +412,7 @@ public sealed class Vhf : AbstractBase
// Calculate VHF
if (closeFilled >= closeBufSize && diffFilled >= period)
{
- // Scan for max/min over close buffer
- double hi = double.MinValue;
- double lo = double.MaxValue;
- for (int k = 0; k < closeBufSize; k++)
- {
- double cv = closeBuf[k];
- if (cv > hi)
- {
- hi = cv;
- }
- if (cv < lo)
- {
- lo = cv;
- }
- }
-
+ var (lo, hi) = ((ReadOnlySpan)closeBuf).MinMaxSIMD();
double numerator = hi - lo;
if (diffSum > 1e-10)
diff --git a/lib/dynamics/vhf/vhf.pine b/lib/dynamics/vhf/vhf.pine
index a2aacd1d..50628f56 100644
--- a/lib/dynamics/vhf/vhf.pine
+++ b/lib/dynamics/vhf/vhf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("VHF: Vertical Horizontal Filter", "VHF", overlay=false)
diff --git a/lib/dynamics/vortex/vortex.pine b/lib/dynamics/vortex/vortex.pine
index 786a21aa..1dac4b56 100644
--- a/lib/dynamics/vortex/vortex.pine
+++ b/lib/dynamics/vortex/vortex.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Vortex Indicator", "VORTEX", overlay=false)
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/huber/huber.pine b/lib/errors/huber/huber.pine
index 75b6b001..e98de7c6 100644
--- a/lib/errors/huber/huber.pine
+++ b/lib/errors/huber/huber.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Huber Loss (HUBER)", "HUBER")
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/logcosh/logcosh.pine b/lib/errors/logcosh/logcosh.pine
index c3c7421c..099545b9 100644
--- a/lib/errors/logcosh/logcosh.pine
+++ b/lib/errors/logcosh/logcosh.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Log-Cosh Loss", "LogCosh", overlay=false)
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/maape/maape.pine b/lib/errors/maape/maape.pine
index e92da552..07722aa0 100644
--- a/lib/errors/maape/maape.pine
+++ b/lib/errors/maape/maape.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Arctangent Absolute Percentage Error", "MAAPE", overlay=false, format=format.percent)
diff --git a/lib/errors/mae/Mae.Validation.Tests.cs b/lib/errors/mae/Mae.Validation.Tests.cs
index 0b8ee3f2..6457e94b 100644
--- a/lib/errors/mae/Mae.Validation.Tests.cs
+++ b/lib/errors/mae/Mae.Validation.Tests.cs
@@ -71,4 +71,30 @@ public sealed class MaeValidationTests : IDisposable
}
}
}
+
+ [Fact]
+ public void Mae_Correction_Recomputes()
+ {
+ var ind = new Mae(20);
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5);
+ }
+
+ // Anchor bar
+ const double anchorActual = 125.0;
+ const double anchorPredicted = 123.0;
+ ind.Update(anchorActual, anchorPredicted, isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with dramatically different values — recompute must yield different result
+ ind.Update(anchorActual * 10, anchorPredicted * 10, isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore original result
+ ind.Update(anchorActual, anchorPredicted, isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
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/mae/mae.pine b/lib/errors/mae/mae.pine
index 31265815..4420cb77 100644
--- a/lib/errors/mae/mae.pine
+++ b/lib/errors/mae/mae.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Absolute Error (MAE)", "MAE")
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/mapd/mapd.pine b/lib/errors/mapd/mapd.pine
index 27edd405..dbc38c8c 100644
--- a/lib/errors/mapd/mapd.pine
+++ b/lib/errors/mapd/mapd.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Absolute %Deviation (MAPD)", "MAPD")
diff --git a/lib/errors/mape/Mape.Validation.Tests.cs b/lib/errors/mape/Mape.Validation.Tests.cs
index a152611c..4ac02ee7 100644
--- a/lib/errors/mape/Mape.Validation.Tests.cs
+++ b/lib/errors/mape/Mape.Validation.Tests.cs
@@ -84,4 +84,31 @@ public sealed class MapeValidationTests : IDisposable
return sum / actual.Length;
}
+
+ [Fact]
+ public void Mape_Correction_Recomputes()
+ {
+ var ind = new Mape(20);
+
+ // Build state well past warmup (actual always > 0 so MAPE denominator is valid)
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5);
+ }
+
+ // Anchor bar
+ const double anchorActual = 125.0;
+ const double anchorPredicted = 123.0;
+ ind.Update(anchorActual, anchorPredicted, isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // MAPE is scale-invariant: ×10 on both actual and predicted leaves ratio unchanged.
+ // Change only predicted to dramatically alter the error percentage.
+ ind.Update(anchorActual, 10.0, isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore original result
+ ind.Update(anchorActual, anchorPredicted, isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
\ No newline at end of file
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/mape/mape.pine b/lib/errors/mape/mape.pine
index d9cfd433..b10beefe 100644
--- a/lib/errors/mape/mape.pine
+++ b/lib/errors/mape/mape.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Absolute %Error (MAPE)", "MAPE")
diff --git a/lib/errors/mase/Mase.cs b/lib/errors/mase/Mase.cs
index e05199bb..e9e34dc0 100644
--- a/lib/errors/mase/Mase.cs
+++ b/lib/errors/mase/Mase.cs
@@ -152,7 +152,7 @@ public sealed class Mase : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew);
+ return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/mase/mase.pine b/lib/errors/mase/mase.pine
index 98cedc41..782fde38 100644
--- a/lib/errors/mase/mase.pine
+++ b/lib/errors/mase/mase.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Absolute Scaled Error (MASE)", "MASE")
diff --git a/lib/errors/mdae/Mdae.cs b/lib/errors/mdae/Mdae.cs
index ea91d3b1..bb8212db 100644
--- a/lib/errors/mdae/Mdae.cs
+++ b/lib/errors/mdae/Mdae.cs
@@ -104,7 +104,7 @@ public sealed class Mdae : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew);
+ return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/mdae/mdae.pine b/lib/errors/mdae/mdae.pine
index fd85189b..28bb4c0a 100644
--- a/lib/errors/mdae/mdae.pine
+++ b/lib/errors/mdae/mdae.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Median Absolute Error", "MdAE", overlay=false)
diff --git a/lib/errors/mdape/Mdape.cs b/lib/errors/mdape/Mdape.cs
index e53e895b..a1edcb37 100644
--- a/lib/errors/mdape/Mdape.cs
+++ b/lib/errors/mdape/Mdape.cs
@@ -58,7 +58,7 @@ public sealed class Mdape : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return UpdateCore(DateTime.UtcNow, actual, predicted, isNew);
+ return UpdateCore(DateTime.MinValue, actual, predicted, isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/mdape/mdape.pine b/lib/errors/mdape/mdape.pine
index ce85f6ad..7e526d7e 100644
--- a/lib/errors/mdape/mdape.pine
+++ b/lib/errors/mdape/mdape.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Median Absolute Percentage Error", "MdAPE", overlay=false, format=format.percent)
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/me/me.pine b/lib/errors/me/me.pine
index 69eaceac..e808b79f 100644
--- a/lib/errors/me/me.pine
+++ b/lib/errors/me/me.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Error (ME)", "ME")
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/mpe/mpe.pine b/lib/errors/mpe/mpe.pine
index 2ccb5707..cda1d0a2 100644
--- a/lib/errors/mpe/mpe.pine
+++ b/lib/errors/mpe/mpe.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean %Error (MPE)", "MPE")
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/mrae/mrae.pine b/lib/errors/mrae/mrae.pine
index 9b5d55e0..7943a2bf 100644
--- a/lib/errors/mrae/mrae.pine
+++ b/lib/errors/mrae/mrae.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Relative Absolute Error", "MRAE", overlay=false)
diff --git a/lib/errors/mse/Mse.Validation.Tests.cs b/lib/errors/mse/Mse.Validation.Tests.cs
index 7a47f20a..0b82dbf7 100644
--- a/lib/errors/mse/Mse.Validation.Tests.cs
+++ b/lib/errors/mse/Mse.Validation.Tests.cs
@@ -71,4 +71,30 @@ public sealed class MseValidationTests : IDisposable
}
}
}
+
+ [Fact]
+ public void Mse_Correction_Recomputes()
+ {
+ var ind = new Mse(20);
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5);
+ }
+
+ // Anchor bar
+ const double anchorActual = 125.0;
+ const double anchorPredicted = 123.0;
+ ind.Update(anchorActual, anchorPredicted, isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with dramatically different values — recompute must yield different result
+ ind.Update(anchorActual * 10, anchorPredicted * 10, isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore original result
+ ind.Update(anchorActual, anchorPredicted, isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
\ No newline at end of file
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/mse/mse.pine b/lib/errors/mse/mse.pine
index cf1957aa..16ca3819 100644
--- a/lib/errors/mse/mse.pine
+++ b/lib/errors/mse/mse.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Squared Error (MSE)", "MSE")
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/msle/msle.pine b/lib/errors/msle/msle.pine
index 3286927c..07c73b75 100644
--- a/lib/errors/msle/msle.pine
+++ b/lib/errors/msle/msle.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Squared Logarithmic Error (MSLE)", "MSLE")
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/pseudohuber/pseudohuber.pine b/lib/errors/pseudohuber/pseudohuber.pine
index db78d082..d0ab747d 100644
--- a/lib/errors/pseudohuber/pseudohuber.pine
+++ b/lib/errors/pseudohuber/pseudohuber.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pseudo-Huber Loss", "PseudoHuber", overlay=false)
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/quantileloss/quantileloss.pine b/lib/errors/quantileloss/quantileloss.pine
index ff1f2f2f..705eeed4 100644
--- a/lib/errors/quantileloss/quantileloss.pine
+++ b/lib/errors/quantileloss/quantileloss.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Quantile Loss (Pinball Loss)", "QuantileLoss", overlay=false)
diff --git a/lib/errors/rae/Rae.cs b/lib/errors/rae/Rae.cs
index 898ad6f4..809ab3c4 100644
--- a/lib/errors/rae/Rae.cs
+++ b/lib/errors/rae/Rae.cs
@@ -149,7 +149,7 @@ public sealed class Rae : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew);
+ return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/rae/rae.pine b/lib/errors/rae/rae.pine
index 93469a22..708b6611 100644
--- a/lib/errors/rae/rae.pine
+++ b/lib/errors/rae/rae.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Relative Absolute Error (RAE)", "RAE")
diff --git a/lib/errors/rmse/Rmse.Validation.Tests.cs b/lib/errors/rmse/Rmse.Validation.Tests.cs
index 062dd7a2..386233f6 100644
--- a/lib/errors/rmse/Rmse.Validation.Tests.cs
+++ b/lib/errors/rmse/Rmse.Validation.Tests.cs
@@ -73,4 +73,30 @@ public sealed class RmseValidationTests : IDisposable
}
}
}
+
+ [Fact]
+ public void Rmse_Correction_Recomputes()
+ {
+ var ind = new Rmse(20);
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5);
+ }
+
+ // Anchor bar
+ const double anchorActual = 125.0;
+ const double anchorPredicted = 123.0;
+ ind.Update(anchorActual, anchorPredicted, isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with dramatically different values — recompute must yield different result
+ ind.Update(anchorActual * 10, anchorPredicted * 10, isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore original result
+ ind.Update(anchorActual, anchorPredicted, isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
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/rmse/rmse.pine b/lib/errors/rmse/rmse.pine
index 5428aaa9..0de3272b 100644
--- a/lib/errors/rmse/rmse.pine
+++ b/lib/errors/rmse/rmse.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Root Mean Squared Error (RMSE)", "RMSE")
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/rmsle/rmsle.pine b/lib/errors/rmsle/rmsle.pine
index 1e67d158..01a95ade 100644
--- a/lib/errors/rmsle/rmsle.pine
+++ b/lib/errors/rmsle/rmsle.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Root Mean Squared Logarithmic Error (RMSLE)", "RMSLE")
diff --git a/lib/errors/rse/Rse.cs b/lib/errors/rse/Rse.cs
index 8037f11e..b3d7c8b4 100644
--- a/lib/errors/rse/Rse.cs
+++ b/lib/errors/rse/Rse.cs
@@ -156,7 +156,7 @@ public sealed class Rse : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew);
+ return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/rse/rse.pine b/lib/errors/rse/rse.pine
index 6c0f27b6..ad4b4503 100644
--- a/lib/errors/rse/rse.pine
+++ b/lib/errors/rse/rse.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Relative Squared Error (RSE)", "RSE")
diff --git a/lib/errors/rsquared/Rsquared.Validation.Tests.cs b/lib/errors/rsquared/Rsquared.Validation.Tests.cs
index 5c60349a..83881a82 100644
--- a/lib/errors/rsquared/Rsquared.Validation.Tests.cs
+++ b/lib/errors/rsquared/Rsquared.Validation.Tests.cs
@@ -194,4 +194,30 @@ public sealed class RsquaredValidationTests : IDisposable
Assert.True(double.IsFinite(rsq.Last.Value), "QuanTAlib R² last must be finite");
Assert.True(rsq.Last.Value <= 1.0 + 1e-9, $"QuanTAlib R² should be ≤ 1, got {rsq.Last.Value}");
}
+
+ [Fact]
+ public void Rsquared_Correction_Recomputes()
+ {
+ var ind = new Rsquared(20);
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5);
+ }
+
+ // Anchor bar
+ const double anchorActual = 125.0;
+ const double anchorPredicted = 123.0;
+ ind.Update(anchorActual, anchorPredicted, isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // R² is scale-invariant: change only predicted (not ×10 both) to break R²
+ ind.Update(anchorActual, 10.0, isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore original result
+ ind.Update(anchorActual, anchorPredicted, isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
diff --git a/lib/errors/rsquared/Rsquared.cs b/lib/errors/rsquared/Rsquared.cs
index addb7218..8d036333 100644
--- a/lib/errors/rsquared/Rsquared.cs
+++ b/lib/errors/rsquared/Rsquared.cs
@@ -150,7 +150,7 @@ public sealed class Rsquared : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew);
+ return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/rsquared/rsquared.pine b/lib/errors/rsquared/rsquared.pine
index 7f319561..6dfcf675 100644
--- a/lib/errors/rsquared/rsquared.pine
+++ b/lib/errors/rsquared/rsquared.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("R² Coefficient of Determination (RSQUARED)", "RSQUARED")
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/smape/smape.pine b/lib/errors/smape/smape.pine
index 0eac678e..f75a7e02 100644
--- a/lib/errors/smape/smape.pine
+++ b/lib/errors/smape/smape.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Symmetric Mean Absolute %Error (SMAPE)", "SMAPE")
diff --git a/lib/errors/theilu/TheilU.cs b/lib/errors/theilu/TheilU.cs
index b21ca932..851971f5 100644
--- a/lib/errors/theilu/TheilU.cs
+++ b/lib/errors/theilu/TheilU.cs
@@ -65,7 +65,7 @@ public sealed class TheilU : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return UpdateCore(DateTime.UtcNow, actual, predicted, isNew);
+ return UpdateCore(DateTime.MinValue, actual, predicted, isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/theilu/theilu.pine b/lib/errors/theilu/theilu.pine
index e24e5080..ca6e6834 100644
--- a/lib/errors/theilu/theilu.pine
+++ b/lib/errors/theilu/theilu.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Theil's U Statistic", "TheilU", overlay=false)
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/tukeybiweight/tukeybiweight.pine b/lib/errors/tukeybiweight/tukeybiweight.pine
index 6dba23dc..c882d7fb 100644
--- a/lib/errors/tukeybiweight/tukeybiweight.pine
+++ b/lib/errors/tukeybiweight/tukeybiweight.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Tukey's Biweight Loss", "TukeyBiweight", overlay=false)
diff --git a/lib/errors/wmape/Wmape.cs b/lib/errors/wmape/Wmape.cs
index 12fdcd56..a68fb068 100644
--- a/lib/errors/wmape/Wmape.cs
+++ b/lib/errors/wmape/Wmape.cs
@@ -128,7 +128,7 @@ public sealed class Wmape : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, bool isNew = true)
{
- return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew);
+ return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew);
}
public override TValue Update(TValue input, bool isNew = true)
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/wmape/wmape.pine b/lib/errors/wmape/wmape.pine
index 12664772..9ccd99d3 100644
--- a/lib/errors/wmape/wmape.pine
+++ b/lib/errors/wmape/wmape.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Weighted Mean Absolute Percentage Error", "WMAPE", overlay=false, format=format.percent)
diff --git a/lib/errors/wrmse/Wrmse.cs b/lib/errors/wrmse/Wrmse.cs
index cb6f70c9..a70cc88f 100644
--- a/lib/errors/wrmse/Wrmse.cs
+++ b/lib/errors/wrmse/Wrmse.cs
@@ -176,7 +176,7 @@ public sealed class Wrmse : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double actual, double predicted, double weight, bool isNew = true)
{
- return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), weight, isNew);
+ return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), weight, isNew);
}
///
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/errors/wrmse/wrmse.pine b/lib/errors/wrmse/wrmse.pine
index f994829d..0b818370 100644
--- a/lib/errors/wrmse/wrmse.pine
+++ b/lib/errors/wrmse/wrmse.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Weighted Root Mean Squared Error", "WRMSE", overlay=false)
diff --git a/lib/feeds/IFeed.md b/lib/feeds/IFeed.md
index af56bfd9..02ab510b 100644
--- a/lib/feeds/IFeed.md
+++ b/lib/feeds/IFeed.md
@@ -60,3 +60,4 @@ When implementing `IFeed`:
* **`GBM`**: Geometric Brownian Motion generator (Synthetic).
* **`CsvFeed`**: Reads OHLCV data from CSV files (Historical).
+* **`AlphaVantage`**: Fetches OHLCV data from the Alpha Vantage REST API (Historical/Live).
diff --git a/lib/feeds/csvfeed/CsvFeed.Tests.cs b/lib/feeds/csvfeed/CsvFeed.Tests.cs
index a8e3d19a..fc914a7c 100644
--- a/lib/feeds/csvfeed/CsvFeed.Tests.cs
+++ b/lib/feeds/csvfeed/CsvFeed.Tests.cs
@@ -20,6 +20,7 @@ public sealed class CsvFeedTests : IDisposable
private static readonly string[] NegativeValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,-100,50,-150,-50,1000"];
private static readonly string[] ScientificNotationData = ["timestamp,open,high,low,close,volume", "2023-01-01,1.5e2,2e2,1e2,1.75e2,1e6"];
private static readonly string[] GapDataReversed = ["timestamp,open,high,low,close,volume", "2023-01-05,103,104,102,103,1000", "2023-01-04,102,103,101,102,1000", "2023-01-02,101,102,100,101,1000", "2023-01-01,100,101,99,100,1000"];
+ private static readonly string[] TwoBarsWithGapData = ["timestamp,open,high,low,close,volume", "2023-01-03,101,102,100,101,1000", "2023-01-01,100,101,99,100,1000"];
private static readonly string[] WhitespaceData = ["timestamp,open,high,low,close,volume", " 2023-01-01 , 100 , 101 , 99 , 100 , 1000 "];
private static readonly string[] ZeroValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,0,0,0,0,0"];
private static readonly string[] LargeValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,999999999.99,1000000000.01,999999999.00,999999999.50,9999999999999"];
@@ -337,6 +338,26 @@ public sealed class CsvFeedTests : IDisposable
Assert.True(bar2.Time > bar1.Time);
}
+ [Fact]
+ public void CsvFeed_ObsoleteOverload_SilentlyMissesEndOfStream()
+ {
+ string tempCsv = CreateTempCsv(SingleBarData);
+ var feed = new CsvFeed(tempCsv);
+
+ // Value overload cannot detect end — silently returns last bar
+ feed.Next(isNew: true); // bar 1
+ var postEnd = feed.Next(isNew: true); // past end — no exception, no signal
+
+ // Must use HasMore to detect end when using value overload
+ Assert.False(feed.HasMore);
+ Assert.Equal(100.0, postEnd.Close); // last bar returned again
+
+ // Ref overload detects end correctly
+ bool isNew = true;
+ feed.Next(ref isNew);
+ Assert.False(isNew); // ref overload signals end
+ }
+
#endregion
#region Fetch Method Tests
@@ -410,6 +431,26 @@ public sealed class CsvFeedTests : IDisposable
Assert.False(feed.HasCurrentBar);
}
+ [Fact]
+ public void CsvFeed_FetchThenNext_StreamsFromFetchStartNotEnd()
+ {
+ var feed = new CsvFeed(TestCsvPath);
+
+ var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
+ var interval = TimeSpan.FromDays(1);
+ var series = feed.Fetch(5, startTime, interval);
+
+ // After Fetch, streaming replays from the START of the fetched window
+ // (not from after the last returned bar)
+ for (int i = 0; i < series.Count; i++)
+ {
+ bool isNew = true;
+ var bar = feed.Next(ref isNew);
+ Assert.Equal(series[i].Time, bar.Time);
+ Assert.True(isNew);
+ }
+ }
+
#endregion
#region Reset Method Tests
@@ -888,5 +929,20 @@ public sealed class CsvFeedTests : IDisposable
}
}
+ [Fact]
+ public void CsvFeed_Fetch_Tolerance_GapBarSkipped()
+ {
+ // Verify gap-skip behavior: Jan 2 is absent, Fetch re-aligns to Jan 3
+ string tempCsv = CreateTempCsv(TwoBarsWithGapData);
+ var feed = new CsvFeed(tempCsv);
+ var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
+ var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
+
+ // Jan 1 and Jan 3 present; Jan 2 absent → Fetch includes both with gap
+ Assert.Equal(2, series.Count);
+ Assert.Equal(startTime, series[0].Time);
+ Assert.Equal(startTime + 2 * TimeSpan.FromDays(1).Ticks, series[1].Time);
+ }
+
#endregion
}
diff --git a/lib/feeds/gbm/Gbm.Tests.cs b/lib/feeds/gbm/Gbm.Tests.cs
index 484572b1..457699ad 100644
--- a/lib/feeds/gbm/Gbm.Tests.cs
+++ b/lib/feeds/gbm/Gbm.Tests.cs
@@ -664,6 +664,36 @@ public class GBMTests
Assert.False(gbm.HasCurrentBar);
}
+ [Fact]
+ public void GBM_FetchThenNext_PriceContinuity()
+ {
+ var gbm = new GBM(startPrice: 100.0, seed: 42);
+ long startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
+ var interval = TimeSpan.FromMinutes(1);
+
+ var series = gbm.Fetch(5, startTime, interval);
+ double lastBatchClose = series[4].Close;
+
+ // Next bar after Fetch must open at the last batch close (price continuity)
+ var nextBar = gbm.Next(isNew: true);
+ Assert.Equal(lastBatchClose, nextBar.Open, 1e-10);
+ }
+
+ [Fact]
+ public void GBM_NextThenFetch_PriceContinuity()
+ {
+ var gbm = new GBM(startPrice: 100.0, seed: 42);
+
+ _ = gbm.Next(isNew: true);
+ var bar2 = gbm.Next(isNew: true); // _lastPrice = bar2.Close
+
+ long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
+ var series = gbm.Fetch(3, startTime, TimeSpan.FromMinutes(1));
+
+ // First bar of Fetch must open at bar2.Close
+ Assert.Equal(bar2.Close, series[0].Open, 1e-10);
+ }
+
#endregion
#region IFeed Interface Tests
diff --git a/lib/feeds/gbm/gbm.cs b/lib/feeds/gbm/gbm.cs
index 02781ad0..0708864d 100644
--- a/lib/feeds/gbm/gbm.cs
+++ b/lib/feeds/gbm/gbm.cs
@@ -120,6 +120,10 @@ public sealed class GBM : IFeed
///
/// Resets the generator to its initial state.
///
+ ///
+ /// Sets the internal time anchor to . For deterministic
+ /// time sequences use with an explicit start time.
+ ///
public void Reset()
{
_lastPrice = StartPrice;
@@ -284,6 +288,15 @@ public sealed class GBM : IFeed
/// A TBarSeries containing the generated bars
/// Thrown when count is not positive
/// Thrown when interval is not positive
+ ///
+ /// Price continuity: batch[0].Open equals _lastPrice at call time, so the
+ /// batch begins exactly where the previous call left off.
+ /// After the call, _lastPrice and _lastTime are updated to the end of the
+ /// generated batch, enabling seamless continuation via subsequent
+ /// calls. need not follow the previous _lastTime —
+ /// this allows replaying a window or generating a non-contiguous batch while preserving
+ /// price continuity.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
{
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/agc/agc.pine b/lib/filters/agc/agc.pine
index 003e3046..23e1e031 100644
--- a/lib/filters/agc/agc.pine
+++ b/lib/filters/agc/agc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Indicator algorithm (C) 2015 John F. Ehlers
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/alaguerre/alaguerre.pine b/lib/filters/alaguerre/alaguerre.pine
index dd7a3401..95204c57 100644
--- a/lib/filters/alaguerre/alaguerre.pine
+++ b/lib/filters/alaguerre/alaguerre.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Indicator algorithm (C) 2004 John F. Ehlers
diff --git a/lib/filters/baxterking/BaxterKing.cs b/lib/filters/baxterking/BaxterKing.cs
index 6089a88e..9d91246b 100644
--- a/lib/filters/baxterking/BaxterKing.cs
+++ b/lib/filters/baxterking/BaxterKing.cs
@@ -198,13 +198,14 @@ public sealed class BaxterKing : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double ComputeFilter()
{
- // Apply symmetric FIR: sum of weights[i] * buffer[i]
- // Buffer[0] is oldest (K bars ago from center), buffer[2K] is newest
+ // Apply symmetric FIR: dot product of weights with buffer (oldest to newest)
// The "center" is buffer[K], which represents time t-K (delayed output)
- double sum = 0.0;
- for (int i = 0; i < _filterLen; i++)
+ _buffer.GetSequencedSpans(out var first, out var second);
+ ReadOnlySpan w = _weights;
+ double sum = w.Slice(0, first.Length).DotProduct(first);
+ if (!second.IsEmpty)
{
- sum += _weights[i] * _buffer[i];
+ sum += w.Slice(first.Length).DotProduct(second);
}
return sum;
}
@@ -270,6 +271,7 @@ public sealed class BaxterKing : AbstractBase
ComputeWeights(weights, pLow, pHigh, k);
int n = source.Length;
+ ReadOnlySpan w = weights;
for (int i = 0; i < n; i++)
{
@@ -279,12 +281,7 @@ public sealed class BaxterKing : AbstractBase
}
else
{
- double sum = 0.0;
- for (int j = 0; j < filterLen; j++)
- {
- sum += weights[j] * source[i - filterLen + 1 + j];
- }
- output[i] = sum;
+ output[i] = w.DotProduct(source.Slice(i - filterLen + 1, filterLen));
}
}
}
diff --git a/lib/filters/baxterking/BaxterKing.md b/lib/filters/baxterking/BaxterKing.md
index 170588b6..ca5e77dc 100644
--- a/lib/filters/baxterking/BaxterKing.md
+++ b/lib/filters/baxterking/BaxterKing.md
@@ -6,16 +6,15 @@
| **Inputs** | Source (close) |
| **Parameters** | `pLow` (default 6), `pHigh` (default 32), `k` (default 12) |
| **Outputs** | Single series (BaxterKing) |
-| **Output range** | Tracks input |
-| **Warmup** | 1 bar |
-| **Signature** | [baxterking_signature](baxterking_signature.md) |
+| **Output range** | Oscillates around zero |
+| **Warmup** | `2K+1` bars (default 25) |
### 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/baxterking/baxterking.pine b/lib/filters/baxterking/baxterking.pine
index 57325f07..8428ea2c 100644
--- a/lib/filters/baxterking/baxterking.pine
+++ b/lib/filters/baxterking/baxterking.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Baxter-King Band-Pass Filter (BAXTERKING)", "BAXTERKING", overlay=false)
diff --git a/lib/filters/baxterking/baxterking_signature.md b/lib/filters/baxterking/baxterking_signature.md
deleted file mode 100644
index 1661f0cf..00000000
--- a/lib/filters/baxterking/baxterking_signature.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# BAXTERKING Signature
-
-
\ No newline at end of file
diff --git a/lib/filters/baxterking/baxterking_signature.svg b/lib/filters/baxterking/baxterking_signature.svg
deleted file mode 100644
index 911bb505..00000000
--- a/lib/filters/baxterking/baxterking_signature.svg
+++ /dev/null
@@ -1,19710 +0,0 @@
-
-
-
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/bessel/bessel.pine b/lib/filters/bessel/bessel.pine
index b339e530..4cded7ea 100644
--- a/lib/filters/bessel/bessel.pine
+++ b/lib/filters/bessel/bessel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bessel 2nd Order Filter (BESSEL)", "BESSEL", overlay=true)
diff --git a/lib/filters/bilateral/Bilateral.cs b/lib/filters/bilateral/Bilateral.cs
index 0c46e767..a1ca235b 100644
--- a/lib/filters/bilateral/Bilateral.cs
+++ b/lib/filters/bilateral/Bilateral.cs
@@ -114,10 +114,10 @@ public sealed class Bilateral : AbstractBase
{
double val = GetValidValue(source[i]);
double removed = _buffer.Add(val);
- _state.SumSq += (val * val);
+ _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq);
if (_buffer.IsFull)
{
- _state.SumSq -= (removed * removed);
+ _state.SumSq = Math.FusedMultiplyAdd(-removed, removed, _state.SumSq);
}
}
@@ -187,10 +187,10 @@ public sealed class Bilateral : AbstractBase
double val = GetValidValue(input.Value);
double removed = _buffer.Add(val);
- _state.SumSq += (val * val);
+ _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq);
if (_buffer.IsFull)
{
- _state.SumSq -= (removed * removed);
+ _state.SumSq = Math.FusedMultiplyAdd(-removed, removed, _state.SumSq);
}
}
else
@@ -207,15 +207,15 @@ public sealed class Bilateral : AbstractBase
if (_buffer.Count == 0)
{
_buffer.Add(val);
- _state.SumSq += (val * val);
+ _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq);
}
else
{
double oldNewest = _buffer.Newest; // Get current newest before overwriting
_buffer.UpdateNewest(val);
- _state.SumSq -= (oldNewest * oldNewest);
- _state.SumSq += (val * val);
+ _state.SumSq = Math.FusedMultiplyAdd(-oldNewest, oldNewest, _state.SumSq);
+ _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq);
}
}
@@ -421,12 +421,12 @@ public sealed class Bilateral : AbstractBase
{
removed = window[windowIdx];
sum -= removed;
- sumSq -= removed * removed;
+ sumSq = Math.FusedMultiplyAdd(-removed, removed, sumSq);
}
window[windowIdx] = val;
sum += val;
- sumSq += val * val;
+ sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
int currentNewestIdx = windowIdx;
windowIdx = (windowIdx + 1) % period;
diff --git a/lib/filters/bilateral/Bilateral.pine b/lib/filters/bilateral/Bilateral.pine
index 91176c8c..b69ef886 100644
--- a/lib/filters/bilateral/Bilateral.pine
+++ b/lib/filters/bilateral/Bilateral.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bilateral Filter (BILATERAL)", "BILATERAL", overlay=true)
diff --git a/lib/filters/bpf/Bpf.md b/lib/filters/bpf/Bpf.md
index e9f90d08..9d03cd08 100644
--- a/lib/filters/bpf/Bpf.md
+++ b/lib/filters/bpf/Bpf.md
@@ -6,15 +6,14 @@
| **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) |
### TL;DR
- 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/bpf/bpf.pine b/lib/filters/bpf/bpf.pine
index 9ea900f0..67b1e8a2 100644
--- a/lib/filters/bpf/bpf.pine
+++ b/lib/filters/bpf/bpf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bandpass Filter (BPF)", "BPF", overlay=true)
diff --git a/lib/filters/bpf/bpf_signature.md b/lib/filters/bpf/bpf_signature.md
deleted file mode 100644
index 5add5eb2..00000000
--- a/lib/filters/bpf/bpf_signature.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# BPF Signature
-
-
\ No newline at end of file
diff --git a/lib/filters/bpf/bpf_signature.svg b/lib/filters/bpf/bpf_signature.svg
deleted file mode 100644
index a5d4e1ba..00000000
--- a/lib/filters/bpf/bpf_signature.svg
+++ /dev/null
@@ -1,19881 +0,0 @@
-
-
-
diff --git a/lib/filters/butter2/butter2.pine b/lib/filters/butter2/butter2.pine
index 3e5c8e8f..8ab7956e 100644
--- a/lib/filters/butter2/butter2.pine
+++ b/lib/filters/butter2/butter2.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers 2-Pole Butterworth Filter (BUTTER2)", "BUTTER2", overlay=true)
diff --git a/lib/filters/butter3/butter3.pine b/lib/filters/butter3/butter3.pine
index 2807386e..8c929455 100644
--- a/lib/filters/butter3/butter3.pine
+++ b/lib/filters/butter3/butter3.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Indicator algorithm (C) 2004-2024 John F. Ehlers
diff --git a/lib/filters/cfitz/Cfitz.md b/lib/filters/cfitz/Cfitz.md
index 70f04c1d..b1b21549 100644
--- a/lib/filters/cfitz/Cfitz.md
+++ b/lib/filters/cfitz/Cfitz.md
@@ -6,15 +6,14 @@
| **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) |
### TL;DR
- 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/cfitz/cfitz.pine b/lib/filters/cfitz/cfitz.pine
index 87cd831a..122acae8 100644
--- a/lib/filters/cfitz/cfitz.pine
+++ b/lib/filters/cfitz/cfitz.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Christiano-Fitzgerald Band-Pass Filter (CFITZ)", "CFITZ", overlay=false, max_bars_back=5000)
diff --git a/lib/filters/cfitz/cfitz_signature.md b/lib/filters/cfitz/cfitz_signature.md
deleted file mode 100644
index 328bf330..00000000
--- a/lib/filters/cfitz/cfitz_signature.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# CFITZ Signature
-
-
\ No newline at end of file
diff --git a/lib/filters/cfitz/cfitz_signature.svg b/lib/filters/cfitz/cfitz_signature.svg
deleted file mode 100644
index 84b043a2..00000000
--- a/lib/filters/cfitz/cfitz_signature.svg
+++ /dev/null
@@ -1,19647 +0,0 @@
-
-
-
diff --git a/lib/filters/cheby1/cheby1.pine b/lib/filters/cheby1/cheby1.pine
index 12ddf593..0b09b8ee 100644
--- a/lib/filters/cheby1/cheby1.pine
+++ b/lib/filters/cheby1/cheby1.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Chebyshev Type I Filter (CHEBY1)", "CHEBY1", overlay=true)
diff --git a/lib/filters/cheby2/cheby2.pine b/lib/filters/cheby2/cheby2.pine
index c177ad24..b9cd342f 100644
--- a/lib/filters/cheby2/cheby2.pine
+++ b/lib/filters/cheby2/cheby2.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Chebyshev Type II Filter (CHEBY2)", "CHEBY2", overlay=true)
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/edcf/edcf.pine b/lib/filters/edcf/edcf.pine
index 521e4585..0b47072f 100644
--- a/lib/filters/edcf/edcf.pine
+++ b/lib/filters/edcf/edcf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("EDCF: Ehlers Distance Coefficient Filter", "EDCF", overlay = true)
diff --git a/lib/filters/elliptic/elliptic.pine b/lib/filters/elliptic/elliptic.pine
index b23e3aa3..ab8b346b 100644
--- a/lib/filters/elliptic/elliptic.pine
+++ b/lib/filters/elliptic/elliptic.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Elliptic 2nd Order Filter (ELLIPTIC)", "ELLIPTIC", overlay=true)
diff --git a/lib/filters/gauss/Gauss.md b/lib/filters/gauss/Gauss.md
index 4a4082cd..9184562f 100644
--- a/lib/filters/gauss/Gauss.md
+++ b/lib/filters/gauss/Gauss.md
@@ -8,13 +8,15 @@
| **Outputs** | Single series (Gauss) |
| **Output range** | Tracks input |
| **Warmup** | 1 bar |
+| **Signature** | [gauss_signature](gauss_signature.md) |
+
### TL;DR
- 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/gauss/gauss.pine b/lib/filters/gauss/gauss.pine
index fc13e893..7a958d7d 100644
--- a/lib/filters/gauss/gauss.pine
+++ b/lib/filters/gauss/gauss.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Gaussian Filter (GAUSS)", "GAUSS", overlay=true)
diff --git a/lib/filters/gauss/gauss_signature.md b/lib/filters/gauss/gauss_signature.md
new file mode 100644
index 00000000..a8b6f344
--- /dev/null
+++ b/lib/filters/gauss/gauss_signature.md
@@ -0,0 +1,3 @@
+# ELLIPTIC Signature
+
+
\ No newline at end of file
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/hann/hann.pine b/lib/filters/hann/hann.pine
index 9306debb..97ec9717 100644
--- a/lib/filters/hann/hann.pine
+++ b/lib/filters/hann/hann.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Hann FIR Filter (HANN)", "HANN", overlay=true)
diff --git a/lib/filters/hp/Hp.md b/lib/filters/hp/Hp.md
index a07fcdf8..2588944b 100644
--- a/lib/filters/hp/Hp.md
+++ b/lib/filters/hp/Hp.md
@@ -8,13 +8,15 @@
| **Outputs** | Single series (HP) |
| **Output range** | Tracks input |
| **Warmup** | 1 bar |
+| **Signature** | [hp_signature](hp_signature.md) |
+
### TL;DR
- 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/hp/hp.pine b/lib/filters/hp/hp.pine
index 6b829902..5dbdf3e5 100644
--- a/lib/filters/hp/hp.pine
+++ b/lib/filters/hp/hp.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Hodrick-Prescott Filter (HP)", "HP", overlay=false)
diff --git a/lib/filters/hp/hp_signature.md b/lib/filters/hp/hp_signature.md
new file mode 100644
index 00000000..f086fab4
--- /dev/null
+++ b/lib/filters/hp/hp_signature.md
@@ -0,0 +1,3 @@
+# ELLIPTIC Signature
+
+
\ No newline at end of file
diff --git a/lib/filters/hpf/Hpf.md b/lib/filters/hpf/Hpf.md
index 6f22de54..f44e6bbb 100644
--- a/lib/filters/hpf/Hpf.md
+++ b/lib/filters/hpf/Hpf.md
@@ -8,14 +8,13 @@
| **Outputs** | Single series (HPF) |
| **Output range** | Tracks input |
| **Warmup** | 1 bar |
-| **Signature** | [hpf_signature](hpf_signature.md) |
### TL;DR
- 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/hpf/hpf.pine b/lib/filters/hpf/hpf.pine
index 6750e046..984881ed 100644
--- a/lib/filters/hpf/hpf.pine
+++ b/lib/filters/hpf/hpf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Highpass Filter (HPF)", "HPF", overlay=true)
diff --git a/lib/filters/hpf/hpf_signature.md b/lib/filters/hpf/hpf_signature.md
deleted file mode 100644
index 9e17f05c..00000000
--- a/lib/filters/hpf/hpf_signature.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# HPF Signature
-
-
\ No newline at end of file
diff --git a/lib/filters/hpf/hpf_signature.svg b/lib/filters/hpf/hpf_signature.svg
deleted file mode 100644
index acf0015d..00000000
--- a/lib/filters/hpf/hpf_signature.svg
+++ /dev/null
@@ -1,19689 +0,0 @@
-
-
-
diff --git a/lib/filters/kalman/Kalman.md b/lib/filters/kalman/Kalman.md
index acd879a1..9dd240ca 100644
--- a/lib/filters/kalman/Kalman.md
+++ b/lib/filters/kalman/Kalman.md
@@ -8,6 +8,8 @@
| **Outputs** | Single series (Kalman) |
| **Output range** | Tracks input |
| **Warmup** | `10` bars |
+| **Signature** | [kalman_signature](kalman_signature.md) |
+
### TL;DR
diff --git a/lib/filters/kalman/kalman.pine b/lib/filters/kalman/kalman.pine
index f14405bf..40f401d7 100644
--- a/lib/filters/kalman/kalman.pine
+++ b/lib/filters/kalman/kalman.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Kalman Filter (KALMAN)", "KALMAN", overlay=true)
diff --git a/lib/filters/kalman/kalman_signature.md b/lib/filters/kalman/kalman_signature.md
new file mode 100644
index 00000000..1f959745
--- /dev/null
+++ b/lib/filters/kalman/kalman_signature.md
@@ -0,0 +1,3 @@
+# ELLIPTIC Signature
+
+
\ No newline at end of file
diff --git a/lib/filters/laguerre/Laguerre.md b/lib/filters/laguerre/Laguerre.md
index 391b255e..add129d3 100644
--- a/lib/filters/laguerre/Laguerre.md
+++ b/lib/filters/laguerre/Laguerre.md
@@ -8,6 +8,8 @@
| **Outputs** | Single series (Laguerre) |
| **Output range** | Tracks input |
| **Warmup** | `WarmupBars` bars |
+| **Signature** | [laguerre_signature](laguerre_signature.md) |
+
### TL;DR
diff --git a/lib/filters/laguerre/laguerre.pine b/lib/filters/laguerre/laguerre.pine
index ce217505..47e681d1 100644
--- a/lib/filters/laguerre/laguerre.pine
+++ b/lib/filters/laguerre/laguerre.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Indicator algorithm (C) 2004 John F. Ehlers
diff --git a/lib/filters/laguerre/laguerre_signature.md b/lib/filters/laguerre/laguerre_signature.md
new file mode 100644
index 00000000..81199426
--- /dev/null
+++ b/lib/filters/laguerre/laguerre_signature.md
@@ -0,0 +1,3 @@
+# ELLIPTIC Signature
+
+
\ No newline at end of file
diff --git a/lib/filters/lms/lms.pine b/lib/filters/lms/lms.pine
index 9f9cac6d..e761621c 100644
--- a/lib/filters/lms/lms.pine
+++ b/lib/filters/lms/lms.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Least Mean Squares Adaptive Filter (LMS)", "LMS", overlay=true)
diff --git a/lib/filters/loess/Loess.cs b/lib/filters/loess/Loess.cs
index 2dcc9a57..bb959812 100644
--- a/lib/filters/loess/Loess.cs
+++ b/lib/filters/loess/Loess.cs
@@ -1,4 +1,3 @@
-using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -309,48 +308,7 @@ public sealed class Loess : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double DotProduct(ReadOnlySpan a, ReadOnlySpan b)
- {
- // a and b expected to be same length (slice called in Update ensures this)
- int length = a.Length;
- if (length == 0)
- {
- return 0;
- }
-
- int i = 0;
- double sum = 0;
-
- if (Vector.IsHardwareAccelerated && length >= Vector.Count)
- {
- var vSum = Vector.Zero;
- ref double rA = ref MemoryMarshal.GetReference(a);
- ref double rB = ref MemoryMarshal.GetReference(b);
-
- int vectorCount = Vector.Count;
- int limit = length - vectorCount;
-
- for (; i <= limit; i += vectorCount)
- {
- var vA = Vector.LoadUnsafe(ref rA, (nuint)i);
- var vB = Vector.LoadUnsafe(ref rB, (nuint)i);
- vSum += vA * vB;
- }
-
- // Reduce vector sum
- for (int j = 0; j < vectorCount; j++)
- {
- sum += vSum[j];
- }
- }
-
- // Remainder
- for (; i < length; i++)
- {
- sum += a[i] * b[i];
- }
-
- return sum;
- }
+ => a.DotProduct(b);
public static (TSeries Results, Loess Indicator) Calculate(TSeries source, int period)
{
diff --git a/lib/filters/loess/loess.pine b/lib/filters/loess/loess.pine
index c67ed154..527b6b18 100644
--- a/lib/filters/loess/loess.pine
+++ b/lib/filters/loess/loess.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("LOESS Filter", "LOESS", overlay=true)
diff --git a/lib/filters/loess/loess_signature.svg b/lib/filters/loess/loess_signature.svg
index d4e24468..03e8381d 100644
--- a/lib/filters/loess/loess_signature.svg
+++ b/lib/filters/loess/loess_signature.svg
@@ -6,7 +6,7 @@
- 2026-03-02T14:19:36.373048
+ 2026-03-10T19:40:09.112705
image/svg+xml
@@ -30,28 +30,313 @@ z
-
+
+
+
+
+
+
+
+
+
-
-
+
-
-
+
+
-
-
-
-
+
+
+
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
+
-
+
-
+
+
+
+
+
-
+
-
+
-
-
-
-
+
@@ -300,126 +840,449 @@ z
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
@@ -627,233 +1399,1176 @@ z
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
-
+
-
-
-
+
+
+
-
+
+
-
+
+
+
+
-
+
-
-
-
+
+
+
-
+
+
-
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
-
+
+
-
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
+
-
+
-
-
+
+
+
+
+
-
+
-
+
-
+
-
-
+
+
+
+
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
@@ -2574,29 +9407,449 @@ z
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
+
-
-
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
-
-
+
+
+
+
+
-