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..8d599a5a 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 |
@@ -432,11 +436,13 @@ Mathematical transformations and derivative indicators. Building blocks for anal
| [**DWT**](../lib/numerics/dwt/Dwt.md) | Discrete Wavelet Transform | À trous Haar stationary DWT — multi-resolution approximation + detail, WarmupPeriod = 2^levels |
| [**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 +498,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.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/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.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.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.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/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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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/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.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/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.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/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.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.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/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.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/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/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/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/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/modf/modf.pine b/lib/filters/modf/modf.pine
index 84269e88..1d402563 100644
--- a/lib/filters/modf/modf.pine
+++ b/lib/filters/modf/modf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Modular Filter (MODF)", "MODF", overlay=true)
diff --git a/lib/filters/notch/notch.pine b/lib/filters/notch/notch.pine
index 56da11bf..aa9f82d7 100644
--- a/lib/filters/notch/notch.pine
+++ b/lib/filters/notch/notch.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Notch Filter (NOTCH)", "NOTCH", overlay=true)
diff --git a/lib/filters/nw/nw.pine b/lib/filters/nw/nw.pine
index 5c52536c..b55620ec 100644
--- a/lib/filters/nw/nw.pine
+++ b/lib/filters/nw/nw.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
diff --git a/lib/filters/oneeuro/oneeuro.pine b/lib/filters/oneeuro/oneeuro.pine
index 3cf087fa..c2f1fe82 100644
--- a/lib/filters/oneeuro/oneeuro.pine
+++ b/lib/filters/oneeuro/oneeuro.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("One Euro Filter (ONEEURO)", "ONEEURO", overlay=true)
diff --git a/lib/filters/rls/rls.pine b/lib/filters/rls/rls.pine
index b3ba8978..b409b7ad 100644
--- a/lib/filters/rls/rls.pine
+++ b/lib/filters/rls/rls.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Recursive Least Squares Adaptive Filter (RLS)", "RLS", overlay=true)
diff --git a/lib/filters/roofing/roofing.pine b/lib/filters/roofing/roofing.pine
index 58032b7b..a993a092 100644
--- a/lib/filters/roofing/roofing.pine
+++ b/lib/filters/roofing/roofing.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/sgf/sgf.pine b/lib/filters/sgf/sgf.pine
index a4efcd1d..15331a00 100644
--- a/lib/filters/sgf/sgf.pine
+++ b/lib/filters/sgf/sgf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Savitzky-Golay Filter (SGF)", "SGF", overlay=true)
diff --git a/lib/filters/spbf/spbf.pine b/lib/filters/spbf/spbf.pine
index 732556e0..7259ed49 100644
--- a/lib/filters/spbf/spbf.pine
+++ b/lib/filters/spbf/spbf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Indicator algorithm (C) 2016 John F. Ehlers
diff --git a/lib/filters/ssf2/ssf2.pine b/lib/filters/ssf2/ssf2.pine
index b16c44c8..4080f479 100644
--- a/lib/filters/ssf2/ssf2.pine
+++ b/lib/filters/ssf2/ssf2.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/ssf3/ssf3.pine b/lib/filters/ssf3/ssf3.pine
index 6a47d4e7..fb0e4115 100644
--- a/lib/filters/ssf3/ssf3.pine
+++ b/lib/filters/ssf3/ssf3.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/usf/usf.pine b/lib/filters/usf/usf.pine
index 1b97c380..7cb533aa 100644
--- a/lib/filters/usf/usf.pine
+++ b/lib/filters/usf/usf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Ultimate Smoother Filter (USF)", "USF", overlay=true)
diff --git a/lib/filters/voss/voss.pine b/lib/filters/voss/voss.pine
index 3d9ad6ee..a2f0588f 100644
--- a/lib/filters/voss/voss.pine
+++ b/lib/filters/voss/voss.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Voss Predictive Filter (VOSS)", "VOSS", overlay=false)
diff --git a/lib/filters/wavelet/wavelet.pine b/lib/filters/wavelet/wavelet.pine
index 49a55579..7d33573e 100644
--- a/lib/filters/wavelet/wavelet.pine
+++ b/lib/filters/wavelet/wavelet.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Wavelet Denoising Filter (WAVELET)", "WAVELET", overlay=true)
diff --git a/lib/filters/wiener/wiener.pine b/lib/filters/wiener/wiener.pine
index f628e571..b4d670fd 100644
--- a/lib/filters/wiener/wiener.pine
+++ b/lib/filters/wiener/wiener.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Wiener Filter (WIENER)", "WIENER", overlay=true)
diff --git a/lib/forecasts/afirma/Afirma.Validation.Tests.cs b/lib/forecasts/afirma/Afirma.Validation.Tests.cs
index 49b6f72a..9a215f4e 100644
--- a/lib/forecasts/afirma/Afirma.Validation.Tests.cs
+++ b/lib/forecasts/afirma/Afirma.Validation.Tests.cs
@@ -310,4 +310,60 @@ public sealed class AfirmaValidationTests : IDisposable
Assert.True(double.IsFinite(rectLast));
Assert.True(double.IsFinite(bhLast));
}
+
+ [Fact]
+ public void Afirma_LeastSquares_Streaming_Matches_Batch()
+ {
+ int[] periods = { 5, 10, 20 };
+
+ foreach (var period in periods)
+ {
+ // Batch calculation with leastSquares=true
+ var afirmaBatch = new Afirma(period, leastSquares: true);
+ var batchResult = afirmaBatch.Update(_testData.Data);
+
+ // Streaming calculation with leastSquares=true
+ var afirmaStream = new Afirma(period, leastSquares: true);
+ var streamResults = new List();
+ foreach (var item in _testData.Data)
+ {
+ streamResults.Add(afirmaStream.Update(item).Value);
+ }
+
+ // Compare last 100 values
+ int compareCount = Math.Min(100, batchResult.Count);
+ for (int i = 0; i < compareCount; i++)
+ {
+ int idx = batchResult.Count - compareCount + i;
+ Assert.Equal(batchResult[idx].Value, streamResults[idx], 1e-10);
+ }
+ }
+ }
+
+ [Fact]
+ public void Afirma_Correction_Recomputes()
+ {
+ var ind = new Afirma(20);
+ var t0 = DateTime.MinValue;
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(new TValue(t0.AddSeconds(i), 100.0 + i * 0.5));
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddSeconds(50);
+ const double anchorValue = 125.0;
+ ind.Update(new TValue(anchorTime, anchorValue), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with dramatically different value — must yield different result
+ ind.Update(new TValue(anchorTime, anchorValue * 10), isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore original result
+ ind.Update(new TValue(anchorTime, anchorValue), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
diff --git a/lib/forecasts/afirma/Afirma.cs b/lib/forecasts/afirma/Afirma.cs
index 4381eb46..d6e67bab 100644
--- a/lib/forecasts/afirma/Afirma.cs
+++ b/lib/forecasts/afirma/Afirma.cs
@@ -106,6 +106,12 @@ public sealed class Afirma : AbstractBase
///
/// Creates AFIRMA with TSeries source for priming.
///
+ ///
+ /// Primes the internal buffer from history, then overwrites
+ /// Last.Time with source.LastTime, replacing the
+ /// placeholder set by .
+ /// Subscribes to future source updates via the publisher event.
+ ///
public Afirma(TSeries source, int period, WindowType window = WindowType.BlackmanHarris, bool leastSquares = false)
: this(period, window, leastSquares)
{
@@ -133,6 +139,11 @@ public sealed class Afirma : AbstractBase
///
/// Initializes the indicator state using the provided history.
///
+ ///
+ /// Sets Last.Time = as a placeholder timestamp.
+ /// Callers that invoke directly must not rely on Last.Time
+ /// until the first call assigns a real timestamp.
+ ///
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
if (source.Length == 0)
@@ -541,7 +552,7 @@ public sealed class Afirma : AbstractBase
int idx = (readIndex + bufferCount - 1 - j + period) % period;
double v = buffer[idx];
sy += v;
- sxy += j * v;
+ sxy = Math.FusedMultiplyAdd(j, v, sxy);
}
double denom = dn * sx2 - sx * sx;
@@ -559,7 +570,7 @@ public sealed class Afirma : AbstractBase
double v_ls;
if (j < n)
{
- v_ls = intercept + slope * j;
+ v_ls = Math.FusedMultiplyAdd(slope, j, intercept);
}
else
{
diff --git a/lib/forecasts/afirma/afirma.pine b/lib/forecasts/afirma/afirma.pine
index 07962748..4fe0443d 100644
--- a/lib/forecasts/afirma/afirma.pine
+++ b/lib/forecasts/afirma/afirma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Autoregressive FIR Moving Average (AFIRMA)", "AFIRMA", overlay=true)
diff --git a/lib/momentum/asi/asi.pine b/lib/momentum/asi/asi.pine
index ca199b8a..6a3a415b 100644
--- a/lib/momentum/asi/asi.pine
+++ b/lib/momentum/asi/asi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Accumulation Swing Index (ASI)", "ASI", overlay=false)
diff --git a/lib/momentum/bias/bias.pine b/lib/momentum/bias/bias.pine
index f8cba461..2142e09f 100644
--- a/lib/momentum/bias/bias.pine
+++ b/lib/momentum/bias/bias.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bias (BIAS)", "BIAS", overlay=false)
diff --git a/lib/momentum/bop/bop.pine b/lib/momentum/bop/bop.pine
index 57c40e30..07bfe7d2 100644
--- a/lib/momentum/bop/bop.pine
+++ b/lib/momentum/bop/bop.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Balance of Power (BOP)", "BOP", overlay=false)
diff --git a/lib/momentum/cci/Cci.Validation.Tests.cs b/lib/momentum/cci/Cci.Validation.Tests.cs
index 49ad13df..09e465da 100644
--- a/lib/momentum/cci/Cci.Validation.Tests.cs
+++ b/lib/momentum/cci/Cci.Validation.Tests.cs
@@ -393,5 +393,34 @@ public sealed class CciValidationTests(ITestOutputHelper output) : IDisposable
_output.WriteLine("CCI Batch vs Streaming consistency validated");
}
+ [Fact]
+ public void Cci_Correction_Recomputes()
+ {
+ var ind = new Cci(20);
+ long t0 = TimeSpan.TicksPerSecond;
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ double p = 100.0 + i * 0.5;
+ ind.Update(new TBar(t0 + i * TimeSpan.TicksPerSecond, p, p + 1, p - 1, p, 1000));
+ }
+
+ // Anchor bar
+ long anchorTime = t0 + 50 * TimeSpan.TicksPerSecond;
+ var anchorBar = new TBar(anchorTime, 125.0, 126.0, 124.0, 125.0, 1000);
+ ind.Update(anchorBar, isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with dramatically different bar
+ var corruptBar = new TBar(anchorTime, 1250.0, 1260.0, 1240.0, 1250.0, 1000);
+ ind.Update(corruptBar, isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore
+ ind.Update(anchorBar, isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
+
#endregion
}
diff --git a/lib/momentum/cci/cci.pine b/lib/momentum/cci/cci.pine
index 13bd5d89..b021794b 100644
--- a/lib/momentum/cci/cci.pine
+++ b/lib/momentum/cci/cci.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Commodity Channel Index (CCI)", "CCI", overlay=false)
diff --git a/lib/momentum/cfb/cfb.pine b/lib/momentum/cfb/cfb.pine
index 85f234a9..656b5cb2 100644
--- a/lib/momentum/cfb/cfb.pine
+++ b/lib/momentum/cfb/cfb.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Jurik Composite Fractal Behavior", "CFB", overlay=false)
diff --git a/lib/momentum/cmo/cmo.pine b/lib/momentum/cmo/cmo.pine
index 286b0e50..43961e14 100644
--- a/lib/momentum/cmo/cmo.pine
+++ b/lib/momentum/cmo/cmo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Chande Momentum Oscillator (CMO)", "CMO", overlay=false)
diff --git a/lib/momentum/macd/Macd.Validation.Tests.cs b/lib/momentum/macd/Macd.Validation.Tests.cs
index 04c86da0..5226f295 100644
--- a/lib/momentum/macd/Macd.Validation.Tests.cs
+++ b/lib/momentum/macd/Macd.Validation.Tests.cs
@@ -238,4 +238,35 @@ public sealed class MacdValidationTests : IDisposable
_output.WriteLine("MACD Streaming validated successfully against Tulip");
}
+
+ [Fact]
+ public void Macd_Correction_Recomputes()
+ {
+ var ind = new Macd();
+ var t0 = DateTime.MinValue;
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(new TValue(t0.AddSeconds(i), 100.0 + i * 0.5));
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddSeconds(50);
+ const double anchorPrice = 125.0;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorMacd = ind.Last.Value;
+ double anchorSignal = ind.Signal.Value;
+ double anchorHistogram = ind.Histogram.Value;
+
+ // Correction with dramatically different value
+ ind.Update(new TValue(anchorTime, anchorPrice * 10), isNew: false);
+ Assert.NotEqual(anchorMacd, ind.Last.Value);
+
+ // Correction back to original — all three outputs must restore exactly
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorMacd, ind.Last.Value, 1e-9);
+ Assert.Equal(anchorSignal, ind.Signal.Value, 1e-9);
+ Assert.Equal(anchorHistogram, ind.Histogram.Value, 1e-9);
+ }
}
diff --git a/lib/momentum/macd/Macd.cs b/lib/momentum/macd/Macd.cs
index 269db203..256f3101 100644
--- a/lib/momentum/macd/Macd.cs
+++ b/lib/momentum/macd/Macd.cs
@@ -158,6 +158,12 @@ public sealed class Macd : ITValuePublisher, IDisposable
/// Calculates the MACD Line (Fast EMA - Slow EMA).
/// Does not calculate Signal or Histogram.
///
+ ///
+ /// For full MACD output (MACD line, Signal, and Histogram) use
+ /// or the streaming
+ /// API together with
+ /// and properties.
+ ///
public static void Batch(ReadOnlySpan source, Span destination, int fastPeriod = 12, int slowPeriod = 26)
{
if (source.Length != destination.Length)
diff --git a/lib/momentum/macd/macd.pine b/lib/momentum/macd/macd.pine
index b0967749..30f91368 100644
--- a/lib/momentum/macd/macd.pine
+++ b/lib/momentum/macd/macd.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Moving Average Convergence Divergence (MACD)", "MACD", overlay=false)
diff --git a/lib/momentum/mom/mom.pine b/lib/momentum/mom/mom.pine
index 04a5d0b0..91eb77aa 100644
--- a/lib/momentum/mom/mom.pine
+++ b/lib/momentum/mom/mom.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Momentum (MOM)", "MOM", overlay=false)
diff --git a/lib/momentum/pmo/pmo.pine b/lib/momentum/pmo/pmo.pine
index 8c422b11..4af15506 100644
--- a/lib/momentum/pmo/pmo.pine
+++ b/lib/momentum/pmo/pmo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Momentum Oscillator (PMO)", "PMO", overlay=false)
diff --git a/lib/momentum/ppo/ppo.pine b/lib/momentum/ppo/ppo.pine
index f30d57dc..d7063e64 100644
--- a/lib/momentum/ppo/ppo.pine
+++ b/lib/momentum/ppo/ppo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Percentage Price Oscillator (PPO)", "PPO", overlay=false)
diff --git a/lib/momentum/prs/Prs.Validation.Tests.cs b/lib/momentum/prs/Prs.Validation.Tests.cs
index f20e28f9..a7ff7a81 100644
--- a/lib/momentum/prs/Prs.Validation.Tests.cs
+++ b/lib/momentum/prs/Prs.Validation.Tests.cs
@@ -548,5 +548,31 @@ public class PrsValidationTests
ValidationHelper.VerifyData(qResults, sResult, s => s.Prs, tolerance: ValidationHelper.SkenderTolerance);
}
+ [Fact]
+ public void Prs_Correction_Recomputes()
+ {
+ var ind = new Prs(smoothPeriod: 5);
+
+ // 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 anchorBase = 125.0;
+ const double anchorComp = 100.0;
+ ind.Update(anchorBase, anchorComp, isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction: change base dramatically — ratio changes from 1.25 to 12.5
+ ind.Update(anchorBase * 10, anchorComp, isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore
+ ind.Update(anchorBase, anchorComp, isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
+
#endregion
}
diff --git a/lib/momentum/prs/Prs.cs b/lib/momentum/prs/Prs.cs
index 84eae8a4..fa329194 100644
--- a/lib/momentum/prs/Prs.cs
+++ b/lib/momentum/prs/Prs.cs
@@ -140,6 +140,11 @@ public sealed class Prs : AbstractBase
///
/// Updates with raw double values.
///
+ ///
+ /// Stamps both inputs with DateTime.UtcNow as their timestamp. For
+ /// deterministic or replay-safe sequences use
+ /// with explicit timestamps instead.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double baseValue, double compValue, bool isNew = true)
{
diff --git a/lib/momentum/prs/prs.pine b/lib/momentum/prs/prs.pine
index fd954124..1c98028a 100644
--- a/lib/momentum/prs/prs.pine
+++ b/lib/momentum/prs/prs.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Relative Strength (PRS)", "PRS", overlay=false)
diff --git a/lib/momentum/roc/roc.pine b/lib/momentum/roc/roc.pine
index 5a41c656..e97687f2 100644
--- a/lib/momentum/roc/roc.pine
+++ b/lib/momentum/roc/roc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Rate of Change (ROC)", "ROC", overlay=false)
diff --git a/lib/momentum/rocp/rocp.pine b/lib/momentum/rocp/rocp.pine
index 71c40740..75fda4d2 100644
--- a/lib/momentum/rocp/rocp.pine
+++ b/lib/momentum/rocp/rocp.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Rate of Change Percentage (ROCP)", "ROCP", overlay=false)
diff --git a/lib/momentum/rocr/rocr.pine b/lib/momentum/rocr/rocr.pine
index 7f5f0d8c..7f2c2f15 100644
--- a/lib/momentum/rocr/rocr.pine
+++ b/lib/momentum/rocr/rocr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Rate of Change Ratio (ROCR)", "ROCR", overlay=false)
diff --git a/lib/momentum/rsi/rsi.pine b/lib/momentum/rsi/rsi.pine
index 81703bc8..e0c937d5 100644
--- a/lib/momentum/rsi/rsi.pine
+++ b/lib/momentum/rsi/rsi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Relative Strength Index (RSI)", "RSI", overlay=false)
diff --git a/lib/momentum/rsx/Rsx.Validation.Tests.cs b/lib/momentum/rsx/Rsx.Validation.Tests.cs
index eba80a3d..141dc5cb 100644
--- a/lib/momentum/rsx/Rsx.Validation.Tests.cs
+++ b/lib/momentum/rsx/Rsx.Validation.Tests.cs
@@ -142,4 +142,31 @@ public class RsxValidationTests
int finiteCount = values.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
}
+
+ [Fact]
+ public void Rsx_Correction_Recomputes()
+ {
+ var ind = new Rsx(14);
+ var t0 = DateTime.MinValue;
+
+ // Build state well past warmup
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(new TValue(t0.AddSeconds(i), 100.0 + i * 0.5));
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddSeconds(50);
+ const double anchorPrice = 125.0;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with dramatically different value — use a large drop to move RSX away from ceiling
+ ind.Update(new TValue(anchorTime, anchorPrice / 10), isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
diff --git a/lib/momentum/rsx/rsx.pine b/lib/momentum/rsx/rsx.pine
index 04b318c4..41b4e799 100644
--- a/lib/momentum/rsx/rsx.pine
+++ b/lib/momentum/rsx/rsx.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Relative Strength Xtra (RSX)", "RSX", overlay=false)
diff --git a/lib/momentum/tsi/tsi.pine b/lib/momentum/tsi/tsi.pine
index 3fb07300..86bbd341 100644
--- a/lib/momentum/tsi/tsi.pine
+++ b/lib/momentum/tsi/tsi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("True Strength Index (TSI)", "TSI", overlay=false)
diff --git a/lib/momentum/vel/vel.pine b/lib/momentum/vel/vel.pine
index 0c4a74ff..d17a1fdc 100644
--- a/lib/momentum/vel/vel.pine
+++ b/lib/momentum/vel/vel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Velocity (VEL)", "VEL", overlay=false)
diff --git a/lib/numerics/_index.md b/lib/numerics/_index.md
index b0605e43..0bd386a9 100644
--- a/lib/numerics/_index.md
+++ b/lib/numerics/_index.md
@@ -18,12 +18,14 @@ Basic mathematical transforms and utility functions for time series. These build
| [FFT](fft/Fft.md) | Fast Fourier Transform | Frequency-domain decomposition via FFT algorithm. |
| [GAMMADIST](gammadist/Gammadist.md) | Gamma Distribution | Gamma probability distribution transform. |
| [HIGHEST](highest/Highest.md) | Rolling Maximum | Maximum value over lookback window. |
+| [MAXINDEX](maxindex/Maxindex.md) | Rolling Maximum Index | Position of maximum value in rolling window. |
| [IFFT](ifft/Ifft.md) | Inverse Fast Fourier Transform | Frequency-to-time domain reconstruction. |
| [JERK](jerk/Jerk.md) | Jerk | Rate of acceleration; third derivative of price. |
| [LINEARTRANS](lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling transformation. |
| [LOGNORMDIST](lognormdist/Lognormdist.md) | Log-normal Distribution | Log-normal probability distribution transform. |
| [LOGTRANS](logtrans/Logtrans.md) | Logarithmic Transform | Natural log for percentage-based analysis. |
| [LOWEST](lowest/Lowest.md) | Rolling Minimum | Minimum value over lookback window. |
+| [MININDEX](minindex/Minindex.md) | Rolling Minimum Index | Position of minimum value in rolling window. |
| [NORMDIST](normdist/Normdist.md) | Normal Distribution | Gaussian probability distribution transform. |
| [NORMALIZE](normalize/Normalize.md) | Min-Max Normalization | Scale to [0,1] range using rolling min/max. |
| [POISSONDIST](poissondist/Poissondist.md) | Poisson Distribution | Poisson probability distribution transform. |
diff --git a/lib/numerics/accel/accel.pine b/lib/numerics/accel/accel.pine
index 15cfee0d..0ca4db72 100644
--- a/lib/numerics/accel/accel.pine
+++ b/lib/numerics/accel/accel.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Acceleration (Slope of Slope) (ACCEL)", "ACCEL", overlay=false, precision=8)
diff --git a/lib/numerics/betadist/betadist.pine b/lib/numerics/betadist/betadist.pine
index 6950a254..b2578f1d 100644
--- a/lib/numerics/betadist/betadist.pine
+++ b/lib/numerics/betadist/betadist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Beta Distribution CDF (BETADIST)", "BETADIST", overlay=false, precision=6)
diff --git a/lib/numerics/binomdist/binomdist.pine b/lib/numerics/binomdist/binomdist.pine
index d4f1f6eb..51545a51 100644
--- a/lib/numerics/binomdist/binomdist.pine
+++ b/lib/numerics/binomdist/binomdist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Binomial Distribution CDF (BINOMDIST)", "BINOMDIST", overlay=false, precision=6)
diff --git a/lib/numerics/change/change.pine b/lib/numerics/change/change.pine
index 210ca593..bd184319 100644
--- a/lib/numerics/change/change.pine
+++ b/lib/numerics/change/change.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Percentage Change (CHANGE)", "CHANGE", overlay=false, format=format.percent)
diff --git a/lib/numerics/cwt/cwt.pine b/lib/numerics/cwt/cwt.pine
index 360da049..00ec982d 100644
--- a/lib/numerics/cwt/cwt.pine
+++ b/lib/numerics/cwt/cwt.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Continuous Wavelet Transform (CWT)", "CWT", overlay=false, precision=6)
diff --git a/lib/numerics/dwt/Dwt.Validation.Tests.cs b/lib/numerics/dwt/Dwt.Validation.Tests.cs
index 2fd43df5..8a85fe58 100644
--- a/lib/numerics/dwt/Dwt.Validation.Tests.cs
+++ b/lib/numerics/dwt/Dwt.Validation.Tests.cs
@@ -322,6 +322,33 @@ public class DwtValidationTests
}
}
+ [Fact]
+ public void Dwt_Correction_Recomputes()
+ {
+ var ind = new Dwt(levels: 4);
+ var t0 = DateTime.MinValue;
+
+ // Build state well past warmup (WarmupPeriod = 2^4 = 16)
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(new TValue(t0.AddSeconds(i), 100.0 + i * 0.5));
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddSeconds(50);
+ const double anchorPrice = 125.0;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Correction with dramatically different value — DWT uses anchor at lag 0
+ ind.Update(new TValue(anchorTime, anchorPrice * 10), isNew: false);
+ Assert.NotEqual(anchorResult, ind.Last.Value);
+
+ // Correction back to original — must exactly restore
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
+
// ─── Helper ──────────────────────────────────────────────────────────────
private static double Variance(List vals)
diff --git a/lib/numerics/dwt/Dwt.cs b/lib/numerics/dwt/Dwt.cs
index 615f5c17..3fc1dab4 100644
--- a/lib/numerics/dwt/Dwt.cs
+++ b/lib/numerics/dwt/Dwt.cs
@@ -313,6 +313,14 @@ public sealed class Dwt : AbstractBase
return result;
}
+ ///
+ /// Primes the indicator with historical values.
+ ///
+ ///
+ /// Synthetic timestamps are generated by subtracting step × source.Length
+ /// from . For deterministic or replay-safe pipelines
+ /// use directly with explicit timestamps.
+ ///
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
diff --git a/lib/numerics/dwt/dwt.pine b/lib/numerics/dwt/dwt.pine
index d6c7c6da..459aa904 100644
--- a/lib/numerics/dwt/dwt.pine
+++ b/lib/numerics/dwt/dwt.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Discrete Wavelet Transform (DWT)", "DWT", overlay=false, precision=6)
diff --git a/lib/numerics/expdist/expdist.pine b/lib/numerics/expdist/expdist.pine
index 94cba8f9..7662efe6 100644
--- a/lib/numerics/expdist/expdist.pine
+++ b/lib/numerics/expdist/expdist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Exponential Distribution CDF (EXPDIST)", "EXPDIST", overlay=false, precision=6)
diff --git a/lib/numerics/exptrans/exptrans.pine b/lib/numerics/exptrans/exptrans.pine
index a942e91d..875926e9 100644
--- a/lib/numerics/exptrans/exptrans.pine
+++ b/lib/numerics/exptrans/exptrans.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Exponential Transformation (EXP)", "Exptrans", overlay=false)
diff --git a/lib/numerics/fdist/fdist.pine b/lib/numerics/fdist/fdist.pine
index 097180e0..75056985 100644
--- a/lib/numerics/fdist/fdist.pine
+++ b/lib/numerics/fdist/fdist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("F-Distribution CDF (FDIST)", "FDIST", overlay=false, precision=6)
diff --git a/lib/numerics/fft/Fft.Validation.Tests.cs b/lib/numerics/fft/Fft.Validation.Tests.cs
index 2ddc83fc..162b7ee6 100644
--- a/lib/numerics/fft/Fft.Validation.Tests.cs
+++ b/lib/numerics/fft/Fft.Validation.Tests.cs
@@ -233,4 +233,32 @@ public class FftValidationTests
$"Output at {i} must be finite, got {dst[i]}");
}
}
+
+ [Fact]
+ public void Fft_Correction_StateRestores()
+ {
+ // The Hanning window maps idx=0 to the newest bar and _hanning[0]=0, so
+ // corrections to the anchor bar carry zero spectral weight. isNew=false
+ // determinism is still correct: restoring the original value reproduces the
+ // original result exactly regardless of any intermediate correction.
+ var ind = new Fft(windowSize: 32, maxPeriod: 16);
+ var t0 = DateTime.MinValue;
+
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(new TValue(t0.AddSeconds(i), 100.0 + 10.0 * Math.Sin(2 * Math.PI * i / 8.0)));
+ }
+
+ var anchorTime = t0.AddSeconds(50);
+ const double anchorPrice = 100.0;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorResult = ind.Last.Value;
+
+ // Apply an arbitrary correction — spectral output is unchanged due to zero Hanning weight
+ ind.Update(new TValue(anchorTime, anchorPrice * 100), isNew: false);
+
+ // Restoring to original must exactly reproduce the original result
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
+ }
}
diff --git a/lib/numerics/fft/Fft.cs b/lib/numerics/fft/Fft.cs
index a2734e9f..88f9fd72 100644
--- a/lib/numerics/fft/Fft.cs
+++ b/lib/numerics/fft/Fft.cs
@@ -212,6 +212,14 @@ public sealed class Fft : AbstractBase
return result;
}
+ ///
+ /// Primes the indicator with historical values.
+ ///
+ ///
+ /// Synthetic timestamps are generated by subtracting step × source.Length
+ /// from . For deterministic or replay-safe pipelines
+ /// use directly with explicit timestamps.
+ ///
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
diff --git a/lib/numerics/fft/fft.pine b/lib/numerics/fft/fft.pine
index 6ecc1bb3..be57ab30 100644
--- a/lib/numerics/fft/fft.pine
+++ b/lib/numerics/fft/fft.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("FFT Dominant Cycle (Radix-2 FFT)", "FFT-DC", overlay=false, precision=2)
diff --git a/lib/numerics/gammadist/gammadist.pine b/lib/numerics/gammadist/gammadist.pine
index e2326bcf..00345d3f 100644
--- a/lib/numerics/gammadist/gammadist.pine
+++ b/lib/numerics/gammadist/gammadist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Gamma Distribution CDF (GAMMADIST)", "GAMMADIST", overlay=false, precision=6)
diff --git a/lib/numerics/highest/highest.pine b/lib/numerics/highest/highest.pine
index 49d9e624..d2c18007 100644
--- a/lib/numerics/highest/highest.pine
+++ b/lib/numerics/highest/highest.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Highest Value (HIGHEST)", "HIGHEST", overlay=true)
diff --git a/lib/numerics/ifft/ifft.pine b/lib/numerics/ifft/ifft.pine
index 0b3b4c95..aebfe067 100644
--- a/lib/numerics/ifft/ifft.pine
+++ b/lib/numerics/ifft/ifft.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Inverse Fast Fourier Transform (IFFT)", "IFFT", overlay=true, precision=6)
diff --git a/lib/numerics/jerk/jerk.pine b/lib/numerics/jerk/jerk.pine
index 4e13c485..70855a90 100644
--- a/lib/numerics/jerk/jerk.pine
+++ b/lib/numerics/jerk/jerk.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Acceleration, Slope of Slope (JERK)", "JERK", overlay=false, precision=8)
diff --git a/lib/numerics/lineartrans/lineartrans.pine b/lib/numerics/lineartrans/lineartrans.pine
index 8bf9ee39..f1502ad5 100644
--- a/lib/numerics/lineartrans/lineartrans.pine
+++ b/lib/numerics/lineartrans/lineartrans.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Linear Transformation (LINEAR)", "Lineartrans", overlay=false)
diff --git a/lib/numerics/lognormdist/lognormdist.pine b/lib/numerics/lognormdist/lognormdist.pine
index 22a2b20f..bad89d0d 100644
--- a/lib/numerics/lognormdist/lognormdist.pine
+++ b/lib/numerics/lognormdist/lognormdist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Log-Normal Distribution CDF (LOGNORMDIST)", "LOGNORMDIST", overlay=false, precision=6)
diff --git a/lib/numerics/logtrans/logtrans.pine b/lib/numerics/logtrans/logtrans.pine
index 4bd792b5..21551c8b 100644
--- a/lib/numerics/logtrans/logtrans.pine
+++ b/lib/numerics/logtrans/logtrans.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Logarithmic Transformation (LOG)", "Logtrans", overlay=false)
diff --git a/lib/numerics/lowest/lowest.pine b/lib/numerics/lowest/lowest.pine
index 9a3b2ec5..5e5d8953 100644
--- a/lib/numerics/lowest/lowest.pine
+++ b/lib/numerics/lowest/lowest.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Lowest Value (LOWEST)", "LOWEST", overlay=true)
diff --git a/lib/numerics/maxindex/Maxindex.cs b/lib/numerics/maxindex/Maxindex.cs
new file mode 100644
index 00000000..44321a97
--- /dev/null
+++ b/lib/numerics/maxindex/Maxindex.cs
@@ -0,0 +1,272 @@
+using System.Runtime.CompilerServices;
+
+namespace QuanTAlib;
+
+///
+/// MAXINDEX: Rolling Maximum Index
+/// Returns the position of the maximum value within a rolling window.
+/// Streaming mode: bars-ago offset (0=current, period-1=oldest).
+/// Batch span mode: absolute array index (TA-Lib compatible).
+///
+///
+/// Key properties:
+/// - Returns the index/position of the highest value, not the value itself
+/// - Streaming output is "bars-ago" offset for natural streaming consumption
+/// - Batch(ReadOnlySpan) output is absolute array index matching TA-Lib MAXINDEX
+/// - Tie-breaking: last occurrence wins (most recent bar, using >= comparison)
+/// - Can be cross-validated: source[Maxindex.Batch[i]] == Highest.Batch[i]
+///
+[SkipLocalsInit]
+public sealed class Maxindex : AbstractBase
+{
+ private readonly int _period;
+ private readonly RingBuffer _buffer;
+ private record struct State(double LastValid);
+ private State _state, _p_state;
+
+ public override bool IsHot => _buffer.Count >= _period;
+
+ ///
+ /// Initializes a new Maxindex indicator with specified lookback period.
+ ///
+ /// Lookback window size (must be >= 2)
+ public Maxindex(int period)
+ {
+ if (period < 2)
+ {
+ throw new ArgumentException("Period must be >= 2", nameof(period));
+ }
+
+ _period = period;
+ _buffer = new RingBuffer(period);
+ Name = $"Maxindex({period})";
+ WarmupPeriod = period;
+ }
+
+ ///
+ /// Initializes a new Maxindex indicator with source for event-based chaining.
+ ///
+ /// Source indicator for chaining
+ /// Lookback window size
+ public Maxindex(ITValuePublisher source, int period) : this(period)
+ {
+ source.Pub += HandleUpdate;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override TValue Update(TValue input, bool isNew = true)
+ {
+ if (isNew)
+ {
+ _p_state = _state;
+ }
+ else
+ {
+ _state = _p_state;
+ }
+
+ double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
+ _state = new State(value);
+
+ _buffer.Add(value, isNew);
+
+ // Scan the ring buffer to find the bars-ago index of the maximum value.
+ // Tie-breaking: >= means last occurrence (most recent) wins.
+ ReadOnlySpan span = _buffer.GetSpan();
+ int len = span.Length;
+ double maxVal = span[0];
+ int maxPos = 0;
+ for (int i = 1; i < len; i++)
+ {
+ if (span[i] >= maxVal)
+ {
+ maxVal = span[i];
+ maxPos = i;
+ }
+ }
+
+ // Convert to bars-ago: newest element is at index (len - 1), oldest at 0.
+ // bars-ago = (len - 1) - maxPos
+ double result = (len - 1) - maxPos;
+
+ Last = new TValue(input.Time, result);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ public override TSeries Update(TSeries source)
+ {
+ var result = new TSeries(source.Count);
+ ReadOnlySpan values = source.Values;
+ ReadOnlySpan times = source.Times;
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
+ result.Add(tv, true);
+ }
+ return result;
+ }
+
+ public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
+ DateTime time = DateTime.UtcNow - (interval * source.Length);
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ Update(input: new TValue(time, source[i]), isNew: true);
+ time += interval;
+ }
+ }
+
+ public static TSeries Batch(TSeries source, int period)
+ {
+ var indicator = new Maxindex(period);
+ return indicator.Update(source);
+ }
+
+ ///
+ /// Calculates rolling maximum index over a span of values.
+ /// Output contains ABSOLUTE array indices (TA-Lib MAXINDEX compatible).
+ ///
+ public static void Batch(ReadOnlySpan source, Span output, int period)
+ {
+ if (source.Length == 0)
+ {
+ throw new ArgumentException("Source cannot be empty", nameof(source));
+ }
+
+ if (output.Length < source.Length)
+ {
+ throw new ArgumentException("Output length must be >= source length", nameof(output));
+ }
+
+ if (period < 2)
+ {
+ throw new ArgumentException("Period must be >= 2", nameof(period));
+ }
+
+ int len = source.Length;
+
+ // Use monotonic deque algorithm — same as Highest but output index, not value.
+ int[]? rentedDeque = null;
+ double[]? rentedValues = null;
+
+#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
+ Span deque = period <= 256
+ ? stackalloc int[period]
+ : (rentedDeque = System.Buffers.ArrayPool.Shared.Rent(period)).AsSpan(0, period);
+
+ Span values = len <= 256
+ ? stackalloc double[len]
+ : (rentedValues = System.Buffers.ArrayPool.Shared.Rent(len)).AsSpan(0, len);
+#pragma warning restore S1121
+
+ try
+ {
+ // First pass: store corrected values (handle NaN/Infinity)
+ double lastValid = 0.0;
+ for (int i = 0; i < len; i++)
+ {
+ double val = source[i];
+ if (double.IsFinite(val))
+ {
+ lastValid = val;
+ values[i] = val;
+ }
+ else
+ {
+ values[i] = lastValid;
+ }
+ }
+
+ // Second pass: compute rolling max index using monotonic deque.
+ // Circular buffer indexing — branch-based wrapping is faster than modulo.
+ int head = 0; // front of deque (oldest/max)
+ int tail = 0; // back of deque (newest)
+ int count = 0; // number of elements in deque
+ int capacity = deque.Length;
+
+ for (int i = 0; i < len; i++)
+ {
+ double value = values[i];
+
+ // Remove indices outside window from front
+ while (count > 0 && deque[head] <= i - period)
+ {
+ head++;
+ if (head >= capacity)
+ {
+ head -= capacity;
+ }
+
+ count--;
+ }
+
+ // Remove smaller-or-equal values from back (>= tie-breaking: last occurrence wins)
+ while (count > 0)
+ {
+ int backIdx = tail - 1;
+ if (backIdx < 0)
+ {
+ backIdx += capacity;
+ }
+
+ if (values[deque[backIdx]] <= value)
+ {
+ tail = backIdx;
+ count--;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // Add current index at tail
+ deque[tail] = i;
+ tail++;
+ if (tail >= capacity)
+ {
+ tail -= capacity;
+ }
+
+ count++;
+
+ // Output the ABSOLUTE index of the maximum (not the value)
+ output[i] = deque[head];
+ }
+ }
+ finally
+ {
+ if (rentedDeque != null)
+ {
+ System.Buffers.ArrayPool.Shared.Return(rentedDeque);
+ }
+
+ if (rentedValues != null)
+ {
+ System.Buffers.ArrayPool.Shared.Return(rentedValues);
+ }
+ }
+ }
+
+ public static (TSeries Results, Maxindex Indicator) Calculate(TSeries source, int period)
+ {
+ var indicator = new Maxindex(period);
+ TSeries results = indicator.Update(source);
+ return (results, indicator);
+ }
+
+ public override void Reset()
+ {
+ _buffer.Clear();
+ _state = default;
+ _p_state = default;
+ Last = default;
+ }
+}
diff --git a/lib/numerics/maxindex/Maxindex.md b/lib/numerics/maxindex/Maxindex.md
new file mode 100644
index 00000000..77923d9d
--- /dev/null
+++ b/lib/numerics/maxindex/Maxindex.md
@@ -0,0 +1,167 @@
+# MAXINDEX: Rolling Maximum Index
+
+| Property | Value |
+| ---------------- | -------------------------------- |
+| **Category** | Numeric |
+| **Inputs** | Source (close) |
+| **Parameters** | `period` (default=14, min=2) |
+| **Outputs** | Single series (Maxindex) |
+| **Output range** | Streaming: 0 to period-1 (bars-ago); Batch span: absolute array index |
+| **Warmup** | `period` bars |
+
+### TL;DR
+
+- MAXINDEX finds the position (index) of the maximum value within a rolling lookback window.
+- Parameterized by `period` (minimum 2).
+- Streaming mode outputs bars-ago offset (0 = current bar holds the max, period-1 = oldest bar).
+- Batch span mode outputs absolute array indices (TA-Lib MAXINDEX compatible).
+- Tie-breaking: last occurrence wins (most recent bar, `>=` comparison).
+- Requires `period` bars of warmup before first valid output (IsHot = true).
+- Cross-validation: `source[Maxindex.Batch[i]] == Highest.Batch[i]` for all bars after warmup.
+
+> "It's not just about the peak — it's about *when* the peak occurred."
+
+MAXINDEX identifies the position of the maximum value within a rolling window. While HIGHEST tells you the peak *value*, MAXINDEX tells you *where* that peak is relative to the current bar. This is essential for pattern recognition, timing analysis, and detecting how "stale" a high is.
+
+## Historical Context
+
+The MAXINDEX function originates from TA-Lib (TA_MAXINDEX), used in quantitative trading systems to identify when the highest price in a lookback window occurred. This timing information is critical for:
+
+- **Breakout freshness**: A max at position 0 means the breakout is happening *now*; at position period-1, the high is stale and fading.
+- **Pattern detection**: Identifying head-and-shoulders, double tops, and other formations requires knowing *when* peaks occurred.
+- **Momentum analysis**: The position of the high within the window indicates whether momentum is building or decaying.
+
+## Architecture & Physics
+
+### 1. Streaming Mode — Bars-Ago Offset
+
+In streaming mode, the output represents how many bars ago the maximum occurred:
+
+$$
+\text{Maxindex}_t = t - \arg\max_{t-n+1 \leq k \leq t} V_k
+$$
+
+where $n$ is the lookback period. A value of 0 means the current bar is the maximum; a value of $n-1$ means the oldest bar in the window holds the maximum.
+
+### 2. Batch Span Mode — Absolute Index
+
+In the `Batch(ReadOnlySpan)` method, output is the absolute array index:
+
+$$
+\text{output}[i] = \arg\max_{i-n+1 \leq k \leq i} V_k
+$$
+
+This matches TA-Lib's MAXINDEX convention and enables direct array lookup: `source[output[i]]` yields the maximum value.
+
+### 3. Tie-Breaking
+
+When multiple values in the window are equal to the maximum, the **most recent** (rightmost) occurrence wins:
+
+$$
+\text{Maxindex}_t = \max \{ k : V_k = \max(\text{window}) \}
+$$
+
+This is achieved using `>=` comparison, matching TA-Lib behavior.
+
+### 4. Monotonic Deque (Batch Mode)
+
+The batch span method uses the same O(n) monotonic deque algorithm as Highest, but outputs the index stored at the deque head rather than the value at that index:
+
+```
+// Highest: output[i] = values[deque.PeekHead()] → the VALUE
+// Maxindex: output[i] = deque.PeekHead() → the INDEX
+```
+
+### 5. Bar Correction via Rollback
+
+When `isNew=false`, the indicator:
+1. Restores previous state (`_state = _p_state`)
+2. Replaces the last value in the buffer
+3. Re-scans the buffer to find the new maximum position
+
+## Mathematical Foundation
+
+### Rolling Maximum Index Definition
+
+$$
+\text{Maxindex}_t = \arg\max_{t-n+1 \leq k \leq t} V_k
+$$
+
+where $n$ is the lookback period and ties are broken in favor of the most recent occurrence.
+
+### Partial Window Behavior
+
+Before the window is full:
+
+$$
+\text{Maxindex}_t = \arg\max_{0 \leq k \leq t} V_k \quad \text{for } t < n
+$$
+
+### Complexity Analysis
+
+| Operation | Streaming | Batch (Deque) |
+| :--- | :---: | :---: |
+| Per-update (worst) | O(n) | O(n) |
+| Per-update (amortized) | O(n) | O(1) |
+| Total for N updates | O(N×n) | O(N) |
+
+Streaming uses a linear scan of the RingBuffer, which is O(period) per bar — acceptable for typical periods (5–30). Batch mode uses the monotonic deque for O(1) amortized.
+
+## Performance Profile
+
+### Streaming Mode (Linear Scan)
+
+| Operation | Count | Cost (cycles) | Subtotal |
+| :--- | :---: | :---: | :---: |
+| CMP (scan) | period | 1 | period |
+| Array access | period | 3 | 3×period |
+| Index arithmetic | 2 | 1 | 2 |
+| **Total** | — | — | **~4×period cycles** |
+
+### Batch Mode (Monotonic Deque)
+
+| Operation | Count | Cost (cycles) | Subtotal |
+| :--- | :---: | :---: | :---: |
+| CMP (expired check) | 1 | 1 | 1 |
+| CMP (monotonicity) | ~2 avg | 1 | 2 |
+| Array access | 3 | 3 | 9 |
+| Index arithmetic | 2 | 1 | 2 |
+| **Total** | **~8** | — | **~14 cycles** |
+
+### Quality Metrics
+
+| Metric | Score | Notes |
+| :--- | :---: | :--- |
+| **Accuracy** | 10/10 | Exact index of maximum |
+| **Timeliness** | 10/10 | Zero lag for index detection |
+| **Smoothness** | 2/10 | Discrete jumps as window slides |
+| **Computational Cost** | 8/10 | O(period) streaming, O(1) batch |
+| **Memory** | 7/10 | O(n) for buffer + deque |
+
+## Validation
+
+| Library | Status | Notes |
+| :--- | :---: | :--- |
+| **TA-Lib MAXINDEX** | ✅ | Batch span output matches absolute indices |
+| **Cross-validation** | ✅ | `source[Maxindex[i]] == Highest[i]` for all valid bars |
+| **Known Values** | ✅ | Manual verification |
+
+## Common Pitfalls
+
+1. **Two Output Modes**: Streaming returns bars-ago offset; Batch(ReadOnlySpan) returns absolute array index. Do not mix them up.
+
+2. **Period Minimum is 2**: Unlike Highest (which accepts period=1), Maxindex requires period >= 2, since the index of a single element is trivially 0.
+
+3. **Tie-Breaking**: Uses `>=` so the most recent (rightmost) occurrence wins ties. This matches TA-Lib convention.
+
+4. **Window Boundary Effects**: When the previous max expires from the window, the index can jump abruptly. This is expected behavior.
+
+5. **Warmup Period**: `IsHot` becomes true after `period` values. Before warmup, returns index within available data.
+
+6. **Using isNew Incorrectly**: Use `isNew: false` only when correcting the current bar. New bars must use `isNew: true`.
+
+## References
+
+- TA-Lib: MAXINDEX function documentation.
+- Lemire, Daniel. (2006). "Streaming Maximum-Minimum Filter Using No More than Three Comparisons per Element."
+- Tarjan, Robert E. (1985). "Amortized Computational Complexity." SIAM Journal on Algebraic Discrete Methods.
diff --git a/lib/numerics/minindex/Minindex.cs b/lib/numerics/minindex/Minindex.cs
new file mode 100644
index 00000000..397c89ce
--- /dev/null
+++ b/lib/numerics/minindex/Minindex.cs
@@ -0,0 +1,272 @@
+using System.Runtime.CompilerServices;
+
+namespace QuanTAlib;
+
+///
+/// MININDEX: Rolling Minimum Index
+/// Returns the position of the minimum value within a rolling window.
+/// Streaming mode: bars-ago offset (0=current, period-1=oldest).
+/// Batch span mode: absolute array index (TA-Lib compatible).
+///
+///
+/// Key properties:
+/// - Returns the index/position of the lowest value, not the value itself
+/// - Streaming output is "bars-ago" offset for natural streaming consumption
+/// - Batch(ReadOnlySpan) output is absolute array index matching TA-Lib MININDEX
+/// - Tie-breaking: last occurrence wins (most recent bar, using <= comparison)
+/// - Can be cross-validated: source[Minindex.Batch[i]] == Lowest.Batch[i]
+///
+[SkipLocalsInit]
+public sealed class Minindex : AbstractBase
+{
+ private readonly int _period;
+ private readonly RingBuffer _buffer;
+ private record struct State(double LastValid);
+ private State _state, _p_state;
+
+ public override bool IsHot => _buffer.Count >= _period;
+
+ ///
+ /// Initializes a new Minindex indicator with specified lookback period.
+ ///
+ /// Lookback window size (must be >= 2)
+ public Minindex(int period)
+ {
+ if (period < 2)
+ {
+ throw new ArgumentException("Period must be >= 2", nameof(period));
+ }
+
+ _period = period;
+ _buffer = new RingBuffer(period);
+ Name = $"Minindex({period})";
+ WarmupPeriod = period;
+ }
+
+ ///
+ /// Initializes a new Minindex indicator with source for event-based chaining.
+ ///
+ /// Source indicator for chaining
+ /// Lookback window size
+ public Minindex(ITValuePublisher source, int period) : this(period)
+ {
+ source.Pub += HandleUpdate;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override TValue Update(TValue input, bool isNew = true)
+ {
+ if (isNew)
+ {
+ _p_state = _state;
+ }
+ else
+ {
+ _state = _p_state;
+ }
+
+ double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
+ _state = new State(value);
+
+ _buffer.Add(value, isNew);
+
+ // Scan the ring buffer to find the bars-ago index of the minimum value.
+ // Tie-breaking: <= means last occurrence (most recent) wins.
+ ReadOnlySpan span = _buffer.GetSpan();
+ int len = span.Length;
+ double minVal = span[0];
+ int minPos = 0;
+ for (int i = 1; i < len; i++)
+ {
+ if (span[i] <= minVal)
+ {
+ minVal = span[i];
+ minPos = i;
+ }
+ }
+
+ // Convert to bars-ago: newest element is at index (len - 1), oldest at 0.
+ // bars-ago = (len - 1) - minPos
+ double result = (len - 1) - minPos;
+
+ Last = new TValue(input.Time, result);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ public override TSeries Update(TSeries source)
+ {
+ var result = new TSeries(source.Count);
+ ReadOnlySpan values = source.Values;
+ ReadOnlySpan times = source.Times;
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
+ result.Add(tv, true);
+ }
+ return result;
+ }
+
+ public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
+ DateTime time = DateTime.UtcNow - (interval * source.Length);
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ Update(input: new TValue(time, source[i]), isNew: true);
+ time += interval;
+ }
+ }
+
+ public static TSeries Batch(TSeries source, int period)
+ {
+ var indicator = new Minindex(period);
+ return indicator.Update(source);
+ }
+
+ ///
+ /// Calculates rolling minimum index over a span of values.
+ /// Output contains ABSOLUTE array indices (TA-Lib MININDEX compatible).
+ ///
+ public static void Batch(ReadOnlySpan source, Span output, int period)
+ {
+ if (source.Length == 0)
+ {
+ throw new ArgumentException("Source cannot be empty", nameof(source));
+ }
+
+ if (output.Length < source.Length)
+ {
+ throw new ArgumentException("Output length must be >= source length", nameof(output));
+ }
+
+ if (period < 2)
+ {
+ throw new ArgumentException("Period must be >= 2", nameof(period));
+ }
+
+ int len = source.Length;
+
+ // Use monotonic deque algorithm — same as Lowest but output index, not value.
+ int[]? rentedDeque = null;
+ double[]? rentedValues = null;
+
+#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
+ Span deque = period <= 256
+ ? stackalloc int[period]
+ : (rentedDeque = System.Buffers.ArrayPool.Shared.Rent(period)).AsSpan(0, period);
+
+ Span values = len <= 256
+ ? stackalloc double[len]
+ : (rentedValues = System.Buffers.ArrayPool.Shared.Rent(len)).AsSpan(0, len);
+#pragma warning restore S1121
+
+ try
+ {
+ // First pass: store corrected values (handle NaN/Infinity)
+ double lastValid = 0.0;
+ for (int i = 0; i < len; i++)
+ {
+ double val = source[i];
+ if (double.IsFinite(val))
+ {
+ lastValid = val;
+ values[i] = val;
+ }
+ else
+ {
+ values[i] = lastValid;
+ }
+ }
+
+ // Second pass: compute rolling min index using monotonic deque.
+ // Circular buffer indexing — branch-based wrapping is faster than modulo.
+ int head = 0; // front of deque (oldest/min)
+ int tail = 0; // back of deque (newest)
+ int count = 0; // number of elements in deque
+ int capacity = deque.Length;
+
+ for (int i = 0; i < len; i++)
+ {
+ double value = values[i];
+
+ // Remove indices outside window from front
+ while (count > 0 && deque[head] <= i - period)
+ {
+ head++;
+ if (head >= capacity)
+ {
+ head -= capacity;
+ }
+
+ count--;
+ }
+
+ // Remove larger-or-equal values from back (<= tie-breaking: last occurrence wins)
+ while (count > 0)
+ {
+ int backIdx = tail - 1;
+ if (backIdx < 0)
+ {
+ backIdx += capacity;
+ }
+
+ if (values[deque[backIdx]] >= value)
+ {
+ tail = backIdx;
+ count--;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ // Add current index at tail
+ deque[tail] = i;
+ tail++;
+ if (tail >= capacity)
+ {
+ tail -= capacity;
+ }
+
+ count++;
+
+ // Output the ABSOLUTE index of the minimum (not the value)
+ output[i] = deque[head];
+ }
+ }
+ finally
+ {
+ if (rentedDeque != null)
+ {
+ System.Buffers.ArrayPool.Shared.Return(rentedDeque);
+ }
+
+ if (rentedValues != null)
+ {
+ System.Buffers.ArrayPool.Shared.Return(rentedValues);
+ }
+ }
+ }
+
+ public static (TSeries Results, Minindex Indicator) Calculate(TSeries source, int period)
+ {
+ var indicator = new Minindex(period);
+ TSeries results = indicator.Update(source);
+ return (results, indicator);
+ }
+
+ public override void Reset()
+ {
+ _buffer.Clear();
+ _state = default;
+ _p_state = default;
+ Last = default;
+ }
+}
diff --git a/lib/numerics/minindex/Minindex.md b/lib/numerics/minindex/Minindex.md
new file mode 100644
index 00000000..73b5a65d
--- /dev/null
+++ b/lib/numerics/minindex/Minindex.md
@@ -0,0 +1,167 @@
+# MININDEX: Rolling Minimum Index
+
+| Property | Value |
+| ---------------- | -------------------------------- |
+| **Category** | Numeric |
+| **Inputs** | Source (close) |
+| **Parameters** | `period` (default=14, min=2) |
+| **Outputs** | Single series (Minindex) |
+| **Output range** | Streaming: 0 to period-1 (bars-ago); Batch span: absolute array index |
+| **Warmup** | `period` bars |
+
+### TL;DR
+
+- MININDEX finds the position (index) of the minimum value within a rolling lookback window.
+- Parameterized by `period` (minimum 2).
+- Streaming mode outputs bars-ago offset (0 = current bar holds the min, period-1 = oldest bar).
+- Batch span mode outputs absolute array indices (TA-Lib MININDEX compatible).
+- Tie-breaking: last occurrence wins (most recent bar, `<=` comparison).
+- Requires `period` bars of warmup before first valid output (IsHot = true).
+- Cross-validation: `source[Minindex.Batch[i]] == Lowest.Batch[i]` for all bars after warmup.
+
+> "Finding support isn't just about the price — it's about *when* the floor was set."
+
+MININDEX identifies the position of the minimum value within a rolling window. While LOWEST tells you the trough *value*, MININDEX tells you *where* that trough is relative to the current bar. This is essential for support analysis, timing studies, and detecting how "stale" a low is.
+
+## Historical Context
+
+The MININDEX function originates from TA-Lib (TA_MININDEX), used in quantitative trading systems to identify when the lowest price in a lookback window occurred. This timing information is critical for:
+
+- **Support freshness**: A min at position 0 means support is being tested *now*; at position period-1, the low is stale and potentially irrelevant.
+- **Pattern detection**: Identifying double bottoms, inverse head-and-shoulders, and other formations requires knowing *when* troughs occurred.
+- **Exhaustion analysis**: The position of the low within the window indicates whether selling pressure is current or historical.
+
+## Architecture & Physics
+
+### 1. Streaming Mode — Bars-Ago Offset
+
+In streaming mode, the output represents how many bars ago the minimum occurred:
+
+$$
+\text{Minindex}_t = t - \arg\min_{t-n+1 \leq k \leq t} V_k
+$$
+
+where $n$ is the lookback period. A value of 0 means the current bar is the minimum; a value of $n-1$ means the oldest bar in the window holds the minimum.
+
+### 2. Batch Span Mode — Absolute Index
+
+In the `Batch(ReadOnlySpan)` method, output is the absolute array index:
+
+$$
+\text{output}[i] = \arg\min_{i-n+1 \leq k \leq i} V_k
+$$
+
+This matches TA-Lib's MININDEX convention and enables direct array lookup: `source[output[i]]` yields the minimum value.
+
+### 3. Tie-Breaking
+
+When multiple values in the window are equal to the minimum, the **most recent** (rightmost) occurrence wins:
+
+$$
+\text{Minindex}_t = \max \{ k : V_k = \min(\text{window}) \}
+$$
+
+This is achieved using `<=` comparison, matching TA-Lib behavior.
+
+### 4. Monotonic Deque (Batch Mode)
+
+The batch span method uses the same O(n) monotonic deque algorithm as Lowest, but outputs the index stored at the deque head rather than the value at that index:
+
+```
+// Lowest: output[i] = values[deque.PeekHead()] → the VALUE
+// Minindex: output[i] = deque.PeekHead() → the INDEX
+```
+
+### 5. Bar Correction via Rollback
+
+When `isNew=false`, the indicator:
+1. Restores previous state (`_state = _p_state`)
+2. Replaces the last value in the buffer
+3. Re-scans the buffer to find the new minimum position
+
+## Mathematical Foundation
+
+### Rolling Minimum Index Definition
+
+$$
+\text{Minindex}_t = \arg\min_{t-n+1 \leq k \leq t} V_k
+$$
+
+where $n$ is the lookback period and ties are broken in favor of the most recent occurrence.
+
+### Partial Window Behavior
+
+Before the window is full:
+
+$$
+\text{Minindex}_t = \arg\min_{0 \leq k \leq t} V_k \quad \text{for } t < n
+$$
+
+### Complexity Analysis
+
+| Operation | Streaming | Batch (Deque) |
+| :--- | :---: | :---: |
+| Per-update (worst) | O(n) | O(n) |
+| Per-update (amortized) | O(n) | O(1) |
+| Total for N updates | O(N×n) | O(N) |
+
+Streaming uses a linear scan of the RingBuffer, which is O(period) per bar — acceptable for typical periods (5–30). Batch mode uses the monotonic deque for O(1) amortized.
+
+## Performance Profile
+
+### Streaming Mode (Linear Scan)
+
+| Operation | Count | Cost (cycles) | Subtotal |
+| :--- | :---: | :---: | :---: |
+| CMP (scan) | period | 1 | period |
+| Array access | period | 3 | 3×period |
+| Index arithmetic | 2 | 1 | 2 |
+| **Total** | — | — | **~4×period cycles** |
+
+### Batch Mode (Monotonic Deque)
+
+| Operation | Count | Cost (cycles) | Subtotal |
+| :--- | :---: | :---: | :---: |
+| CMP (expired check) | 1 | 1 | 1 |
+| CMP (monotonicity) | ~2 avg | 1 | 2 |
+| Array access | 3 | 3 | 9 |
+| Index arithmetic | 2 | 1 | 2 |
+| **Total** | **~8** | — | **~14 cycles** |
+
+### Quality Metrics
+
+| Metric | Score | Notes |
+| :--- | :---: | :--- |
+| **Accuracy** | 10/10 | Exact index of minimum |
+| **Timeliness** | 10/10 | Zero lag for index detection |
+| **Smoothness** | 2/10 | Discrete jumps as window slides |
+| **Computational Cost** | 8/10 | O(period) streaming, O(1) batch |
+| **Memory** | 7/10 | O(n) for buffer + deque |
+
+## Validation
+
+| Library | Status | Notes |
+| :--- | :---: | :--- |
+| **TA-Lib MININDEX** | ✅ | Batch span output matches absolute indices |
+| **Cross-validation** | ✅ | `source[Minindex[i]] == Lowest[i]` for all valid bars |
+| **Known Values** | ✅ | Manual verification |
+
+## Common Pitfalls
+
+1. **Two Output Modes**: Streaming returns bars-ago offset; Batch(ReadOnlySpan) returns absolute array index. Do not mix them up.
+
+2. **Period Minimum is 2**: Unlike Lowest (which accepts period=1), Minindex requires period >= 2, since the index of a single element is trivially 0.
+
+3. **Tie-Breaking**: Uses `<=` so the most recent (rightmost) occurrence wins ties. This matches TA-Lib convention.
+
+4. **Window Boundary Effects**: When the previous min expires from the window, the index can jump abruptly. This is expected behavior.
+
+5. **Warmup Period**: `IsHot` becomes true after `period` values. Before warmup, returns index within available data.
+
+6. **Using isNew Incorrectly**: Use `isNew: false` only when correcting the current bar. New bars must use `isNew: true`.
+
+## References
+
+- TA-Lib: MININDEX function documentation.
+- Lemire, Daniel. (2006). "Streaming Maximum-Minimum Filter Using No More than Three Comparisons per Element."
+- Tarjan, Robert E. (1985). "Amortized Computational Complexity." SIAM Journal on Algebraic Discrete Methods.
diff --git a/lib/numerics/normalize/normalize.pine b/lib/numerics/normalize/normalize.pine
index 76592ea0..4cdae3cc 100644
--- a/lib/numerics/normalize/normalize.pine
+++ b/lib/numerics/normalize/normalize.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Min-Max Normalization (NORMALIZE)", "NORMALIZE", overlay=false, precision=6)
diff --git a/lib/numerics/normdist/normdist.pine b/lib/numerics/normdist/normdist.pine
index 1e972e8f..d0903db2 100644
--- a/lib/numerics/normdist/normdist.pine
+++ b/lib/numerics/normdist/normdist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Normal Distribution CDF (NORMDIST)", "NORMDIST", overlay=false, precision=6)
diff --git a/lib/numerics/poissondist/poissondist.pine b/lib/numerics/poissondist/poissondist.pine
index c2c6cfa5..0f17353d 100644
--- a/lib/numerics/poissondist/poissondist.pine
+++ b/lib/numerics/poissondist/poissondist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Poisson Distribution CDF (POISSONDIST)", "POISSONDIST", overlay=false, precision=6)
diff --git a/lib/numerics/relu/relu.pine b/lib/numerics/relu/relu.pine
index 791003c0..c181d4f7 100644
--- a/lib/numerics/relu/relu.pine
+++ b/lib/numerics/relu/relu.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Rectified Linear Unit (ReLU)", "ReLU", overlay=false, precision=6)
diff --git a/lib/numerics/sigmoid/sigmoid.pine b/lib/numerics/sigmoid/sigmoid.pine
index 597d103a..c4b60309 100644
--- a/lib/numerics/sigmoid/sigmoid.pine
+++ b/lib/numerics/sigmoid/sigmoid.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Logistic Function (SIGMOID)", "SIGMOID", overlay=false, precision=6)
diff --git a/lib/numerics/slope/slope.pine b/lib/numerics/slope/slope.pine
index 832c09d5..9fa4ded9 100644
--- a/lib/numerics/slope/slope.pine
+++ b/lib/numerics/slope/slope.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Slope, Linear Regression (SLOPE)", "SLOPE", overlay=false, precision=8)
diff --git a/lib/numerics/sqrttrans/sqrttrans.pine b/lib/numerics/sqrttrans/sqrttrans.pine
index 4eeaa705..11f95146 100644
--- a/lib/numerics/sqrttrans/sqrttrans.pine
+++ b/lib/numerics/sqrttrans/sqrttrans.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Square Root Transformation (SQRT)", "Sqrttrans", overlay=false)
diff --git a/lib/numerics/tdist/tdist.pine b/lib/numerics/tdist/tdist.pine
index 7c9045e8..eaa2c9f5 100644
--- a/lib/numerics/tdist/tdist.pine
+++ b/lib/numerics/tdist/tdist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Student's t-Distribution CDF (TDIST)", "TDIST", overlay=false, precision=6)
diff --git a/lib/numerics/weibulldist/weibulldist.pine b/lib/numerics/weibulldist/weibulldist.pine
index c32d5931..e3bcf1f8 100644
--- a/lib/numerics/weibulldist/weibulldist.pine
+++ b/lib/numerics/weibulldist/weibulldist.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Weibull Distribution CDF (WEIBULLDIST)", "WEIBULLDIST", overlay=false, precision=6)
diff --git a/lib/oscillators/ac/ac.pine b/lib/oscillators/ac/ac.pine
index 311f6b18..528ac4bb 100644
--- a/lib/oscillators/ac/ac.pine
+++ b/lib/oscillators/ac/ac.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Accelerator Oscillator (AC)", "AC", overlay=false)
diff --git a/lib/oscillators/ao/ao.pine b/lib/oscillators/ao/ao.pine
index 24d226cd..8ce41f72 100644
--- a/lib/oscillators/ao/ao.pine
+++ b/lib/oscillators/ao/ao.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Awesome Oscillator (AO)", "AO", overlay=false)
diff --git a/lib/oscillators/apo/apo.pine b/lib/oscillators/apo/apo.pine
index e62d5828..cae5d931 100644
--- a/lib/oscillators/apo/apo.pine
+++ b/lib/oscillators/apo/apo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Absolute Price Oscillator (APO)", "APO", overlay=false)
diff --git a/lib/oscillators/bbb/bbb.pine b/lib/oscillators/bbb/bbb.pine
index cdd3dce1..dd3da7c0 100644
--- a/lib/oscillators/bbb/bbb.pine
+++ b/lib/oscillators/bbb/bbb.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bollinger %B", "BBB", overlay=false)
diff --git a/lib/oscillators/bbi/bbi.pine b/lib/oscillators/bbi/bbi.pine
index 3b51f447..fb0abcf3 100644
--- a/lib/oscillators/bbi/bbi.pine
+++ b/lib/oscillators/bbi/bbi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bulls Bears Index (BBI)", "BBI", overlay=true)
diff --git a/lib/oscillators/bbs/bbs.pine b/lib/oscillators/bbs/bbs.pine
index 6c376fe0..a4bed776 100644
--- a/lib/oscillators/bbs/bbs.pine
+++ b/lib/oscillators/bbs/bbs.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bollinger Band Squeeze (BBS)", "BBS", overlay=true)
diff --git a/lib/oscillators/brar/brar.pine b/lib/oscillators/brar/brar.pine
index 0e8979af..9fcd7c2a 100644
--- a/lib/oscillators/brar/brar.pine
+++ b/lib/oscillators/brar/brar.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bull-Bear Power Ratio (BRAR)", "BRAR", overlay=false)
diff --git a/lib/oscillators/cfo/cfo.pine b/lib/oscillators/cfo/cfo.pine
index a8d2a681..b7079fcd 100644
--- a/lib/oscillators/cfo/cfo.pine
+++ b/lib/oscillators/cfo/cfo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Chande Forecast Oscillator", "CFO", overlay=false)
diff --git a/lib/oscillators/coppock/coppock.pine b/lib/oscillators/coppock/coppock.pine
index 359319f8..0e7a7eaf 100644
--- a/lib/oscillators/coppock/coppock.pine
+++ b/lib/oscillators/coppock/coppock.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Coppock Curve (COPPOCK)", "COPPOCK", overlay=false)
diff --git a/lib/oscillators/crsi/crsi.pine b/lib/oscillators/crsi/crsi.pine
index ac003171..03cbab99 100644
--- a/lib/oscillators/crsi/crsi.pine
+++ b/lib/oscillators/crsi/crsi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Connors RSI (CRSI)", "CRSI", overlay=false)
diff --git a/lib/oscillators/cti/cti.pine b/lib/oscillators/cti/cti.pine
index 81a5d289..aa17cba4 100644
--- a/lib/oscillators/cti/cti.pine
+++ b/lib/oscillators/cti/cti.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Correlation Trend Indicator (CTI)", "CTI", overlay=false)
diff --git a/lib/oscillators/deco/deco.pine b/lib/oscillators/deco/deco.pine
index 0e750db5..194ff21a 100644
--- a/lib/oscillators/deco/deco.pine
+++ b/lib/oscillators/deco/deco.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Decycler Oscillator (DECO)", "DECO", overlay=false)
diff --git a/lib/oscillators/dem/dem.pine b/lib/oscillators/dem/dem.pine
index 6cb636c2..70de1f85 100644
--- a/lib/oscillators/dem/dem.pine
+++ b/lib/oscillators/dem/dem.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("DeMarker Oscillator (DEM)", "DEM", overlay=false)
diff --git a/lib/oscillators/dosc/dosc.pine b/lib/oscillators/dosc/dosc.pine
index 69594457..2d1f97c8 100644
--- a/lib/oscillators/dosc/dosc.pine
+++ b/lib/oscillators/dosc/dosc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Derivative Oscillator (DOSC)", "DOSC", overlay=false, precision=4)
diff --git a/lib/oscillators/dpo/dpo.pine b/lib/oscillators/dpo/dpo.pine
index aada689a..59a58919 100644
--- a/lib/oscillators/dpo/dpo.pine
+++ b/lib/oscillators/dpo/dpo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Detrended Price Oscillator (DPO)", "DPO", overlay=false)
diff --git a/lib/oscillators/dymoi/dymoi.pine b/lib/oscillators/dymoi/dymoi.pine
index 79a88d44..b4b02312 100644
--- a/lib/oscillators/dymoi/dymoi.pine
+++ b/lib/oscillators/dymoi/dymoi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Dynamic Momentum Index (DYMOI)", "DYMOI", overlay=false)
diff --git a/lib/oscillators/er/er.pine b/lib/oscillators/er/er.pine
index 371350f7..6ccc8e88 100644
--- a/lib/oscillators/er/er.pine
+++ b/lib/oscillators/er/er.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Efficiency Ratio (ER)", "ER", overlay=false)
diff --git a/lib/oscillators/eri/eri.pine b/lib/oscillators/eri/eri.pine
index 869675a0..8b018774 100644
--- a/lib/oscillators/eri/eri.pine
+++ b/lib/oscillators/eri/eri.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Elder Ray Index (ERI)", "ERI", overlay=false)
diff --git a/lib/oscillators/fi/fi.pine b/lib/oscillators/fi/fi.pine
index 3c38c9fc..e3ec07f1 100644
--- a/lib/oscillators/fi/fi.pine
+++ b/lib/oscillators/fi/fi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Force Index (FI)", "FI", overlay=false)
diff --git a/lib/oscillators/fisher/fisher.pine b/lib/oscillators/fisher/fisher.pine
index 2f7329e7..c4201969 100644
--- a/lib/oscillators/fisher/fisher.pine
+++ b/lib/oscillators/fisher/fisher.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Fisher Transform (FISHER)", "FISHER", overlay=false)
diff --git a/lib/oscillators/gator/gator.pine b/lib/oscillators/gator/gator.pine
index 0b5d3ac6..a22fa9ca 100644
--- a/lib/oscillators/gator/gator.pine
+++ b/lib/oscillators/gator/gator.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Williams Gator Oscillator", "GATOR", overlay=false)
diff --git a/lib/oscillators/imi/imi.pine b/lib/oscillators/imi/imi.pine
index 3b24c12c..d92080f3 100644
--- a/lib/oscillators/imi/imi.pine
+++ b/lib/oscillators/imi/imi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Intraday Momentum Index (IMI)", "IMI", overlay=false)
diff --git a/lib/oscillators/inertia/inertia.pine b/lib/oscillators/inertia/inertia.pine
index aa558089..55e15f99 100644
--- a/lib/oscillators/inertia/inertia.pine
+++ b/lib/oscillators/inertia/inertia.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Inertia", "INERTIA", overlay=false)
diff --git a/lib/oscillators/kdj/kdj.pine b/lib/oscillators/kdj/kdj.pine
index 0afc07a6..12f9782b 100644
--- a/lib/oscillators/kdj/kdj.pine
+++ b/lib/oscillators/kdj/kdj.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("KDJ", "KDJ", overlay=false)
diff --git a/lib/oscillators/kri/kri.pine b/lib/oscillators/kri/kri.pine
index 12030f52..c5b2b4d4 100644
--- a/lib/oscillators/kri/kri.pine
+++ b/lib/oscillators/kri/kri.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Kairi Relative Index (KRI)", "KRI", overlay=false, precision=4)
diff --git a/lib/oscillators/kst/kst.pine b/lib/oscillators/kst/kst.pine
index f7eceea0..0ed6ca26 100644
--- a/lib/oscillators/kst/kst.pine
+++ b/lib/oscillators/kst/kst.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("KST Oscillator (KST)", "KST", overlay=false, precision=4)
diff --git a/lib/oscillators/mstoch/mstoch.pine b/lib/oscillators/mstoch/mstoch.pine
index 4fb9e1bb..d33332f1 100644
--- a/lib/oscillators/mstoch/mstoch.pine
+++ b/lib/oscillators/mstoch/mstoch.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Indicator algorithm (C) 2013 John F. Ehlers
diff --git a/lib/oscillators/pgo/pgo.pine b/lib/oscillators/pgo/pgo.pine
index 1e84e5b2..f3f44589 100644
--- a/lib/oscillators/pgo/pgo.pine
+++ b/lib/oscillators/pgo/pgo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pretty Good Oscillator", "PGO", overlay=false)
diff --git a/lib/oscillators/psl/psl.pine b/lib/oscillators/psl/psl.pine
index 4d9dd031..956f0165 100644
--- a/lib/oscillators/psl/psl.pine
+++ b/lib/oscillators/psl/psl.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Psychological Line (PSL)", "PSL", overlay=false)
diff --git a/lib/oscillators/qqe/Qqe.Validation.Tests.cs b/lib/oscillators/qqe/Qqe.Validation.Tests.cs
index ca3c31a8..84f3d662 100644
--- a/lib/oscillators/qqe/Qqe.Validation.Tests.cs
+++ b/lib/oscillators/qqe/Qqe.Validation.Tests.cs
@@ -241,4 +241,33 @@ public sealed class QqeValidationTests
Assert.NotEqual(ind1.QqeValue, ind2.QqeValue);
_output.WriteLine($"QQE(14,5,4.236)={ind1.QqeValue:F6} QQE(7,3,2)={ind2.QqeValue:F6}");
}
+
+ [Fact]
+ public void Qqe_Correction_Recomputes()
+ {
+ var ind = new Qqe();
+ var t0 = DateTime.MinValue;
+
+ // Build state well past warmup (WarmupPeriod ≈ 37)
+ for (int i = 0; i < 60; i++)
+ {
+ ind.Update(new TValue(t0.AddSeconds(i), 100.0 + i * 0.5));
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddSeconds(60);
+ const double anchorPrice = 130.0;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorQqe = ind.QqeValue;
+ double anchorSignal = ind.Signal;
+
+ // Use large downward spike (÷10) to move RSI away from ceiling
+ ind.Update(new TValue(anchorTime, anchorPrice / 10), isNew: false);
+ Assert.NotEqual(anchorQqe, ind.QqeValue);
+
+ // Correction back to original — both outputs must restore exactly
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorQqe, ind.QqeValue, 1e-9);
+ Assert.Equal(anchorSignal, ind.Signal, 1e-9);
+ }
}
diff --git a/lib/oscillators/qqe/Qqe.cs b/lib/oscillators/qqe/Qqe.cs
index 53bb35db..3ff61751 100644
--- a/lib/oscillators/qqe/Qqe.cs
+++ b/lib/oscillators/qqe/Qqe.cs
@@ -278,6 +278,10 @@ public sealed class Qqe : AbstractBase
}
/// Span-based batch calculation (QQE line only).
+ ///
+ /// Computes only the QQE line (EMA-smoothed RSI). The dynamic trailing line
+ /// is not available from this API; use streaming to access both outputs.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan source, Span output,
int rsiPeriod = DefaultRsiPeriod,
diff --git a/lib/oscillators/qqe/qqe.pine b/lib/oscillators/qqe/qqe.pine
index 72a05201..306233d4 100644
--- a/lib/oscillators/qqe/qqe.pine
+++ b/lib/oscillators/qqe/qqe.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Quantitative Qualitative Estimation (QQE)", "QQE", overlay=false)
diff --git a/lib/oscillators/reverseema/reverseema.pine b/lib/oscillators/reverseema/reverseema.pine
index c78a7b61..15b95ff9 100644
--- a/lib/oscillators/reverseema/reverseema.pine
+++ b/lib/oscillators/reverseema/reverseema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
// Indicator algorithm (C) 2017 John F. Ehlers
//@version=6
diff --git a/lib/oscillators/rvgi/rvgi.pine b/lib/oscillators/rvgi/rvgi.pine
index 56df8114..805c3f47 100644
--- a/lib/oscillators/rvgi/rvgi.pine
+++ b/lib/oscillators/rvgi/rvgi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Relative Vigor Index (RVGI)", "RVGI", overlay=false)
diff --git a/lib/oscillators/smi/smi.pine b/lib/oscillators/smi/smi.pine
index 8c5d23dc..69637d3d 100644
--- a/lib/oscillators/smi/smi.pine
+++ b/lib/oscillators/smi/smi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Stochastic Momentum Index (SMI)", "SMI", overlay=false)
diff --git a/lib/oscillators/squeeze/Squeeze.Validation.Tests.cs b/lib/oscillators/squeeze/Squeeze.Validation.Tests.cs
index c7dd433e..0986d47e 100644
--- a/lib/oscillators/squeeze/Squeeze.Validation.Tests.cs
+++ b/lib/oscillators/squeeze/Squeeze.Validation.Tests.cs
@@ -215,4 +215,35 @@ public sealed class SqueezeValidationTests
Assert.Equal(sqStream.Momentum, sqBatch.Momentum, precision: 8);
Assert.Equal(sqStream.SqueezeOn, sqBatch.SqueezeOn);
}
+
+ [Fact]
+ public void Squeeze_Correction_Recomputes()
+ {
+ var ind = new Squeeze(period: 20);
+ long t0 = TimeSpan.TicksPerSecond;
+
+ // Build state well past warmup (WarmupPeriod = 20)
+ for (int i = 0; i < 50; i++)
+ {
+ double p = 100.0 + i * 0.5;
+ ind.Update(new TBar(t0 + i * TimeSpan.TicksPerSecond, p, p + 1, p - 1, p, 1000), isNew: true);
+ }
+
+ // Anchor bar
+ long anchorTime = t0 + 50 * TimeSpan.TicksPerSecond;
+ var anchorBar = new TBar(anchorTime, 125.0, 126.0, 124.0, 125.0, 1000);
+ ind.Update(anchorBar, isNew: true);
+ double anchorMomentum = ind.Momentum;
+ bool anchorSqueezeOn = ind.SqueezeOn;
+
+ // Correction with dramatically different values — Momentum must change
+ var corruptBar = new TBar(anchorTime, 1250.0, 1260.0, 1240.0, 1250.0, 1000);
+ ind.Update(corruptBar, isNew: false);
+ Assert.NotEqual(anchorMomentum, ind.Momentum);
+
+ // Correction back to original — both outputs must restore exactly
+ ind.Update(anchorBar, isNew: false);
+ Assert.Equal(anchorMomentum, ind.Momentum, 1e-9);
+ Assert.Equal(anchorSqueezeOn, ind.SqueezeOn);
+ }
}
diff --git a/lib/oscillators/squeeze/Squeeze.cs b/lib/oscillators/squeeze/Squeeze.cs
index edbbfe78..7a922ecf 100644
--- a/lib/oscillators/squeeze/Squeeze.cs
+++ b/lib/oscillators/squeeze/Squeeze.cs
@@ -369,6 +369,11 @@ public sealed class Squeeze : ITValuePublisher
SqueezeOn = false;
}
+ ///
+ /// Span-based batch Squeeze calculation. Populates both the momentum and squeeze-state output spans.
+ ///
+ /// Output span for Squeeze momentum values (linear regression of detrended price).
+ /// Output span for squeeze-state values (positive when Bollinger Bands are inside Keltner Channel).
public static void Batch(
ReadOnlySpan high,
ReadOnlySpan low,
diff --git a/lib/oscillators/squeeze/squeeze.pine b/lib/oscillators/squeeze/squeeze.pine
index cce92536..90618d4c 100644
--- a/lib/oscillators/squeeze/squeeze.pine
+++ b/lib/oscillators/squeeze/squeeze.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Squeeze Momentum (SQUEEZE)", "SQUEEZE", overlay=false)
diff --git a/lib/oscillators/stc/stc.pine b/lib/oscillators/stc/stc.pine
index a8e93c26..ac27a010 100644
--- a/lib/oscillators/stc/stc.pine
+++ b/lib/oscillators/stc/stc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Schaff Trend Cycle (STC)", "STC", overlay=false)
diff --git a/lib/oscillators/stoch/stoch.pine b/lib/oscillators/stoch/stoch.pine
index 1a48abd7..06c22b47 100644
--- a/lib/oscillators/stoch/stoch.pine
+++ b/lib/oscillators/stoch/stoch.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Stochastic Oscillator (STOCH)", "Stoch", overlay=false)
diff --git a/lib/oscillators/stochf/stochf.pine b/lib/oscillators/stochf/stochf.pine
index 1ca9f6d8..a26dce81 100644
--- a/lib/oscillators/stochf/stochf.pine
+++ b/lib/oscillators/stochf/stochf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Stochastic Fast (STOCHF)", "STOCHF", overlay=false)
diff --git a/lib/oscillators/stochrsi/Stochrsi.Validation.Tests.cs b/lib/oscillators/stochrsi/Stochrsi.Validation.Tests.cs
index 88b79d23..0fbd96d8 100644
--- a/lib/oscillators/stochrsi/Stochrsi.Validation.Tests.cs
+++ b/lib/oscillators/stochrsi/Stochrsi.Validation.Tests.cs
@@ -405,4 +405,33 @@ public sealed class StochrsiValidationTests : IDisposable
_output.WriteLine($"Skender span: {totalCompared} compared, {mismatches} mismatches ({mismatchRate:P2})");
Assert.True(mismatchRate < 0.05, $"Skender span mismatch rate {mismatchRate:P2} exceeds 5% ({mismatches}/{totalCompared})");
}
+
+ [Fact]
+ public void Stochrsi_Correction_Recomputes()
+ {
+ var ind = new Stochrsi();
+ var t0 = DateTime.MinValue;
+
+ // Build state well past warmup (WarmupPeriod ≈ 31)
+ for (int i = 0; i < 50; i++)
+ {
+ ind.Update(new TValue(t0.AddSeconds(i), 100.0 + i * 0.5));
+ }
+
+ // Anchor bar
+ var anchorTime = t0.AddSeconds(50);
+ const double anchorPrice = 125.0;
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
+ double anchorK = ind.K;
+ double anchorD = ind.D;
+
+ // Use large downward spike (÷10) to move StochRSI away from ceiling
+ ind.Update(new TValue(anchorTime, anchorPrice / 10), isNew: false);
+ Assert.NotEqual(anchorK, ind.K);
+
+ // Correction back to original — both outputs must restore exactly
+ ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
+ Assert.Equal(anchorK, ind.K, 1e-9);
+ Assert.Equal(anchorD, ind.D, 1e-9);
+ }
}
diff --git a/lib/oscillators/stochrsi/Stochrsi.cs b/lib/oscillators/stochrsi/Stochrsi.cs
index 77ec3591..25cf3678 100644
--- a/lib/oscillators/stochrsi/Stochrsi.cs
+++ b/lib/oscillators/stochrsi/Stochrsi.cs
@@ -348,6 +348,10 @@ public sealed class Stochrsi : AbstractBase
///
/// High-performance span-based StochRSI %K calculation.
///
+ ///
+ /// Computes only the %K line (SMA-smoothed Stochastic RSI). The %D signal line ()
+ /// is not available from this API; use streaming to access both outputs.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan source, Span output,
int rsiLength, int stochLength, int kSmooth, int dSmooth)
diff --git a/lib/oscillators/stochrsi/stochrsi.pine b/lib/oscillators/stochrsi/stochrsi.pine
index 331e901b..0ffb9d65 100644
--- a/lib/oscillators/stochrsi/stochrsi.pine
+++ b/lib/oscillators/stochrsi/stochrsi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Stochastic RSI (STOCHRSI)", "StochRSI", overlay=false)
diff --git a/lib/oscillators/td_seq/td_seq.pine b/lib/oscillators/td_seq/td_seq.pine
index 36fef105..a16c6745 100644
--- a/lib/oscillators/td_seq/td_seq.pine
+++ b/lib/oscillators/td_seq/td_seq.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("TD Sequential (TD_SEQ)", "TD_SEQ", overlay=false)
diff --git a/lib/oscillators/trendflex/trendflex.pine b/lib/oscillators/trendflex/trendflex.pine
index 9107b9e4..776c65e3 100644
--- a/lib/oscillators/trendflex/trendflex.pine
+++ b/lib/oscillators/trendflex/trendflex.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Indicator algorithm (C) 2013 John F. Ehlers
diff --git a/lib/oscillators/trix/trix.pine b/lib/oscillators/trix/trix.pine
index 0ce61d3c..5c95e51d 100644
--- a/lib/oscillators/trix/trix.pine
+++ b/lib/oscillators/trix/trix.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("TRIX", "TRIX", overlay=false)
diff --git a/lib/oscillators/ultosc/ultosc.pine b/lib/oscillators/ultosc/ultosc.pine
index 7f3403fb..d5991084 100644
--- a/lib/oscillators/ultosc/ultosc.pine
+++ b/lib/oscillators/ultosc/ultosc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ultimate Oscillator (ULTOSC)", "ULTOSC", overlay=false)
diff --git a/lib/oscillators/willr/willr.pine b/lib/oscillators/willr/willr.pine
index 40b0fd65..439d3901 100644
--- a/lib/oscillators/willr/willr.pine
+++ b/lib/oscillators/willr/willr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Williams %R (WILLR)", "WILLR", overlay=false)
diff --git a/lib/reversals/_index.md b/lib/reversals/_index.md
index eaeb26ef..02f1c8fe 100644
--- a/lib/reversals/_index.md
+++ b/lib/reversals/_index.md
@@ -16,5 +16,6 @@ Reversal indicators identify potential turning points where price may change dir
| [PIVOTFIB](pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Fibonacci-ratio based pivots; Golden Ratio (61.8%) at R2/S2. |
| [PIVOTWOOD](pivotwood/Pivotwood.md) | Woodie's Pivot Points | Weighted close pivots (2× close weight) for intraday trading. |
| [PSAR](psar/Psar.md) | Parabolic Stop And Reverse | Trailing stop that accelerates with trend; SAR dots mark entry/exit signals. |
+| [SAREXT](sarext/Sarext.md) | Parabolic SAR Extended | PSAR with asymmetric long/short acceleration factors. Sign-encoded output. |
| [SWINGS](swings/Swings.md) | Swing High/Low Detection | Configurable-lookback pattern detector for swing highs/lows; dual SwingHigh/SwingLow. |
| [TTM_SCALPER](ttm_scalper/TtmScalper.md) | TTM Scalper Alert | 3-bar pivot high/low detection for scalping entries. John Carter. |
diff --git a/lib/reversals/fractals/fractals.pine b/lib/reversals/fractals/fractals.pine
index d455661b..b64fca01 100644
--- a/lib/reversals/fractals/fractals.pine
+++ b/lib/reversals/fractals/fractals.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Williams Fractals", "FRACTALS", overlay=true)
diff --git a/lib/reversals/pivot/pivot.pine b/lib/reversals/pivot/pivot.pine
index b6e8dee0..a2c69c79 100644
--- a/lib/reversals/pivot/pivot.pine
+++ b/lib/reversals/pivot/pivot.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pivot Points (Classic)", "PIVOT", overlay=true)
diff --git a/lib/reversals/pivotcam/pivotcam.pine b/lib/reversals/pivotcam/pivotcam.pine
index d72f69e5..af06c1d6 100644
--- a/lib/reversals/pivotcam/pivotcam.pine
+++ b/lib/reversals/pivotcam/pivotcam.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pivot Points (Camarilla)", "PIVOTCAM", overlay=true)
diff --git a/lib/reversals/pivotdem/pivotdem.pine b/lib/reversals/pivotdem/pivotdem.pine
index ea66cbf2..70aa27b9 100644
--- a/lib/reversals/pivotdem/pivotdem.pine
+++ b/lib/reversals/pivotdem/pivotdem.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pivot Points (DeMark)", "PIVOTDEM", overlay=true)
diff --git a/lib/reversals/pivotext/pivotext.pine b/lib/reversals/pivotext/pivotext.pine
index bad51673..14db5be3 100644
--- a/lib/reversals/pivotext/pivotext.pine
+++ b/lib/reversals/pivotext/pivotext.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pivot Points (Extended)", "PIVOTEXT", overlay=true)
diff --git a/lib/reversals/pivotfib/pivotfib.pine b/lib/reversals/pivotfib/pivotfib.pine
index 613bb21c..e8c888b3 100644
--- a/lib/reversals/pivotfib/pivotfib.pine
+++ b/lib/reversals/pivotfib/pivotfib.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pivot Points (Fibonacci)", "PIVOTFIB", overlay=true)
diff --git a/lib/reversals/pivotwood/pivotwood.pine b/lib/reversals/pivotwood/pivotwood.pine
index ec28ace3..0a48872e 100644
--- a/lib/reversals/pivotwood/pivotwood.pine
+++ b/lib/reversals/pivotwood/pivotwood.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pivot Points (Woodie)", "PIVOTWOOD", overlay=true)
diff --git a/lib/reversals/psar/psar.pine b/lib/reversals/psar/psar.pine
index a26cad93..b5882ffe 100644
--- a/lib/reversals/psar/psar.pine
+++ b/lib/reversals/psar/psar.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Parabolic SAR", "PSAR", overlay=true)
diff --git a/lib/reversals/sarext/Sarext.cs b/lib/reversals/sarext/Sarext.cs
new file mode 100644
index 00000000..74c0692c
--- /dev/null
+++ b/lib/reversals/sarext/Sarext.cs
@@ -0,0 +1,658 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace QuanTAlib;
+
+///
+/// SAREXT: Parabolic SAR Extended (TA-Lib)
+///
+///
+/// Extended Parabolic Stop And Reverse with asymmetric acceleration factors.
+/// Separate AF initialization, increment, and maximum for long vs short positions.
+/// Sign-encoded output: positive = long (SAR below price), negative = short (SAR above price).
+///
+/// Calculation extends Wilder's PSAR:
+///
+/// Long: SAR = SAR + AF_long × (EP - SAR), output = +SAR
+/// Short: SAR = SAR + AF_short × (EP - SAR), output = -SAR
+///
+/// Bar 0: Collect OHLC data
+/// Bar 1: Determine direction from startValue or DM auto-detect
+/// Bar 2+: Standard SAR state machine with asymmetric AF parameters
+///
+///
+/// Key characteristics:
+/// - O(1) per-bar state machine with long/short mode transitions
+/// - Asymmetric acceleration factors for long and short positions
+/// - startValue parameter forces initial direction (0 = auto-detect from DM)
+/// - offsetOnReverse adds gap buffer on trend reversal
+/// - Sign-encoded output matches TA-Lib SAREXT convention
+/// - Default parameters: afInitLong/Short=0.02, afLong/Short=0.02, afMaxLong/Short=0.20
+///
+/// Detailed documentation
+[SkipLocalsInit]
+public sealed class Sarext : ITValuePublisher
+{
+ private const double DefaultStartValue = 0;
+ private const double DefaultOffsetOnReverse = 0;
+ private const double DefaultAfInitLong = 0.02;
+ private const double DefaultAfLong = 0.02;
+ private const double DefaultAfMaxLong = 0.20;
+ private const double DefaultAfInitShort = 0.02;
+ private const double DefaultAfShort = 0.02;
+ private const double DefaultAfMaxShort = 0.20;
+
+ private readonly double _startValue;
+ private readonly double _offsetOnReverse;
+ private readonly double _afInitLong;
+ private readonly double _afLong;
+ private readonly double _afMaxLong;
+ private readonly double _afInitShort;
+ private readonly double _afShort;
+ private readonly double _afMaxShort;
+
+ private int _samples;
+ private int _p_samples;
+
+ [StructLayout(LayoutKind.Auto)]
+ private record struct State(
+ bool IsLong,
+ double Sar,
+ double Ep,
+ double Af,
+ double Prev1High,
+ double Prev1Low,
+ double Prev2High,
+ double Prev2Low,
+ double LastValidOpen,
+ double LastValidHigh,
+ double LastValidLow,
+ double LastValidClose);
+
+ private State _s;
+ private State _ps;
+
+ private readonly TBarPublishedHandler _barHandler;
+
+ /// Display name for the indicator.
+ public string Name { get; }
+
+ /// Bars required for the indicator to warm up.
+ public int WarmupPeriod { get; }
+
+ /// Current SAR value (unsigned).
+ public double Sar => _s.Sar;
+
+ /// True when the SAREXT is in long (uptrend) mode.
+ public bool IsLong => _s.IsLong;
+
+ /// Primary output value (sign-encoded SAR: positive = long, negative = short).
+ public TValue Last { get; private set; }
+
+ /// True when enough bars have been processed for valid output.
+ public bool IsHot => _samples >= 2;
+
+ ///
+ public event TValuePublishedHandler? Pub;
+
+ ///
+ /// Creates a Parabolic SAR Extended indicator.
+ ///
+ /// Initial direction: positive = long, negative = short, 0 = auto-detect from DM.
+ /// Gap added to SAR on reversal (default 0).
+ /// Initial acceleration factor for long positions (default 0.02).
+ /// AF increment per new EP in long positions (default 0.02).
+ /// Maximum AF for long positions (default 0.20).
+ /// Initial acceleration factor for short positions (default 0.02).
+ /// AF increment per new EP in short positions (default 0.02).
+ /// Maximum AF for short positions (default 0.20).
+ public Sarext(
+ double startValue = DefaultStartValue,
+ double offsetOnReverse = DefaultOffsetOnReverse,
+ double afInitLong = DefaultAfInitLong,
+ double afLong = DefaultAfLong,
+ double afMaxLong = DefaultAfMaxLong,
+ double afInitShort = DefaultAfInitShort,
+ double afShort = DefaultAfShort,
+ double afMaxShort = DefaultAfMaxShort)
+ {
+ if (afInitLong <= 0)
+ {
+ throw new ArgumentException("afInitLong must be > 0.", nameof(afInitLong));
+ }
+ if (afLong <= 0)
+ {
+ throw new ArgumentException("afLong must be > 0.", nameof(afLong));
+ }
+ if (afMaxLong <= afInitLong)
+ {
+ throw new ArgumentException("afMaxLong must be > afInitLong.", nameof(afMaxLong));
+ }
+ if (afInitShort <= 0)
+ {
+ throw new ArgumentException("afInitShort must be > 0.", nameof(afInitShort));
+ }
+ if (afShort <= 0)
+ {
+ throw new ArgumentException("afShort must be > 0.", nameof(afShort));
+ }
+ if (afMaxShort <= afInitShort)
+ {
+ throw new ArgumentException("afMaxShort must be > afInitShort.", nameof(afMaxShort));
+ }
+ if (offsetOnReverse < 0)
+ {
+ throw new ArgumentException("offsetOnReverse must be >= 0.", nameof(offsetOnReverse));
+ }
+
+ _startValue = startValue;
+ _offsetOnReverse = offsetOnReverse;
+ _afInitLong = afInitLong;
+ _afLong = afLong;
+ _afMaxLong = afMaxLong;
+ _afInitShort = afInitShort;
+ _afShort = afShort;
+ _afMaxShort = afMaxShort;
+
+ _samples = 0;
+ _p_samples = 0;
+ _s = new State(
+ IsLong: true,
+ Sar: double.NaN,
+ Ep: double.NaN,
+ Af: afInitLong,
+ Prev1High: double.NaN,
+ Prev1Low: double.NaN,
+ Prev2High: double.NaN,
+ Prev2Low: double.NaN,
+ LastValidOpen: double.NaN,
+ LastValidHigh: double.NaN,
+ LastValidLow: double.NaN,
+ LastValidClose: double.NaN);
+ _ps = _s;
+
+ Name = "Sarext";
+ WarmupPeriod = 2;
+ _barHandler = HandleBar;
+ }
+
+ ///
+ /// Creates a SAREXT indicator chained to a TBarSeries source.
+ ///
+ public Sarext(TBarSeries source,
+ double startValue = DefaultStartValue,
+ double offsetOnReverse = DefaultOffsetOnReverse,
+ double afInitLong = DefaultAfInitLong,
+ double afLong = DefaultAfLong,
+ double afMaxLong = DefaultAfMaxLong,
+ double afInitShort = DefaultAfInitShort,
+ double afShort = DefaultAfShort,
+ double afMaxShort = DefaultAfMaxShort)
+ : this(startValue, offsetOnReverse, afInitLong, afLong, afMaxLong, afInitShort, afShort, afMaxShort)
+ {
+ Prime(source);
+ source.Pub += _barHandler;
+ }
+
+ private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void PubEvent(TValue value, bool isNew = true) =>
+ Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
+
+ ///
+ /// Updates the SAREXT with a new OHLC bar.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TBar input, bool isNew = true)
+ {
+ if (isNew)
+ {
+ _ps = _s;
+ _p_samples = _samples;
+ _samples++;
+ }
+ else
+ {
+ _s = _ps;
+ _samples = _p_samples + 1;
+ }
+
+ var s = _s;
+
+ // Validate inputs — substitute last-valid on NaN/Infinity
+ double open = input.Open;
+ double high = input.High;
+ double low = input.Low;
+ double close = input.Close;
+
+ if (double.IsFinite(open)) { s.LastValidOpen = open; }
+ else { open = s.LastValidOpen; }
+
+ if (double.IsFinite(high)) { s.LastValidHigh = high; }
+ else { high = s.LastValidHigh; }
+
+ if (double.IsFinite(low)) { s.LastValidLow = low; }
+ else { low = s.LastValidLow; }
+
+ if (double.IsFinite(close)) { s.LastValidClose = close; }
+ else { close = s.LastValidClose; }
+
+ // If still no valid data, return NaN
+ if (double.IsNaN(open) || double.IsNaN(high) || double.IsNaN(low) || double.IsNaN(close))
+ {
+ _s = s;
+ Last = new TValue(input.Time, double.NaN);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ double sarResult;
+
+ if (_samples == 1)
+ {
+ // Bar 0: Collect first bar's OHLC, no output yet
+ s.Prev1High = high;
+ s.Prev1Low = low;
+ s.Prev2High = high;
+ s.Prev2Low = low;
+ s.LastValidOpen = open;
+ s.LastValidHigh = high;
+ s.LastValidLow = low;
+ s.LastValidClose = close;
+
+ // Tentative initialization — will be finalized on bar 1
+ s.Sar = high;
+ s.Ep = low;
+ s.Af = _afInitShort;
+ s.IsLong = false;
+
+ _s = s;
+ Last = new TValue(input.Time, double.NaN);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+ else if (_samples == 2)
+ {
+ // Bar 1: Determine initial direction
+ double prevHigh = s.Prev1High;
+ double prevLow = s.Prev1Low;
+
+ if (_startValue > 0)
+ {
+ // Force long
+ s.IsLong = true;
+ s.Sar = Math.Min(prevLow, low);
+ s.Ep = Math.Max(prevHigh, high);
+ s.Af = _afInitLong;
+ }
+ else if (_startValue < 0)
+ {
+ // Force short
+ s.IsLong = false;
+ s.Sar = Math.Max(prevHigh, high);
+ s.Ep = Math.Min(prevLow, low);
+ s.Af = _afInitShort;
+ }
+ else
+ {
+ // Auto-detect from DM: compare plusDM vs minusDM
+ double plusDM = high - prevHigh;
+ double minusDM = prevLow - low;
+
+ if (plusDM > minusDM && plusDM > 0)
+ {
+ // Long
+ s.IsLong = true;
+ s.Sar = Math.Min(prevLow, low);
+ s.Ep = Math.Max(prevHigh, high);
+ s.Af = _afInitLong;
+ }
+ else
+ {
+ // Short (default when equal or minusDM dominates)
+ s.IsLong = false;
+ s.Sar = Math.Max(prevHigh, high);
+ s.Ep = Math.Min(prevLow, low);
+ s.Af = _afInitShort;
+ }
+ }
+
+ sarResult = s.Sar;
+
+ // Update prev-bar tracking
+ s.Prev2High = s.Prev1High;
+ s.Prev2Low = s.Prev1Low;
+ s.Prev1High = high;
+ s.Prev1Low = low;
+
+ _s = s;
+ double output = s.IsLong ? sarResult : -sarResult;
+ Last = new TValue(input.Time, output);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ // Bar 2+: Standard SAR state machine with asymmetric AF
+ // Compute new SAR using FMA: sar + af * (ep - sar)
+ double newSar = Math.FusedMultiplyAdd(s.Af, s.Ep - s.Sar, s.Sar);
+
+ if (s.IsLong)
+ {
+ // Long mode: SAR must be at or below prior two bars' lows
+ newSar = Math.Min(newSar, s.Prev1Low);
+ newSar = Math.Min(newSar, s.Prev2Low);
+
+ // Check for reversal: price crosses below SAR
+ if (low <= newSar)
+ {
+ // Reverse to short
+ s.IsLong = false;
+ newSar = s.Ep + _offsetOnReverse;
+ s.Ep = low;
+ s.Af = _afInitShort;
+ }
+ else
+ {
+ // Check for new extreme point (new high)
+ if (high > s.Ep)
+ {
+ s.Ep = high;
+ s.Af = Math.Min(s.Af + _afLong, _afMaxLong);
+ }
+ }
+ }
+ else
+ {
+ // Short mode: SAR must be at or above prior two bars' highs
+ newSar = Math.Max(newSar, s.Prev1High);
+ newSar = Math.Max(newSar, s.Prev2High);
+
+ // Check for reversal: price crosses above SAR
+ if (high >= newSar)
+ {
+ // Reverse to long
+ s.IsLong = true;
+ newSar = s.Ep - _offsetOnReverse;
+ s.Ep = high;
+ s.Af = _afInitLong;
+ }
+ else
+ {
+ // Check for new extreme point (new low)
+ if (low < s.Ep)
+ {
+ s.Ep = low;
+ s.Af = Math.Min(s.Af + _afShort, _afMaxShort);
+ }
+ }
+ }
+
+ s.Sar = newSar;
+ sarResult = newSar;
+
+ // Shift prior bar tracking
+ if (isNew)
+ {
+ s.Prev2High = s.Prev1High;
+ s.Prev2Low = s.Prev1Low;
+ s.Prev1High = high;
+ s.Prev1Low = low;
+ }
+ else
+ {
+ // Bar correction: update current bar's values
+ s.Prev1High = high;
+ s.Prev1Low = low;
+ }
+
+ _s = s;
+
+ double signedResult = s.IsLong ? sarResult : -sarResult;
+ Last = new TValue(input.Time, signedResult);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ ///
+ /// Updates the SAREXT with a TValue (uses value as OHLC proxy).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public TValue Update(TValue input, bool isNew = true) =>
+ Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
+
+ ///
+ /// Processes a full TBarSeries and returns sign-encoded SAREXT output.
+ ///
+ public TSeries Update(TBarSeries source)
+ {
+ if (source.Count == 0)
+ {
+ return new TSeries([], []);
+ }
+
+ int len = source.Count;
+ var t = new List(len);
+ var v = new List(len);
+
+ CollectionsMarshal.SetCount(t, len);
+ CollectionsMarshal.SetCount(v, len);
+
+ Batch(source.OpenValues, source.HighValues, source.LowValues, source.CloseValues,
+ CollectionsMarshal.AsSpan(v), len,
+ _startValue, _offsetOnReverse,
+ _afInitLong, _afLong, _afMaxLong,
+ _afInitShort, _afShort, _afMaxShort);
+
+ source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
+
+ // Prime internal state for continued streaming
+ Prime(source);
+
+ var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc);
+ Last = new TValue(lastTime, CollectionsMarshal.AsSpan(v)[^1]);
+
+ return new TSeries(t, v);
+ }
+
+ ///
+ /// Primes the indicator from a TBarSeries (replays all bars to set state).
+ ///
+ public void Prime(TBarSeries source)
+ {
+ Reset();
+
+ if (source.Count == 0)
+ {
+ return;
+ }
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ Update(source[i], isNew: true);
+ }
+ }
+
+ ///
+ /// Primes the indicator from a span of doubles (uses each value as OHLC proxy).
+ ///
+ public void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ Reset();
+
+ if (source.Length == 0)
+ {
+ return;
+ }
+
+ long t = DateTime.UtcNow.Ticks;
+ long stepTicks = (step ?? TimeSpan.FromMinutes(1)).Ticks;
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ double val = source[i];
+ Update(new TBar(t, val, val, val, val, 0), isNew: true);
+ t += stepTicks;
+ }
+ }
+
+ ///
+ /// Resets the indicator to its initial state.
+ ///
+ public void Reset()
+ {
+ _samples = 0;
+ _p_samples = 0;
+ _s = new State(
+ IsLong: true,
+ Sar: double.NaN,
+ Ep: double.NaN,
+ Af: _afInitLong,
+ Prev1High: double.NaN,
+ Prev1Low: double.NaN,
+ Prev2High: double.NaN,
+ Prev2Low: double.NaN,
+ LastValidOpen: double.NaN,
+ LastValidHigh: double.NaN,
+ LastValidLow: double.NaN,
+ LastValidClose: double.NaN);
+ _ps = _s;
+ Last = default;
+ }
+
+ ///
+ /// Span-based batch computation of SAREXT.
+ ///
+ /// Input open prices.
+ /// Input high prices.
+ /// Input low prices.
+ /// Input close prices.
+ /// Output span for sign-encoded SAR values.
+ /// Number of bars to process.
+ /// Initial direction: positive = long, negative = short, 0 = auto-detect.
+ /// Gap added to SAR on reversal.
+ /// Initial AF for long positions.
+ /// AF increment for long positions.
+ /// Maximum AF for long positions.
+ /// Initial AF for short positions.
+ /// AF increment for short positions.
+ /// Maximum AF for short positions.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Batch(
+ ReadOnlySpan open,
+ ReadOnlySpan high,
+ ReadOnlySpan low,
+ ReadOnlySpan close,
+ Span output,
+ int n,
+ double startValue = DefaultStartValue,
+ double offsetOnReverse = DefaultOffsetOnReverse,
+ double afInitLong = DefaultAfInitLong,
+ double afLong = DefaultAfLong,
+ double afMaxLong = DefaultAfMaxLong,
+ double afInitShort = DefaultAfInitShort,
+ double afShort = DefaultAfShort,
+ double afMaxShort = DefaultAfMaxShort)
+ {
+ if (afInitLong <= 0 || afInitLong >= afMaxLong)
+ {
+ throw new ArgumentException("afInitLong must be > 0 and < afMaxLong.", nameof(afInitLong));
+ }
+ if (afLong <= 0)
+ {
+ throw new ArgumentException("afLong must be > 0.", nameof(afLong));
+ }
+ if (afInitShort <= 0 || afInitShort >= afMaxShort)
+ {
+ throw new ArgumentException("afInitShort must be > 0 and < afMaxShort.", nameof(afInitShort));
+ }
+ if (afShort <= 0)
+ {
+ throw new ArgumentException("afShort must be > 0.", nameof(afShort));
+ }
+ if (high.Length != low.Length || high.Length != close.Length || high.Length != open.Length)
+ {
+ throw new ArgumentException("Input spans must have the same length.", nameof(high));
+ }
+ if (output.Length < n)
+ {
+ throw new ArgumentException("Output span must be at least n elements.", nameof(output));
+ }
+
+ if (n == 0)
+ {
+ return;
+ }
+
+ // State machine prevents SIMD — compute via streaming instance
+ var indicator = new Sarext(startValue, offsetOnReverse,
+ afInitLong, afLong, afMaxLong, afInitShort, afShort, afMaxShort);
+
+ long baseTime = DateTime.UtcNow.Ticks;
+ for (int i = 0; i < n; i++)
+ {
+ _ = indicator.Update(
+ new TBar(baseTime + i, open[i], high[i], low[i], close[i], 0),
+ isNew: true);
+ output[i] = indicator.Last.Value;
+ }
+ }
+
+ ///
+ /// Batch computation from a TBarSeries.
+ ///
+ public static TSeries Batch(
+ TBarSeries source,
+ double startValue = DefaultStartValue,
+ double offsetOnReverse = DefaultOffsetOnReverse,
+ double afInitLong = DefaultAfInitLong,
+ double afLong = DefaultAfLong,
+ double afMaxLong = DefaultAfMaxLong,
+ double afInitShort = DefaultAfInitShort,
+ double afShort = DefaultAfShort,
+ double afMaxShort = DefaultAfMaxShort)
+ {
+ if (source == null || source.Count == 0)
+ {
+ return new TSeries([], []);
+ }
+
+ int len = source.Count;
+ var t = new List(len);
+ var v = new List(len);
+
+ CollectionsMarshal.SetCount(t, len);
+ CollectionsMarshal.SetCount(v, len);
+
+ Batch(source.OpenValues, source.HighValues, source.LowValues, source.CloseValues,
+ CollectionsMarshal.AsSpan(v), len,
+ startValue, offsetOnReverse,
+ afInitLong, afLong, afMaxLong,
+ afInitShort, afShort, afMaxShort);
+
+ source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
+
+ return new TSeries(t, v);
+ }
+
+ ///
+ /// Calculates SAREXT and returns both the result series and the primed indicator.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static (TSeries Results, Sarext Indicator) Calculate(
+ TBarSeries source,
+ double startValue = DefaultStartValue,
+ double offsetOnReverse = DefaultOffsetOnReverse,
+ double afInitLong = DefaultAfInitLong,
+ double afLong = DefaultAfLong,
+ double afMaxLong = DefaultAfMaxLong,
+ double afInitShort = DefaultAfInitShort,
+ double afShort = DefaultAfShort,
+ double afMaxShort = DefaultAfMaxShort)
+ {
+ var indicator = new Sarext(startValue, offsetOnReverse,
+ afInitLong, afLong, afMaxLong, afInitShort, afShort, afMaxShort);
+ var results = indicator.Update(source);
+ return (results, indicator);
+ }
+}
diff --git a/lib/reversals/sarext/Sarext.md b/lib/reversals/sarext/Sarext.md
new file mode 100644
index 00000000..2fb3ed32
--- /dev/null
+++ b/lib/reversals/sarext/Sarext.md
@@ -0,0 +1,177 @@
+# SAREXT: Parabolic SAR Extended
+
+| Property | Value |
+| ---------------- | -------------------------------- |
+| **Category** | Reversal |
+| **Inputs** | OHLCV bar (TBar) |
+| **Parameters** | `startValue` (0), `offsetOnReverse` (0), `afInitLong` (0.02), `afLong` (0.02), `afMaxLong` (0.20), `afInitShort` (0.02), `afShort` (0.02), `afMaxShort` (0.20) |
+| **Outputs** | Single series (sign-encoded SAR) |
+| **Output range** | ±price level (positive = long, negative = short) |
+| **Warmup** | `2` bars |
+
+### TL;DR
+
+- Extended Parabolic SAR with **asymmetric acceleration factors** for long and short positions.
+- Sign-encoded output: positive = long (SAR below price), negative = short (SAR above price).
+- Matches TA-Lib `TA_SAREXT` specification with 8 parameters.
+- Auto-detects initial direction from Directional Movement when `startValue == 0`.
+- Requires `2` bars of warmup before first valid output (IsHot = true).
+- Validated against TA-Lib reference implementation.
+
+> "The trend is your friend — but which way it accelerates depends on whether you're long or short." — QuanTAlib
+
+## Introduction
+
+The Parabolic SAR Extended (SAREXT) is an enhanced version of Wilder's Parabolic Stop And Reverse that allows **separate acceleration factor configurations for long and short positions**. While standard PSAR uses the same AF start, increment, and maximum for both trend directions, SAREXT provides six independent AF parameters (three for long, three for short), plus a `startValue` to force initial direction and `offsetOnReverse` to add a gap buffer when the indicator reverses.
+
+This design makes SAREXT suitable for markets where bullish and bearish trends have different characteristics — for example, equity markets where rallies tend to be gradual (lower AF) and selloffs tend to be sharp (higher AF).
+
+## Historical Context
+
+SAREXT originates from the TA-Lib open-source technical analysis library, where it appears as `TA_SAREXT`. It extends Wilder's original 1978 PSAR with asymmetric parameters, addressing a common criticism: that markets don't behave symmetrically in both directions. The TA-Lib implementation adds the `startValue` parameter for deterministic initialization (useful in backtesting) and `offsetOnReverse` for creating a buffer zone that reduces whipsaw on reversals.
+
+## Architecture and Physics
+
+### 1. State Machine
+
+SAREXT operates as a two-state machine identical to PSAR: **Long** (uptrend) and **Short** (downtrend). Each state tracks:
+
+- **SAR**: Current stop level
+- **EP** (Extreme Point): Highest high in long mode, lowest low in short mode
+- **AF** (Acceleration Factor): Uses direction-specific parameters
+
+### 2. Initialization (Bars 0–1)
+
+| Bar | Action |
+|-----|--------|
+| Bar 0 | Collect first OHLC data, no output |
+| Bar 1 | Determine direction: `startValue > 0` → long, `startValue < 0` → short, `startValue == 0` → auto-detect from DM |
+
+**Auto-detection**: Compares plusDM (High[1] - High[0]) vs minusDM (Low[0] - Low[1]). If plusDM > minusDM and plusDM > 0, start long; otherwise start short.
+
+### 3. SAR Update Rule (Asymmetric)
+
+**Long mode:**
+
+$$\text{SAR}_{t} = \text{SAR}_{t-1} + \text{AF}_{\text{long}} \times (\text{EP} - \text{SAR}_{t-1})$$
+
+**Short mode:**
+
+$$\text{SAR}_{t} = \text{SAR}_{t-1} + \text{AF}_{\text{short}} \times (\text{EP} - \text{SAR}_{t-1})$$
+
+Both computed using `Math.FusedMultiplyAdd` for numerical precision.
+
+### 4. SAR Clamping
+
+Identical to PSAR:
+
+- Long: $\text{SAR}_{t} = \min(\text{SAR}_{t}, \text{Low}_{t-1}, \text{Low}_{t-2})$
+- Short: $\text{SAR}_{t} = \max(\text{SAR}_{t}, \text{High}_{t-1}, \text{High}_{t-2})$
+
+### 5. Reversal Detection with Offset
+
+- **Long → Short**: When $\text{Low}_t \leq \text{SAR}_t$:
+ - $\text{SAR} = \text{EP} + \text{offsetOnReverse}$
+ - $\text{EP} = \text{Low}_t$, $\text{AF} = \text{afInitShort}$
+
+- **Short → Long**: When $\text{High}_t \geq \text{SAR}_t$:
+ - $\text{SAR} = \text{EP} - \text{offsetOnReverse}$
+ - $\text{EP} = \text{High}_t$, $\text{AF} = \text{afInitLong}$
+
+### 6. EP/AF Update (No Reversal)
+
+- Long: if $\text{High}_t > \text{EP}$, then $\text{EP} = \text{High}$, $\text{AF} = \min(\text{AF} + \text{afLong}, \text{afMaxLong})$
+- Short: if $\text{Low}_t < \text{EP}$, then $\text{EP} = \text{Low}$, $\text{AF} = \min(\text{AF} + \text{afShort}, \text{afMaxShort})$
+
+### 7. Sign-Encoded Output
+
+$$\text{output} = \begin{cases} +\text{SAR} & \text{if long (SAR below price)} \\ -\text{SAR} & \text{if short (SAR above price)} \end{cases}$$
+
+## Mathematical Foundation
+
+The SAR update is a first-order IIR filter with time-varying, direction-dependent coefficient:
+
+$$y_t = y_{t-1} + \alpha_t^{(d)} (x^* - y_{t-1})$$
+
+where $d \in \{\text{long}, \text{short}\}$ selects the parameter set. The asymmetric AF progression:
+
+$$\text{AF}_t^{(\text{long})} = \min(\text{afInitLong} + n_{\text{long}} \times \text{afLong}, \text{afMaxLong})$$
+
+$$\text{AF}_t^{(\text{short})} = \min(\text{afInitShort} + n_{\text{short}} \times \text{afShort}, \text{afMaxShort})$$
+
+### Parameter Reference
+
+| Parameter | Default | Effect |
+|-----------|---------|--------|
+| startValue | 0 | Initial direction: >0 long, <0 short, 0 auto-detect |
+| offsetOnReverse | 0 | Gap added to SAR on reversal (reduces whipsaw) |
+| afInitLong | 0.02 | Initial AF for long positions |
+| afLong | 0.02 | AF increment per new high in long mode |
+| afMaxLong | 0.20 | Maximum AF for long positions |
+| afInitShort | 0.02 | Initial AF for short positions |
+| afShort | 0.02 | AF increment per new low in short mode |
+| afMaxShort | 0.20 | Maximum AF for short positions |
+
+## Performance Profile
+
+### Operation Count (Streaming Mode)
+
+SAREXT is O(1) per bar — identical to PSAR with minor overhead for parameter selection.
+
+| Operation | Count | Cost (cycles) | Subtotal |
+| :--- | :---: | :---: | :---: |
+| Direction check + param select | 1 | 3 cy | ~3 cy |
+| EP (extreme point) update | 1 | 2 cy | ~2 cy |
+| AF increment (conditional) | 1 | 2 cy | ~2 cy |
+| SAR = SAR + AF*(EP - SAR) via FMA | 1 | 1 cy | ~1 cy |
+| Reversal detection + offset | 1 | 4 cy | ~4 cy |
+| Sign encoding + state update | 1 | 2 cy | ~2 cy |
+| **Total** | **O(1)** | — | **~14 cy** |
+
+| Operation | Complexity | Notes |
+|-----------|-----------|-------|
+| Update (streaming) | O(1) | State machine: constant work per bar |
+| Batch (span) | O(n) | Sequential state machine (no SIMD possible) |
+| Memory | O(1) | Fixed state: 12 doubles + 1 bool |
+| Warmup | 2 bars | Bar 0 collects data, bar 1 determines direction |
+
+### SIMD Analysis
+
+SAREXT cannot be vectorized. The state machine has data-dependent branches (reversal detection, direction-specific AF selection) and sequential dependencies. The Batch API delegates to streaming for correctness.
+
+### Quality Metrics (1–10 Scale)
+
+| Metric | Score | Rationale |
+|--------|-------|-----------|
+| Trend detection | 7 | Same as PSAR; asymmetric AF can reduce false reversals |
+| Responsiveness | 9 | Independent AF tuning per direction improves adaptability |
+| False signals | 6 | offsetOnReverse helps reduce whipsaw vs standard PSAR |
+| Flexibility | 10 | 8 parameters allow fine-grained control |
+| TA-Lib compatibility | 10 | Matches TA_SAREXT specification |
+
+## Validation
+
+| Library | Match | Tolerance | Notes |
+|---------|-------|-----------|-------|
+| TA-Lib | ✅ | 1e-8 | `Functions.SarExt(highs, lows, ...)` with all 8 parameters |
+| Self | ✅ | 1e-10 | Streaming == Batch == Span |
+
+## Common Pitfalls
+
+1. **Sign interpretation**: Output is sign-encoded. Use `Math.Abs(output)` for the raw SAR level. Check `output > 0` for long, `output < 0` for short.
+
+2. **Bar 0 outputs NaN**: The first bar collects data only. Valid output starts at bar 1 (sample index 2).
+
+3. **offsetOnReverse too large**: Large offsets create SAR values far from price, delaying re-entry. Start with 0 and increase incrementally.
+
+4. **Asymmetric AF interaction**: Setting `afMaxShort` much higher than `afMaxLong` makes short-side SAR track price tightly while long-side SAR lags. This is intentional for bearish-bias strategies but may surprise.
+
+5. **Auto-detect sensitivity**: When `startValue == 0`, the DM comparison on bars 0–1 determines initial direction. A single bar's DM can be noisy; use `startValue` for deterministic behavior in backtests.
+
+6. **No SIMD path**: Sequential state machine with data-dependent branches prevents vectorization. Batch API is O(n) sequential.
+
+## References
+
+- TA-Lib. "TA_SAREXT — SAR Extended." Open-source technical analysis library.
+- Wilder, J. W. Jr. (1978). *New Concepts in Technical Trading Systems*. Trend Research. ISBN 978-0894590276.
+- Kaufman, P. J. (2013). *Trading Systems and Methods*, 5th ed. Wiley.
diff --git a/lib/reversals/swings/swings.pine b/lib/reversals/swings/swings.pine
index 93e3035f..f6e6f768 100644
--- a/lib/reversals/swings/swings.pine
+++ b/lib/reversals/swings/swings.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Swing High/Low Detection", "SWINGS", overlay=true)
diff --git a/lib/reversals/ttm_scalper/ttmscalper.pine b/lib/reversals/ttm_scalper/ttmscalper.pine
index e2161cf0..9bff66b2 100644
--- a/lib/reversals/ttm_scalper/ttmscalper.pine
+++ b/lib/reversals/ttm_scalper/ttmscalper.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("TTM Scalper Alert", "TTM_SCALPER", overlay=true)
diff --git a/lib/statistics/acf/Acf.cs b/lib/statistics/acf/Acf.cs
index f0a6c093..9409554b 100644
--- a/lib/statistics/acf/Acf.cs
+++ b/lib/statistics/acf/Acf.cs
@@ -367,7 +367,7 @@ public sealed class Acf : AbstractBase
for (int j = 0; j < bufferCount; j++)
{
double diff = buffer[j] - mean;
- variance += diff * diff;
+ variance = Math.FusedMultiplyAdd(diff, diff, variance);
}
variance /= bufferCount;
@@ -388,7 +388,7 @@ public sealed class Acf : AbstractBase
int laggedIdx = (effectiveStart + t - lag) % period;
double xt = buffer[currentIdx];
double xtk = buffer[laggedIdx];
- autocovariance += (xt - mean) * (xtk - mean);
+ autocovariance = Math.FusedMultiplyAdd(xt - mean, xtk - mean, autocovariance);
}
autocovariance /= bufferCount;
diff --git a/lib/statistics/beta/beta.pine b/lib/statistics/beta/beta.pine
index 13460082..b74818a7 100644
--- a/lib/statistics/beta/beta.pine
+++ b/lib/statistics/beta/beta.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Beta Function (BETA)", "BETA", overlay=false)
diff --git a/lib/statistics/cma/cma.pine b/lib/statistics/cma/cma.pine
index 4eac7fa6..1cdeb04b 100644
--- a/lib/statistics/cma/cma.pine
+++ b/lib/statistics/cma/cma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Cumulative Moving Average", "CMA", overlay=true)
diff --git a/lib/statistics/cointegration/Cointegration.cs b/lib/statistics/cointegration/Cointegration.cs
index 4b4d9f1c..584e0de4 100644
--- a/lib/statistics/cointegration/Cointegration.cs
+++ b/lib/statistics/cointegration/Cointegration.cs
@@ -55,6 +55,7 @@ public sealed class Cointegration : AbstractBase
private const int ResyncInterval = 1000;
private const double Epsilon = 1e-10;
+ ///
public override bool IsHot => _bufferA.IsFull && _hasPrevResidual;
///
@@ -109,16 +110,23 @@ public sealed class Cointegration : AbstractBase
///
/// Updates with raw double values.
///
+ ///
+ /// Stamps both inputs with DateTime.UtcNow as their timestamp. For
+ /// deterministic or replay-safe sequences use
+ /// with explicit timestamps instead.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double seriesA, double seriesB, bool isNew = true)
{
- return Update(new TValue(DateTime.MinValue, seriesA), new TValue(DateTime.MinValue, seriesB), isNew);
+ return Update(new TValue(DateTime.UtcNow, seriesA), new TValue(DateTime.UtcNow, seriesB), isNew);
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for bi-input indicator. Use Update(seriesA, seriesB) instead.
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("Cointegration requires two inputs (seriesA and seriesB). Use Update(seriesA, seriesB).");
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for bi-input indicator. Use Calculate(seriesA, seriesB, period) instead.
public override TSeries Update(TSeries source)
{
@@ -398,11 +406,13 @@ public sealed class Cointegration : AbstractBase
_sumDelta2 = FusedMultiplyAdd(delta, delta, _sumDelta2);
}
}
+ /// Not supported. This indicator requires two input spans.
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
throw new NotSupportedException("Cointegration requires two inputs.");
}
+ ///
public override void Reset()
{
_bufferA.Clear();
@@ -473,6 +483,9 @@ public sealed class Cointegration : AbstractBase
}
}
+ ///
+ /// Calculates the ADF cointegration statistic for two time series and returns both the result series and the live indicator instance.
+ ///
public static (TSeries Results, Cointegration Indicator) Calculate(TSeries seriesA, TSeries seriesB, int period = 20)
{
if (seriesA.Count != seriesB.Count)
diff --git a/lib/statistics/cointegration/cointegration.pine b/lib/statistics/cointegration/cointegration.pine
index a5fa0279..48268a5c 100644
--- a/lib/statistics/cointegration/cointegration.pine
+++ b/lib/statistics/cointegration/cointegration.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Cointegration (COINTEGRATION)", "COINTEGRATION", overlay=false)
diff --git a/lib/statistics/correlation/Correlation.cs b/lib/statistics/correlation/Correlation.cs
index 6896d29a..5e3ea0eb 100644
--- a/lib/statistics/correlation/Correlation.cs
+++ b/lib/statistics/correlation/Correlation.cs
@@ -45,6 +45,7 @@ public sealed class Correlation : AbstractBase
private const int ResyncInterval = 1000;
private const double Epsilon = 1e-10;
+ ///
public override bool IsHot => _bufferX.Count >= WarmupPeriod;
///
@@ -108,16 +109,23 @@ public sealed class Correlation : AbstractBase
///
/// Updates with raw double values.
///
+ ///
+ /// Stamps both inputs with DateTime.UtcNow as their timestamp. For
+ /// deterministic or replay-safe sequences use
+ /// with explicit timestamps instead.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double seriesX, double seriesY, bool isNew = true)
{
- return Update(new TValue(DateTime.MinValue, seriesX), new TValue(DateTime.MinValue, seriesY), isNew);
+ return Update(new TValue(DateTime.UtcNow, seriesX), new TValue(DateTime.UtcNow, seriesY), isNew);
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for bi-input indicator. Use Update(seriesX, seriesY) instead.
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("Correlation requires two inputs (seriesX and seriesY). Use Update(seriesX, seriesY).");
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for bi-input indicator. Use Calculate(seriesX, seriesY, period) instead.
public override TSeries Update(TSeries source)
{
@@ -259,11 +267,13 @@ public sealed class Correlation : AbstractBase
_sumXY = FusedMultiplyAdd(x, y, _sumXY);
}
}
+ /// Not supported. This indicator requires two input spans.
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
throw new NotSupportedException("Correlation requires two inputs.");
}
+ ///
public override void Reset()
{
_bufferX.Clear();
@@ -323,6 +333,9 @@ public sealed class Correlation : AbstractBase
}
}
+ ///
+ /// Calculates Pearson correlation for two time series and returns both the result series and the live indicator instance.
+ ///
public static (TSeries Results, Correlation Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20)
{
if (seriesX.Count != seriesY.Count)
diff --git a/lib/statistics/correlation/correlation.pine b/lib/statistics/correlation/correlation.pine
index 11c7f442..b79ebb90 100644
--- a/lib/statistics/correlation/correlation.pine
+++ b/lib/statistics/correlation/correlation.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pearson's Correlation (CORRELATION)", "CORRELATION", overlay=false)
diff --git a/lib/statistics/covariance/Covariance.cs b/lib/statistics/covariance/Covariance.cs
index 90e5fbc9..5fb28e3b 100644
--- a/lib/statistics/covariance/Covariance.cs
+++ b/lib/statistics/covariance/Covariance.cs
@@ -309,7 +309,7 @@ public sealed class Covariance : AbstractBase
sumX = sumX - oldX + x;
sumY = sumY - oldY + y;
- sumXY = sumXY - (oldX * oldY) + (x * y);
+ sumXY = sumXY - oldX * oldY + x * y;
bufferX[bufferIndex] = x;
bufferY[bufferIndex] = y;
@@ -337,7 +337,7 @@ public sealed class Covariance : AbstractBase
double by = bufferY[k];
recalcSumX += bx;
recalcSumY += by;
- recalcSumXY += bx * by;
+ recalcSumXY = Math.FusedMultiplyAdd(bx, by, recalcSumXY);
}
sumX = recalcSumX;
sumY = recalcSumY;
@@ -477,7 +477,7 @@ public sealed class Covariance : AbstractBase
double y = Unsafe.Add(ref srcYRef, startIdx + k);
recalcSumX += x;
recalcSumY += y;
- recalcSumXY += x * y;
+ recalcSumXY = Math.FusedMultiplyAdd(x, y, recalcSumXY);
}
sumX = recalcSumX;
sumY = recalcSumY;
@@ -513,7 +513,7 @@ public sealed class Covariance : AbstractBase
sumX = sumX - oldX + x;
sumY = sumY - oldY + y;
- sumXY = sumXY - (oldX * oldY) + (x * y);
+ sumXY = sumXY - oldX * oldY + x * y;
double numerator = sumXY - sumX * sumY * invN;
Unsafe.Add(ref outRef, i) = numerator * invDenom;
diff --git a/lib/statistics/covariance/covariance.pine b/lib/statistics/covariance/covariance.pine
index 87cf821c..0d2fa775 100644
--- a/lib/statistics/covariance/covariance.pine
+++ b/lib/statistics/covariance/covariance.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Covariance (COVARIANCE)", "COVARIANCE", overlay=false)
diff --git a/lib/statistics/entropy/entropy.pine b/lib/statistics/entropy/entropy.pine
index a073637e..36fb7ddb 100644
--- a/lib/statistics/entropy/entropy.pine
+++ b/lib/statistics/entropy/entropy.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Entropy (ENTROPY)", "ENTROPY", overlay=false)
diff --git a/lib/statistics/geomean/geomean.pine b/lib/statistics/geomean/geomean.pine
index f35d8420..390159ec 100644
--- a/lib/statistics/geomean/geomean.pine
+++ b/lib/statistics/geomean/geomean.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Geometric Mean (GEOMEAN)", "GEOMEAN", overlay=true)
diff --git a/lib/statistics/granger/Granger.cs b/lib/statistics/granger/Granger.cs
index f1530f53..8514b17c 100644
--- a/lib/statistics/granger/Granger.cs
+++ b/lib/statistics/granger/Granger.cs
@@ -48,6 +48,7 @@ public sealed class Granger : AbstractBase
private const int ResyncInterval = 1000;
private const double Epsilon = 1e-10;
+ ///
public override bool IsHot => _windowY.IsFull;
///
@@ -103,16 +104,23 @@ public sealed class Granger : AbstractBase
///
/// Updates with raw double values.
///
+ ///
+ /// Stamps both inputs with DateTime.UtcNow as their timestamp. For
+ /// deterministic or replay-safe sequences use
+ /// with explicit timestamps instead.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double seriesY, double seriesX, bool isNew = true)
{
return Update(new TValue(DateTime.UtcNow, seriesY), new TValue(DateTime.UtcNow, seriesX), isNew);
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for dual-input indicator. Use Update(seriesY, seriesX) instead.
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("Granger requires two inputs (seriesY and seriesX). Use Update(seriesY, seriesX).");
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for dual-input indicator. Use Batch(seriesY, seriesX, period) instead.
public override TSeries Update(TSeries source)
{
@@ -366,11 +374,13 @@ public sealed class Granger : AbstractBase
_sumYLagXLag = FusedMultiplyAdd(yLag, xLag, _sumYLagXLag);
}
}
+ /// Not supported. This indicator requires two input spans.
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
throw new NotSupportedException("Granger requires two inputs.");
}
+ ///
public override void Reset()
{
_bufferY.Clear();
@@ -465,6 +475,9 @@ public sealed class Granger : AbstractBase
}
}
+ ///
+ /// Calculates the Granger Causality F-statistic for two time series and returns both the result series and the live indicator instance.
+ ///
public static (TSeries Results, Granger Indicator) Calculate(TSeries seriesY, TSeries seriesX, int period = 20)
{
if (seriesY.Count != seriesX.Count)
diff --git a/lib/statistics/granger/granger.pine b/lib/statistics/granger/granger.pine
index 1441de65..a67947b6 100644
--- a/lib/statistics/granger/granger.pine
+++ b/lib/statistics/granger/granger.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Granger Causality Test (GRANGER)", "GRANGER", overlay=false, precision=4)
diff --git a/lib/statistics/harmean/harmean.pine b/lib/statistics/harmean/harmean.pine
index f958cb95..8e6e0aba 100644
--- a/lib/statistics/harmean/harmean.pine
+++ b/lib/statistics/harmean/harmean.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Harmonic Mean (HARMEAN)", "HARMEAN", overlay=false, precision=6)
diff --git a/lib/statistics/hurst/hurst.pine b/lib/statistics/hurst/hurst.pine
index db30d7c3..61c3c62c 100644
--- a/lib/statistics/hurst/hurst.pine
+++ b/lib/statistics/hurst/hurst.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Hurst Exponent (HURST)", "HURST", overlay=false, precision=4)
diff --git a/lib/statistics/iqr/iqr.pine b/lib/statistics/iqr/iqr.pine
index 1c999c6e..34d24004 100644
--- a/lib/statistics/iqr/iqr.pine
+++ b/lib/statistics/iqr/iqr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Interquartile Range (IQR)", "IQR", overlay=false, precision=4)
diff --git a/lib/statistics/jb/jb.pine b/lib/statistics/jb/jb.pine
index 1b3d5d0e..bda341d2 100644
--- a/lib/statistics/jb/jb.pine
+++ b/lib/statistics/jb/jb.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Jarque-Bera Test (JB)", "JB", overlay=false, precision=4)
diff --git a/lib/statistics/kendall/Kendall.cs b/lib/statistics/kendall/Kendall.cs
index 3e3ee825..0087eab1 100644
--- a/lib/statistics/kendall/Kendall.cs
+++ b/lib/statistics/kendall/Kendall.cs
@@ -34,6 +34,7 @@ public sealed class Kendall : AbstractBase
private const double Epsilon = 1e-10;
+ ///
public override bool IsHot => _bufferX.Count >= 2;
///
@@ -88,16 +89,23 @@ public sealed class Kendall : AbstractBase
///
/// Updates with raw double values.
///
+ ///
+ /// Stamps both inputs with DateTime.UtcNow as their timestamp. For
+ /// deterministic or replay-safe sequences use
+ /// with explicit timestamps instead.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double seriesX, double seriesY, bool isNew = true)
{
return Update(new TValue(DateTime.UtcNow, seriesX), new TValue(DateTime.UtcNow, seriesY), isNew);
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for dual-input indicator. Use Update(seriesX, seriesY) instead.
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("Kendall requires two inputs (seriesX and seriesY). Use Update(seriesX, seriesY).");
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for dual-input indicator. Use Batch(seriesX, seriesY, period) instead.
public override TSeries Update(TSeries source)
{
@@ -169,11 +177,13 @@ public sealed class Kendall : AbstractBase
return (concordant - discordant) / denominator;
}
+ /// Not supported. This indicator requires two input spans.
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
throw new NotSupportedException("Kendall requires two inputs.");
}
+ ///
public override void Reset()
{
_bufferX.Clear();
@@ -268,6 +278,9 @@ public sealed class Kendall : AbstractBase
}
}
+ ///
+ /// Calculates Kendall Tau-a for two time series and returns both the result series and the live indicator instance.
+ ///
public static (TSeries Results, Kendall Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20)
{
var indicator = new Kendall(period);
diff --git a/lib/statistics/kendall/kendall.pine b/lib/statistics/kendall/kendall.pine
index ea8481db..eef803c6 100644
--- a/lib/statistics/kendall/kendall.pine
+++ b/lib/statistics/kendall/kendall.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Kendall Rank Correlation (KENDALL)", "KENDALL", overlay=false, precision=4)
diff --git a/lib/statistics/kurtosis/Kurtosis.cs b/lib/statistics/kurtosis/Kurtosis.cs
index f0eb5ee2..1cd93ddf 100644
--- a/lib/statistics/kurtosis/Kurtosis.cs
+++ b/lib/statistics/kurtosis/Kurtosis.cs
@@ -256,8 +256,8 @@ public sealed class Kurtosis : AbstractBase
double valSq = val * val;
sum += val;
sumSq += valSq;
- sumCu += valSq * val;
- sumQu += valSq * valSq;
+ sumCu = Math.FusedMultiplyAdd(valSq, val, sumCu);
+ sumQu = Math.FusedMultiplyAdd(valSq, valSq, sumQu);
}
_sum = sum;
_sumSq = sumSq;
@@ -415,8 +415,8 @@ public sealed class Kurtosis : AbstractBase
double oldSq = oldVal * oldVal;
sum = sum - oldVal + val;
sumSq = sumSq - oldSq + valSq;
- sumCu = sumCu - (oldSq * oldVal) + (valSq * val);
- sumQu = sumQu - (oldSq * oldSq) + (valSq * valSq);
+ sumCu = sumCu - oldSq * oldVal + valSq * val;
+ sumQu = sumQu - oldSq * oldSq + valSq * valSq;
output[i] = CalculateKurtosisFromSums(sum, sumSq, sumCu, sumQu, period, isPopulation);
@@ -440,8 +440,8 @@ public sealed class Kurtosis : AbstractBase
double vSq = v * v;
recalcSum += v;
recalcSumSq += vSq;
- recalcSumCu += vSq * v;
- recalcSumQu += vSq * vSq;
+ recalcSumCu = Math.FusedMultiplyAdd(vSq, v, recalcSumCu);
+ recalcSumQu = Math.FusedMultiplyAdd(vSq, vSq, recalcSumQu);
}
sum = recalcSum;
sumSq = recalcSumSq;
@@ -633,8 +633,8 @@ public sealed class Kurtosis : AbstractBase
double vSq = v * v;
recalcSum += v;
recalcSumSq += vSq;
- recalcSumCu += vSq * v;
- recalcSumQu += vSq * vSq;
+ recalcSumCu = Math.FusedMultiplyAdd(vSq, v, recalcSumCu);
+ recalcSumQu = Math.FusedMultiplyAdd(vSq, vSq, recalcSumQu);
}
sum = recalcSum;
sumSq = recalcSumSq;
@@ -652,8 +652,8 @@ public sealed class Kurtosis : AbstractBase
double oldSq = oldVal * oldVal;
sum = sum - oldVal + val;
sumSq = sumSq - oldSq + valSq;
- sumCu = sumCu - (oldSq * oldVal) + (valSq * val);
- sumQu = sumQu - (oldSq * oldSq) + (valSq * valSq);
+ sumCu = Math.FusedMultiplyAdd(valSq, val, Math.FusedMultiplyAdd(-oldSq, oldVal, sumCu));
+ sumQu = Math.FusedMultiplyAdd(valSq, valSq, Math.FusedMultiplyAdd(-oldSq, oldSq, sumQu));
Unsafe.Add(ref outRef, i) = CalculateKurtosisFromSums(sum, sumSq, sumCu, sumQu, n, isPopulation);
}
diff --git a/lib/statistics/kurtosis/kurtosis.pine b/lib/statistics/kurtosis/kurtosis.pine
index 7f618d4d..5a6c6a38 100644
--- a/lib/statistics/kurtosis/kurtosis.pine
+++ b/lib/statistics/kurtosis/kurtosis.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Kurtosis, tailedness (KURTOSIS)", "KURTOSIS", overlay=false, precision=8)
diff --git a/lib/statistics/linreg/linreg.pine b/lib/statistics/linreg/linreg.pine
index 04adecea..d85dc210 100644
--- a/lib/statistics/linreg/linreg.pine
+++ b/lib/statistics/linreg/linreg.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Linear Regression (LINREG)", "LINREG", overlay=false, precision=8)
diff --git a/lib/statistics/meandev/meandev.pine b/lib/statistics/meandev/meandev.pine
index f4fab594..61b79369 100644
--- a/lib/statistics/meandev/meandev.pine
+++ b/lib/statistics/meandev/meandev.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mean Deviation (MEANDEV)", "MEANDEV", overlay=false, precision=8)
diff --git a/lib/statistics/median/median.pine b/lib/statistics/median/median.pine
index 672bf0a7..182bc513 100644
--- a/lib/statistics/median/median.pine
+++ b/lib/statistics/median/median.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Median", "MEDIAN", overlay=false, precision=8)
diff --git a/lib/statistics/mode/mode.pine b/lib/statistics/mode/mode.pine
index 78412881..4e328579 100644
--- a/lib/statistics/mode/mode.pine
+++ b/lib/statistics/mode/mode.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mode", "MODE", overlay=false)
diff --git a/lib/statistics/percentile/percentile.pine b/lib/statistics/percentile/percentile.pine
index cbb04dda..b081a220 100644
--- a/lib/statistics/percentile/percentile.pine
+++ b/lib/statistics/percentile/percentile.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Percentile", "PERCENTILE", overlay=true, precision=8)
diff --git a/lib/statistics/polyfit/polyfit.pine b/lib/statistics/polyfit/polyfit.pine
index 103cffaa..4f96fbc1 100644
--- a/lib/statistics/polyfit/polyfit.pine
+++ b/lib/statistics/polyfit/polyfit.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Polynomial Fitting (POLYFIT)", "POLYFIT", overlay=true, precision=8)
diff --git a/lib/statistics/quantile/quantile.pine b/lib/statistics/quantile/quantile.pine
index bfad21e2..13f495a2 100644
--- a/lib/statistics/quantile/quantile.pine
+++ b/lib/statistics/quantile/quantile.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Quantile (QUANTILE)", shorttitle="QUANTILE", overlay=true, precision=8)
diff --git a/lib/statistics/skew/Skew.cs b/lib/statistics/skew/Skew.cs
index dc630b57..0263a826 100644
--- a/lib/statistics/skew/Skew.cs
+++ b/lib/statistics/skew/Skew.cs
@@ -201,8 +201,8 @@ public sealed class Skew : AbstractBase
{
double val = span[i];
sum += val;
- sumSq += val * val;
- sumCu += val * val * val;
+ sumSq = Math.FusedMultiplyAdd(val, val, sumSq);
+ sumCu = Math.FusedMultiplyAdd(val * val, val, sumCu);
}
_sum = sum;
_sumSq = sumSq;
@@ -312,8 +312,8 @@ public sealed class Skew : AbstractBase
}
sum = sum - oldVal + val;
- sumSq = sumSq - (oldVal * oldVal) + (val * val);
- sumCu = sumCu - (oldVal * oldVal * oldVal) + (val * val * val);
+ sumSq = sumSq - oldVal * oldVal + val * val;
+ sumCu = sumCu - oldVal * oldVal * oldVal + val * val * val;
output[i] = CalculateSkewFromSums(sum, sumSq, sumCu, period, isPopulation);
@@ -334,8 +334,8 @@ public sealed class Skew : AbstractBase
}
recalcSum += v;
- recalcSumSq += v * v;
- recalcSumCu += v * v * v;
+ recalcSumSq = Math.FusedMultiplyAdd(v, v, recalcSumSq);
+ recalcSumCu = Math.FusedMultiplyAdd(v * v, v, recalcSumCu);
}
sum = recalcSum;
sumSq = recalcSumSq;
@@ -533,8 +533,8 @@ public sealed class Skew : AbstractBase
{
double v = Unsafe.Add(ref srcRef, startIdx + k);
recalcSum += v;
- recalcSumSq += v * v;
- recalcSumCu += v * v * v;
+ recalcSumSq = Math.FusedMultiplyAdd(v, v, recalcSumSq);
+ recalcSumCu = Math.FusedMultiplyAdd(v * v, v, recalcSumCu);
}
sum = recalcSum;
sumSq = recalcSumSq;
@@ -548,8 +548,8 @@ public sealed class Skew : AbstractBase
double oldVal = Unsafe.Add(ref srcRef, i - period);
sum = sum - oldVal + val;
- sumSq = sumSq - (oldVal * oldVal) + (val * val);
- sumCu = sumCu - (oldVal * oldVal * oldVal) + (val * val * val);
+ sumSq = Math.FusedMultiplyAdd(val, val, Math.FusedMultiplyAdd(-oldVal, oldVal, sumSq));
+ sumCu = Math.FusedMultiplyAdd(val * val, val, Math.FusedMultiplyAdd(-(oldVal * oldVal), oldVal, sumCu));
Unsafe.Add(ref outRef, i) = CalculateSkewFromSums(sum, sumSq, sumCu, n, isPopulation);
}
diff --git a/lib/statistics/skew/skew.pine b/lib/statistics/skew/skew.pine
index 3dc34513..e1857f07 100644
--- a/lib/statistics/skew/skew.pine
+++ b/lib/statistics/skew/skew.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Skewness (SKEW)", "SKEW", overlay=false, precision=6)
diff --git a/lib/statistics/spearman/Spearman.cs b/lib/statistics/spearman/Spearman.cs
index 87716d2b..65599e24 100644
--- a/lib/statistics/spearman/Spearman.cs
+++ b/lib/statistics/spearman/Spearman.cs
@@ -39,6 +39,7 @@ public sealed class Spearman : AbstractBase
private const double Epsilon = 1e-10;
private const int StackallocThreshold = 256;
+ ///
public override bool IsHot => _bufferX.Count >= 2;
///
@@ -93,16 +94,23 @@ public sealed class Spearman : AbstractBase
///
/// Updates with raw double values.
///
+ ///
+ /// Stamps both inputs with DateTime.UtcNow as their timestamp. For
+ /// deterministic or replay-safe sequences use
+ /// with explicit timestamps instead.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double seriesX, double seriesY, bool isNew = true)
{
return Update(new TValue(DateTime.UtcNow, seriesX), new TValue(DateTime.UtcNow, seriesY), isNew);
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for dual-input indicator. Use Update(seriesX, seriesY) instead.
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("Spearman requires two inputs (seriesX and seriesY). Use Update(seriesX, seriesY).");
}
+ /// Not supported. This indicator requires two inputs; use instead.
/// Not supported for dual-input indicator. Use Batch(seriesX, seriesY, period) instead.
public override TSeries Update(TSeries source)
{
@@ -232,11 +240,13 @@ public sealed class Spearman : AbstractBase
ranks[i] = countSmaller + (countEqual - 1) * 0.5 + 1.0;
}
}
+ /// Not supported. This indicator requires two input spans.
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
throw new NotSupportedException("Spearman requires two inputs.");
}
+ ///
public override void Reset()
{
_bufferX.Clear();
@@ -331,6 +341,9 @@ public sealed class Spearman : AbstractBase
}
}
+ ///
+ /// Calculates Spearman's ρ for two time series and returns both the result series and the live indicator instance.
+ ///
public static (TSeries Results, Spearman Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20)
{
var indicator = new Spearman(period);
diff --git a/lib/statistics/spearman/spearman.pine b/lib/statistics/spearman/spearman.pine
index f1ab5809..4a5ace25 100644
--- a/lib/statistics/spearman/spearman.pine
+++ b/lib/statistics/spearman/spearman.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Spearman Rank Correlation (SPEARMAN)", "SPEARMAN", overlay=false, precision=4)
diff --git a/lib/statistics/stddev/stddev.pine b/lib/statistics/stddev/stddev.pine
index 539da7e5..b28d4c42 100644
--- a/lib/statistics/stddev/stddev.pine
+++ b/lib/statistics/stddev/stddev.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Standard Deviation (STDDEV)", "STDDEV", overlay=false)
diff --git a/lib/statistics/stderr/stderr.pine b/lib/statistics/stderr/stderr.pine
index 641e91b9..7efbb36a 100644
--- a/lib/statistics/stderr/stderr.pine
+++ b/lib/statistics/stderr/stderr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Standard Error of Regression (STDERR)", "STDERR", overlay=false, precision=8)
diff --git a/lib/statistics/sum/sum.pine b/lib/statistics/sum/sum.pine
index dcb97600..e83e6ebe 100644
--- a/lib/statistics/sum/sum.pine
+++ b/lib/statistics/sum/sum.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Rolling Sum", "SUM", overlay=false)
diff --git a/lib/statistics/theil/theil.pine b/lib/statistics/theil/theil.pine
index 2800037a..b8edfc1f 100644
--- a/lib/statistics/theil/theil.pine
+++ b/lib/statistics/theil/theil.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Theil Index (THEIL)", "THEIL", overlay=false, precision=6)
diff --git a/lib/statistics/variance/Variance.cs b/lib/statistics/variance/Variance.cs
index 6816d125..93f99b68 100644
--- a/lib/statistics/variance/Variance.cs
+++ b/lib/statistics/variance/Variance.cs
@@ -449,7 +449,7 @@ public sealed class Variance : AbstractBase
{
double v = Unsafe.Add(ref srcRef, startIdx + k);
recalcSum += v;
- recalcSumSq += v * v;
+ recalcSumSq = Math.FusedMultiplyAdd(v, v, recalcSumSq);
}
sum = recalcSum;
sumSq = recalcSumSq;
@@ -553,7 +553,7 @@ public sealed class Variance : AbstractBase
{
double v = Unsafe.Add(ref srcRef, startIdx + k);
recalcSum += v;
- recalcSumSq += v * v;
+ recalcSumSq = Math.FusedMultiplyAdd(v, v, recalcSumSq);
}
sum = recalcSum;
sumSq = recalcSumSq;
@@ -676,7 +676,7 @@ public sealed class Variance : AbstractBase
{
double v = Unsafe.Add(ref srcRef, startIdx + k);
recalcSum += v;
- recalcSumSq += v * v;
+ recalcSumSq = Math.FusedMultiplyAdd(v, v, recalcSumSq);
}
sum = recalcSum;
sumSq = recalcSumSq;
diff --git a/lib/statistics/variance/variance.pine b/lib/statistics/variance/variance.pine
index 0c5fb8ad..18257496 100644
--- a/lib/statistics/variance/variance.pine
+++ b/lib/statistics/variance/variance.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Variance, Dispersion or Spread (VARIANCE)", "VARIANCE", overlay=false)
diff --git a/lib/statistics/wavg/wavg.pine b/lib/statistics/wavg/wavg.pine
index b176f1df..35d6a522 100644
--- a/lib/statistics/wavg/wavg.pine
+++ b/lib/statistics/wavg/wavg.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Weighted Average (WAVG)", "WAVG", overlay=true)
diff --git a/lib/statistics/zscore/zscore.pine b/lib/statistics/zscore/zscore.pine
index 19ed9785..1e7b89fd 100644
--- a/lib/statistics/zscore/zscore.pine
+++ b/lib/statistics/zscore/zscore.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Z-Score (ZSCORE)", "ZSCORE", overlay=false)
diff --git a/lib/statistics/ztest/ztest.pine b/lib/statistics/ztest/ztest.pine
index a5119835..2f64962b 100644
--- a/lib/statistics/ztest/ztest.pine
+++ b/lib/statistics/ztest/ztest.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("One-Sample t-Test (ZTEST)", "t-TEST", overlay=false)
diff --git a/lib/trends_FIR/alma/alma.pine b/lib/trends_FIR/alma/alma.pine
index 8e6b2767..f93bde10 100644
--- a/lib/trends_FIR/alma/alma.pine
+++ b/lib/trends_FIR/alma/alma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Arnaud Legoux Moving Average (ALMA)", "ALMA", overlay=true)
diff --git a/lib/trends_FIR/blma/blma.pine b/lib/trends_FIR/blma/blma.pine
index ba26335c..1bf17065 100644
--- a/lib/trends_FIR/blma/blma.pine
+++ b/lib/trends_FIR/blma/blma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Blackman Moving Average (BLMA)", "BLMA", overlay=true)
diff --git a/lib/trends_FIR/bwma/bwma.pine b/lib/trends_FIR/bwma/bwma.pine
index 7ee2a45e..1c1c8dd9 100644
--- a/lib/trends_FIR/bwma/bwma.pine
+++ b/lib/trends_FIR/bwma/bwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bessel-Weighted Moving Average (BWMA)", "BWMA", overlay=true)
diff --git a/lib/trends_FIR/conv/conv.pine b/lib/trends_FIR/conv/conv.pine
index 5a595a16..77c3bade 100644
--- a/lib/trends_FIR/conv/conv.pine
+++ b/lib/trends_FIR/conv/conv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Convolution Moving Average (CONV)", "CONV", overlay=true)
diff --git a/lib/trends_FIR/crma/crma.pine b/lib/trends_FIR/crma/crma.pine
index 3638607d..66a9b3bc 100644
--- a/lib/trends_FIR/crma/crma.pine
+++ b/lib/trends_FIR/crma/crma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Cubic Regression Moving Average (CRMA)", "CRMA", overlay=true)
diff --git a/lib/trends_FIR/dwma/dwma.pine b/lib/trends_FIR/dwma/dwma.pine
index 41867519..986e828d 100644
--- a/lib/trends_FIR/dwma/dwma.pine
+++ b/lib/trends_FIR/dwma/dwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Double Weighted Moving Average (DWMA)", "DWMA", overlay=true)
diff --git a/lib/trends_FIR/fwma/fwma.pine b/lib/trends_FIR/fwma/fwma.pine
index c7805384..8cdd9457 100644
--- a/lib/trends_FIR/fwma/fwma.pine
+++ b/lib/trends_FIR/fwma/fwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Fibonacci Weighted Moving Average (FWMA)", "FWMA", overlay=true)
diff --git a/lib/trends_FIR/gwma/gwma.pine b/lib/trends_FIR/gwma/gwma.pine
index 758b0616..9015a940 100644
--- a/lib/trends_FIR/gwma/gwma.pine
+++ b/lib/trends_FIR/gwma/gwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Gaussian-Weighted Moving Average (GWMA)", "GWMA", overlay=true)
diff --git a/lib/trends_FIR/hamma/hamma.pine b/lib/trends_FIR/hamma/hamma.pine
index 187ae0df..5f8cbf49 100644
--- a/lib/trends_FIR/hamma/hamma.pine
+++ b/lib/trends_FIR/hamma/hamma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Hamming Moving Average (HAMMA)", "HAMMA", overlay=true)
diff --git a/lib/trends_FIR/hanma/hanma.pine b/lib/trends_FIR/hanma/hanma.pine
index 09f1b1ed..ea066847 100644
--- a/lib/trends_FIR/hanma/hanma.pine
+++ b/lib/trends_FIR/hanma/hanma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Hanning Moving Average (HANMA)", "HANMA", overlay=true)
diff --git a/lib/trends_FIR/hend/hend.pine b/lib/trends_FIR/hend/hend.pine
index a0ede244..fcd743b6 100644
--- a/lib/trends_FIR/hend/hend.pine
+++ b/lib/trends_FIR/hend/hend.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Henderson Moving Average (HEND)", "HEND", overlay=true)
diff --git a/lib/trends_FIR/hma/hma.pine b/lib/trends_FIR/hma/hma.pine
index b5f7276d..f7fe51e2 100644
--- a/lib/trends_FIR/hma/hma.pine
+++ b/lib/trends_FIR/hma/hma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Hull Moving Average (HMA)", "HMA", overlay=true)
diff --git a/lib/trends_FIR/ilrs/ilrs.pine b/lib/trends_FIR/ilrs/ilrs.pine
index 268049f1..da8eea98 100644
--- a/lib/trends_FIR/ilrs/ilrs.pine
+++ b/lib/trends_FIR/ilrs/ilrs.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Integral of Linear Regression Slope (ILRS)", "ILRS", overlay=true)
diff --git a/lib/trends_FIR/kaiser/kaiser.pine b/lib/trends_FIR/kaiser/kaiser.pine
index 34610f8a..ad51c1ff 100644
--- a/lib/trends_FIR/kaiser/kaiser.pine
+++ b/lib/trends_FIR/kaiser/kaiser.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Kaiser Window Moving Average (KAISER)", "KAISER", overlay=true)
diff --git a/lib/trends_FIR/lanczos/lanczos.pine b/lib/trends_FIR/lanczos/lanczos.pine
index a85abe79..00c715c4 100644
--- a/lib/trends_FIR/lanczos/lanczos.pine
+++ b/lib/trends_FIR/lanczos/lanczos.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Lanczos (Sinc) Window Moving Average (LANCZOS)", "LANCZOS", overlay=true)
diff --git a/lib/trends_FIR/lsma/lsma.pine b/lib/trends_FIR/lsma/lsma.pine
index 8827f618..e393b480 100644
--- a/lib/trends_FIR/lsma/lsma.pine
+++ b/lib/trends_FIR/lsma/lsma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Least Squares Moving Average (LSMA)", "LSMA", overlay=true)
diff --git a/lib/trends_FIR/nlma/nlma.pine b/lib/trends_FIR/nlma/nlma.pine
index 447956a7..9369cf28 100644
--- a/lib/trends_FIR/nlma/nlma.pine
+++ b/lib/trends_FIR/nlma/nlma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Non-Lag Moving Average (NLMA)", "NLMA", overlay=true)
diff --git a/lib/trends_FIR/nyqma/nyqma.pine b/lib/trends_FIR/nyqma/nyqma.pine
index 56397dd2..4ee7ecd0 100644
--- a/lib/trends_FIR/nyqma/nyqma.pine
+++ b/lib/trends_FIR/nyqma/nyqma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Nyquist Moving Average (NYQMA)", "NYQMA", overlay=true)
diff --git a/lib/trends_FIR/parzen/parzen.pine b/lib/trends_FIR/parzen/parzen.pine
index c319c825..4f22d63e 100644
--- a/lib/trends_FIR/parzen/parzen.pine
+++ b/lib/trends_FIR/parzen/parzen.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Parzen Window Moving Average (PARZEN)", "PARZEN", overlay=true)
diff --git a/lib/trends_FIR/pma/pma.pine b/lib/trends_FIR/pma/pma.pine
index 4d55a256..2429905a 100644
--- a/lib/trends_FIR/pma/pma.pine
+++ b/lib/trends_FIR/pma/pma.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/trends_FIR/pwma/pwma.pine b/lib/trends_FIR/pwma/pwma.pine
index 07fbcd16..c5c02180 100644
--- a/lib/trends_FIR/pwma/pwma.pine
+++ b/lib/trends_FIR/pwma/pwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Pascal Weighted Moving Average (PWMA)", "PWMA", overlay=true)
diff --git a/lib/trends_FIR/rwma/Rwma.cs b/lib/trends_FIR/rwma/Rwma.cs
index 5c0ceb76..7a2af81c 100644
--- a/lib/trends_FIR/rwma/Rwma.cs
+++ b/lib/trends_FIR/rwma/Rwma.cs
@@ -132,7 +132,7 @@ public sealed class Rwma : ITValuePublisher
{
double c = _closeBuffer[i];
double r = _rangeBuffer[i];
- sumCR += c * r;
+ sumCR = Math.FusedMultiplyAdd(c, r, sumCR);
sumR += r;
}
@@ -222,12 +222,12 @@ public sealed class Rwma : ITValuePublisher
if (s.Count >= _period)
{
- s.SumCR -= oldClose * oldRange;
+ s.SumCR = Math.FusedMultiplyAdd(-oldClose, oldRange, s.SumCR);
s.SumR -= oldRange;
}
// Add new values
- s.SumCR += currentClose * currentRange;
+ s.SumCR = Math.FusedMultiplyAdd(currentClose, currentRange, s.SumCR);
s.SumR += currentRange;
// Store in circular buffer
@@ -421,12 +421,12 @@ public sealed class Rwma : ITValuePublisher
if (count >= period)
{
- sumCR -= oldClose * oldRange;
+ sumCR = Math.FusedMultiplyAdd(-oldClose, oldRange, sumCR);
sumR -= oldRange;
}
// Add new values
- sumCR += currentClose * currentRange;
+ sumCR = Math.FusedMultiplyAdd(currentClose, currentRange, sumCR);
sumR += currentRange;
// Store in circular buffer
@@ -449,7 +449,7 @@ public sealed class Rwma : ITValuePublisher
sumR = 0;
for (int j = 0; j < period; j++)
{
- sumCR += closeBuffer[j] * rangeBuffer[j];
+ sumCR = Math.FusedMultiplyAdd(closeBuffer[j], rangeBuffer[j], sumCR);
sumR += rangeBuffer[j];
}
}
diff --git a/lib/trends_FIR/sgma/sgma.pine b/lib/trends_FIR/sgma/sgma.pine
index 0f8a8dfc..93a11131 100644
--- a/lib/trends_FIR/sgma/sgma.pine
+++ b/lib/trends_FIR/sgma/sgma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Savitzky-Golay Moving Average (SGMA)", "SGMA", overlay=true)
diff --git a/lib/trends_FIR/sinema/sinema.pine b/lib/trends_FIR/sinema/sinema.pine
index 39e759e1..f7f38895 100644
--- a/lib/trends_FIR/sinema/sinema.pine
+++ b/lib/trends_FIR/sinema/sinema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Sine-weighted Moving Average (SINEMA)", "SINEMA", overlay=true)
diff --git a/lib/trends_FIR/sma/sma.pine b/lib/trends_FIR/sma/sma.pine
index 1399a1b0..255f0079 100644
--- a/lib/trends_FIR/sma/sma.pine
+++ b/lib/trends_FIR/sma/sma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Simple Moving Average (SMA)", "SMA", overlay=true)
diff --git a/lib/trends_FIR/trima/trima.pine b/lib/trends_FIR/trima/trima.pine
index 949192a0..339168d9 100644
--- a/lib/trends_FIR/trima/trima.pine
+++ b/lib/trends_FIR/trima/trima.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Triangular Moving Average (TRIMA)", "TRIMA", overlay=true)
diff --git a/lib/trends_FIR/wma/wma.pine b/lib/trends_FIR/wma/wma.pine
index 46bfeffc..9c849206 100644
--- a/lib/trends_FIR/wma/wma.pine
+++ b/lib/trends_FIR/wma/wma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Weighted Moving Average (WMA)", "WMA", overlay=true)
diff --git a/lib/trends_IIR/adxvma/adxvma.pine b/lib/trends_IIR/adxvma/adxvma.pine
index c1fce462..76b7dd29 100644
--- a/lib/trends_IIR/adxvma/adxvma.pine
+++ b/lib/trends_IIR/adxvma/adxvma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("ADX Variable Moving Average (ADXVMA)", "ADXVMA", overlay=true)
diff --git a/lib/trends_IIR/ahrens/ahrens.pine b/lib/trends_IIR/ahrens/ahrens.pine
index 08f6721e..2ee0a553 100644
--- a/lib/trends_IIR/ahrens/ahrens.pine
+++ b/lib/trends_IIR/ahrens/ahrens.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ahrens Moving Average (AHRENS)", "AHRENS", overlay=true)
diff --git a/lib/trends_IIR/coral/coral.pine b/lib/trends_IIR/coral/coral.pine
index 76754c6b..f1e140e0 100644
--- a/lib/trends_IIR/coral/coral.pine
+++ b/lib/trends_IIR/coral/coral.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Coral Filter (CORAL)", "CORAL", overlay=true)
diff --git a/lib/trends_IIR/decay/Decay.Quantower.Tests.cs b/lib/trends_IIR/decay/Decay.Quantower.Tests.cs
new file mode 100644
index 00000000..26ea769c
--- /dev/null
+++ b/lib/trends_IIR/decay/Decay.Quantower.Tests.cs
@@ -0,0 +1,210 @@
+using TradingPlatform.BusinessLayer;
+
+namespace QuanTAlib.Tests;
+
+public class DecayIndicatorTests
+{
+ [Fact]
+ public void DecayIndicator_Constructor_SetsDefaults()
+ {
+ var indicator = new DecayIndicator();
+
+ Assert.Equal(5, indicator.Period);
+ Assert.Equal(SourceType.Close, indicator.Source);
+ Assert.True(indicator.ShowColdValues);
+ Assert.Equal("DECAY - Linear Decay", indicator.Name);
+ Assert.False(indicator.SeparateWindow);
+ Assert.True(indicator.OnBackGround);
+ }
+
+ [Fact]
+ public void DecayIndicator_MinHistoryDepths_EqualsZero()
+ {
+ var indicator = new DecayIndicator { Period = 20 };
+
+ Assert.Equal(0, DecayIndicator.MinHistoryDepths);
+ Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
+ }
+
+ [Fact]
+ public void DecayIndicator_ShortName_IncludesPeriodAndSource()
+ {
+ var indicator = new DecayIndicator { Period = 15 };
+
+ Assert.Contains("DECAY", indicator.ShortName, StringComparison.Ordinal);
+ Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void DecayIndicator_Initialize_CreatesLineSeries()
+ {
+ var indicator = new DecayIndicator { Period = 5 };
+ indicator.Initialize();
+
+ Assert.Single(indicator.LinesSeries);
+ }
+
+ [Fact]
+ public void DecayIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
+ {
+ var indicator = new DecayIndicator { Period = 5 };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+ indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
+
+ var args = new UpdateArgs(UpdateReason.HistoricalBar);
+ indicator.ProcessUpdate(args);
+
+ Assert.Equal(1, indicator.LinesSeries[0].Count);
+ Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
+ }
+
+ [Fact]
+ public void DecayIndicator_ProcessUpdate_NewBar_ComputesValue()
+ {
+ var indicator = new DecayIndicator { Period = 5 };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+ indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
+ indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
+
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
+
+ Assert.Equal(2, indicator.LinesSeries[0].Count);
+ }
+
+ [Fact]
+ public void DecayIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
+ {
+ var indicator = new DecayIndicator { Period = 5 };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+ indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
+
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
+
+ Assert.Equal(2, indicator.LinesSeries[0].Count);
+ }
+
+ [Fact]
+ public void DecayIndicator_MultipleUpdates_ProducesCorrectSequence()
+ {
+ var indicator = new DecayIndicator { Period = 5 };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+
+ for (int i = 0; i < 20; i++)
+ {
+ indicator.HistoricalData.AddBar(
+ now.AddMinutes(i),
+ 100 + i * 2,
+ 105 + i * 2,
+ 95 + i * 2,
+ 102 + i * 2);
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
+ }
+
+ Assert.Equal(20, indicator.LinesSeries[0].Count);
+
+ for (int i = 0; i < 20; i++)
+ {
+ Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
+ }
+ }
+
+ [Fact]
+ public void DecayIndicator_DifferentSourceTypes_Work()
+ {
+ var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
+
+ foreach (var source in sources)
+ {
+ var indicator = new DecayIndicator { Period = 5, Source = source };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+ indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
+
+ Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
+ $"Source {source} should produce finite value");
+ }
+ }
+
+ [Fact]
+ public void DecayIndicator_Period_CanBeChanged()
+ {
+ var indicator = new DecayIndicator { Period = 10 };
+ Assert.Equal(10, indicator.Period);
+
+ indicator.Period = 20;
+ Assert.Equal(20, indicator.Period);
+ Assert.Equal(0, DecayIndicator.MinHistoryDepths);
+ }
+
+ [Fact]
+ public void DecayIndicator_Uptrend_OutputFollowsPrice()
+ {
+ var indicator = new DecayIndicator { Period = 5 };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+
+ for (int i = 0; i < 10; i++)
+ {
+ double price = 100 + i * 5;
+ indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
+ }
+
+ // In uptrend, decay output should equal close price (input > decayed)
+ double lastValue = indicator.LinesSeries[0].GetValue(0);
+ Assert.Equal(145, lastValue, 1); // last close = 100 + 9*5 = 145
+ }
+
+ [Fact]
+ public void DecayIndicator_FlatPrices_OutputEqualsInput()
+ {
+ var indicator = new DecayIndicator { Period = 5 };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+
+ for (int i = 0; i < 5; i++)
+ {
+ indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100);
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
+ }
+
+ double lastValue = indicator.LinesSeries[0].GetValue(0);
+ Assert.Equal(100, lastValue, 1);
+ }
+
+ [Fact]
+ public void DecayIndicator_DifferentPeriods_Work()
+ {
+ var periods = new[] { 1, 5, 10, 20 };
+
+ foreach (var period in periods)
+ {
+ var indicator = new DecayIndicator { Period = period };
+ indicator.Initialize();
+
+ var now = DateTime.UtcNow;
+
+ for (int i = 0; i < 10; i++)
+ {
+ indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 102 + i, 98 + i, 101 + i);
+ indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
+ }
+
+ Assert.Equal(10, indicator.LinesSeries[0].Count);
+ }
+ }
+}
diff --git a/lib/trends_IIR/decay/Decay.Quantower.cs b/lib/trends_IIR/decay/Decay.Quantower.cs
new file mode 100644
index 00000000..8aba7297
--- /dev/null
+++ b/lib/trends_IIR/decay/Decay.Quantower.cs
@@ -0,0 +1,60 @@
+using System.Drawing;
+using System.Runtime.CompilerServices;
+using TradingPlatform.BusinessLayer;
+
+namespace QuanTAlib;
+
+///
+/// DECAY (Linear Decay) Quantower indicator.
+/// Tracks peaks and decays linearly at a rate of 1/period per bar.
+/// Formula: output = max(input, prev_output - 1/period)
+///
+[SkipLocalsInit]
+public class DecayIndicator : Indicator, IWatchlistIndicator
+{
+ [InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
+ public int Period { get; set; } = 5;
+
+ [IndicatorExtensions.DataSourceInput]
+ public SourceType Source { get; set; } = SourceType.Close;
+
+ [InputParameter("Show cold values", sortIndex: 21)]
+ public bool ShowColdValues { get; set; } = true;
+
+ private Decay _decay = null!;
+ protected LineSeries Series;
+ protected string SourceName = null!;
+ private Func _priceSelector = null!;
+
+ public static int MinHistoryDepths => 0;
+ int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
+
+ public override string ShortName => $"DECAY {Period}:{SourceName}";
+
+ public DecayIndicator()
+ {
+ OnBackGround = true;
+ SeparateWindow = false;
+ SourceName = Source.ToString();
+ Name = "DECAY - Linear Decay";
+ Description = "Linear Decay: output = max(input, prev_output - 1/period)";
+ Series = new LineSeries(name: $"DECAY {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
+ AddLineSeries(Series);
+ }
+
+ protected override void OnInit()
+ {
+ _decay = new Decay(Period);
+ SourceName = Source.ToString();
+ _priceSelector = Source.GetPriceSelector();
+ base.OnInit();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected override void OnUpdate(UpdateArgs args)
+ {
+ var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
+ TValue result = _decay.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
+ Series.SetValue(result.Value, _decay.IsHot, ShowColdValues);
+ }
+}
diff --git a/lib/trends_IIR/decay/Decay.Tests.cs b/lib/trends_IIR/decay/Decay.Tests.cs
new file mode 100644
index 00000000..315328e2
--- /dev/null
+++ b/lib/trends_IIR/decay/Decay.Tests.cs
@@ -0,0 +1,427 @@
+using Xunit;
+
+namespace QuanTAlib.Tests;
+
+public class DecayTests
+{
+ private readonly TSeries _gbm;
+ private const int TestPeriod = 5;
+ private const int DataPoints = 100;
+
+ public DecayTests()
+ {
+ var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.5, seed: 42);
+ var bars = gbm.Fetch(DataPoints, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
+ _gbm = bars.Close;
+ }
+
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_WithValidPeriod_SetsProperties()
+ {
+ var decay = new Decay(TestPeriod);
+ Assert.Equal($"Decay({TestPeriod})", decay.Name);
+ Assert.Equal(1, decay.WarmupPeriod);
+ }
+
+ [Fact]
+ public void Constructor_WithZeroPeriod_ThrowsArgumentException()
+ {
+ var ex = Assert.Throws(() => new Decay(0));
+ Assert.Equal("period", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_WithNegativePeriod_ThrowsArgumentException()
+ {
+ var ex = Assert.Throws(() => new Decay(-1));
+ Assert.Equal("period", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_WithSource_SubscribesToEvents()
+ {
+ var source = new TSeries(DataPoints);
+ var decay = new Decay(source, TestPeriod);
+ Assert.NotNull(decay);
+ }
+
+ #endregion
+
+ #region Basic Calculation Tests
+
+ [Fact]
+ public void Update_FirstBar_ReturnsInputValue()
+ {
+ var decay = new Decay(TestPeriod);
+ var tv = decay.Update(new TValue(DateTime.UtcNow, 100.0));
+ Assert.Equal(100.0, tv.Value);
+ }
+
+ [Fact]
+ public void Update_DecayingValues_OutputDecaysLinearly()
+ {
+ var decay = new Decay(5); // scale = 0.2
+ var time = DateTime.UtcNow;
+
+ // First bar at 1.0
+ decay.Update(new TValue(time, 1.0), true);
+
+ // Next bars at 0.0 — output should decay by 0.2 per bar
+ var tv1 = decay.Update(new TValue(time.AddSeconds(1), 0.0), true);
+ Assert.Equal(0.8, tv1.Value, 10); // 1.0 - 0.2
+
+ var tv2 = decay.Update(new TValue(time.AddSeconds(2), 0.0), true);
+ Assert.Equal(0.6, tv2.Value, 10); // 0.8 - 0.2
+
+ var tv3 = decay.Update(new TValue(time.AddSeconds(3), 0.0), true);
+ Assert.Equal(0.4, tv3.Value, 10); // 0.6 - 0.2
+ }
+
+ [Fact]
+ public void Update_RisingInput_FollowsInput()
+ {
+ var decay = new Decay(5);
+ var time = DateTime.UtcNow;
+
+ decay.Update(new TValue(time, 100.0), true);
+ var tv = decay.Update(new TValue(time.AddSeconds(1), 105.0), true);
+ Assert.Equal(105.0, tv.Value, 10); // input > decayed, so follows input
+ }
+
+ [Fact]
+ public void Last_IsAccessible()
+ {
+ var decay = new Decay(TestPeriod);
+ decay.Update(new TValue(DateTime.UtcNow, 100.0));
+ Assert.Equal(100.0, decay.Last.Value, 10);
+ }
+
+ [Fact]
+ public void IsHot_ReturnsFalseBeforeFirstBar()
+ {
+ var decay = new Decay(TestPeriod);
+ Assert.False(decay.IsHot);
+ }
+
+ [Fact]
+ public void IsHot_ReturnsTrueAfterFirstBar()
+ {
+ var decay = new Decay(TestPeriod);
+ decay.Update(new TValue(DateTime.UtcNow, 100.0));
+ Assert.True(decay.IsHot);
+ }
+
+ [Fact]
+ public void Name_IsAccessible()
+ {
+ var decay = new Decay(TestPeriod);
+ Assert.Equal($"Decay({TestPeriod})", decay.Name);
+ }
+
+ #endregion
+
+ #region State Management Tests
+
+ [Fact]
+ public void Update_WithIsNewTrue_AdvancesState()
+ {
+ var decay = new Decay(TestPeriod);
+ var time = DateTime.UtcNow;
+
+ decay.Update(new TValue(time, 100.0), true);
+ decay.Update(new TValue(time.AddSeconds(1), 105.0), true);
+ decay.Update(new TValue(time.AddSeconds(2), 110.0), true);
+
+ Assert.NotEqual(default, decay.Last);
+ }
+
+ [Fact]
+ public void Update_WithIsNewFalse_UpdatesCurrentState()
+ {
+ var decay = new Decay(5);
+ var time = DateTime.UtcNow;
+
+ decay.Update(new TValue(time, 1.0), true);
+ var first = decay.Update(new TValue(time.AddSeconds(1), 0.0), true);
+
+ // Correct same bar with different value
+ var corrected = decay.Update(new TValue(time.AddSeconds(1), 0.5), false);
+
+ // first: max(0.0, 1.0-0.2)=0.8
+ Assert.Equal(0.8, first.Value, 10);
+ // corrected: max(0.5, 1.0-0.2)=0.8
+ Assert.Equal(0.8, corrected.Value, 10);
+ }
+
+ [Fact]
+ public void Update_IterativeCorrections_RestoresPreviousState()
+ {
+ var decay = new Decay(5);
+ var time = DateTime.UtcNow;
+
+ decay.Update(new TValue(time, 1.0), true);
+ var baseline = decay.Update(new TValue(time.AddSeconds(1), 0.5), true);
+
+ // Make several corrections
+ decay.Update(new TValue(time.AddSeconds(1), 0.9), false);
+ decay.Update(new TValue(time.AddSeconds(1), 0.1), false);
+ var restored = decay.Update(new TValue(time.AddSeconds(1), 0.5), false);
+
+ Assert.Equal(baseline.Value, restored.Value, 10);
+ }
+
+ [Fact]
+ public void Reset_ClearsStateAndLastValidTracking()
+ {
+ var decay = new Decay(TestPeriod);
+ var time = DateTime.UtcNow;
+
+ for (int i = 0; i < 10; i++)
+ {
+ decay.Update(new TValue(time.AddSeconds(i), 100.0 + i));
+ }
+
+ decay.Reset();
+
+ Assert.Equal(default, decay.Last);
+ Assert.False(decay.IsHot);
+ }
+
+ #endregion
+
+ #region Robustness Tests
+
+ [Fact]
+ public void Update_WithNaN_UsesLastValidValue()
+ {
+ var decay = new Decay(5);
+ var time = DateTime.UtcNow;
+
+ decay.Update(new TValue(time, 1.0), true);
+ var afterNaN = decay.Update(new TValue(time.AddSeconds(1), double.NaN), true);
+
+ Assert.True(double.IsFinite(afterNaN.Value));
+ // NaN uses last valid (1.0), so max(1.0, 1.0-0.2)=1.0
+ Assert.Equal(1.0, afterNaN.Value, 10);
+ }
+
+ [Fact]
+ public void Update_WithInfinity_UsesLastValidValue()
+ {
+ var decay = new Decay(5);
+ var time = DateTime.UtcNow;
+
+ decay.Update(new TValue(time, 1.0), true);
+ var afterInf = decay.Update(new TValue(time.AddSeconds(1), double.PositiveInfinity), true);
+
+ Assert.True(double.IsFinite(afterInf.Value));
+ }
+
+ [Fact]
+ public void Update_BatchNaN_HandlesSafely()
+ {
+ var decay = new Decay(TestPeriod);
+ var time = DateTime.UtcNow;
+
+ for (int i = 0; i < 20; i++)
+ {
+ var value = i % 3 == 0 ? double.NaN : 100.0 + i;
+ var tv = decay.Update(new TValue(time.AddSeconds(i), value), true);
+ Assert.True(double.IsFinite(tv.Value));
+ }
+ }
+
+ #endregion
+
+ #region Consistency Tests (All 4 modes must match)
+
+ [Fact]
+ public void AllModes_ProduceSameResults()
+ {
+ // Mode 1: Batch via TSeries
+ var batchResult = Decay.Batch(_gbm, TestPeriod);
+
+ // Mode 2: Streaming
+ var streamingDecay = new Decay(TestPeriod);
+ var streamingResult = new TSeries(DataPoints);
+ for (int i = 0; i < _gbm.Count; i++)
+ {
+ var tv = streamingDecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true);
+ streamingResult.Add(tv, true);
+ }
+
+ // Mode 3: Span-based
+ Span spanOutput = stackalloc double[DataPoints];
+ Decay.Batch(_gbm.Values, spanOutput, TestPeriod);
+
+ // Mode 4: Event-driven
+ var eventDecay = new Decay(TestPeriod);
+ var eventResult = new TSeries(DataPoints);
+ eventDecay.Pub += (object? _, in TValueEventArgs e) => eventResult.Add(e.Value, e.IsNew);
+ for (int i = 0; i < _gbm.Count; i++)
+ {
+ eventDecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true);
+ }
+
+ int compareCount = Math.Min(100, DataPoints);
+ for (int i = DataPoints - compareCount; i < DataPoints; i++)
+ {
+ Assert.Equal(batchResult[i].Value, streamingResult[i].Value, 10);
+ Assert.Equal(batchResult[i].Value, spanOutput[i], 10);
+ Assert.Equal(batchResult[i].Value, eventResult[i].Value, 10);
+ }
+ }
+
+ #endregion
+
+ #region Span API Tests
+
+ [Fact]
+ public void Calculate_Span_ValidatesEmptySource()
+ {
+ var ex = Assert.Throws(() =>
+ {
+ ReadOnlySpan empty = [];
+ Span output = stackalloc double[1];
+ Decay.Batch(empty, output, TestPeriod);
+ });
+ Assert.Equal("source", ex.ParamName);
+ }
+
+ [Fact]
+ public void Calculate_Span_ValidatesOutputLength()
+ {
+ var ex = Assert.Throws(() =>
+ {
+ ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 };
+ Span output = stackalloc double[3]; // too short
+ Decay.Batch(source, output, TestPeriod);
+ });
+ Assert.Equal("output", ex.ParamName);
+ }
+
+ [Fact]
+ public void Calculate_Span_ValidatesPeriod()
+ {
+ var ex = Assert.Throws(() =>
+ {
+ ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 };
+ Span output = stackalloc double[5];
+ Decay.Batch(source, output, 0);
+ });
+ Assert.Equal("period", ex.ParamName);
+ }
+
+ [Fact]
+ public void Calculate_Span_MatchesTSeries()
+ {
+ var batchResult = Decay.Batch(_gbm, TestPeriod);
+
+ Span spanOutput = stackalloc double[DataPoints];
+ Decay.Batch(_gbm.Values, spanOutput, TestPeriod);
+
+ for (int i = 0; i < DataPoints; i++)
+ {
+ Assert.Equal(batchResult[i].Value, spanOutput[i], 10);
+ }
+ }
+
+ [Fact]
+ public void Calculate_Span_LargeData_NoStackOverflow()
+ {
+ int largeSize = 10000;
+ double[] source = new double[largeSize];
+ double[] output = new double[largeSize];
+
+ for (int i = 0; i < largeSize; i++)
+ {
+ source[i] = 100.0 + i * 0.1;
+ }
+
+ Decay.Batch(source, output, TestPeriod);
+
+ Assert.Equal(largeSize, output.Length);
+ }
+
+ #endregion
+
+ #region Chainability Tests
+
+ [Fact]
+ public void Pub_FiresOnUpdate()
+ {
+ var decay = new Decay(TestPeriod);
+ bool eventFired = false;
+
+ decay.Pub += (object? _, in TValueEventArgs e) => eventFired = true;
+ decay.Update(new TValue(DateTime.UtcNow, 100.0));
+
+ Assert.True(eventFired);
+ }
+
+ [Fact]
+ public void EventBasedChaining_Works()
+ {
+ var source = new TSeries(10);
+ var decay = new Decay(source, 2);
+ var results = new List();
+
+ decay.Pub += (object? _, in TValueEventArgs e) => results.Add(e.Value.Value);
+
+ for (int i = 0; i < 10; i++)
+ {
+ source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), true);
+ }
+
+ Assert.Equal(10, results.Count);
+ }
+
+ #endregion
+
+ #region Decay-Specific Tests
+
+ [Fact]
+ public void Decay_Period1_DecaysByOneEachBar()
+ {
+ var decay = new Decay(1); // scale = 1.0
+ var time = DateTime.UtcNow;
+
+ decay.Update(new TValue(time, 5.0), true);
+ var tv = decay.Update(new TValue(time.AddSeconds(1), 0.0), true);
+ // max(0.0, 5.0-1.0) = 4.0
+ Assert.Equal(4.0, tv.Value, 10);
+ }
+
+ [Fact]
+ public void Decay_ConstantInput_OutputEqualsInput()
+ {
+ var decay = new Decay(5);
+ var time = DateTime.UtcNow;
+
+ for (int i = 0; i < 20; i++)
+ {
+ var tv = decay.Update(new TValue(time.AddSeconds(i), 100.0), true);
+ Assert.Equal(100.0, tv.Value, 10);
+ }
+ }
+
+ [Fact]
+ public void Decay_OutputNeverBelowInput()
+ {
+ var decay = new Decay(10);
+ var time = DateTime.UtcNow;
+ var rng = new Random(42);
+
+ for (int i = 0; i < 100; i++)
+ {
+ double input = rng.NextDouble() * 200;
+ var tv = decay.Update(new TValue(time.AddSeconds(i), input), true);
+ Assert.True(tv.Value >= input || Math.Abs(tv.Value - input) < 1e-10,
+ $"Output {tv.Value} should be >= input {input}");
+ }
+ }
+
+ #endregion
+}
diff --git a/lib/trends_IIR/decay/Decay.Validation.Tests.cs b/lib/trends_IIR/decay/Decay.Validation.Tests.cs
new file mode 100644
index 00000000..7721a3ef
--- /dev/null
+++ b/lib/trends_IIR/decay/Decay.Validation.Tests.cs
@@ -0,0 +1,230 @@
+using Xunit;
+using Xunit.Abstractions;
+
+namespace QuanTAlib.Tests;
+
+///
+/// Validation tests for DECAY (Linear Decay) against the Tulip Indicators algorithm.
+/// The Tulip .NET binding does not expose decay/edecay directly, so validation
+/// uses manual computation of the Tulip ti_decay algorithm:
+/// output[0] = input[0]
+/// output[i] = max(input[i], output[i-1] - 1.0/period)
+///
+public sealed class DecayValidationTests(ITestOutputHelper output) : IDisposable
+{
+ private readonly ValidationTestData _testData = new();
+ private readonly ITestOutputHelper _output = output;
+ private bool _disposed;
+
+ private const int TestPeriod = 5;
+ private const double TulipTolerance = 1e-9;
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposed) { return; }
+ _disposed = true;
+ if (disposing) { _testData?.Dispose(); }
+ }
+
+ ///
+ /// Reference implementation of Tulip ti_decay for validation.
+ ///
+ private static double[] TulipDecay(double[] input, int period)
+ {
+ double[] output = new double[input.Length];
+ double scale = 1.0 / period;
+ output[0] = input[0];
+ for (int i = 1; i < input.Length; i++)
+ {
+ double d = output[i - 1] - scale;
+ output[i] = input[i] > d ? input[i] : d;
+ }
+ return output;
+ }
+
+ #region Tulip Algorithm Validation
+
+ [Fact]
+ public void Decay_MatchesTulipDecay_Batch()
+ {
+ double[] input = _testData.RawData.ToArray();
+
+ var quantResult = Decay.Batch(_testData.Data, TestPeriod);
+ double[] tulipResult = TulipDecay(input, TestPeriod);
+
+ int count = quantResult.Count;
+ int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
+
+ for (int i = start; i < count; i++)
+ {
+ Assert.True(
+ Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance,
+ $"Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}");
+ }
+
+ _output.WriteLine("Decay Batch validated successfully against Tulip decay algorithm");
+ }
+
+ [Fact]
+ public void Decay_MatchesTulipDecay_Streaming()
+ {
+ double[] input = _testData.RawData.ToArray();
+
+ var decay = new Decay(TestPeriod);
+ var streamingResults = new List();
+
+ foreach (var item in _testData.Data)
+ {
+ streamingResults.Add(decay.Update(item).Value);
+ }
+
+ double[] tulipResult = TulipDecay(input, TestPeriod);
+
+ int count = streamingResults.Count;
+ int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
+
+ for (int i = start; i < count; i++)
+ {
+ Assert.True(
+ Math.Abs(streamingResults[i] - tulipResult[i]) <= TulipTolerance,
+ $"Mismatch at index {i}: QuanTAlib={streamingResults[i]:G17}, Tulip={tulipResult[i]:G17}");
+ }
+
+ _output.WriteLine("Decay Streaming validated successfully against Tulip decay algorithm");
+ }
+
+ [Fact]
+ public void Decay_MatchesTulipDecay_Span()
+ {
+ double[] input = _testData.RawData.ToArray();
+
+ var quantOutput = new double[input.Length];
+ Decay.Batch(new ReadOnlySpan(input), quantOutput, TestPeriod);
+
+ double[] tulipResult = TulipDecay(input, TestPeriod);
+
+ int count = quantOutput.Length;
+ int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
+
+ for (int i = start; i < count; i++)
+ {
+ Assert.True(
+ Math.Abs(quantOutput[i] - tulipResult[i]) <= TulipTolerance,
+ $"Mismatch at index {i}: QuanTAlib={quantOutput[i]:G17}, Tulip={tulipResult[i]:G17}");
+ }
+
+ _output.WriteLine("Decay Span validated successfully against Tulip decay algorithm");
+ }
+
+ #endregion
+
+ #region Different Periods
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(5)]
+ [InlineData(10)]
+ [InlineData(20)]
+ [InlineData(50)]
+ public void Decay_MatchesTulipDecay_DifferentPeriods(int period)
+ {
+ double[] input = _testData.RawData.ToArray();
+
+ var quantResult = Decay.Batch(_testData.Data, period);
+ double[] tulipResult = TulipDecay(input, period);
+
+ int count = quantResult.Count;
+ int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
+
+ for (int i = start; i < count; i++)
+ {
+ Assert.True(
+ Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance,
+ $"Period={period}, Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}");
+ }
+ }
+
+ #endregion
+
+ #region Edge Cases
+
+ [Fact]
+ public void Decay_HandlesConstantValues()
+ {
+ var constantData = new TSeries(100);
+ for (int i = 0; i < 100; i++)
+ {
+ constantData.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0), true);
+ }
+
+ var result = Decay.Batch(constantData, TestPeriod);
+
+ // Constant input: output always equals input since input >= decayed
+ for (int i = 0; i < 100; i++)
+ {
+ Assert.Equal(100.0, result[i].Value, TulipTolerance);
+ }
+ }
+
+ [Fact]
+ public void Decay_HandlesLinearlyDecreasing()
+ {
+ double[] input = new double[20];
+ for (int i = 0; i < 20; i++)
+ {
+ input[i] = 100.0 - i;
+ }
+
+ var quantOutput = new double[20];
+ Decay.Batch(input, quantOutput, TestPeriod);
+
+ double[] tulipResult = TulipDecay(input, TestPeriod);
+
+ for (int i = 0; i < 20; i++)
+ {
+ Assert.Equal(tulipResult[i], quantOutput[i], TulipTolerance);
+ }
+ }
+
+ [Fact]
+ public void Batch_MatchesStreaming_IdenticalResults()
+ {
+ var batchResult = Decay.Batch(_testData.Data, TestPeriod);
+
+ var decay = new Decay(TestPeriod);
+ var streamingResults = new List();
+ foreach (var item in _testData.Data)
+ {
+ streamingResults.Add(decay.Update(item).Value);
+ }
+
+ int count = _testData.Data.Count;
+ int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
+ for (int i = start; i < count; i++)
+ {
+ Assert.Equal(batchResult[i].Value, streamingResults[i], ValidationHelper.DefaultTolerance);
+ }
+ _output.WriteLine("Decay Batch vs Streaming consistency validated");
+ }
+
+ [Fact]
+ public void Decay_OutputAlwaysGreaterOrEqualInput()
+ {
+ double[] input = _testData.RawData.ToArray();
+ var quantOutput = new double[input.Length];
+ Decay.Batch(input, quantOutput, TestPeriod);
+
+ for (int i = 0; i < input.Length; i++)
+ {
+ Assert.True(quantOutput[i] >= input[i] - 1e-15,
+ $"Output {quantOutput[i]} must be >= input {input[i]} at index {i}");
+ }
+ }
+
+ #endregion
+}
diff --git a/lib/trends_IIR/decay/Decay.cs b/lib/trends_IIR/decay/Decay.cs
new file mode 100644
index 00000000..75efb753
--- /dev/null
+++ b/lib/trends_IIR/decay/Decay.cs
@@ -0,0 +1,197 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace QuanTAlib;
+
+///
+/// DECAY: Linear Decay
+///
+///
+/// Tracks the maximum of the current input and the previous output minus a fixed
+/// step of 1/period per bar. When price is rising or flat the output follows price;
+/// when price drops the output decays linearly toward it.
+///
+/// Calculation: output = max(input, prev_output - 1/period).
+/// Origin: Tulip Indicators (ti_decay).
+///
+/// Detailed documentation
+[SkipLocalsInit]
+public sealed class Decay : AbstractBase
+{
+ private readonly int _period;
+ private readonly double _scale;
+ private int _count;
+ private record struct State(double LastValid, double LastOutput);
+ private State _state, _p_state;
+ private int _p_count;
+ private ITValuePublisher? _source;
+ private bool _disposed;
+
+ public override bool IsHot => _count > 0;
+
+ ///
+ /// Initializes a new Linear Decay indicator with specified period.
+ ///
+ /// Decay period (must be >= 1)
+ public Decay(int period = 5)
+ {
+ if (period < 1)
+ {
+ throw new ArgumentException("Period must be >= 1", nameof(period));
+ }
+
+ _period = period;
+ _scale = 1.0 / period;
+ Name = $"Decay({period})";
+ WarmupPeriod = 1;
+ }
+
+ ///
+ /// Initializes a new Linear Decay indicator with source for event-based chaining.
+ ///
+ /// Source indicator for chaining
+ /// Decay period
+ public Decay(ITValuePublisher source, int period = 5) : this(period)
+ {
+ _source = source;
+ _source.Pub += HandleUpdate;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override TValue Update(TValue input, bool isNew = true)
+ {
+ if (isNew)
+ {
+ _p_state = _state;
+ _p_count = _count;
+ }
+ else
+ {
+ _state = _p_state;
+ _count = _p_count;
+ }
+
+ double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
+
+ double result;
+ if (_count == 0)
+ {
+ result = value;
+ }
+ else
+ {
+ double decayed = _state.LastOutput - _scale;
+ result = value > decayed ? value : decayed;
+ }
+
+ _state = new State(value, result);
+ if (isNew)
+ {
+ _count++;
+ }
+
+ Last = new TValue(input.Time, result);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ public override TSeries Update(TSeries source)
+ {
+ var result = new TSeries(source.Count);
+ ReadOnlySpan values = source.Values;
+ ReadOnlySpan times = source.Times;
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
+ result.Add(tv, true);
+ }
+ return result;
+ }
+
+ public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
+ DateTime time = DateTime.UtcNow - (interval * source.Length);
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ Update(new TValue(time, source[i]), true);
+ time += interval;
+ }
+ }
+
+ public static TSeries Batch(TSeries source, int period = 5)
+ {
+ var indicator = new Decay(period);
+ return indicator.Update(source);
+ }
+
+ ///
+ /// Calculates linear decay over a span of values. Zero-allocation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public static void Batch(ReadOnlySpan source, Span output, int period = 5)
+ {
+ if (source.Length == 0)
+ {
+ throw new ArgumentException("Source cannot be empty", nameof(source));
+ }
+
+ if (output.Length < source.Length)
+ {
+ throw new ArgumentException("Output length must be >= source length", nameof(output));
+ }
+
+ if (period < 1)
+ {
+ throw new ArgumentException("Period must be >= 1", nameof(period));
+ }
+
+ double scale = 1.0 / period;
+ ref double srcRef = ref MemoryMarshal.GetReference(source);
+ ref double outRef = ref MemoryMarshal.GetReference(output);
+
+ Unsafe.Add(ref outRef, 0) = Unsafe.Add(ref srcRef, 0);
+
+ for (int i = 1; i < source.Length; i++)
+ {
+ double d = Unsafe.Add(ref outRef, i - 1) - scale;
+ double s = Unsafe.Add(ref srcRef, i);
+ Unsafe.Add(ref outRef, i) = s > d ? s : d;
+ }
+ }
+
+ public static (TSeries Results, Decay Indicator) Calculate(TSeries source, int period = 5)
+ {
+ var indicator = new Decay(period);
+ TSeries results = indicator.Update(source);
+ return (results, indicator);
+ }
+
+ public override void Reset()
+ {
+ _count = 0;
+ _p_count = 0;
+ _state = default;
+ _p_state = default;
+ Last = default;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing && _source != null)
+ {
+ _source.Pub -= HandleUpdate;
+ _source = null;
+ }
+ _disposed = true;
+ }
+ base.Dispose(disposing);
+ }
+}
diff --git a/lib/trends_IIR/decay/Decay.md b/lib/trends_IIR/decay/Decay.md
new file mode 100644
index 00000000..4cfbed52
--- /dev/null
+++ b/lib/trends_IIR/decay/Decay.md
@@ -0,0 +1,153 @@
+# DECAY: Linear Decay
+
+| Property | Value |
+| ---------------- | -------------------------------- |
+| **Category** | Trends (IIR) |
+| **Inputs** | Source (close) |
+| **Parameters** | `period` (default 5) |
+| **Outputs** | Single series (Decay) |
+| **Output range** | Same as input (overlay) |
+| **Warmup** | `1` bar |
+
+### TL;DR
+
+- DECAY (Linear Decay) tracks the maximum of the current input and the previous output minus a fixed absolute step of `1/period`.
+- Parameterized by `period` (default 5).
+- Output range: Same as input — this is an overlay indicator.
+- Requires `1` bar of warmup before first valid output (IsHot = true).
+- Validated against Tulip Indicators `ti_decay` reference algorithm.
+
+> "A ratchet that only moves down slowly: price can push it up instantly, but gravity pulls it back at a steady, linear pace."
+
+DECAY implements the Tulip Indicators `ti_decay` function. When price is above the decayed level, output snaps to price. When price falls below, the output decays linearly at a rate of `1/period` per bar, creating a ceiling that gradually descends. This produces a one-sided envelope that hugs price from above.
+
+## Historical Context
+
+The linear decay indicator originates from the Tulip Indicators library, a high-performance C library of technical indicators. It provides a simple peak-tracking mechanism where the tracked level decays at a constant absolute rate. The indicator is useful for:
+
+- **Trailing stops**: The decaying level acts as a simple trailing stop that descends at a fixed rate.
+- **Peak detection**: Identifies when price last reached a new high relative to the decay rate.
+- **Signal filtering**: Removes noise by requiring price to exceed the decayed level to register as significant.
+
+## Architecture & Physics
+
+### 1. Pure IIR (No Buffer)
+
+The indicator requires no history buffer — only the previous output value is needed:
+
+$$
+\text{state} = \{y_{t-1}\}
+$$
+
+This makes it O(1) in both time and space.
+
+### 2. Linear Decay Calculation
+
+$$
+y_t = \max(x_t, \; y_{t-1} - \frac{1}{p})
+$$
+
+where:
+- $x_t$ = current input value
+- $y_{t-1}$ = previous output value
+- $p$ = period parameter
+- $\frac{1}{p}$ = fixed decay step per bar
+
+### 3. First Bar Initialization
+
+$$
+y_0 = x_0
+$$
+
+The first bar simply passes through the input value.
+
+### 4. State Management
+
+The indicator uses state rollback for bar correction:
+
+```
+if isNew:
+ save current state as previous
+else:
+ restore previous state
+```
+
+## Mathematical Foundation
+
+### Core Formula
+
+$$
+y_t = \max(x_t, \; y_{t-1} - s)
+$$
+
+where $s = \frac{1}{p}$ is the fixed linear decay rate.
+
+### Decay Behavior
+
+After a peak at value $v$, with no new inputs exceeding the decayed level, the output follows:
+
+$$
+y_{t+k} = v - k \cdot s
+$$
+
+reaching zero after $k = v \cdot p$ bars (assuming $v > 0$).
+
+### Properties
+
+| Property | Value |
+|----------|-------|
+| Lookback | 0 |
+| Output ≥ Input | Always (by construction) |
+| Decay rate | Constant absolute $\frac{1}{p}$ |
+| Monotonic when decaying | Yes (strictly decreasing) |
+
+## Performance Profile
+
+### Operation Count (Streaming Mode)
+
+| Operation | Count | Notes |
+| :--- | :---: | :--- |
+| SUB | 1 | prev_output - scale |
+| MAX/CMP | 1 | max(input, decayed) |
+| State copy | 1 | rollback support |
+| **Total** | **~3 ops** | Extremely lightweight |
+
+### Batch Mode (Span-based)
+
+| Operation | Complexity | Notes |
+| :--- | :---: | :--- |
+| Per-element | O(1) | Sub + compare |
+| Total | O(n) | Linear scan |
+| Memory | O(1) | No additional allocation |
+
+### Quality Metrics
+
+| Metric | Score | Notes |
+| :--- | :---: | :--- |
+| **Accuracy** | 10/10 | Exact arithmetic, no approximation |
+| **Timeliness** | 10/10 | Zero lag on upward moves |
+| **Smoothness** | 2/10 | No smoothing — linear staircase |
+| **Simplicity** | 10/10 | Single subtraction + compare |
+
+## Validation
+
+| Library | Status | Notes |
+| :--- | :---: | :--- |
+| **Tulip** | ✅ | Manual ti_decay algorithm matches exactly |
+
+## Common Pitfalls
+
+1. **Not a moving average**: Decay is a peak-tracking/envelope indicator, not a smoothing filter. It only descends when price is below the decayed level.
+
+2. **Absolute decay rate**: The decay step is `1/period` in absolute terms, regardless of price level. For a stock at $100 with period=5, the decay is $0.20/bar; for a stock at $10, it's the same $0.20/bar. Consider normalizing if comparing across instruments.
+
+3. **Period interpretation**: Period=5 means the output decays by 1.0 over 5 bars (0.2 per bar), not that it looks back 5 bars.
+
+4. **First bar**: The first bar always equals the input — there is no warmup period in the traditional sense.
+
+5. **Asymmetric behavior**: Upward moves are instant (output = input), but downward moves are rate-limited to `1/period` per bar.
+
+## References
+
+- Tulip Indicators Library: https://tulipindicators.org/decay
+- Kegel, L. "Tulip Indicators" — Open-source C library of technical indicators.
diff --git a/lib/trends_IIR/decay/decay.pine b/lib/trends_IIR/decay/decay.pine
new file mode 100644
index 00000000..b0c9a354
--- /dev/null
+++ b/lib/trends_IIR/decay/decay.pine
@@ -0,0 +1,34 @@
+// Licensed under the Apache License, Version 2.0
+// © mihakralj
+//@version=6
+indicator("Linear Decay (DECAY)", "DECAY", overlay=true)
+
+//@function Calculates linear decay: output = max(input, prev_output - 1/period)
+//@param source Source price series
+//@param length Decay period
+//@returns Decayed value that tracks peaks and descends linearly
+decay(series float source, simple int length) =>
+ if length <= 0
+ runtime.error("Length must be greater than 0")
+ var float prev = na
+ float scale = 1.0 / length
+ float result = na
+ if na(prev)
+ result := source
+ else
+ float d = prev - scale
+ result := source > d ? source : d
+ prev := result
+ result
+
+// ---------- Main loop ----------
+
+// Inputs
+i_source = input.source(close, "Source")
+i_length = input.int(5, "Length", minval=1)
+
+// Calculate Decay
+float decay_val = decay(i_source, i_length)
+
+// Plot
+plot(decay_val, "Decay", color=color.yellow, linewidth=2)
diff --git a/lib/trends_IIR/decycler/decycler.pine b/lib/trends_IIR/decycler/decycler.pine
index b1265bb1..08ae0de8 100644
--- a/lib/trends_IIR/decycler/decycler.pine
+++ b/lib/trends_IIR/decycler/decycler.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/trends_IIR/dema/dema.pine b/lib/trends_IIR/dema/dema.pine
index 58e11bcb..c1a61611 100644
--- a/lib/trends_IIR/dema/dema.pine
+++ b/lib/trends_IIR/dema/dema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Double Exponential Moving Average (DEMA)", "DEMA", overlay=true)
diff --git a/lib/trends_IIR/dsma/dsma.pine b/lib/trends_IIR/dsma/dsma.pine
index 5634f76f..f15f9735 100644
--- a/lib/trends_IIR/dsma/dsma.pine
+++ b/lib/trends_IIR/dsma/dsma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Deviation-Scaled Moving Average (DSMA)", "DSMA", overlay=true)
diff --git a/lib/trends_IIR/edecay/Edecay.cs b/lib/trends_IIR/edecay/Edecay.cs
new file mode 100644
index 00000000..b011f44a
--- /dev/null
+++ b/lib/trends_IIR/edecay/Edecay.cs
@@ -0,0 +1,197 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace QuanTAlib;
+
+///
+/// EDECAY: Exponential Decay
+///
+///
+/// Tracks the maximum of the current input and the previous output multiplied by
+/// a decay factor of (period-1)/period per bar. When price is rising or flat the
+/// output follows price; when price drops the output decays exponentially toward it.
+///
+/// Calculation: output = max(input, prev_output * (period-1)/period).
+/// Origin: Tulip Indicators (ti_edecay).
+///
+/// Detailed documentation
+[SkipLocalsInit]
+public sealed class Edecay : AbstractBase
+{
+ private readonly int _period;
+ private readonly double _scale;
+ private int _count;
+ private record struct State(double LastValid, double LastOutput);
+ private State _state, _p_state;
+ private int _p_count;
+ private ITValuePublisher? _source;
+ private bool _disposed;
+
+ public override bool IsHot => _count > 0;
+
+ ///
+ /// Initializes a new Exponential Decay indicator with specified period.
+ ///
+ /// Decay period (must be >= 1)
+ public Edecay(int period = 5)
+ {
+ if (period < 1)
+ {
+ throw new ArgumentException("Period must be >= 1", nameof(period));
+ }
+
+ _period = period;
+ _scale = (period - 1.0) / period;
+ Name = $"Edecay({period})";
+ WarmupPeriod = 1;
+ }
+
+ ///
+ /// Initializes a new Exponential Decay indicator with source for event-based chaining.
+ ///
+ /// Source indicator for chaining
+ /// Decay period
+ public Edecay(ITValuePublisher source, int period = 5) : this(period)
+ {
+ _source = source;
+ _source.Pub += HandleUpdate;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override TValue Update(TValue input, bool isNew = true)
+ {
+ if (isNew)
+ {
+ _p_state = _state;
+ _p_count = _count;
+ }
+ else
+ {
+ _state = _p_state;
+ _count = _p_count;
+ }
+
+ double value = double.IsFinite(input.Value) ? input.Value : _state.LastValid;
+
+ double result;
+ if (_count == 0)
+ {
+ result = value;
+ }
+ else
+ {
+ double decayed = _state.LastOutput * _scale;
+ result = value > decayed ? value : decayed;
+ }
+
+ _state = new State(value, result);
+ if (isNew)
+ {
+ _count++;
+ }
+
+ Last = new TValue(input.Time, result);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ public override TSeries Update(TSeries source)
+ {
+ var result = new TSeries(source.Count);
+ ReadOnlySpan values = source.Values;
+ ReadOnlySpan times = source.Times;
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
+ result.Add(tv, true);
+ }
+ return result;
+ }
+
+ public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
+ {
+ TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
+ DateTime time = DateTime.UtcNow - (interval * source.Length);
+
+ for (int i = 0; i < source.Length; i++)
+ {
+ Update(new TValue(time, source[i]), true);
+ time += interval;
+ }
+ }
+
+ public static TSeries Batch(TSeries source, int period = 5)
+ {
+ var indicator = new Edecay(period);
+ return indicator.Update(source);
+ }
+
+ ///
+ /// Calculates exponential decay over a span of values. Zero-allocation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public static void Batch(ReadOnlySpan source, Span output, int period = 5)
+ {
+ if (source.Length == 0)
+ {
+ throw new ArgumentException("Source cannot be empty", nameof(source));
+ }
+
+ if (output.Length < source.Length)
+ {
+ throw new ArgumentException("Output length must be >= source length", nameof(output));
+ }
+
+ if (period < 1)
+ {
+ throw new ArgumentException("Period must be >= 1", nameof(period));
+ }
+
+ double scale = (period - 1.0) / period;
+ ref double srcRef = ref MemoryMarshal.GetReference(source);
+ ref double outRef = ref MemoryMarshal.GetReference(output);
+
+ Unsafe.Add(ref outRef, 0) = Unsafe.Add(ref srcRef, 0);
+
+ for (int i = 1; i < source.Length; i++)
+ {
+ double d = Unsafe.Add(ref outRef, i - 1) * scale;
+ double s = Unsafe.Add(ref srcRef, i);
+ Unsafe.Add(ref outRef, i) = s > d ? s : d;
+ }
+ }
+
+ public static (TSeries Results, Edecay Indicator) Calculate(TSeries source, int period = 5)
+ {
+ var indicator = new Edecay(period);
+ TSeries results = indicator.Update(source);
+ return (results, indicator);
+ }
+
+ public override void Reset()
+ {
+ _count = 0;
+ _p_count = 0;
+ _state = default;
+ _p_state = default;
+ Last = default;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing && _source != null)
+ {
+ _source.Pub -= HandleUpdate;
+ _source = null;
+ }
+ _disposed = true;
+ }
+ base.Dispose(disposing);
+ }
+}
diff --git a/lib/trends_IIR/ema/ema.pine b/lib/trends_IIR/ema/ema.pine
index 28ec4357..c59ceff9 100644
--- a/lib/trends_IIR/ema/ema.pine
+++ b/lib/trends_IIR/ema/ema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Exponential Moving Average (EMA)", "EMA", overlay=true)
diff --git a/lib/trends_IIR/frama/frama.pine b/lib/trends_IIR/frama/frama.pine
index f66f1ec1..dbd800e4 100644
--- a/lib/trends_IIR/frama/frama.pine
+++ b/lib/trends_IIR/frama/frama.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Fractal Adaptive Moving Average (FRAMA)", "FRAMA", overlay=true)
diff --git a/lib/trends_IIR/gdema/gdema.pine b/lib/trends_IIR/gdema/gdema.pine
index 00931d9d..b56049bd 100644
--- a/lib/trends_IIR/gdema/gdema.pine
+++ b/lib/trends_IIR/gdema/gdema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Generalized Double Exponential Moving Average (GDEMA)", "GDEMA", overlay=true)
diff --git a/lib/trends_IIR/hema/hema.pine b/lib/trends_IIR/hema/hema.pine
index fb67861b..e74d3da5 100644
--- a/lib/trends_IIR/hema/hema.pine
+++ b/lib/trends_IIR/hema/hema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("HEMA (Exponential Hull Analog)", "HEMAx", overlay=true)
diff --git a/lib/trends_IIR/holt/holt.pine b/lib/trends_IIR/holt/holt.pine
index 1f0208ba..aa7e912e 100644
--- a/lib/trends_IIR/holt/holt.pine
+++ b/lib/trends_IIR/holt/holt.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Holt Exponential Moving Average (HOLT)", "HOLT", overlay=true)
diff --git a/lib/trends_IIR/htit/htit.pine b/lib/trends_IIR/htit/htit.pine
index 2ebe9838..755a328b 100644
--- a/lib/trends_IIR/htit/htit.pine
+++ b/lib/trends_IIR/htit/htit.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers Hilbert Transform Instantaneous Trend (HTIT)", "HTIT", overlay=true)
diff --git a/lib/trends_IIR/hwma/hwma.pine b/lib/trends_IIR/hwma/hwma.pine
index 3b66147d..7a363756 100644
--- a/lib/trends_IIR/hwma/hwma.pine
+++ b/lib/trends_IIR/hwma/hwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Holt-Winters Moving Average (HWMA)", "HWMA", overlay=true)
diff --git a/lib/trends_IIR/jma/jma.pine b/lib/trends_IIR/jma/jma.pine
index 52418851..37e05e53 100644
--- a/lib/trends_IIR/jma/jma.pine
+++ b/lib/trends_IIR/jma/jma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Jurik Moving Average", "JMA", overlay=true)
diff --git a/lib/trends_IIR/kama/kama.pine b/lib/trends_IIR/kama/kama.pine
index 18390bcf..b5fdf482 100644
--- a/lib/trends_IIR/kama/kama.pine
+++ b/lib/trends_IIR/kama/kama.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Kaufman's Adaptive Moving Average (KAMA)", "KAMA", overlay=true)
diff --git a/lib/trends_IIR/lema/lema.pine b/lib/trends_IIR/lema/lema.pine
index 90cfd90f..226da8b8 100644
--- a/lib/trends_IIR/lema/lema.pine
+++ b/lib/trends_IIR/lema/lema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Leader EMA (LEMA)", "LEMA", overlay=true)
diff --git a/lib/trends_IIR/ltma/ltma.pine b/lib/trends_IIR/ltma/ltma.pine
index 39e3a643..0ca4ac38 100644
--- a/lib/trends_IIR/ltma/ltma.pine
+++ b/lib/trends_IIR/ltma/ltma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Linear Trend Moving Average (LTMA)", "LTMA", overlay=true)
diff --git a/lib/trends_IIR/mama/mama.pine b/lib/trends_IIR/mama/mama.pine
index 7f019a7c..4e700a0e 100644
--- a/lib/trends_IIR/mama/mama.pine
+++ b/lib/trends_IIR/mama/mama.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ehlers MESA Adaptive Moving Average (MAMA)", "MAMA", overlay=true)
diff --git a/lib/trends_IIR/mavp/mavp.pine b/lib/trends_IIR/mavp/mavp.pine
index 6733c849..a7dc6668 100644
--- a/lib/trends_IIR/mavp/mavp.pine
+++ b/lib/trends_IIR/mavp/mavp.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Moving Average Variable Period (MAVP)", "MAVP", overlay=true)
diff --git a/lib/trends_IIR/mcnma/mcnma.pine b/lib/trends_IIR/mcnma/mcnma.pine
index ec423678..6f7c3e7f 100644
--- a/lib/trends_IIR/mcnma/mcnma.pine
+++ b/lib/trends_IIR/mcnma/mcnma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
diff --git a/lib/trends_IIR/mgdi/mgdi.pine b/lib/trends_IIR/mgdi/mgdi.pine
index 641d6d5b..688ee5dd 100644
--- a/lib/trends_IIR/mgdi/mgdi.pine
+++ b/lib/trends_IIR/mgdi/mgdi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("McGinley Dynamic Indicator (MGDI)", "MGDI", overlay=true)
diff --git a/lib/trends_IIR/mma/mma.pine b/lib/trends_IIR/mma/mma.pine
index 9c2e4bc0..115c00a3 100644
--- a/lib/trends_IIR/mma/mma.pine
+++ b/lib/trends_IIR/mma/mma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Modified Moving Average (MMA)", "MMA", overlay=true)
diff --git a/lib/trends_IIR/nma/nma.pine b/lib/trends_IIR/nma/nma.pine
index c3e3cdf0..6f707d4b 100644
--- a/lib/trends_IIR/nma/nma.pine
+++ b/lib/trends_IIR/nma/nma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
diff --git a/lib/trends_IIR/qema/qema.pine b/lib/trends_IIR/qema/qema.pine
index 4fd868ae..4093c014 100644
--- a/lib/trends_IIR/qema/qema.pine
+++ b/lib/trends_IIR/qema/qema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("QEMA (OptA, progressive α, period-only)", "QEMA OptA", overlay=true)
diff --git a/lib/trends_IIR/rema/rema.pine b/lib/trends_IIR/rema/rema.pine
index ae113e3b..fb2948c4 100644
--- a/lib/trends_IIR/rema/rema.pine
+++ b/lib/trends_IIR/rema/rema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Regularized EMA (REMA)", "REMA", overlay=true)
diff --git a/lib/trends_IIR/rgma/rgma.pine b/lib/trends_IIR/rgma/rgma.pine
index f3a1f3dd..724812b3 100644
--- a/lib/trends_IIR/rgma/rgma.pine
+++ b/lib/trends_IIR/rgma/rgma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Recursive Gaussian Moving Average (RGMA)", "RGMA", overlay=true)
diff --git a/lib/trends_IIR/rma/rma.pine b/lib/trends_IIR/rma/rma.pine
index babf37cf..7195ee1e 100644
--- a/lib/trends_IIR/rma/rma.pine
+++ b/lib/trends_IIR/rma/rma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Wilder's Moving Average (RMA)", "RMA", overlay=true)
diff --git a/lib/trends_IIR/t3/t3.pine b/lib/trends_IIR/t3/t3.pine
index b8c7da6b..1c56589b 100644
--- a/lib/trends_IIR/t3/t3.pine
+++ b/lib/trends_IIR/t3/t3.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Tillson T3 Moving Average (T3)", "T3", overlay=true)
diff --git a/lib/trends_IIR/tema/tema.pine b/lib/trends_IIR/tema/tema.pine
index 285dea9b..0345fdb8 100644
--- a/lib/trends_IIR/tema/tema.pine
+++ b/lib/trends_IIR/tema/tema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Triple Exponential Moving Average (TEMA)", "TEMA", overlay=true)
diff --git a/lib/trends_IIR/vama/vama.pine b/lib/trends_IIR/vama/vama.pine
index 684aa0af..7f106519 100644
--- a/lib/trends_IIR/vama/vama.pine
+++ b/lib/trends_IIR/vama/vama.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volatility Adjusted Moving Average (VAMA)", "VAMA", overlay=true)
diff --git a/lib/trends_IIR/vidya/vidya.pine b/lib/trends_IIR/vidya/vidya.pine
index c1306d31..65eeba52 100644
--- a/lib/trends_IIR/vidya/vidya.pine
+++ b/lib/trends_IIR/vidya/vidya.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Variable Index Dynamic Average (VIDYA)", "VIDYA", overlay=true)
diff --git a/lib/trends_IIR/yzvama/yzvama.pine b/lib/trends_IIR/yzvama/yzvama.pine
index e6e60def..b7f0a9f4 100644
--- a/lib/trends_IIR/yzvama/yzvama.pine
+++ b/lib/trends_IIR/yzvama/yzvama.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Yang-Zhang Volatility Adjusted Moving Average (YZVAMA)", "YZVAMA", overlay=true)
diff --git a/lib/trends_IIR/zldema/zldema.pine b/lib/trends_IIR/zldema/zldema.pine
index 1d74fdbd..4d004dfb 100644
--- a/lib/trends_IIR/zldema/zldema.pine
+++ b/lib/trends_IIR/zldema/zldema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Zero-Lag Double EMA (ZLDEMA)", "ZLDEMA", overlay=true)
diff --git a/lib/trends_IIR/zlema/zlema.pine b/lib/trends_IIR/zlema/zlema.pine
index f86f2fd9..e7464a5d 100644
--- a/lib/trends_IIR/zlema/zlema.pine
+++ b/lib/trends_IIR/zlema/zlema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Zero-Lag EMA (ZLEMA)", "ZLEMA", overlay=true)
diff --git a/lib/trends_IIR/zltema/zltema.pine b/lib/trends_IIR/zltema/zltema.pine
index 7d708bf3..7d23438e 100644
--- a/lib/trends_IIR/zltema/zltema.pine
+++ b/lib/trends_IIR/zltema/zltema.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Zero-Lag Triple EMA (ZLTEMA)", "ZLTEMA", overlay=true)
diff --git a/lib/volatility/adr/adr.pine b/lib/volatility/adr/adr.pine
index de4e930a..e3ee7dc3 100644
--- a/lib/volatility/adr/adr.pine
+++ b/lib/volatility/adr/adr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Average Daily Range (ADR)", "ADR", overlay=false)
diff --git a/lib/volatility/atr/atr.pine b/lib/volatility/atr/atr.pine
index 7321e598..b9c87a9a 100644
--- a/lib/volatility/atr/atr.pine
+++ b/lib/volatility/atr/atr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Average True Range (ATR)", "ATR", overlay=false)
diff --git a/lib/volatility/atrn/atrn.pine b/lib/volatility/atrn/atrn.pine
index 86396788..23b1faf5 100644
--- a/lib/volatility/atrn/atrn.pine
+++ b/lib/volatility/atrn/atrn.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Average True Range Normalized (ATRN)", "ATRN", overlay=false, format=format.percent, precision=2)
diff --git a/lib/volatility/bbw/bbw.pine b/lib/volatility/bbw/bbw.pine
index 2a94e5b6..6d3fafa7 100644
--- a/lib/volatility/bbw/bbw.pine
+++ b/lib/volatility/bbw/bbw.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bollinger Band Width (BBW)", "BBW", overlay=false)
diff --git a/lib/volatility/bbwn/bbwn.pine b/lib/volatility/bbwn/bbwn.pine
index 17fbbd4b..363a267e 100644
--- a/lib/volatility/bbwn/bbwn.pine
+++ b/lib/volatility/bbwn/bbwn.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bollinger Band Width Normalized (BBWN)", "BBWN", overlay=false)
diff --git a/lib/volatility/bbwp/bbwp.pine b/lib/volatility/bbwp/bbwp.pine
index 0987efcc..53b8c7b5 100644
--- a/lib/volatility/bbwp/bbwp.pine
+++ b/lib/volatility/bbwp/bbwp.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bollinger Band Width Percentile (BBWP)", "BBWP", overlay=false, format=format.percent)
diff --git a/lib/volatility/ccv/ccv.pine b/lib/volatility/ccv/ccv.pine
index 24e716ec..553fcb1a 100644
--- a/lib/volatility/ccv/ccv.pine
+++ b/lib/volatility/ccv/ccv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Close-to-Close Volatility (CCV)", "CCV", overlay=false)
diff --git a/lib/volatility/cv/cv.pine b/lib/volatility/cv/cv.pine
index af7880dd..c1f842ce 100644
--- a/lib/volatility/cv/cv.pine
+++ b/lib/volatility/cv/cv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Conditional Volatility (CV)", "CV", overlay=false)
diff --git a/lib/volatility/cvi/cvi.pine b/lib/volatility/cvi/cvi.pine
index 05beafb7..66598555 100644
--- a/lib/volatility/cvi/cvi.pine
+++ b/lib/volatility/cvi/cvi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Chaikin's Volatility (CVI)", "CVI", overlay=false)
diff --git a/lib/volatility/etherm/Etherm.cs b/lib/volatility/etherm/Etherm.cs
index fed347be..d7a93f47 100644
--- a/lib/volatility/etherm/Etherm.cs
+++ b/lib/volatility/etherm/Etherm.cs
@@ -12,9 +12,8 @@ namespace QuanTAlib;
///
/// Calculation steps:
///
-/// - highDiff = |High - prevHigh|, lowDiff = |prevLow - Low|
-/// - Inside bar (High < prevHigh AND Low > prevLow) → Temperature = 0
-/// - Otherwise Temperature = max(highDiff, lowDiff)
+/// - highDiff = max(High − prevHigh, 0), lowDiff = max(prevLow − Low, 0)
+/// - Temperature = max(highDiff, lowDiff)
/// - Signal = EMA(Temperature, period) with bias compensation
///
///
@@ -271,10 +270,9 @@ public sealed class Etherm : AbstractBase
prevL = lastValidLow;
}
- double highDiff = Math.Abs(h - prevH);
- double lowDiff = Math.Abs(prevL - l);
- bool isInsideBar = h < prevH && l > prevL;
- temp = isInsideBar ? 0 : Math.Max(highDiff, lowDiff);
+ double highDiff = Math.Max(h - prevH, 0.0);
+ double lowDiff = Math.Max(prevL - l, 0.0);
+ temp = Math.Max(highDiff, lowDiff);
}
if (!double.IsFinite(temp) || temp < 0)
@@ -345,10 +343,9 @@ public sealed class Etherm : AbstractBase
}
else
{
- double highDiff = Math.Abs(high - s.PrevHigh);
- double lowDiff = Math.Abs(s.PrevLow - low);
- bool isInsideBar = high < s.PrevHigh && low > s.PrevLow;
- temp = isInsideBar ? 0 : Math.Max(highDiff, lowDiff);
+ double highDiff = Math.Max(high - s.PrevHigh, 0.0);
+ double lowDiff = Math.Max(s.PrevLow - low, 0.0);
+ temp = Math.Max(highDiff, lowDiff);
}
// NaN/Infinity safety on computed temp
diff --git a/lib/volatility/etherm/Etherm.md b/lib/volatility/etherm/Etherm.md
index f9608750..ed5f3f72 100644
--- a/lib/volatility/etherm/Etherm.md
+++ b/lib/volatility/etherm/Etherm.md
@@ -2,275 +2,125 @@
| Property | Value |
| ---------------- | -------------------------------- |
-| **Category** | Volatility |
-| **Inputs** | OHLCV bar (TBar) |
-| **Parameters** | `period` (default 22) |
-| **Outputs** | Single series (Etherm) |
-| **Output range** | $\geq 0$ |
-| **Warmup** | `period` bars |
+| **Category** | Volatility |
+| **Inputs** | OHLCV bar (TBar) |
+| **Parameters** | `period` (default 22) |
+| **Outputs** | Temperature + Signal (EMA) |
+| **Output range** | $\geq 0$ |
+| **Warmup** | `period` bars |
### TL;DR
-- Elder's Thermometer (ETHERM) measures how far today's price bar extends beyond yesterday's range, capturing the maximum absolute expansion in eithe...
-- Parameterized by `period` (default 22).
-- Output range: $\geq 0$.
+- Elder's Thermometer (ETHERM) measures how far today's price bar protrudes beyond yesterday's range, capturing the maximum outward extension in either direction.
+- Parameterized by `period` (default 22) for the EMA signal line.
+- Output range: $\geq 0$ (same units as price).
- Requires `period` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
> "Markets run a fever before they crash. The thermometer tells you when to reach for the aspirin."
-Elder's Thermometer (ETHERM) measures how far today's price bar extends beyond yesterday's range, capturing the maximum absolute expansion in either direction. Developed by Dr. Alexander Elder and described in *Come Into My Trading Room* (2002, p.162), the indicator distinguishes between sleepy, quiet periods and hot episodes when market crowds become excited. The raw thermometer reading is smoothed with an EMA to produce a signal line; when temperature spikes to triple the signal, it flags an explosive move worth fading. At 5 operations per bar for the raw value and O(1) EMA update, ETHERM is among the cheapest volatility measures to compute.
+Elder's Thermometer (ETHERM) measures bar-to-bar range extension — the maximum outward protrusion of the current bar beyond the previous bar's high or low. Developed by Dr. Alexander Elder, it captures only outward expansions; inward contractions clamp to zero. An EMA signal line with bias compensation provides a smoothed reference for detecting explosive moves (temperature significantly exceeding the signal).
## Historical Context
-Dr. Alexander Elder, a psychiatrist-turned-trader who emigrated from the Soviet Union in the 1970s, built his reputation on applying behavioral psychology to market analysis. His first book *Trading for a Living* (1993) introduced the Elder-Ray Index and the Triple Screen system. His second, *Come Into My Trading Room* (2002), added the Market Thermometer on page 162, filling a gap he identified: existing volatility tools (ATR, Bollinger Width) measured absolute dispersion, but none specifically isolated the *bar-to-bar range extension* that characterizes crowd excitement.
-
-Elder's insight was deceptively simple. Adjacent bars in a quiet market overlap. The high barely exceeds yesterday's high; the low barely undercuts yesterday's low. When the crowd gets excited, bars start pushing outside previous ranges. The thermometer captures exactly this phenomenon: how many price units did today's bar extend beyond yesterday's boundaries?
-
-The formula differs from True Range in a critical way. TR measures the total possible price excursion including gaps (max of H-L, |H-prevC|, |L-prevC|). ETHERM ignores the close entirely and focuses on high-to-high and low-to-low comparisons. A stock that gaps up 5 points but trades within a 1-point range registers TR=5 but ETHERM near zero (assuming yesterday's high was close to today's high). The two indicators answer different questions: TR asks "how far could price have traveled?" while ETHERM asks "how much did today's bar escape yesterday's?"
-
-Several implementations exist across platforms. The ProRealCode and MotiveWave versions match Elder's original formula precisely. The LightningChart JS version diverges significantly, comparing current bars to N-periods-ago bars rather than the previous bar. This QuanTAlib implementation follows Elder's original specification: previous bar comparison with inside-bar detection.
+Dr. Alexander Elder introduced the Market Thermometer in *Come Into My Trading Room* (2002) as part of his Triple Screen trading system refinements. Elder observed that bars extending well beyond the prior bar's range signaled heightened volatility — the market "running a fever." The thermometer provides a simple, bar-level volatility measure that distinguishes between outward breakouts and inward consolidation, making it ideal for stop placement and position sizing decisions.
## Architecture & Physics
-ETHERM has three components: raw temperature calculation, EMA signal smoothing, and threshold detection.
+ETHERM is a **two-stage pipeline**: a per-bar range-extension measurement followed by an exponential smoother.
-### 1. Raw Temperature Calculation
+**Stage 1 — Temperature:** For each bar, compute how far the high protrudes above the previous high and how far the low protrudes below the previous low. Only outward extensions count; inward contractions clamp to zero. The temperature is the larger of the two protrusions.
-The thermometer measures the maximum absolute extension beyond the previous bar:
+**Stage 2 — Signal:** A bias-compensated EMA of the temperature provides a smoothed baseline. The bias compensation ensures accuracy from the first bar by dividing out the geometric decay factor $e_t$, converging to a standard EMA as $e_t \to 0$.
-$$
-\text{highDiff}_t = |H_t - H_{t-1}|
-$$
+### Transfer Function
-$$
-\text{lowDiff}_t = |L_{t-1} - L_t|
-$$
+The signal line is a standard EMA applied to the temperature series:
-Three cases determine the output:
+$$H(z) = \frac{\alpha}{1 - \beta z^{-1}}, \quad \alpha = \frac{2}{N+1}, \quad \beta = 1 - \alpha$$
-$$
-T_t = \begin{cases}
-0 & \text{if } H_t < H_{t-1} \text{ AND } L_t > L_{t-1} \text{ (inside bar)} \\
-\max(\text{highDiff}_t, \text{lowDiff}_t) & \text{otherwise}
-\end{cases}
-$$
+### Half-Life
-The inside bar case is significant. When today's entire range fits within yesterday's range, there is zero range extension in either direction. The crowd is dormant.
+$$t_{1/2} = \frac{-\ln 2}{\ln \beta}$$
-### 2. EMA Signal Line
+For `period = 22`: $\beta \approx 0.913$, $t_{1/2} \approx 7.6$ bars.
-The raw temperature is smoothed with an exponential moving average:
+### Warmup Period
-$$
-\alpha = \frac{2}{N + 1}
-$$
-
-$$
-S_t = \alpha \cdot T_t + (1 - \alpha) \cdot S_{t-1}
-$$
-
-Default period $N = 22$ (approximately one trading month). The EMA provides a baseline "normal temperature" against which spikes and troughs are measured.
-
-### 3. Threshold Detection
-
-Elder defined two key thresholds:
-
-**Explosive move:** When the thermometer reaches or exceeds the signal multiplied by a factor (default 3.0):
-
-$$
-\text{Explosive} = T_t \geq S_t \times M
-$$
-
-where $M$ is the multiplier (default 3.0).
-
-**Idle market:** When the thermometer remains below the signal for a sustained number of consecutive bars (Elder suggested 5-7 bars). This is a secondary signal not computed in the indicator itself but observable from the histogram.
-
-### 4. First Bar Handling
-
-For the first bar (no previous bar available):
-
-$$
-T_0 = 0
-$$
-
-Using `nz(high[1], high)` maps the previous high to today's high, making highDiff and lowDiff both zero. This is correct: with no history, there is no range extension to measure.
+QuanTAlib uses bias-compensated EMA, which converges after approximately `period` bars. During warmup, outputs are produced but `IsHot` returns false until the compensator $e_t \leq 0.05$.
## Mathematical Foundation
-### Why Absolute Values?
+### Step 1: Outward Protrusions
-Consider a bar where today's high is 102 and yesterday's high was 105. The extension is $|102 - 105| = 3$. Without the absolute value, the result would be $-3$, hiding the magnitude. Elder's thermometer cares about *size* of escape, not direction. A 3-point high compression and a 3-point low extension represent equal amounts of crowd activity.
+$$\text{highDiff}_t = \max(H_t - H_{t-1},\; 0)$$
+$$\text{lowDiff}_t = \max(L_{t-1} - L_t,\; 0)$$
-### Relationship to True Range
+### Step 2: Temperature
-True Range and ETHERM share a structural similarity but measure different phenomena:
+$$T_t = \max(\text{highDiff}_t,\; \text{lowDiff}_t)$$
-| Scenario | TR | ETHERM |
-| :--- | :--- | :--- |
-| No gap, wide bar | $H - L$ | $\max(\|H-H_{-1}\|, \|L_{-1}-L\|)$ |
-| Large gap up, narrow bar | $H - C_{-1}$ (large) | Near 0 (similar H-to-H) |
-| Breakout bar exceeding prior range | $H - L$ | Large (extension detected) |
-| Inside bar | $H - L$ (positive) | 0 (no extension) |
+### Step 3: EMA Signal with Bias Compensation
-ETHERM specifically detects range *expansion*. TR detects total price travel. A market that gaps and then consolidates shows high TR but low ETHERM.
+$$\text{ema}_t = \beta \cdot \text{ema}_{t-1} + \alpha \cdot T_t$$
-### EMA Warmup Compensation
+$$e_t = \beta \cdot e_{t-1}, \quad e_0 = 1$$
-The PineScript reference implementation uses warmup-compensated EMA to eliminate initialization bias:
+$$\text{Signal}_t = \begin{cases} \frac{\text{ema}_t}{1 - e_t} & \text{if } e_t > \epsilon \\ \text{ema}_t & \text{otherwise} \end{cases}$$
-$$
-e_t = e_{t-1} \cdot (1 - \alpha), \quad e_0 = 1
-$$
-
-$$
-S_{compensated} = \frac{S_{raw}}{1 - e_t} \quad \text{when } e_t > \epsilon
-$$
-
-This ensures accurate signal values from the first bar rather than waiting for the EMA to "fill up."
-
-### Convergence
-
-For EMA period $N = 22$, $\alpha = 2/23 \approx 0.087$:
-
-$$
-\text{WarmupPeriod} \approx \frac{\ln(0.05)}{\ln(1 - \alpha)} \approx \frac{-3.0}{-0.091} \approx 33 \text{ bars}
-$$
-
-After 33 bars, the initialization bias drops below 5%.
-
-### Inside Bar Probability
-
-In typical equity markets, inside bars occur approximately 15-25% of trading days. The zero-temperature reading for inside bars creates a natural floor that keeps the EMA signal from rising without genuine range extension. This asymmetry is intentional: Elder wanted the thermometer to measure heat, not cold.
+where $N$ = `period`, $H_t$ = High, $L_t$ = Low, $\epsilon = 10^{-10}$.
## Performance Profile
-### Operation Count (Streaming Mode, Scalar)
+### Operation Count (per bar)
-Per-bar operations:
+| Operation | Count | Notes |
+| --------------- | ----- | ---------------------------------- |
+| Subtract | 2 | High/low diffs |
+| Max | 3 | Clamp to 0 (×2), final max |
+| FMA | 1 | EMA update |
+| Multiply | 2 | $\alpha \cdot T$, $\beta \cdot e$ |
+| Division | 1 | Bias compensation |
+| Compare/branch | 2 | Finite check, bias threshold |
+| **Total** | ~11 | O(1) per bar, no allocations |
-| Operation | Count | Cost (cycles) | Subtotal |
-| :--- | :---: | :---: | :---: |
-| SUB | 2 | 1 | 2 |
-| ABS | 2 | 1 | 2 |
-| CMP | 3 | 1 | 3 |
-| MAX | 1 | 1 | 1 |
-| FMA | 1 | 5 | 5 |
-| MUL | 2 | 3 | 6 |
-| DIV | 1 | 15 | 15 |
-| **Total** | **12** | | **~34 cycles** |
+### SIMD Applicability
-ETHERM is extremely lightweight. No logarithms, no square roots, no transcendental functions. The EMA update dominates at ~60% of total cost.
+Not beneficial — the recursive EMA dependency prevents vectorization. Each bar depends on the previous bar's state.
-### Batch Mode (512 values, SIMD/FMA)
+### Memory Layout
-| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
-| :--- | :---: | :---: | :---: |
-| Subtractions (H-prevH, prevL-L) | 1024 | 128 | 8x |
-| Absolute values | 1024 | 128 | 8x |
-| Comparisons + MAX | 1536 | 192 | 8x |
-| EMA update | 512 | 512 | 1x (sequential) |
-
-The raw temperature calculation vectorizes perfectly. The EMA is inherently sequential (each value depends on the previous), limiting overall batch speedup to roughly 3-4x.
-
-### Memory Profile
-
-- **Per instance:** ~64 bytes (state struct with prevHigh, prevLow, EMA state, warmup)
-- **No ring buffer required** (only needs previous bar's high and low)
-- **100 instances:** ~6.4 KB
-
-### Quality Metrics
-
-| Metric | Score | Notes |
-| :--- | :---: | :--- |
-| **Accuracy** | 10/10 | Exact calculation, no approximations |
-| **Timeliness** | 9/10 | Minimal lag; raw value is instantaneous, EMA adds slight delay |
-| **Smoothness** | 4/10 | Raw thermometer is spiky by design; signal line smooths |
-| **Simplicity** | 9/10 | Two subtractions, two abs, one max, one EMA |
-| **Interpretability** | 8/10 | Direct physical meaning: price units of range extension |
+| Field | Type | Bytes | Purpose |
+| -------------- | -------- | ----- | ---------------------------- |
+| `PrevHigh` | `double` | 8 | Previous bar's high |
+| `PrevLow` | `double` | 8 | Previous bar's low |
+| `Ema` | `double` | 8 | Running EMA of temperature |
+| `E` | `double` | 8 | Bias compensator |
+| `LastValidHigh`| `double` | 8 | NaN fallback for high |
+| `LastValidLow` | `double` | 8 | NaN fallback for low |
+| `LastValidTemp`| `double` | 8 | NaN fallback for temperature |
+| `Count` | `int` | 4 | Bar counter |
+| **Total** | | 60 | Single cache line |
## Validation
-ETHERM is not widely implemented in major open-source libraries under a standard name. Most implementations are custom scripts.
-
-| Library | Status | Notes |
-| :--- | :---: | :--- |
-| **TA-Lib** | N/A | Not implemented |
-| **Skender** | N/A | Not implemented |
-| **Tulip** | N/A | Not implemented |
-| **OoplesFinance** | N/A | Not implemented |
-| **PineScript** | ✅ | Matches etherm.pine reference |
-| **ProRealCode** | ✅ | Matches Elder's original formula |
-| **MotiveWave** | ✅ | Confirms formula: "highest absolute difference" |
-| **Manual** | ✅ | Validated against Elder p.162 formula |
-
-The absence from standard libraries is unsurprising. ETHERM was published in a trading book, not an academic paper. It lacks the institutional pedigree of Wilder's indicators (ATR, RSI) or Bollinger's Bands. The algorithm is simple enough that most platforms implement it as a custom script rather than a built-in function.
+| Library | Match | Notes |
+| -------- | ----- | ---------------------------------------- |
+| TA-Lib | — | No Elder Thermometer function |
+| Skender | — | No direct equivalent |
+| Tulip | — | No direct equivalent |
+| Self | ✓ | Batch ⟷ streaming ⟷ span consistency |
+| Pine | ✓ | `etherm.pine` matches C# output |
## Common Pitfalls
-1. **Confusing ETHERM with ATR**: ATR measures total price excursion including gaps (uses close). ETHERM measures bar-to-bar range extension (ignores close entirely). A large gap-up with a narrow range produces high ATR but near-zero ETHERM. Using one where the other is intended produces meaningfully wrong signals.
-
-2. **Inside bar handling**: Some implementations omit the inside bar check, computing `max(highDiff, lowDiff)` even when both differences are negative (meaning compression, not expansion). This incorrectly reports range contraction as if it were expansion. Elder's original formula explicitly returns zero for inside bars.
-
-3. **Absolute value omission**: The formula requires absolute values of the differences. When `Low_today > Low_yesterday`, `Low_yesterday - Low_today` is negative. Without `abs()`, the max function may select highDiff by default even when lowDiff is the dominant extension. Approximately 10-15% of signals will be wrong.
-
-4. **EMA period sensitivity**: Elder's default of 22 bars (roughly one trading month) works for daily charts. On 5-minute charts, 22 bars spans less than 2 hours. For intraday use, scale the period proportionally: ~250 for 5-min, ~50 for hourly. Using period 22 on intraday data produces an overly responsive signal line.
-
-5. **Multiplier calibration**: The default 3.0 multiplier for explosive moves was designed for daily equity data in the late 1990s. Crypto and high-volatility assets may need higher multipliers (4.0-5.0) to avoid false positives. Low-volatility instruments (bonds, utilities) may need lower multipliers (2.0-2.5). Test the multiplier against historical data before relying on it.
-
-6. **Zero-temperature clustering**: Inside bars cluster during consolidation. Extended periods of zero readings followed by a breakout bar produce a spike that appears dramatic relative to the suppressed EMA. This is feature, not bug: Elder designed the indicator to flag exactly this transition. But traders should be aware that the spike magnitude reflects the prior calm as much as the current excitement.
-
-7. **No directional information**: ETHERM measures magnitude of range extension but not direction. A 10-point extension could be bullish (new highs) or bearish (new lows). Pair ETHERM with directional indicators (Elder-Ray, Impulse System) for complete context.
-
-## Trading Applications
-
-### Entry Timing
-
-Elder's primary recommendation: enter positions when Thermometer < Signal:
-
-```text
-If system generates entry signal AND ETHERM < Signal:
- Execute entry (low slippage environment)
-If system generates entry signal AND ETHERM > Signal:
- Wait or reduce size (hot market, slippage likely)
-```
-
-### Profit-Taking on Spikes
-
-Exit (or take partial profits) when Thermometer >= Signal x 3:
-
-```text
-If ETHERM >= Signal × multiplier:
- Take profits on existing positions
- Panics are short-lived; cash in before reversion
-```
-
-### Volatility Regime Filter
-
-Track consecutive bars below the signal line:
-
-```text
-If ETHERM < Signal for 7+ consecutive bars:
- Market is idle/consolidating
- Prepare for potential breakout
- Tighten stops or reduce position size
-```
-
-## Relationship to Other Indicators
-
-| Indicator | Relationship to ETHERM |
-| :--- | :--- |
-| **TR** | TR measures total price travel (with gaps); ETHERM measures range extension only |
-| **ATR** | Smoothed TR; both measure volatility but from different perspectives |
-| **Elder-Ray** | Bull/Bear Power measures distance from EMA; complements ETHERM's range extension |
-| **Impulse System** | Directional classification; pair with ETHERM for timing |
-| **Bollinger Width** | Measures band expansion/contraction; slower-moving volatility gauge |
-| **ADX** | Trend strength; ETHERM measures volatility regardless of trend |
+1. **Using close-only data** — ETHERM requires High and Low prices. When fed a single value (TValue), it treats H=L, producing zero temperature. Always use `Update(TBar)`.
+2. **Confusing temperature with signal** — The `Value` property returns the raw temperature (current bar only); the `Signal` property returns the smoothed EMA. Use signal for trend comparisons.
+3. **Inside bars** — Both protrusions clamp to zero, so inside bars always produce temperature = 0. This is by design, not a bug.
+4. **First bar** — No previous bar exists, so temperature = 0. The EMA signal starts building from the second bar.
+5. **Explosive threshold** — A common strategy is to flag bars where temperature exceeds `Signal × multiplier` (e.g., 3×) as explosive moves.
## References
-- Elder, A. (2002). *Come Into My Trading Room: A Complete Guide to Trading*. John Wiley & Sons. pp. 162-164.
-- Elder, A. (1993). *Trading for a Living: Psychology, Trading Tactics, Money Management*. John Wiley & Sons.
-- Elder, A. (2014). *The New Trading for a Living*. John Wiley & Sons. (Updated treatment of the Thermometer.)
-- LazyBear. (2015). "Elder's Market Thermometer." TradingView Community Scripts.
-- MotiveWave Documentation. "Elders Thermometer (THER)." docs.motivewave.com.
+- **Elder, Alexander** (2002). *Come Into My Trading Room: A Complete Guide to Trading*, Wiley. p. 162.
+- **Elder, Alexander** (1993). *Trading for a Living*, Wiley. (Earlier discussion of volatility-based stops.)
\ No newline at end of file
diff --git a/lib/volatility/etherm/etherm.pine b/lib/volatility/etherm/etherm.pine
index 2f3bb0b6..0345cda6 100644
--- a/lib/volatility/etherm/etherm.pine
+++ b/lib/volatility/etherm/etherm.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Elder's Thermometer", "ETHERM", overlay=false)
@@ -12,14 +12,13 @@ etherm(simple int period) =>
runtime.error("Period must be greater than 0")
// Step 1: Calculate raw thermometer value
- // Temperature = max(abs(High - prevHigh), abs(prevLow - Low))
- // Inside bar (High < prevHigh AND Low > prevLow) => 0
+ // Temperature = max of upward high protrusion and downward low protrusion
+ // Only outward extensions count; contractions clamp to zero
float prevHigh = nz(high[1], high)
float prevLow = nz(low[1], low)
- float highDiff = math.abs(high - prevHigh)
- float lowDiff = math.abs(prevLow - low)
- bool isInsideBar = high < prevHigh and low > prevLow
- float temp = isInsideBar ? 0.0 : math.max(highDiff, lowDiff)
+ float highDiff = math.max(high - prevHigh, 0.0)
+ float lowDiff = math.max(prevLow - low, 0.0)
+ float temp = math.max(highDiff, lowDiff)
// Step 2: EMA of thermometer with warmup compensation
float alpha = 2.0 / float(period + 1)
@@ -51,4 +50,4 @@ color thermColor = isExplosive ? color.red : isHot ? color.orange : color.new(co
// Plot
plot(thermValue, "Thermometer", color=thermColor, style=plot.style_histogram, linewidth=2)
-plot(signalValue, "Signal", color=color.yellow, linewidth=2)
+plot(signalValue, "Signal", color=color.yellow, linewidth=2)
\ No newline at end of file
diff --git a/lib/volatility/ewma/ewma.pine b/lib/volatility/ewma/ewma.pine
index 7f3d4461..fd709a11 100644
--- a/lib/volatility/ewma/ewma.pine
+++ b/lib/volatility/ewma/ewma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Exponential Weighted MA Volatility", "EWMA Volty", overlay=false)
diff --git a/lib/volatility/gkv/gkv.pine b/lib/volatility/gkv/gkv.pine
index 588427e8..9da61a35 100644
--- a/lib/volatility/gkv/gkv.pine
+++ b/lib/volatility/gkv/gkv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Garman-Klass Volatility (GKV)", "GKV", overlay=false)
diff --git a/lib/volatility/hlv/hlv.pine b/lib/volatility/hlv/hlv.pine
index 18279a7d..b3e838e0 100644
--- a/lib/volatility/hlv/hlv.pine
+++ b/lib/volatility/hlv/hlv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("High-Low Volatility (HLV)", "HLV", overlay=false)
diff --git a/lib/volatility/hv/hv.pine b/lib/volatility/hv/hv.pine
index ebba636e..2ff26172 100644
--- a/lib/volatility/hv/hv.pine
+++ b/lib/volatility/hv/hv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Historical Volatility (HV)", "HV", overlay=false)
diff --git a/lib/volatility/jvolty/jvolty.pine b/lib/volatility/jvolty/jvolty.pine
index 0b4ec0b6..93ccb972 100644
--- a/lib/volatility/jvolty/jvolty.pine
+++ b/lib/volatility/jvolty/jvolty.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Jurik Volatility", "Jvolty", overlay=false)
diff --git a/lib/volatility/jvoltyn/jvoltyn.pine b/lib/volatility/jvoltyn/jvoltyn.pine
index d32d0026..47e56942 100644
--- a/lib/volatility/jvoltyn/jvoltyn.pine
+++ b/lib/volatility/jvoltyn/jvoltyn.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("JVOLTYN - Normalized Jurik Volatility", shorttitle="JVOLTYN", overlay=false)
diff --git a/lib/volatility/massi/massi.pine b/lib/volatility/massi/massi.pine
index 19afd5ab..cbe72986 100644
--- a/lib/volatility/massi/massi.pine
+++ b/lib/volatility/massi/massi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Mass Index (MASSI)", "MASSI", overlay=false)
diff --git a/lib/volatility/natr/natr.pine b/lib/volatility/natr/natr.pine
index f77a9025..d0dd537b 100644
--- a/lib/volatility/natr/natr.pine
+++ b/lib/volatility/natr/natr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Normalized Average True Range", "NATR", overlay=false, format=format.percent, precision=2)
diff --git a/lib/volatility/rsv/rsv.pine b/lib/volatility/rsv/rsv.pine
index 816c74aa..36509075 100644
--- a/lib/volatility/rsv/rsv.pine
+++ b/lib/volatility/rsv/rsv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Rogers-Satchell Volatility (RSV)", "RSV", overlay=false)
diff --git a/lib/volatility/rv/rv.pine b/lib/volatility/rv/rv.pine
index 1dbd5b5a..f5a1573b 100644
--- a/lib/volatility/rv/rv.pine
+++ b/lib/volatility/rv/rv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Realized Volatility (RV)", "RV", overlay=false)
diff --git a/lib/volatility/rvi/Rvi.Tests.cs b/lib/volatility/rvi/Rvi.Tests.cs
index f2904f31..7db94fcc 100644
--- a/lib/volatility/rvi/Rvi.Tests.cs
+++ b/lib/volatility/rvi/Rvi.Tests.cs
@@ -108,12 +108,35 @@ public class RviTests
}
[Fact]
- public void Update_WithTBar_UsesClosePrice()
+ public void Update_WithTBar_UsesHighAndLow()
{
var rvi = new Rvi();
var bar = new TBar(DateTime.UtcNow, 98, 102, 97, 100, 1000);
var result = rvi.Update(bar);
- Assert.Equal(50.0, result.Value, Tolerance); // First value
+ Assert.Equal(50.0, result.Value, Tolerance); // First value is always neutral
+ }
+
+ [Fact]
+ public void Update_WithTBar_RevisedDiffersFromOriginal()
+ {
+ // The revised RVI (high+low avg) should differ from original (close-only)
+ // Use oscillating close with asymmetric high/low
+ var rviBar = new Rvi(stdevLength: 5, rmaLength: 5);
+ var rviClose = new Rvi(stdevLength: 5, rmaLength: 5);
+
+ for (int i = 0; i < 50; i++)
+ {
+ var time = DateTime.UtcNow.AddSeconds(i);
+ double close = 100.0 + Math.Sin(i * 0.5) * 3.0; // oscillating
+ double high = close + 2.0 + Math.Sin(i * 0.3) * 1.5; // asymmetric highs
+ double low = close - 1.0 - Math.Cos(i * 0.7) * 0.8; // asymmetric lows
+
+ rviBar.Update(new TBar(time, close - 0.5, high, low, close, 1000));
+ rviClose.Update(new TValue(time, close));
+ }
+
+ // With asymmetric high/low, revised RVI should differ from close-only
+ Assert.NotEqual(rviBar.Last.Value, rviClose.Last.Value, 0.01);
}
[Fact]
diff --git a/lib/volatility/rvi/Rvi.cs b/lib/volatility/rvi/Rvi.cs
index 64be846b..5b767fdb 100644
--- a/lib/volatility/rvi/Rvi.cs
+++ b/lib/volatility/rvi/Rvi.cs
@@ -1,5 +1,5 @@
-// Relative Volatility Index (RVI) Indicator
-// Measures the direction of volatility using standard deviation and RMA smoothing
+// Relative Volatility Index (RVI) Indicator — Revised (1995) version
+// Averages original RVI computed on High and Low series separately
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -7,29 +7,23 @@ using System.Runtime.InteropServices;
namespace QuanTAlib;
///
-/// RVI: Relative Volatility Index
-/// Measures the direction of volatility by comparing upward and downward price movements
-/// weighted by their standard deviations, smoothed with Wilder's RMA.
+/// RVI: Relative Volatility Index (Revised)
+/// Computes original RVI on the High series and on the Low series, then averages.
+/// Each channel classifies stddev direction based on its own price change.
///
///
-/// Calculation steps:
+/// Calculation steps (per channel — High and Low independently):
///
-/// - Calculate population standard deviation of prices over stdevLength
+/// - Calculate population standard deviation over stdevLength
/// - Classify by price change: if up, upStd = stddev; if down, downStd = stddev
/// - Smooth upStd and downStd with RMA (Wilder's smoothing with bias correction)
-/// - RVI = 100 × avgUpStd / (avgUpStd + avgDownStd)
-///
-///
-/// Key characteristics:
-///
-/// - Oscillator ranging from 0 to 100
-/// - Values above 50 indicate upward volatility momentum
-/// - Values below 50 indicate downward volatility momentum
-/// - Often used to confirm RSI signals or as a standalone indicator
+/// - channelRVI = 100 × avgUpStd / (avgUpStd + avgDownStd)
///
+/// Final: RVI = (RVI_high + RVI_low) / 2
///
/// Sources:
-/// Donald Dorsey (1993). "The Relative Volatility Index". Technical Analysis of Stocks & Commodities.
+/// Donald Dorsey (1993, original; 1995, revised). Technical Analysis of Stocks & Commodities.
+/// FM Labs: https://www.fmlabs.com/reference/RVI.htm
///
[SkipLocalsInit]
public sealed class Rvi : AbstractBase
@@ -39,10 +33,11 @@ public sealed class Rvi : AbstractBase
private readonly int _stdevLength;
private readonly int _rmaLength;
private readonly double _alpha;
- private readonly RingBuffer _priceBuffer;
+ private readonly RingBuffer _hiBuf;
+ private readonly RingBuffer _loBuf;
[StructLayout(LayoutKind.Auto)]
- private record struct State(
+ private record struct ChState(
double PrevPrice,
double Sum,
double SumSq,
@@ -50,46 +45,34 @@ public sealed class Rvi : AbstractBase
double EUp,
double RawRmaDown,
double EDown,
- double LastValue,
int FillCount
);
- private State _s;
- private State _ps;
- ///
- /// Initializes a new instance of the Rvi class.
- ///
- /// The lookback period for standard deviation calculation (default 10).
- /// The lookback period for RMA smoothing (default 14).
- ///
- /// Thrown when stdevLength is less than 2, or rmaLength is less than 1.
- ///
+ private ChState _hi, _phi;
+ private ChState _lo, _plo;
+ private double _lastValue, _pLastValue;
+
public Rvi(int stdevLength = 10, int rmaLength = 14)
{
if (stdevLength < 2)
- {
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
- }
if (rmaLength < 1)
- {
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
- }
+
_stdevLength = stdevLength;
_rmaLength = rmaLength;
_alpha = 1.0 / rmaLength;
- _priceBuffer = new RingBuffer(stdevLength);
+ _hiBuf = new RingBuffer(stdevLength);
+ _loBuf = new RingBuffer(stdevLength);
WarmupPeriod = stdevLength + rmaLength;
Name = $"Rvi({stdevLength},{rmaLength})";
- _s = new State(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 50.0, 0);
- _ps = _s;
+
+ var init = new ChState(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 0);
+ _hi = _phi = init;
+ _lo = _plo = init;
+ _lastValue = _pLastValue = 50.0;
}
- ///
- /// Initializes a new instance of the Rvi class with a source.
- ///
- /// The data source for chaining.
- /// The lookback period for standard deviation calculation (default 10).
- /// The lookback period for RMA smoothing (default 14).
public Rvi(ITValuePublisher source, int stdevLength = 10, int rmaLength = 14)
: this(stdevLength, rmaLength)
{
@@ -98,56 +81,27 @@ public sealed class Rvi : AbstractBase
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
- ///
- /// True if the indicator has enough data for valid results.
- ///
- public override bool IsHot => _s.FillCount >= _stdevLength;
+ public override bool IsHot => _hi.FillCount >= _stdevLength;
- ///
- /// The lookback period for standard deviation calculation.
- ///
public int StdevLength => _stdevLength;
-
- ///
- /// The lookback period for RMA smoothing.
- ///
public int RmaLength => _rmaLength;
- ///
- /// Updates the indicator with a new price value.
- ///
- /// The input price value.
- /// Whether this is a new bar or an update.
- /// The calculated RVI value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
- return UpdateCore(input.Time, input.Value, isNew);
+ return UpdateCore(input.Time, input.Value, input.Value, isNew);
}
- ///
- /// Updates the indicator with a new bar (uses Close price).
- ///
- /// The input bar.
- /// Whether this is a new bar or an update.
- /// The calculated RVI value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
- return UpdateCore(bar.Time, bar.Close, isNew);
+ return UpdateCore(bar.Time, bar.High, bar.Low, isNew);
}
- ///
- /// Updates the indicator with a bar series.
- ///
- /// The source bar series.
- /// A TSeries containing the RVI values.
public TSeries Update(TBarSeries source)
{
if (source.Count == 0)
- {
return [];
- }
int len = source.Count;
var t = new List(len);
@@ -158,31 +112,29 @@ public sealed class Rvi : AbstractBase
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
- // Extract close prices
- Span closes = len <= 128 ? stackalloc double[len] : new double[len];
+ Span highs = len <= 128 ? stackalloc double[len] : new double[len];
+ Span lows = len <= 128 ? stackalloc double[len] : new double[len];
for (int i = 0; i < len; i++)
{
- closes[i] = source[i].Close;
+ highs[i] = source[i].High;
+ lows[i] = source[i].Low;
tSpan[i] = source[i].Time;
}
- Batch(closes, vSpan, _stdevLength, _rmaLength);
+ BatchDual(highs, lows, vSpan, _stdevLength, _rmaLength);
- // Update internal state
+ // Sync internal state
for (int i = 0; i < len; i++)
- {
- Update(new TValue(source[i].Time, source[i].Close), isNew: true);
- }
+ Update(source[i], isNew: true);
return new TSeries(t, v);
}
+
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
- {
return [];
- }
int len = source.Count;
var t = new List(len);
@@ -193,49 +145,64 @@ public sealed class Rvi : AbstractBase
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
+ // Single-price series: same value to both channels
Batch(source.Values, vSpan, _stdevLength, _rmaLength);
source.Times.CopyTo(tSpan);
- // Update internal state
for (int i = 0; i < len; i++)
- {
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
- }
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private TValue UpdateCore(long timeTicks, double price, bool isNew)
+ private TValue UpdateCore(long timeTicks, double hiPrice, double loPrice, bool isNew)
{
if (isNew)
{
- _ps = _s;
- _priceBuffer.Snapshot();
+ _phi = _hi;
+ _plo = _lo;
+ _pLastValue = _lastValue;
+ _hiBuf.Snapshot();
+ _loBuf.Snapshot();
}
else
{
- _s = _ps;
- _priceBuffer.Restore();
+ _hi = _phi;
+ _lo = _plo;
+ _lastValue = _pLastValue;
+ _hiBuf.Restore();
+ _loBuf.Restore();
}
- var s = _s;
-
- // Handle non-finite price
- if (!double.IsFinite(price))
+ // Handle non-finite
+ if (!double.IsFinite(hiPrice) || !double.IsFinite(loPrice))
{
- Last = new TValue(timeTicks, s.LastValue);
+ Last = new TValue(timeTicks, _lastValue);
PubEvent(Last, isNew);
return Last;
}
- double rviValue;
+ double rviHi = UpdateChannel(ref _hi, _hiBuf, hiPrice);
+ double rviLo = UpdateChannel(ref _lo, _loBuf, loPrice);
+ double rviValue = (rviHi + rviLo) * 0.5;
- // Need previous price for direction
+ if (!double.IsFinite(rviValue))
+ rviValue = _lastValue;
+ else
+ _lastValue = rviValue;
+
+ Last = new TValue(timeTicks, rviValue);
+ PubEvent(Last, isNew);
+ return Last;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private double UpdateChannel(ref ChState s, RingBuffer buf, double price)
+ {
if (double.IsNaN(s.PrevPrice))
{
- // First price - add to buffer but no RVI yet
- _priceBuffer.Add(price);
+ buf.Add(price);
s = s with
{
PrevPrice = price,
@@ -243,137 +210,87 @@ public sealed class Rvi : AbstractBase
SumSq = price * price,
FillCount = 1
};
- rviValue = 50.0; // Neutral
+ return 50.0;
}
- else
+
+ double priceChange = price - s.PrevPrice;
+
+ double oldSum = s.Sum;
+ double oldSumSq = s.SumSq;
+ int oldCount = s.FillCount;
+
+ if (buf.Count == _stdevLength)
{
- // Calculate price change direction
- double priceChange = price - s.PrevPrice;
-
- // Update price buffer for stddev calculation
- double oldSum = s.Sum;
- double oldSumSq = s.SumSq;
- int oldCount = s.FillCount;
-
- // Remove oldest if buffer full
- if (_priceBuffer.Count == _stdevLength)
- {
- double oldest = _priceBuffer[0];
- oldSum -= oldest;
- oldSumSq -= oldest * oldest;
- oldCount--;
- }
-
- // Add new price
- _priceBuffer.Add(price);
- double newSum = oldSum + price;
- double newSumSq = oldSumSq + (price * price);
- int newCount = oldCount + 1;
-
- // Calculate population stddev
- double currentStdDev = 0.0;
- if (newCount > 1)
- {
- double mean = newSum / newCount;
- double variance = (newSumSq / newCount) - (mean * mean);
- variance = Math.Max(0.0, variance);
- currentStdDev = Math.Sqrt(variance);
- }
-
- // Classify stddev by direction
- double upStdVal = 0.0;
- double downStdVal = 0.0;
-
- if (priceChange > 0)
- {
- upStdVal = currentStdDev;
- }
- else if (priceChange < 0)
- {
- downStdVal = currentStdDev;
- }
- // If priceChange == 0, both stay 0
-
- // RMA with bias correction for upward stddev
- double rawRmaUp = s.RawRmaUp;
- double eUp = s.EUp;
-
- rawRmaUp = Math.FusedMultiplyAdd(rawRmaUp, _rmaLength - 1, upStdVal) / _rmaLength;
- eUp = (1 - _alpha) * eUp;
- double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
-
- // RMA with bias correction for downward stddev
- double rawRmaDown = s.RawRmaDown;
- double eDown = s.EDown;
-
- rawRmaDown = Math.FusedMultiplyAdd(rawRmaDown, _rmaLength - 1, downStdVal) / _rmaLength;
- eDown = (1 - _alpha) * eDown;
- double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
-
- // Calculate RVI
- double sumAvgStd = avgUpStd + avgDownStd;
- rviValue = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
-
- s = s with
- {
- PrevPrice = price,
- Sum = newSum,
- SumSq = newSumSq,
- RawRmaUp = rawRmaUp,
- EUp = eUp,
- RawRmaDown = rawRmaDown,
- EDown = eDown,
- FillCount = newCount
- };
+ double oldest = buf[0];
+ oldSum -= oldest;
+ oldSumSq -= oldest * oldest;
+ oldCount--;
}
- if (!double.IsFinite(rviValue))
+ buf.Add(price);
+ double newSum = oldSum + price;
+ double newSumSq = oldSumSq + (price * price);
+ int newCount = oldCount + 1;
+
+ double currentStdDev = 0.0;
+ if (newCount > 1)
{
- rviValue = s.LastValue;
- }
- else
- {
- s = s with { LastValue = rviValue };
+ double mean = newSum / newCount;
+ double variance = (newSumSq / newCount) - (mean * mean);
+ variance = Math.Max(0.0, variance);
+ currentStdDev = Math.Sqrt(variance);
}
- _s = s;
+ double upStdVal = 0.0;
+ double downStdVal = 0.0;
+ if (priceChange > 0)
+ upStdVal = currentStdDev;
+ else if (priceChange < 0)
+ downStdVal = currentStdDev;
- Last = new TValue(timeTicks, rviValue);
- PubEvent(Last, isNew);
- return Last;
+ double rawRmaUp = Math.FusedMultiplyAdd(s.RawRmaUp, _rmaLength - 1, upStdVal) / _rmaLength;
+ double eUp = (1 - _alpha) * s.EUp;
+ double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
+
+ double rawRmaDown = Math.FusedMultiplyAdd(s.RawRmaDown, _rmaLength - 1, downStdVal) / _rmaLength;
+ double eDown = (1 - _alpha) * s.EDown;
+ double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
+
+ double sumAvgStd = avgUpStd + avgDownStd;
+ double rvi = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
+
+ s = new ChState(price, newSum, newSumSq, rawRmaUp, eUp, rawRmaDown, eDown, newCount);
+ return rvi;
}
+
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
for (int i = 0; i < source.Length; i++)
- {
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
- }
}
+
public override void Reset()
{
- _s = new State(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 50.0, 0);
- _ps = _s;
- _priceBuffer.Clear();
+ var init = new ChState(double.NaN, 0, 0, 0, 1.0, 0, 1.0, 0);
+ _hi = _phi = init;
+ _lo = _plo = init;
+ _lastValue = _pLastValue = 50.0;
+ _hiBuf.Clear();
+ _loBuf.Clear();
Last = default;
}
+ // --- Static Batch methods ---
+
///
- /// Calculates Relative Volatility Index for a price series (static).
+ /// Batch RVI for a single-price series (same value to both channels → original behavior).
///
- /// The source price series.
- /// The lookback period for standard deviation.
- /// The lookback period for RMA smoothing.
- /// A TSeries containing the RVI values.
public static TSeries Batch(TSeries source, int stdevLength = 10, int rmaLength = 14)
{
if (stdevLength < 2)
- {
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
- }
if (rmaLength < 1)
- {
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
- }
int len = source.Count;
var t = new List(len);
@@ -381,17 +298,14 @@ public sealed class Rvi : AbstractBase
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
- var tSpan = CollectionsMarshal.AsSpan(t);
- var vSpan = CollectionsMarshal.AsSpan(v);
-
- Batch(source.Values, vSpan, stdevLength, rmaLength);
- source.Times.CopyTo(tSpan);
+ Batch(source.Values, CollectionsMarshal.AsSpan(v), stdevLength, rmaLength);
+ source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
return new TSeries(t, v);
}
///
- /// Calculates RVI for a bar series (static).
+ /// Batch RVI for a bar series (revised: high+low average).
///
public static TSeries Batch(TBarSeries source, int stdevLength = 10, int rmaLength = 14)
{
@@ -400,12 +314,8 @@ public sealed class Rvi : AbstractBase
}
///
- /// Batch calculation using spans.
+ /// Span-based batch for single-price series. Same price to both channels → original behavior.
///
- /// Price values.
- /// Output RVI values.
- /// The lookback period for standard deviation.
- /// The lookback period for RMA smoothing.
public static void Batch(
ReadOnlySpan prices,
Span output,
@@ -413,27 +323,64 @@ public sealed class Rvi : AbstractBase
int rmaLength = 14)
{
if (stdevLength < 2)
- {
throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
- }
if (rmaLength < 1)
- {
throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
- }
if (output.Length < prices.Length)
- {
throw new ArgumentException("Output span must be at least as long as prices span", nameof(output));
- }
+ // Single-price: feed same data to both channels, average = original
+ BatchDual(prices, prices, output, stdevLength, rmaLength);
+ }
+
+ ///
+ /// Span-based batch for dual-channel (high + low) revised RVI.
+ ///
+ public static void BatchDual(
+ ReadOnlySpan highs,
+ ReadOnlySpan lows,
+ Span output,
+ int stdevLength = 10,
+ int rmaLength = 14)
+ {
+ if (stdevLength < 2)
+ throw new ArgumentException("Standard deviation length must be at least 2", nameof(stdevLength));
+ if (rmaLength < 1)
+ throw new ArgumentException("RMA length must be at least 1", nameof(rmaLength));
+
+ int len = highs.Length;
+ if (len == 0)
+ return;
+ if (output.Length < len)
+ throw new ArgumentException("Output span must be at least as long as input span", nameof(output));
+
+ // Allocate temp buffers for each channel's RVI output
+ Span rviHi = len <= 256 ? stackalloc double[len] : new double[len];
+ Span rviLo = len <= 256 ? stackalloc double[len] : new double[len];
+
+ BatchSingleChannel(highs, rviHi, stdevLength, rmaLength);
+ BatchSingleChannel(lows, rviLo, stdevLength, rmaLength);
+
+ // Average
+ for (int i = 0; i < len; i++)
+ output[i] = (rviHi[i] + rviLo[i]) * 0.5;
+ }
+
+ ///
+ /// Computes original (single-channel) RVI for one price series.
+ ///
+ private static void BatchSingleChannel(
+ ReadOnlySpan prices,
+ Span output,
+ int stdevLength,
+ int rmaLength)
+ {
int len = prices.Length;
if (len == 0)
- {
return;
- }
double alpha = 1.0 / rmaLength;
- // Price buffer for stddev
Span priceBuffer = stdevLength <= 256 ? stackalloc double[stdevLength] : new double[stdevLength];
int head = 0;
int count = 0;
@@ -442,7 +389,6 @@ public sealed class Rvi : AbstractBase
double prevPrice = double.NaN;
double lastValue = 50.0;
- // RMA state
double rawRmaUp = 0;
double eUp = 1.0;
double rawRmaDown = 0;
@@ -452,21 +398,16 @@ public sealed class Rvi : AbstractBase
{
double price = prices[i];
- // First price
if (double.IsNaN(prevPrice))
{
- // Handle invalid first price - output neutral and continue
if (!double.IsFinite(price))
{
output[i] = lastValue;
continue;
}
- // Add to buffer
if (count < stdevLength)
- {
count++;
- }
else
{
double oldest = priceBuffer[head];
@@ -483,22 +424,17 @@ public sealed class Rvi : AbstractBase
continue;
}
- // Handle invalid price
if (!double.IsFinite(price))
{
output[i] = lastValue;
continue;
}
- // Price change direction
double priceChange = price - prevPrice;
prevPrice = price;
- // Update buffer
if (count < stdevLength)
- {
count++;
- }
else
{
double oldest = priceBuffer[head];
@@ -510,7 +446,6 @@ public sealed class Rvi : AbstractBase
sum += price;
sumSq += price * price;
- // Population stddev
double currentStdDev = 0.0;
if (count > 1)
{
@@ -520,19 +455,13 @@ public sealed class Rvi : AbstractBase
currentStdDev = Math.Sqrt(variance);
}
- // Classify by direction
double upStdVal = 0.0;
double downStdVal = 0.0;
if (priceChange > 0)
- {
upStdVal = currentStdDev;
- }
else if (priceChange < 0)
- {
downStdVal = currentStdDev;
- }
- // RMA with bias correction
rawRmaUp = Math.FusedMultiplyAdd(rawRmaUp, rmaLength - 1, upStdVal) / rmaLength;
eUp = (1 - alpha) * eUp;
double avgUpStd = eUp > Epsilon ? rawRmaUp / (1.0 - eUp) : rawRmaUp;
@@ -541,18 +470,13 @@ public sealed class Rvi : AbstractBase
eDown = (1 - alpha) * eDown;
double avgDownStd = eDown > Epsilon ? rawRmaDown / (1.0 - eDown) : rawRmaDown;
- // RVI
double sumAvgStd = avgUpStd + avgDownStd;
double rviValue = sumAvgStd > Epsilon ? (100.0 * avgUpStd / sumAvgStd) : 50.0;
if (!double.IsFinite(rviValue))
- {
rviValue = lastValue;
- }
else
- {
lastValue = rviValue;
- }
output[i] = rviValue;
}
@@ -564,5 +488,4 @@ public sealed class Rvi : AbstractBase
TSeries results = indicator.Update(source);
return (results, indicator);
}
-
-}
+}
\ No newline at end of file
diff --git a/lib/volatility/rvi/Rvi.md b/lib/volatility/rvi/Rvi.md
index 6c10ce7b..57fc36c6 100644
--- a/lib/volatility/rvi/Rvi.md
+++ b/lib/volatility/rvi/Rvi.md
@@ -3,7 +3,7 @@
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Volatility |
-| **Inputs** | OHLCV bar (TBar) |
+| **Inputs** | OHLCV bar (TBar) or single price (TValue) |
| **Parameters** | `stdevLength` (default 10), `rmaLength` (default 14) |
| **Outputs** | Single series (Rvi) |
| **Output range** | $0$ to $100$ |
@@ -11,15 +11,16 @@
### TL;DR
-- The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility.
-- Parameterized by `stdevlength` (default 10), `rmalength` (default 14).
+- The Relative Volatility Index (RVI) implements Dorsey's **revised (1995)** version: computes original RVI separately on High and Low series, then averages.
+- When fed single-price data (TValue), both channels receive the same value, reducing to the original (1993) formula.
+- Parameterized by `stdevLength` (default 10), `rmaLength` (default 14).
- Output range: $0$ to $100$.
-- Requires 1 bar of warmup before first valid output (IsHot = true).
-- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
+- Requires `stdevLength` bars of warmup before first valid output (IsHot = true).
+- Validated against FM Labs revised RVI specification.
> "Not all volatility is created equal—upward volatility feels like profit, downward volatility feels like loss. RVI separates these psychological experiences into a quantifiable measure."
-The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility. Originally developed by Donald Dorsey in 1993, RVI measures the standard deviation of closing prices and categorizes this volatility based on whether prices are rising or falling. The result is an oscillator bounded between 0 and 100, where values above 50 indicate upward volatility dominance and values below 50 indicate downward volatility dominance.
+The Relative Volatility Index (RVI) is a directional volatility oscillator that distinguishes between upward and downward price volatility. Originally developed by Donald Dorsey in 1993 using close prices only, RVI was **revised in 1995** to compute separate RVI values on the High and Low price series and average them. This implementation follows the revised version: when fed OHLCV bars (TBar), it runs independent RVI channels on High and Low; when fed single prices (TValue), both channels receive the same value, reducing to the original formula.
## Historical Context
@@ -27,13 +28,29 @@ Donald Dorsey introduced the Relative Volatility Index in the June 1993 issue of
The key innovation was separating volatility into directional components. Traditional volatility measures (standard deviation, ATR) treat upward and downward price movements identically. Dorsey recognized that traders experience these movements differently: upward volatility in a long position feels like opportunity, while downward volatility feels like risk.
-The original 1993 formula used a 10-period standard deviation and 14-period Wilder's smoothing (RMA). This implementation follows the PineScript reference which uses bias-corrected RMA to ensure proper warmup behavior during the initial periods.
+The original 1993 formula used a 10-period standard deviation of closing prices with 14-period Wilder's smoothing (RMA). In 1995, Dorsey revised the formula to average RVI computed independently on the High and Low series, capturing volatility structure across the full price range rather than just closes.
+
+FM Labs documents both versions: the original (close-only) and the revised (high+low average). This implementation follows the **revised** version with bias-corrected RMA for proper warmup behavior.
## Architecture & Physics
-### 1. Rolling Population Standard Deviation
+### 1. Dual-Channel Architecture (Revised 1995)
-First, compute the population standard deviation of closing prices over `stdevLength` periods:
+The revised RVI computes the original RVI algorithm independently on two channels:
+- **High channel:** uses bar High prices
+- **Low channel:** uses bar Low prices
+
+The final RVI is their average:
+
+$$
+\text{RVI}_{\text{revised}} = \frac{\text{RVI}_{\text{high}} + \text{RVI}_{\text{low}}}{2}
+$$
+
+When fed single prices (TValue), both channels receive the same value: $\text{RVI} = \frac{\text{RVI}_p + \text{RVI}_p}{2} = \text{RVI}_p$ (original behavior).
+
+### 2. Per-Channel: Rolling Population Standard Deviation
+
+For each channel, compute the population standard deviation over `stdevLength` periods:
$$
\sigma_t = \sqrt{\frac{\sum_{i=0}^{n-1}(P_{t-i} - \bar{P})^2}{n}}
@@ -51,7 +68,7 @@ $$
\sigma_t = \sqrt{\frac{\sum P_i^2}{n} - \left(\frac{\sum P_i}{n}\right)^2}
$$
-### 2. Directional Classification
+### 3. Directional Classification
Based on price change direction, assign the volatility to either upward or downward:
@@ -71,7 +88,7 @@ $$
Note: When $P_t = P_{t-1}$ (unchanged), both upStd and downStd are zero. The volatility is "orphaned" rather than assigned to either direction.
-### 3. Bias-Corrected RMA Smoothing
+### 4. Bias-Corrected RMA Smoothing
Both directional volatilities are smoothed using Wilder's RMA (Exponential Moving Average with $\alpha = 1/n$) with bias correction for proper warmup:
@@ -100,15 +117,21 @@ where $\alpha = 1/\text{rmaLength}$ and $\epsilon = 10^{-10}$.
This bias correction compensates for the zero initialization of raw RMA, preventing artificially low values during warmup.
-### 4. Final RVI Calculation
+### 5. Per-Channel RVI
$$
-\text{RVI}_t = \begin{cases}
+\text{RVI}_{\text{channel}} = \begin{cases}
100 \times \frac{\text{avgUpStd}_t}{\text{avgUpStd}_t + \text{avgDownStd}_t} & \text{if sum} > 0 \\
50 & \text{otherwise}
\end{cases}
$$
+### 6. Final Revised RVI
+
+$$
+\text{RVI}_t = \frac{\text{RVI}_{\text{high}} + \text{RVI}_{\text{low}}}{2}
+$$
+
## Mathematical Foundation
### Relationship to RSI
@@ -192,13 +215,14 @@ Dominant cost: five divisions (63%) for variance calculation and RMA updates.
| Library | Status | Notes |
| :--- | :---: | :--- |
+| **FM Labs** | ✅ | Matches revised (1995) dual-channel specification |
| **TA-Lib** | N/A | Not implemented |
| **Skender** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented |
| **OoplesFinance** | ❔ | Different algorithm (RSI-based) |
-| **PineScript** | ✅ | Matches rvi.pine reference |
+| **PineScript** | ✅ | Matches rvi.pine reference (original algorithm per channel) |
-Note: Some libraries implement "RVI" as a different indicator (often RSI applied to volatility). This implementation follows Dorsey's original design using directional standard deviation.
+Note: Some libraries implement "RVI" as a different indicator (often RSI applied to volatility). FM Labs distinguishes between original (1993, close-only) and revised (1995, high+low average). This implementation follows the **revised** version.
## Common Pitfalls
@@ -261,4 +285,5 @@ Price making lower lows + RVI making higher lows: Bullish divergence
- Dorsey, D. (1993). "The Relative Volatility Index." *Technical Analysis of Stocks & Commodities*, 11(6), 253-256.
- Dorsey, D. (1995). "Refining the Relative Volatility Index." *Technical Analysis of Stocks & Commodities*, 13(9).
+- FM Labs. "Relative Volatility Index." https://www.fmlabs.com/reference/RVI.htm (Original vs Revised versions).
- TradingView. (2024). "PineScript Reference Implementation." rvi.pine source file.
diff --git a/lib/volatility/rvi/rvi.pine b/lib/volatility/rvi/rvi.pine
index 3e8e2c6e..007d299f 100644
--- a/lib/volatility/rvi/rvi.pine
+++ b/lib/volatility/rvi/rvi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Relative Volatility Index (RVI)", shorttitle="RVI", overlay=false)
diff --git a/lib/volatility/tr/tr.pine b/lib/volatility/tr/tr.pine
index e6e1ab8f..8ecc9ddd 100644
--- a/lib/volatility/tr/tr.pine
+++ b/lib/volatility/tr/tr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("True Range", "TR", overlay=false)
diff --git a/lib/volatility/ui/ui.pine b/lib/volatility/ui/ui.pine
index f9423e59..9e97a3b6 100644
--- a/lib/volatility/ui/ui.pine
+++ b/lib/volatility/ui/ui.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ulcer Index (UI)", shorttitle="UI", format=format.price, precision=2, overlay=false)
diff --git a/lib/volatility/vov/vov.pine b/lib/volatility/vov/vov.pine
index 3d1aaef2..d276e0b7 100644
--- a/lib/volatility/vov/vov.pine
+++ b/lib/volatility/vov/vov.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volatility of Volatility (VOV)", shorttitle="VOV", format=format.price, precision=4, overlay=false)
diff --git a/lib/volatility/vr/vr.pine b/lib/volatility/vr/vr.pine
index f51a7653..086c7d21 100644
--- a/lib/volatility/vr/vr.pine
+++ b/lib/volatility/vr/vr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volatility Ratio (VR)", shorttitle="VR", format=format.price, precision=2, overlay=false)
diff --git a/lib/volatility/yzv/yzv.pine b/lib/volatility/yzv/yzv.pine
index e63515a9..ce673443 100644
--- a/lib/volatility/yzv/yzv.pine
+++ b/lib/volatility/yzv/yzv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Yang-Zhang Volatility (YZV)", shorttitle="YZV", overlay=false)
diff --git a/lib/volume/adl/adl.pine b/lib/volume/adl/adl.pine
index 7317ddec..838f0d98 100644
--- a/lib/volume/adl/adl.pine
+++ b/lib/volume/adl/adl.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Accumulation/Distribution Line (ADL)", "ADL", overlay=false)
diff --git a/lib/volume/adosc/adosc.pine b/lib/volume/adosc/adosc.pine
index 7784ebb6..d86f0e1b 100644
--- a/lib/volume/adosc/adosc.pine
+++ b/lib/volume/adosc/adosc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Chaikin A/D Oscillator (ADOSC)", "ADOSC", overlay=false)
diff --git a/lib/volume/aobv/aobv.pine b/lib/volume/aobv/aobv.pine
index d959e02b..77c9a387 100644
--- a/lib/volume/aobv/aobv.pine
+++ b/lib/volume/aobv/aobv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("AOBV - Archer On-Balance Volume (AOBV)", "AOBV", overlay=false)
diff --git a/lib/volume/cmf/cmf.pine b/lib/volume/cmf/cmf.pine
index bc8eca7b..2303c053 100644
--- a/lib/volume/cmf/cmf.pine
+++ b/lib/volume/cmf/cmf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Chaikin Money Flow (CMF)", "CMF", overlay=false)
diff --git a/lib/volume/efi/efi.pine b/lib/volume/efi/efi.pine
index 603fc80a..1fda4818 100644
--- a/lib/volume/efi/efi.pine
+++ b/lib/volume/efi/efi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Elder's Force Index (EFI)", "EFI", overlay=false)
diff --git a/lib/volume/eom/eom.pine b/lib/volume/eom/eom.pine
index 730e14e6..bdc67c86 100644
--- a/lib/volume/eom/eom.pine
+++ b/lib/volume/eom/eom.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Ease of Movement (EOM)", "EOM", overlay=false)
diff --git a/lib/volume/evwma/evwma.pine b/lib/volume/evwma/evwma.pine
index a7666394..6240c5c5 100644
--- a/lib/volume/evwma/evwma.pine
+++ b/lib/volume/evwma/evwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Elastic Volume Weighted Moving Average (EVWMA)", "EVWMA", overlay=true)
diff --git a/lib/volume/iii/iii.pine b/lib/volume/iii/iii.pine
index 3e0e4319..7dc3d522 100644
--- a/lib/volume/iii/iii.pine
+++ b/lib/volume/iii/iii.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Intraday Intensity Index (III)", "III", overlay=false)
diff --git a/lib/volume/kvo/kvo.pine b/lib/volume/kvo/kvo.pine
index a1bc9d4c..9881d5ef 100644
--- a/lib/volume/kvo/kvo.pine
+++ b/lib/volume/kvo/kvo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Klinger Volume Oscillator (KVO)", "KVO", overlay=false)
diff --git a/lib/volume/mfi/mfi.pine b/lib/volume/mfi/mfi.pine
index 7e88a9a3..f56e7ef9 100644
--- a/lib/volume/mfi/mfi.pine
+++ b/lib/volume/mfi/mfi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Money Flow Index (MFI)", "MFI", overlay=false)
diff --git a/lib/volume/nvi/nvi.pine b/lib/volume/nvi/nvi.pine
index 2d49e1c7..bff788d0 100644
--- a/lib/volume/nvi/nvi.pine
+++ b/lib/volume/nvi/nvi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Negative Volume Index (NVI)", "NVI", overlay=false)
diff --git a/lib/volume/obv/obv.pine b/lib/volume/obv/obv.pine
index d6a7b699..42164536 100644
--- a/lib/volume/obv/obv.pine
+++ b/lib/volume/obv/obv.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("On Balance Volume (OBV)", "OBV", overlay=false)
diff --git a/lib/volume/pvd/pvd.pine b/lib/volume/pvd/pvd.pine
index fbfdc347..662e0ef3 100644
--- a/lib/volume/pvd/pvd.pine
+++ b/lib/volume/pvd/pvd.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Volume Divergence (PVD)", "PVD", overlay=false)
diff --git a/lib/volume/pvi/pvi.pine b/lib/volume/pvi/pvi.pine
index f35d6357..f77da085 100644
--- a/lib/volume/pvi/pvi.pine
+++ b/lib/volume/pvi/pvi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Positive Volume Index (PVI)", "PVI", overlay=false)
diff --git a/lib/volume/pvo/pvo.pine b/lib/volume/pvo/pvo.pine
index 0ef2336c..34b9204b 100644
--- a/lib/volume/pvo/pvo.pine
+++ b/lib/volume/pvo/pvo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Percentage Volume Oscillator (PVO)", "PVO", overlay=false)
diff --git a/lib/volume/pvr/pvr.pine b/lib/volume/pvr/pvr.pine
index 174b4bf3..774473f8 100644
--- a/lib/volume/pvr/pvr.pine
+++ b/lib/volume/pvr/pvr.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Volume Rank (PVR)", "PVR", overlay=false)
diff --git a/lib/volume/pvt/pvt.pine b/lib/volume/pvt/pvt.pine
index 91354a99..a08607c5 100644
--- a/lib/volume/pvt/pvt.pine
+++ b/lib/volume/pvt/pvt.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Volume Trend (PVT)", "PVT", overlay=false)
diff --git a/lib/volume/tvi/tvi.pine b/lib/volume/tvi/tvi.pine
index 6439eaa5..f843dc47 100644
--- a/lib/volume/tvi/tvi.pine
+++ b/lib/volume/tvi/tvi.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Trade Volume Index (TVI)", "TVI", overlay=false)
diff --git a/lib/volume/twap/twap.pine b/lib/volume/twap/twap.pine
index f1cad38f..f071880d 100644
--- a/lib/volume/twap/twap.pine
+++ b/lib/volume/twap/twap.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Time Weighted Average Price (TWAP)", "TWAP", overlay=true)
diff --git a/lib/volume/va/va.pine b/lib/volume/va/va.pine
index f681c1ee..67c78857 100644
--- a/lib/volume/va/va.pine
+++ b/lib/volume/va/va.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volume Accumulation (VA)", "VA", overlay=false)
diff --git a/lib/volume/vf/vf.pine b/lib/volume/vf/vf.pine
index 18c43649..93201f7c 100644
--- a/lib/volume/vf/vf.pine
+++ b/lib/volume/vf/vf.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volume Force (VF)", "VF", overlay=false)
diff --git a/lib/volume/vo/vo.pine b/lib/volume/vo/vo.pine
index 17e0c17b..b6d04e2d 100644
--- a/lib/volume/vo/vo.pine
+++ b/lib/volume/vo/vo.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volume Oscillator (VO)", "VO", overlay=false)
diff --git a/lib/volume/vroc/vroc.pine b/lib/volume/vroc/vroc.pine
index 347367c4..dc37d68b 100644
--- a/lib/volume/vroc/vroc.pine
+++ b/lib/volume/vroc/vroc.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volume Rate of Change (VROC)", "VROC", overlay=false)
diff --git a/lib/volume/vwad/vwad.pine b/lib/volume/vwad/vwad.pine
index aeed0f5d..4892faa9 100644
--- a/lib/volume/vwad/vwad.pine
+++ b/lib/volume/vwad/vwad.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volume Weighted Accumulation/Distribution (VWAD)", "VWAD", overlay=false)
diff --git a/lib/volume/vwap/vwap.pine b/lib/volume/vwap/vwap.pine
index 0be657cb..68a30151 100644
--- a/lib/volume/vwap/vwap.pine
+++ b/lib/volume/vwap/vwap.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volume Weighted Average Price (VWAP)", "VWAP", overlay=true)
diff --git a/lib/volume/vwma/Vwma.cs b/lib/volume/vwma/Vwma.cs
index 0cc904f2..0433aab3 100644
--- a/lib/volume/vwma/Vwma.cs
+++ b/lib/volume/vwma/Vwma.cs
@@ -135,7 +135,7 @@ public sealed class Vwma : ITValuePublisher
double v = _volBuffer[i];
if (v > 0)
{
- sumPV += p * v;
+ sumPV = Math.FusedMultiplyAdd(p, v, sumPV);
sumVol += v;
}
}
@@ -224,14 +224,14 @@ public sealed class Vwma : ITValuePublisher
if (s.Count >= _period && oldVol > 0)
{
- s.SumPV -= oldPrice * oldVol;
+ s.SumPV = Math.FusedMultiplyAdd(-oldPrice, oldVol, s.SumPV);
s.SumVol -= oldVol;
}
// Add new values
if (currentVol > 0)
{
- s.SumPV += currentPrice * currentVol;
+ s.SumPV = Math.FusedMultiplyAdd(currentPrice, currentVol, s.SumPV);
s.SumVol += currentVol;
}
@@ -437,14 +437,14 @@ public sealed class Vwma : ITValuePublisher
if (count >= period && oldVol > 0)
{
- sumPV -= oldPrice * oldVol;
+ sumPV = Math.FusedMultiplyAdd(-oldPrice, oldVol, sumPV);
sumVol -= oldVol;
}
// Add new values
if (currentVol > 0)
{
- sumPV += currentPrice * currentVol;
+ sumPV = Math.FusedMultiplyAdd(currentPrice, currentVol, sumPV);
sumVol += currentVol;
}
@@ -474,7 +474,7 @@ public sealed class Vwma : ITValuePublisher
double vj = volBuffer[j];
if (vj > 0)
{
- sumPV += pj * vj;
+ sumPV = Math.FusedMultiplyAdd(pj, vj, sumPV);
sumVol += vj;
}
}
diff --git a/lib/volume/vwma/vwma.pine b/lib/volume/vwma/vwma.pine
index 5807f79f..6ad3be68 100644
--- a/lib/volume/vwma/vwma.pine
+++ b/lib/volume/vwma/vwma.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Volume Weighted Moving Average (VWMA)", "VWMA", overlay=true)
diff --git a/lib/volume/wad/wad.pine b/lib/volume/wad/wad.pine
index 683ba2af..8958bc8b 100644
--- a/lib/volume/wad/wad.pine
+++ b/lib/volume/wad/wad.pine
@@ -1,4 +1,4 @@
-// The MIT License (MIT)
+// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Williams Accumulation/Distribution (WAD)", "WAD", overlay=false)
diff --git a/perf/roslyn.sarif b/perf/roslyn.sarif
deleted file mode 100644
index 23af1be8..00000000
--- a/perf/roslyn.sarif
+++ /dev/null
@@ -1,102 +0,0 @@
-{
- "$schema": "http://json.schemastore.org/sarif-1.0.0",
- "version": "1.0.0",
- "runs": [
- {
- "tool": {
- "name": "Microsoft (R) Visual C# Compiler",
- "version": "5.0.0.0",
- "fileVersion": "5.0.0-2.25523.111 (b0f34d51)",
- "semanticVersion": "5.0.0",
- "language": "en-US"
- },
- "results": [
- {
- "ruleId": "MA0007",
- "level": "note",
- "message": "Add comma after the last value",
- "locations": [
- {
- "resultFile": {
- "uri": "file:///C:/github/quantalib/perf/Benchmark.cs",
- "region": {
- "startLine": 108,
- "startColumn": 17,
- "endLine": 108,
- "endColumn": 56
- }
- }
- }
- ],
- "properties": {
- "warningLevel": 1
- }
- },
- {
- "ruleId": "MA0007",
- "level": "note",
- "message": "Add comma after the last value",
- "locations": [
- {
- "resultFile": {
- "uri": "file:///C:/github/quantalib/perf/Benchmark.cs",
- "region": {
- "startLine": 123,
- "startColumn": 17,
- "endLine": 123,
- "endColumn": 47
- }
- }
- }
- ],
- "properties": {
- "warningLevel": 1
- }
- },
- {
- "ruleId": "RCS1118",
- "level": "note",
- "message": "Mark local variable as const",
- "locations": [
- {
- "resultFile": {
- "uri": "file:///C:/github/quantalib/perf/Benchmark.cs",
- "region": {
- "startLine": 131,
- "startColumn": 9,
- "endLine": 131,
- "endColumn": 12
- }
- }
- }
- ],
- "properties": {
- "warningLevel": 1
- }
- }
- ],
- "rules": {
- "MA0007": {
- "id": "MA0007",
- "shortDescription": "Add a comma after the last value",
- "defaultLevel": "note",
- "helpUri": "https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0007.md",
- "properties": {
- "category": "Style",
- "isEnabledByDefault": true
- }
- },
- "RCS1118": {
- "id": "RCS1118",
- "shortDescription": "Mark local variable as const",
- "defaultLevel": "note",
- "helpUri": "https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1118",
- "properties": {
- "category": "Roslynator",
- "isEnabledByDefault": true
- }
- }
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/python/README.md b/python/README.md
index 771e8d10..d1d7f80b 100644
--- a/python/README.md
+++ b/python/README.md
@@ -101,4 +101,4 @@ The `ctypes` call adds 5-15 μs overhead. For arrays above a few hundred bars, N
## License
-[MIT](https://github.com/mihakralj/quantalib/blob/main/LICENSE)
+[Apache License 2.0](https://github.com/mihakralj/quantalib/blob/main/LICENSE)
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 9ba631e6..02d41cc8 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -7,7 +7,7 @@ name = "quantalib"
dynamic = ["version"]
description = "High-performance technical analysis wrappers over QuanTAlib NativeAOT"
readme = "README.md"
-license = "MIT"
+license = "Apache-2.0"
requires-python = ">=3.10"
dependencies = ["numpy>=1.24"]
authors = [
@@ -18,7 +18,7 @@ classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Financial and Insurance Industry",
"Intended Audience :: Science/Research",
- "License :: OSI Approved :: MIT License",
+ "License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
diff --git a/python/python.csproj b/python/python.csproj
index 01f8fac0..9824f491 100644
--- a/python/python.csproj
+++ b/python/python.csproj
@@ -32,7 +32,4 @@
-
-
-
diff --git a/python/quantalib/dynamics.py b/python/quantalib/dynamics.py
index af81577e..9e15c57c 100644
--- a/python/quantalib/dynamics.py
+++ b/python/quantalib/dynamics.py
@@ -17,11 +17,15 @@ __all__ = [
"chop",
"dmx",
"dx",
+ "minus_di",
+ "minus_dm",
"ghla",
"ht_trendmode",
"ichimoku",
"impulse",
"pfe",
+ "plus_di",
+ "plus_dm",
"qstick",
"ravi",
"supertrend",
@@ -140,6 +144,28 @@ def dx(high: object, low: object, close: object, period: int = 14, offset: int =
return _wrap(destination, idx, f"DX_{period}", "dynamics", offset)
+def minus_di(high: object, low: object, close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
+ """Minus Directional Indicator."""
+ period = int(kwargs.get("length", period))
+ offset = int(offset)
+ h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
+ n = len(h)
+ destination = _out(n)
+ _check(_lib.qtl_minusdi(_ptr(h), _ptr(h), _ptr(l), _ptr(c), _ptr(destination), period, n, _ptr(destination)))
+ return _wrap(destination, idx, f"MINUS_DI_{period}", "dynamics", offset)
+
+
+def minus_dm(high: object, low: object, close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
+ """Minus Directional Movement."""
+ period = int(kwargs.get("length", period))
+ offset = int(offset)
+ h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
+ n = len(h)
+ destination = _out(n)
+ _check(_lib.qtl_minusdm(_ptr(h), _ptr(h), _ptr(l), _ptr(c), _ptr(destination), period, n, _ptr(destination)))
+ return _wrap(destination, idx, f"MINUS_DM_{period}", "dynamics", offset)
+
+
def ghla(high: object, low: object, close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
"""Gann Hi-Lo Activator."""
period = int(kwargs.get("length", period))
@@ -206,6 +232,28 @@ def pfe(close: object, period: int = 14, smoothPeriod: int = 5, offset: int = 0,
return _wrap(output, idx, f"PFE_{period}", "dynamics", offset)
+def plus_di(high: object, low: object, close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
+ """Plus Directional Indicator."""
+ period = int(kwargs.get("length", period))
+ offset = int(offset)
+ h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
+ n = len(h)
+ destination = _out(n)
+ _check(_lib.qtl_plusdi(_ptr(h), _ptr(h), _ptr(l), _ptr(c), _ptr(destination), period, n, _ptr(destination)))
+ return _wrap(destination, idx, f"PLUS_DI_{period}", "dynamics", offset)
+
+
+def plus_dm(high: object, low: object, close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
+ """Plus Directional Movement."""
+ period = int(kwargs.get("length", period))
+ offset = int(offset)
+ h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
+ n = len(h)
+ destination = _out(n)
+ _check(_lib.qtl_plusdm(_ptr(h), _ptr(h), _ptr(l), _ptr(c), _ptr(destination), period, n, _ptr(destination)))
+ return _wrap(destination, idx, f"PLUS_DM_{period}", "dynamics", offset)
+
+
def qstick(open: object, high: object, low: object, close: object, volume: object, period: int = 14, useEma: int = 0, offset: int = 0, **kwargs) -> object:
"""QStick."""
period = int(kwargs.get("length", period))
diff --git a/python/quantalib/numerics.py b/python/quantalib/numerics.py
index 4f4d6251..9be9a839 100644
--- a/python/quantalib/numerics.py
+++ b/python/quantalib/numerics.py
@@ -13,12 +13,14 @@ __all__ = [
"fft",
"gammadist",
"highest",
+ "maxindex",
"ifft",
"jerk",
"lineartrans",
"lognormdist",
"logtrans",
"lowest",
+ "minindex",
"normalize",
"normdist",
"poissondist",
@@ -98,6 +100,17 @@ def highest(close: object, period: int = 14, offset: int = 0, **kwargs) -> objec
return _wrap(output, idx, f"HIGHEST_{period}", "numerics", offset)
+def maxindex(close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
+ """Index of Highest Value."""
+ period = int(kwargs.get("length", period))
+ offset = int(offset)
+ src, idx = _arr(close)
+ n = len(src)
+ output = _out(n)
+ _check(_lib.qtl_maxindex(_ptr(src), _ptr(output), n, period))
+ return _wrap(output, idx, f"MAXINDEX_{period}", "numerics", offset)
+
+
def ifft(close: object, windowSize: int = 256, numHarmonics: int = 10, offset: int = 0, **kwargs) -> object:
"""Inverse FFT."""
windowSize = int(windowSize)
@@ -166,6 +179,17 @@ def lowest(close: object, period: int = 14, offset: int = 0, **kwargs) -> object
return _wrap(output, idx, f"LOWEST_{period}", "numerics", offset)
+def minindex(close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
+ """Index of Lowest Value."""
+ period = int(kwargs.get("length", period))
+ offset = int(offset)
+ src, idx = _arr(close)
+ n = len(src)
+ output = _out(n)
+ _check(_lib.qtl_minindex(_ptr(src), _ptr(output), n, period))
+ return _wrap(output, idx, f"MININDEX_{period}", "numerics", offset)
+
+
def normalize(close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
"""Normalization."""
period = int(kwargs.get("length", period))
diff --git a/python/quantalib/reversals.py b/python/quantalib/reversals.py
index 2fca8f0a..20dc0a34 100644
--- a/python/quantalib/reversals.py
+++ b/python/quantalib/reversals.py
@@ -18,6 +18,7 @@ __all__ = [
"pivotfib",
"pivotwood",
"psar",
+ "sarext",
"swings",
"ttm_scalper",
]
@@ -132,6 +133,24 @@ def psar(open: object, high: object, low: object, close: object, afStart: float
return _wrap(output, idx, "PSAR", "reversals", offset)
+def sarext(open: object, high: object, low: object, close: object, startValue: float = 0.0, offsetOnReverse: float = 0.0, afInitLong: float = 0.02, afLong: float = 0.02, afMaxLong: float = 0.2, afInitShort: float = 0.02, afShort: float = 0.02, afMaxShort: float = 0.2, offset: int = 0, **kwargs) -> object:
+ """Parabolic SAR Extended."""
+ startValue = float(startValue)
+ offsetOnReverse = float(offsetOnReverse)
+ afInitLong = float(afInitLong)
+ afLong = float(afLong)
+ afMaxLong = float(afMaxLong)
+ afInitShort = float(afInitShort)
+ afShort = float(afShort)
+ afMaxShort = float(afMaxShort)
+ offset = int(offset)
+ o, idx = _arr(open); h, _ = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
+ n = len(o)
+ output = _out(n)
+ _check(_lib.qtl_sarext(_ptr(o), _ptr(h), _ptr(l), _ptr(c), _ptr(output), n, startValue, offsetOnReverse, afInitLong, afLong, afMaxLong, afInitShort, afShort, afMaxShort))
+ return _wrap(output, idx, "SAREXT", "reversals", offset)
+
+
def swings(high: object, low: object, lookback: int = 5, offset: int = 0, **kwargs) -> object:
"""Swing High/Low."""
lookback = int(lookback)
diff --git a/python/src/ArrayBridge.cs b/python/src/ArrayBridge.cs
index af118358..f8549834 100644
--- a/python/src/ArrayBridge.cs
+++ b/python/src/ArrayBridge.cs
@@ -1,3 +1,4 @@
+using System;
using System.Runtime.CompilerServices;
namespace QuanTAlib.Python;
@@ -5,7 +6,7 @@ namespace QuanTAlib.Python;
internal static class ArrayBridge
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static unsafe bool IsNull(double* ptr) => ptr == null;
+ public static bool IsNull(IntPtr ptr) => ptr == IntPtr.Zero;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int ValidateLength(int n) =>
diff --git a/python/src/Exports.Generated.cs b/python/src/Exports.Generated.cs
index 1db853ce..e75f5367 100644
--- a/python/src/Exports.Generated.cs
+++ b/python/src/Exports.Generated.cs
@@ -6,6 +6,10 @@ using QuanTAlib;
namespace QuanTAlib.Python;
+[System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Security Hotspot",
+ "S6640",
+ Justification = "Generated NativeAOT export helpers bridge unmanaged caller-owned buffers into managed series objects. The unsafe context is required by the ABI and remains constrained to pointer-to-span copies over validated inputs.")]
public static unsafe partial class Exports
{
private static TSeries BuildSeries(double* src, int n)
@@ -1580,6 +1584,19 @@ public static unsafe partial class Exports
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
+ [UnmanagedCallersOnly(EntryPoint = "qtl_maxindex")]
+ public static int QtlMaxindex(double* source, double* output, int n, int period)
+ {
+ if (source == null || output == null) return StatusCodes.QTL_ERR_NULL_PTR;
+ if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ try
+ {
+ Maxindex.Batch(Src(source, n), Dst(output, n), period);
+ return StatusCodes.QTL_OK;
+ }
+ catch { return StatusCodes.QTL_ERR_INTERNAL; }
+ }
+
[UnmanagedCallersOnly(EntryPoint = "qtl_mcnma")]
public static int QtlMcnma(double* source, double* output, int n, int period)
{
@@ -1697,6 +1714,57 @@ public static unsafe partial class Exports
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
+ [UnmanagedCallersOnly(EntryPoint = "qtl_minindex")]
+ public static int QtlMinindex(double* source, double* output, int n, int period)
+ {
+ if (source == null || output == null) return StatusCodes.QTL_ERR_NULL_PTR;
+ if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ try
+ {
+ Minindex.Batch(Src(source, n), Dst(output, n), period);
+ return StatusCodes.QTL_OK;
+ }
+ catch { return StatusCodes.QTL_ERR_INTERNAL; }
+ }
+
+ [UnmanagedCallersOnly(EntryPoint = "qtl_minusdi")]
+ public static int QtlMinusDi(double* sourceOpen, double* sourceHigh, double* sourceLow, double* sourceClose, double* sourceVolume, int period, int n, double* dst)
+ {
+ if (sourceOpen == null || sourceHigh == null || sourceLow == null || sourceClose == null || sourceVolume == null || dst == null) return StatusCodes.QTL_ERR_NULL_PTR;
+ if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ try
+ {
+ var sourceBars = BuildBars(sourceOpen, sourceHigh, sourceLow, sourceClose, sourceVolume, n);
+ var result = MinusDi.Batch(sourceBars, period);
+ var values = result.Values;
+ if (values.Length > n) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ var outSpan = Dst(dst, n);
+ outSpan.Fill(double.NaN);
+ values.CopyTo(outSpan);
+ return StatusCodes.QTL_OK;
+ }
+ catch { return StatusCodes.QTL_ERR_INTERNAL; }
+ }
+
+ [UnmanagedCallersOnly(EntryPoint = "qtl_minusdm")]
+ public static int QtlMinusDm(double* sourceOpen, double* sourceHigh, double* sourceLow, double* sourceClose, double* sourceVolume, int period, int n, double* dst)
+ {
+ if (sourceOpen == null || sourceHigh == null || sourceLow == null || sourceClose == null || sourceVolume == null || dst == null) return StatusCodes.QTL_ERR_NULL_PTR;
+ if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ try
+ {
+ var sourceBars = BuildBars(sourceOpen, sourceHigh, sourceLow, sourceClose, sourceVolume, n);
+ var result = MinusDm.Batch(sourceBars, period);
+ var values = result.Values;
+ if (values.Length > n) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ var outSpan = Dst(dst, n);
+ outSpan.Fill(double.NaN);
+ values.CopyTo(outSpan);
+ return StatusCodes.QTL_OK;
+ }
+ catch { return StatusCodes.QTL_ERR_INTERNAL; }
+ }
+
[UnmanagedCallersOnly(EntryPoint = "qtl_mma")]
public static int QtlMma(double* source, double* output, int n, int period)
{
@@ -2067,6 +2135,44 @@ public static unsafe partial class Exports
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
+ [UnmanagedCallersOnly(EntryPoint = "qtl_plusdi")]
+ public static int QtlPlusDi(double* sourceOpen, double* sourceHigh, double* sourceLow, double* sourceClose, double* sourceVolume, int period, int n, double* dst)
+ {
+ if (sourceOpen == null || sourceHigh == null || sourceLow == null || sourceClose == null || sourceVolume == null || dst == null) return StatusCodes.QTL_ERR_NULL_PTR;
+ if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ try
+ {
+ var sourceBars = BuildBars(sourceOpen, sourceHigh, sourceLow, sourceClose, sourceVolume, n);
+ var result = PlusDi.Batch(sourceBars, period);
+ var values = result.Values;
+ if (values.Length > n) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ var outSpan = Dst(dst, n);
+ outSpan.Fill(double.NaN);
+ values.CopyTo(outSpan);
+ return StatusCodes.QTL_OK;
+ }
+ catch { return StatusCodes.QTL_ERR_INTERNAL; }
+ }
+
+ [UnmanagedCallersOnly(EntryPoint = "qtl_plusdm")]
+ public static int QtlPlusDm(double* sourceOpen, double* sourceHigh, double* sourceLow, double* sourceClose, double* sourceVolume, int period, int n, double* dst)
+ {
+ if (sourceOpen == null || sourceHigh == null || sourceLow == null || sourceClose == null || sourceVolume == null || dst == null) return StatusCodes.QTL_ERR_NULL_PTR;
+ if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ try
+ {
+ var sourceBars = BuildBars(sourceOpen, sourceHigh, sourceLow, sourceClose, sourceVolume, n);
+ var result = PlusDm.Batch(sourceBars, period);
+ var values = result.Values;
+ if (values.Length > n) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ var outSpan = Dst(dst, n);
+ outSpan.Fill(double.NaN);
+ values.CopyTo(outSpan);
+ return StatusCodes.QTL_OK;
+ }
+ catch { return StatusCodes.QTL_ERR_INTERNAL; }
+ }
+
[UnmanagedCallersOnly(EntryPoint = "qtl_pma")]
public static int QtlPma(double* source, double* pmaOutput, double* triggerOutput, int n, int period)
{
@@ -2554,6 +2660,19 @@ public static unsafe partial class Exports
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
+ [UnmanagedCallersOnly(EntryPoint = "qtl_sarext")]
+ public static int QtlSarext(double* open, double* high, double* low, double* close, double* output, int n, double startValue, double offsetOnReverse, double afInitLong, double afLong, double afMaxLong, double afInitShort, double afShort, double afMaxShort)
+ {
+ if (open == null || high == null || low == null || close == null || output == null) return StatusCodes.QTL_ERR_NULL_PTR;
+ if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
+ try
+ {
+ Sarext.Batch(Src(open, n), Src(high, n), Src(low, n), Src(close, n), Dst(output, n), n, startValue, offsetOnReverse, afInitLong, afLong, afMaxLong, afInitShort, afShort, afMaxShort);
+ return StatusCodes.QTL_OK;
+ }
+ catch { return StatusCodes.QTL_ERR_INTERNAL; }
+ }
+
[UnmanagedCallersOnly(EntryPoint = "qtl_sdchannel")]
public static int QtlSdchannel(double* source, double* middle, double* upper, double* lower, int n, int period, double multiplier)
{
diff --git a/python/src/Exports.cs b/python/src/Exports.cs
index 82c65de7..4f6b4d30 100644
--- a/python/src/Exports.cs
+++ b/python/src/Exports.cs
@@ -24,6 +24,10 @@ namespace QuanTAlib.Python;
#pragma warning disable CA1031 // catch general exception types — ABI boundary requires catching all
#pragma warning disable IDE0060 // unused parameters — some reserved for future use
+[System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Security Hotspot",
+ "S6640",
+ Justification = "NativeAOT unmanaged exports must accept raw caller-owned buffers. Each entry point validates null pointers and lengths before projecting them into spans, and the ABI surface intentionally centralizes the required unsafe context on the export type.")]
[SkipLocalsInit]
public static unsafe partial class Exports
{
diff --git a/python/tools/add_length_alias.py b/python/tools/add_length_alias.py
deleted file mode 100644
index 1a0eeb8a..00000000
--- a/python/tools/add_length_alias.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""Add length→period alias to all old-style Python wrapper functions.
-
-Replaces: period = int(period)
-With: period = int(kwargs.get("length", period))
-
-Only targets exact standalone `period = int(period)` lines (4-space indent).
-Does NOT touch compound names like fastPeriod, slowPeriod, etc.
-"""
-import re
-import os
-import glob
-
-base = os.path.join(os.path.dirname(__file__), '..', 'quantalib')
-base = os.path.normpath(base)
-files = sorted(glob.glob(os.path.join(base, '*.py')))
-total = 0
-
-for fpath in files:
- fname = os.path.basename(fpath)
- if fname.startswith('_'):
- continue
- with open(fpath, 'r', encoding='utf-8') as f:
- content = f.read()
-
- # Match exactly ' period = int(period)' (4-space indent, standalone)
- pattern = r'^( )period = int\(period\)$'
- matches = re.findall(pattern, content, re.MULTILINE)
- count = len(matches)
- if count > 0:
- new_content = re.sub(
- pattern,
- r'\1period = int(kwargs.get("length", period))',
- content,
- flags=re.MULTILINE,
- )
- with open(fpath, 'w', encoding='utf-8') as f:
- f.write(new_content)
- print(f'{fname}: {count} replacements')
- total += count
- else:
- print(f'{fname}: 0 (skipped)')
-
-print(f'\nTotal: {total} replacements across {len(files)} files')
diff --git a/python/tools/complete_bridge.py b/python/tools/complete_bridge.py
deleted file mode 100644
index 8aa6b63a..00000000
--- a/python/tools/complete_bridge.py
+++ /dev/null
@@ -1,1707 +0,0 @@
-#!/usr/bin/env python3
-"""Complete the Python bridge by adding all Exports.cs (manual) bindings and wrappers.
-
-This script:
-1. Rewrites _bridge.py with ALL bindings (Generated + Manual)
-2. Appends missing wrappers to each category .py file
-3. Rewrites indicators.py as thin re-export
-4. Updates __init__.py
-"""
-import os
-import sys
-
-ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-PKG = os.path.join(ROOT, "quantalib")
-
-
-def write(path: str, content: str) -> None:
- with open(path, "w", encoding="utf-8", newline="\n") as f:
- f.write(content)
- print(f" wrote {os.path.relpath(path, ROOT)}")
-
-
-def append(path: str, content: str) -> None:
- with open(path, "a", encoding="utf-8", newline="\n") as f:
- f.write(content)
- print(f" appended to {os.path.relpath(path, ROOT)}")
-
-
-def read(path: str) -> str:
- with open(path, "r", encoding="utf-8") as f:
- return f.read()
-
-
-# ═══════════════════════════════════════════════════════════════════════════
-# Step 1: Read existing _bridge.py and add missing manual bindings
-# ═══════════════════════════════════════════════════════════════════════════
-print("Step 1: Adding missing bindings to _bridge.py ...")
-
-bridge_path = os.path.join(PKG, "_bridge.py")
-bridge = read(bridge_path)
-
-# Check which bindings already exist
-MANUAL_BINDINGS = {
- # ── Core ──
- "qtl_avgprice": ["{dp}", "{dp}", "{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_medprice": ["{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_typprice": ["{dp}", "{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_midbody": ["{dp}", "{dp}", "{ci}", "{dp}"],
- # ── Momentum ──
- "qtl_rsi": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_roc": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_mom": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_cmo": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_tsi": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_apo": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_bias": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_cfo": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_cfb": ["{dp}", "{ci}", "{dp}", "{ip}", "{ci}"],
- "qtl_asi": ["{dp}", "{dp}", "{dp}", "{dp}", "{ci}", "{dp}", "{cd}"],
- # ── Oscillators ──
- "qtl_fisher": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_fisher04": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_dpo": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_trix": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_inertia": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_rsx": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_er": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_cti": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_reflex": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_trendflex": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_kri": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_psl": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_deco": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_dosc": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}", "{ci}"],
- "qtl_dymoi": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}", "{ci}", "{ci}"],
- "qtl_crsi": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}"],
- "qtl_bbb": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_bbi": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}", "{ci}"],
- "qtl_dem": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_brar": ["{dp}", "{dp}", "{dp}", "{dp}", "{ci}", "{dp}", "{dp}", "{ci}"],
- # ── Trends FIR ──
- "qtl_sma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_wma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_hma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_trima": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_swma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_dwma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_blma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_alma": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}", "{cd}"],
- "qtl_lsma": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{cd}"],
- "qtl_sgma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_sinema": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_hanma": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_parzen": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_tsf": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_conv": ["{dp}", "{ci}", "{dp}", "{dp}", "{ci}"],
- "qtl_bwma": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_crma": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_sp15": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_tukey_w": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_rain": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_afirma": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}"],
- # ── Trends IIR ──
- "qtl_ema": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_ema_alpha": ["{dp}", "{ci}", "{dp}", "{cd}"],
- "qtl_dema": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_dema_alpha":["{dp}", "{ci}", "{dp}", "{cd}"],
- "qtl_tema": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_lema": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_hema": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_ahrens": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_decycler": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_dsma": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_gdema": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_coral": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_agc": ["{dp}", "{ci}", "{dp}", "{cd}"],
- "qtl_ccyc": ["{dp}", "{ci}", "{dp}", "{cd}"],
- # ── Channels ──
- "qtl_bbands": ["{dp}", "{ci}", "{dp}", "{dp}", "{dp}", "{ci}", "{cd}"],
- "qtl_aberr": ["{dp}", "{ci}", "{dp}", "{dp}", "{dp}", "{ci}", "{cd}"],
- "qtl_atrbands": ["{dp}", "{dp}", "{dp}", "{ci}", "{dp}", "{dp}", "{dp}", "{ci}", "{cd}"],
- "qtl_apchannel": ["{dp}", "{dp}", "{ci}", "{dp}", "{dp}", "{cd}"],
- # ── Volatility ──
- "qtl_tr": ["{dp}", "{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_bbw": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_bbwn": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}", "{ci}"],
- "qtl_bbwp": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}", "{ci}"],
- "qtl_stddev": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_variance": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_etherm": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_ccv": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_cv": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}", "{cd}"],
- "qtl_cvi": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_ewma": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}"],
- # ── Volume ──
- "qtl_obv": ["{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_pvt": ["{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_pvr": ["{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_vf": ["{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_nvi": ["{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_pvi": ["{dp}", "{dp}", "{ci}", "{dp}"],
- "qtl_tvi": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_pvd": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_vwma": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_evwma": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_efi": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_aobv": ["{dp}", "{dp}", "{ci}", "{dp}", "{dp}"],
- "qtl_mfi": ["{dp}", "{dp}", "{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_cmf": ["{dp}", "{dp}", "{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_eom": ["{dp}", "{dp}", "{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_pvo": ["{dp}", "{ci}", "{dp}", "{dp}", "{dp}", "{ci}", "{ci}", "{ci}"],
- # ── Statistics ──
- "qtl_zscore": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_cma": ["{dp}", "{ci}", "{dp}"],
- "qtl_entropy": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_correlation": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_covariance": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_cointegration": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- # ── Errors ──
- "qtl_mse": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_rmse": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_mae": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_mape": ["{dp}", "{dp}", "{ci}", "{dp}", "{ci}"],
- # ── Filters ──
- "qtl_bessel": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_butter2": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_butter3": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_cheby1": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_cheby2": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_elliptic": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_edcf": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_bpf": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_alaguerre": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_bilateral": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}", "{cd}"],
- "qtl_baxterking": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}"],
- "qtl_cfitz": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- # ── Cycles ──
- "qtl_cg": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_dsp": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_ccor": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_ebsw": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
- "qtl_eacp": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}", "{ci}"],
- # ── Numerics ──
- "qtl_change": ["{dp}", "{ci}", "{dp}", "{ci}"],
- "qtl_exptrans": ["{dp}", "{ci}", "{dp}"],
- "qtl_betadist": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}", "{cd}"],
- "qtl_expdist": ["{dp}", "{ci}", "{dp}", "{ci}", "{cd}"],
- "qtl_binomdist":["{dp}", "{ci}", "{dp}", "{ci}", "{ci}", "{ci}"],
- "qtl_cwt": ["{dp}", "{ci}", "{dp}", "{cd}", "{cd}"],
- "qtl_dwt": ["{dp}", "{ci}", "{dp}", "{ci}", "{ci}"],
-}
-
-# Build binding lines
-TYPE_MAP = {"{dp}": "_dp", "{ci}": "_ci", "{cd}": "_cd", "{ip}": "_ip"}
-
-missing_bindings = {} # category -> list of lines
-categories_order = [
- ("Core", ["qtl_avgprice","qtl_medprice","qtl_typprice","qtl_midbody"]),
- ("Momentum", ["qtl_rsi","qtl_roc","qtl_mom","qtl_cmo","qtl_tsi","qtl_apo","qtl_bias","qtl_cfo","qtl_cfb","qtl_asi"]),
- ("Oscillators", ["qtl_fisher","qtl_fisher04","qtl_dpo","qtl_trix","qtl_inertia","qtl_rsx","qtl_er","qtl_cti","qtl_reflex","qtl_trendflex","qtl_kri","qtl_psl","qtl_deco","qtl_dosc","qtl_dymoi","qtl_crsi","qtl_bbb","qtl_bbi","qtl_dem","qtl_brar"]),
- ("Trends — FIR", ["qtl_sma","qtl_wma","qtl_hma","qtl_trima","qtl_swma","qtl_dwma","qtl_blma","qtl_alma","qtl_lsma","qtl_sgma","qtl_sinema","qtl_hanma","qtl_parzen","qtl_tsf","qtl_conv","qtl_bwma","qtl_crma","qtl_sp15","qtl_tukey_w","qtl_rain","qtl_afirma"]),
- ("Trends — IIR", ["qtl_ema","qtl_ema_alpha","qtl_dema","qtl_dema_alpha","qtl_tema","qtl_lema","qtl_hema","qtl_ahrens","qtl_decycler","qtl_dsma","qtl_gdema","qtl_coral","qtl_agc","qtl_ccyc"]),
- ("Channels", ["qtl_bbands","qtl_aberr","qtl_atrbands","qtl_apchannel"]),
- ("Volatility", ["qtl_tr","qtl_bbw","qtl_bbwn","qtl_bbwp","qtl_stddev","qtl_variance","qtl_etherm","qtl_ccv","qtl_cv","qtl_cvi","qtl_ewma"]),
- ("Volume", ["qtl_obv","qtl_pvt","qtl_pvr","qtl_vf","qtl_nvi","qtl_pvi","qtl_tvi","qtl_pvd","qtl_vwma","qtl_evwma","qtl_efi","qtl_aobv","qtl_mfi","qtl_cmf","qtl_eom","qtl_pvo"]),
- ("Statistics", ["qtl_zscore","qtl_cma","qtl_entropy","qtl_correlation","qtl_covariance","qtl_cointegration"]),
- ("Errors", ["qtl_mse","qtl_rmse","qtl_mae","qtl_mape"]),
- ("Filters", ["qtl_bessel","qtl_butter2","qtl_butter3","qtl_cheby1","qtl_cheby2","qtl_elliptic","qtl_edcf","qtl_bpf","qtl_alaguerre","qtl_bilateral","qtl_baxterking","qtl_cfitz"]),
- ("Cycles", ["qtl_cg","qtl_dsp","qtl_ccor","qtl_ebsw","qtl_eacp"]),
- ("Numerics", ["qtl_change","qtl_exptrans","qtl_betadist","qtl_expdist","qtl_binomdist","qtl_cwt","qtl_dwt"]),
-]
-
-added_count = 0
-new_lines = []
-for cat, names in categories_order:
- cat_lines = []
- for name in names:
- if f'"{name}"' in bridge:
- continue # already bound
- args = MANUAL_BINDINGS[name]
- arg_str = ", ".join(TYPE_MAP[a] for a in args)
- var = "HAS_" + name.replace("qtl_", "").upper()
- cat_lines.append(f'{var} = _bind("{name}", [{arg_str}])')
- added_count += 1
- if cat_lines:
- new_lines.append(f"\n# ── {cat} (Exports.cs — manual) ──")
- new_lines.extend(cat_lines)
-
-if new_lines:
- # Append to end of _bridge.py
- with open(bridge_path, "a", encoding="utf-8", newline="\n") as f:
- f.write("\n")
- f.write("\n".join(new_lines))
- f.write("\n")
- print(f" Added {added_count} bindings to _bridge.py")
-else:
- print(" All bindings already present in _bridge.py")
-
-
-# ═══════════════════════════════════════════════════════════════════════════
-# Step 2: Add missing wrappers to category .py files
-# ═══════════════════════════════════════════════════════════════════════════
-print("\nStep 2: Adding missing wrappers to category files ...")
-
-# For each category file, check what's in __all__ and add missing funcs
-
-
-# ── core.py ──
-core_additions = '''
-
-def avgprice(open: object, high: object, low: object, close: object,
- offset: int = 0, **kwargs) -> object:
- """Average Price = (O+H+L+C)/4."""
- offset = int(offset)
- o, idx = _arr(open); h, _ = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
- n = len(o); dst = _out(n)
- _check(_lib.qtl_avgprice(_ptr(o), _ptr(h), _ptr(l), _ptr(c), n, _ptr(dst)))
- return _wrap(dst, idx, "AVGPRICE", "core", offset)
-
-
-def medprice(high: object, low: object, offset: int = 0, **kwargs) -> object:
- """Median Price = (H+L)/2."""
- h, idx = _arr(high); l, _ = _arr(low)
- n = len(h); dst = _out(n)
- _check(_lib.qtl_medprice(_ptr(h), _ptr(l), n, _ptr(dst)))
- return _wrap(dst, idx, "MEDPRICE", "core", int(offset))
-
-
-def typprice(open: object, high: object, low: object,
- offset: int = 0, **kwargs) -> object:
- """Typical Price = (O+H+L)/3 (QuanTAlib variant)."""
- o, idx = _arr(open); h, _ = _arr(high); l, _ = _arr(low)
- n = len(o); dst = _out(n)
- _check(_lib.qtl_typprice(_ptr(o), _ptr(h), _ptr(l), n, _ptr(dst)))
- return _wrap(dst, idx, "TYPPRICE", "core", int(offset))
-
-
-def midbody(open: object, close: object, offset: int = 0, **kwargs) -> object:
- """Mid Body = (O+C)/2."""
- o, idx = _arr(open); c, _ = _arr(close)
- n = len(o); dst = _out(n)
- _check(_lib.qtl_midbody(_ptr(o), _ptr(c), n, _ptr(dst)))
- return _wrap(dst, idx, "MIDBODY", "core", int(offset))
-'''
-
-
-# ── momentum.py ──
-momentum_additions = '''
-import ctypes
-
-
-def rsi(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Relative Strength Index."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_rsi(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"RSI_{length}", "momentum", offset)
-
-
-def roc(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Rate of Change."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_roc(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"ROC_{length}", "momentum", offset)
-
-
-def mom(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Momentum."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_mom(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"MOM_{length}", "momentum", offset)
-
-
-def cmo(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Chande Momentum Oscillator."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cmo(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"CMO_{length}", "momentum", offset)
-
-
-def tsi(close: object, long_period: int = 25, short_period: int = 13,
- offset: int = 0, **kwargs) -> object:
- """True Strength Index."""
- long_period = int(long_period); short_period = int(short_period); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_tsi(_ptr(src), n, _ptr(dst), long_period, short_period))
- return _wrap(dst, idx, f"TSI_{long_period}_{short_period}", "momentum", offset)
-
-
-def apo(close: object, fast: int = 12, slow: int = 26,
- offset: int = 0, **kwargs) -> object:
- """Absolute Price Oscillator."""
- fast = int(fast); slow = int(slow); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_apo(_ptr(src), n, _ptr(dst), fast, slow))
- return _wrap(dst, idx, f"APO_{fast}_{slow}", "momentum", offset)
-
-
-def bias(close: object, length: int = 26, offset: int = 0, **kwargs) -> object:
- """Bias."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bias(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"BIAS_{length}", "momentum", offset)
-
-
-def cfo(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Chande Forecast Oscillator."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cfo(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"CFO_{length}", "momentum", offset)
-
-
-def cfb(close: object, lengths: list | None = None,
- offset: int = 0, **kwargs) -> object:
- """Composite Fractal Behavior."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- if lengths:
- arr_t = (ctypes.c_int * len(lengths))(*lengths)
- _check(_lib.qtl_cfb(_ptr(src), n, _ptr(dst), arr_t, len(lengths)))
- else:
- _check(_lib.qtl_cfb(_ptr(src), n, _ptr(dst), None, 0))
- return _wrap(dst, idx, "CFB", "momentum", offset)
-
-
-def asi(open: object, high: object, low: object, close: object,
- limit: float = 3.0, offset: int = 0, **kwargs) -> object:
- """Accumulative Swing Index."""
- o, idx = _arr(open); h, _ = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
- n = len(o); dst = _out(n)
- _check(_lib.qtl_asi(_ptr(o), _ptr(h), _ptr(l), _ptr(c), n, _ptr(dst), float(limit)))
- return _wrap(dst, idx, "ASI", "momentum", int(offset))
-'''
-
-
-# ── oscillators.py ──
-oscillators_additions = '''
-
-def fisher(close: object, length: int = 9, offset: int = 0, **kwargs) -> object:
- """Fisher Transform."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_fisher(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"FISHER_{length}", "oscillators", offset)
-
-
-def fisher04(close: object, length: int = 9, offset: int = 0, **kwargs) -> object:
- """Fisher Transform (0.4 variant)."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_fisher04(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"FISHER04_{length}", "oscillators", offset)
-
-
-def dpo(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Detrended Price Oscillator."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dpo(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"DPO_{length}", "oscillators", offset)
-
-
-def trix(close: object, length: int = 18, offset: int = 0, **kwargs) -> object:
- """Triple EMA Rate of Change."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_trix(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"TRIX_{length}", "oscillators", offset)
-
-
-def inertia(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Inertia."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_inertia(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"INERTIA_{length}", "oscillators", offset)
-
-
-def rsx(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Relative Strength Xtra."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_rsx(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"RSX_{length}", "oscillators", offset)
-
-
-def er(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Efficiency Ratio."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_er(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"ER_{length}", "oscillators", offset)
-
-
-def cti(close: object, length: int = 12, offset: int = 0, **kwargs) -> object:
- """Correlation Trend Indicator."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cti(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"CTI_{length}", "oscillators", offset)
-
-
-def reflex(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Reflex."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_reflex(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"REFLEX_{length}", "oscillators", offset)
-
-
-def trendflex(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Trendflex."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_trendflex(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"TRENDFLEX_{length}", "oscillators", offset)
-
-
-def kri(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Kairi Relative Index."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_kri(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"KRI_{length}", "oscillators", offset)
-
-
-def psl(close: object, length: int = 12, offset: int = 0, **kwargs) -> object:
- """Psychological Line."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_psl(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"PSL_{length}", "oscillators", offset)
-
-
-def deco(close: object, short_period: int = 30, long_period: int = 60,
- offset: int = 0, **kwargs) -> object:
- """DECO."""
- short_period = int(short_period); long_period = int(long_period); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_deco(_ptr(src), n, _ptr(dst), short_period, long_period))
- return _wrap(dst, idx, f"DECO_{short_period}_{long_period}", "oscillators", offset)
-
-
-def dosc(close: object, rsi_period: int = 14, ema1_period: int = 5,
- ema2_period: int = 3, signal_period: int = 9,
- offset: int = 0, **kwargs) -> object:
- """DeMarker Oscillator."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dosc(_ptr(src), n, _ptr(dst),
- int(rsi_period), int(ema1_period), int(ema2_period), int(signal_period)))
- return _wrap(dst, idx, f"DOSC_{rsi_period}", "oscillators", offset)
-
-
-def dymoi(close: object, base_period: int = 14, short_period: int = 5,
- long_period: int = 10, min_period: int = 3, max_period: int = 30,
- offset: int = 0, **kwargs) -> object:
- """Dynamic Momentum Index."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dymoi(_ptr(src), n, _ptr(dst),
- int(base_period), int(short_period), int(long_period),
- int(min_period), int(max_period)))
- return _wrap(dst, idx, "DYMOI", "oscillators", offset)
-
-
-def crsi(close: object, rsi_period: int = 3, streak_period: int = 2,
- rank_period: int = 100, offset: int = 0, **kwargs) -> object:
- """Connors RSI."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_crsi(_ptr(src), n, _ptr(dst),
- int(rsi_period), int(streak_period), int(rank_period)))
- return _wrap(dst, idx, f"CRSI_{rsi_period}", "oscillators", offset)
-
-
-def bbb(close: object, length: int = 20, mult: float = 2.0,
- offset: int = 0, **kwargs) -> object:
- """Bollinger Band Bounce."""
- length = int(length); mult = float(mult); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bbb(_ptr(src), n, _ptr(dst), length, mult))
- return _wrap(dst, idx, f"BBB_{length}", "oscillators", offset)
-
-
-def bbi(close: object, p1: int = 3, p2: int = 6, p3: int = 12, p4: int = 24,
- offset: int = 0, **kwargs) -> object:
- """Bull Bear Index."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bbi(_ptr(src), n, _ptr(dst), int(p1), int(p2), int(p3), int(p4)))
- return _wrap(dst, idx, "BBI", "oscillators", offset)
-
-
-def dem(high: object, low: object, length: int = 14,
- offset: int = 0, **kwargs) -> object:
- """DeMarker."""
- length = int(length)
- h, idx = _arr(high); l, _ = _arr(low)
- n = len(h); dst = _out(n)
- _check(_lib.qtl_dem(_ptr(h), _ptr(l), n, _ptr(dst), length))
- return _wrap(dst, idx, f"DEM_{length}", "oscillators", int(offset))
-
-
-def brar(open: object, high: object, low: object, close: object,
- length: int = 26, offset: int = 0, **kwargs) -> object:
- """Bull-Bear Ratio (BRAR)."""
- length = int(length); offset = int(offset)
- o, idx = _arr(open); h, _ = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
- n = len(o); br = _out(n); ar = _out(n)
- _check(_lib.qtl_brar(_ptr(o), _ptr(h), _ptr(l), _ptr(c), n, _ptr(br), _ptr(ar), length))
- return _wrap_multi({f"BR_{length}": br, f"AR_{length}": ar}, idx, "oscillators", offset)
-'''
-
-
-# ── trends_fir.py ──
-trends_fir_additions = '''
-import numpy as np
-
-_F64 = np.float64
-
-
-def sma(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Simple Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_sma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"SMA_{length}", "trends_fir", offset)
-
-
-def wma(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Weighted Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_wma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"WMA_{length}", "trends_fir", offset)
-
-
-def hma(close: object, length: int = 9, offset: int = 0, **kwargs) -> object:
- """Hull Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_hma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"HMA_{length}", "trends_fir", offset)
-
-
-def trima(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Triangular Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_trima(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"TRIMA_{length}", "trends_fir", offset)
-
-
-def swma(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Symmetric Weighted Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_swma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"SWMA_{length}", "trends_fir", offset)
-
-
-def dwma(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Double Weighted Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dwma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"DWMA_{length}", "trends_fir", offset)
-
-
-def blma(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Blackman Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_blma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"BLMA_{length}", "trends_fir", offset)
-
-
-def alma(close: object, length: int = 10, alma_offset: float = 0.85,
- sigma: float = 6.0, offset: int = 0, **kwargs) -> object:
- """Arnaud Legoux Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_alma(_ptr(src), n, _ptr(dst), length, float(alma_offset), float(sigma)))
- return _wrap(dst, idx, f"ALMA_{length}", "trends_fir", offset)
-
-
-def lsma(close: object, length: int = 25, offset: int = 0, **kwargs) -> object:
- """Least Squares Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_lsma(_ptr(src), n, _ptr(dst), length, 0, 1.0))
- return _wrap(dst, idx, f"LSMA_{length}", "trends_fir", offset)
-
-
-def sgma(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Savitzky-Golay Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_sgma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"SGMA_{length}", "trends_fir", offset)
-
-
-def sinema(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Sine-weighted Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_sinema(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"SINEMA_{length}", "trends_fir", offset)
-
-
-def hanma(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Hann-weighted Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_hanma(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"HANMA_{length}", "trends_fir", offset)
-
-
-def parzen(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Parzen-weighted Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_parzen(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"PARZEN_{length}", "trends_fir", offset)
-
-
-def tsf(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Time Series Forecast."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_tsf(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"TSF_{length}", "trends_fir", offset)
-
-
-def conv(close: object, kernel: list | None = None,
- offset: int = 0, **kwargs) -> object:
- """Convolution with custom kernel."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- if kernel is None:
- kernel = [1.0]
- k = np.ascontiguousarray(kernel, dtype=_F64)
- _check(_lib.qtl_conv(_ptr(src), n, _ptr(dst), _ptr(k), len(k)))
- return _wrap(dst, idx, "CONV", "trends_fir", offset)
-
-
-def bwma(close: object, length: int = 10, order: int = 0,
- offset: int = 0, **kwargs) -> object:
- """Butterworth-weighted Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bwma(_ptr(src), n, _ptr(dst), length, int(order)))
- return _wrap(dst, idx, f"BWMA_{length}", "trends_fir", offset)
-
-
-def crma(close: object, length: int = 10, volume_factor: float = 1.0,
- offset: int = 0, **kwargs) -> object:
- """Cosine-Ramp Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_crma(_ptr(src), n, _ptr(dst), length, float(volume_factor)))
- return _wrap(dst, idx, f"CRMA_{length}", "trends_fir", offset)
-
-
-def sp15(close: object, length: int = 15, offset: int = 0, **kwargs) -> object:
- """SP-15 Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_sp15(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"SP15_{length}", "trends_fir", offset)
-
-
-def tukey_w(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Tukey-windowed Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_tukey_w(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"TUKEY_{length}", "trends_fir", offset)
-
-
-def rain(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """RAIN Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_rain(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"RAIN_{length}", "trends_fir", offset)
-
-
-def afirma(close: object, length: int = 10, window_type: int = 0,
- use_simd: bool = False, offset: int = 0, **kwargs) -> object:
- """Adaptive FIR Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_afirma(_ptr(src), n, _ptr(dst), length, int(window_type), int(use_simd)))
- return _wrap(dst, idx, f"AFIRMA_{length}", "trends_fir", offset)
-'''
-
-
-# ── trends_iir.py ──
-trends_iir_additions = '''
-
-def ema(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Exponential Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ema(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"EMA_{length}", "trends_iir", offset)
-
-
-def ema_alpha(close: object, alpha: float = 0.1, offset: int = 0, **kwargs) -> object:
- """EMA with explicit alpha."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ema_alpha(_ptr(src), n, _ptr(dst), float(alpha)))
- return _wrap(dst, idx, f"EMA_a{alpha:.4f}", "trends_iir", offset)
-
-
-def dema(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Double Exponential Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dema(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"DEMA_{length}", "trends_iir", offset)
-
-
-def dema_alpha(close: object, alpha: float = 0.1, offset: int = 0, **kwargs) -> object:
- """DEMA with explicit alpha."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dema_alpha(_ptr(src), n, _ptr(dst), float(alpha)))
- return _wrap(dst, idx, f"DEMA_a{alpha:.4f}", "trends_iir", offset)
-
-
-def tema(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Triple Exponential Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_tema(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"TEMA_{length}", "trends_iir", offset)
-
-
-def lema(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Laguerre-based EMA."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_lema(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"LEMA_{length}", "trends_iir", offset)
-
-
-def hema(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Henderson EMA."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_hema(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"HEMA_{length}", "trends_iir", offset)
-
-
-def ahrens(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Ahrens Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ahrens(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"AHRENS_{length}", "trends_iir", offset)
-
-
-def decycler(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Simple Decycler."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_decycler(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"DECYCLER_{length}", "trends_iir", offset)
-
-
-def dsma(close: object, length: int = 10, factor: float = 0.5,
- offset: int = 0, **kwargs) -> object:
- """Deviation-Scaled Moving Average."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dsma(_ptr(src), n, _ptr(dst), length, float(factor)))
- return _wrap(dst, idx, f"DSMA_{length}", "trends_iir", offset)
-
-
-def gdema(close: object, length: int = 10, vfactor: float = 1.0,
- offset: int = 0, **kwargs) -> object:
- """Generalized DEMA."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_gdema(_ptr(src), n, _ptr(dst), length, float(vfactor)))
- return _wrap(dst, idx, f"GDEMA_{length}", "trends_iir", offset)
-
-
-def coral(close: object, length: int = 10, friction: float = 0.4,
- offset: int = 0, **kwargs) -> object:
- """CORAL Trend."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_coral(_ptr(src), n, _ptr(dst), length, float(friction)))
- return _wrap(dst, idx, f"CORAL_{length}", "trends_iir", offset)
-
-
-def agc(close: object, alpha: float = 0.1, offset: int = 0, **kwargs) -> object:
- """Automatic Gain Control."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_agc(_ptr(src), n, _ptr(dst), float(alpha)))
- return _wrap(dst, idx, f"AGC_a{alpha:.4f}", "trends_iir", offset)
-
-
-def ccyc(close: object, alpha: float = 0.1, offset: int = 0, **kwargs) -> object:
- """Cyber Cycle."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ccyc(_ptr(src), n, _ptr(dst), float(alpha)))
- return _wrap(dst, idx, f"CCYC_a{alpha:.4f}", "trends_iir", offset)
-'''
-
-
-# ── channels.py ──
-channels_additions = '''
-
-def bbands(close: object, length: int = 20, std: float = 2.0,
- offset: int = 0, **kwargs) -> object:
- """Bollinger Bands -> (upper, mid, lower) or DataFrame."""
- length = int(length); std = float(std); offset = int(offset)
- src, idx = _arr(close); n = len(src)
- upper = _out(n); mid = _out(n); lower = _out(n)
- _check(_lib.qtl_bbands(_ptr(src), n, _ptr(upper), _ptr(mid), _ptr(lower), length, std))
- return _wrap_multi(
- {f"BBU_{length}_{std}": upper, f"BBM_{length}_{std}": mid, f"BBL_{length}_{std}": lower},
- idx, "channels", offset)
-
-
-def aberr(close: object, length: int = 20, mult: float = 2.0,
- offset: int = 0, **kwargs) -> object:
- """Aberration Bands -> (upper, mid, lower) or DataFrame."""
- length = int(length); mult = float(mult); offset = int(offset)
- src, idx = _arr(close); n = len(src)
- upper = _out(n); mid = _out(n); lower = _out(n)
- _check(_lib.qtl_aberr(_ptr(src), n, _ptr(mid), _ptr(upper), _ptr(lower), length, mult))
- return _wrap_multi(
- {f"ABERRU_{length}_{mult}": upper, f"ABERRM_{length}_{mult}": mid, f"ABERRL_{length}_{mult}": lower},
- idx, "channels", offset)
-
-
-def atrbands(high: object, low: object, close: object,
- length: int = 14, mult: float = 2.0,
- offset: int = 0, **kwargs) -> object:
- """ATR Bands -> (upper, mid, lower) or DataFrame."""
- length = int(length); mult = float(mult); offset = int(offset)
- h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
- n = len(h)
- upper = _out(n); mid = _out(n); lower = _out(n)
- _check(_lib.qtl_atrbands(_ptr(h), _ptr(l), _ptr(c), n, _ptr(upper), _ptr(mid), _ptr(lower), length, mult))
- return _wrap_multi(
- {f"ATRBU_{length}_{mult}": upper, f"ATRBM_{length}_{mult}": mid, f"ATRBL_{length}_{mult}": lower},
- idx, "channels", offset)
-
-
-def apchannel(high: object, low: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Average Price Channel -> (upper, lower) or DataFrame."""
- length = int(length); offset = int(offset)
- h, idx = _arr(high); l, _ = _arr(low)
- n = len(h)
- upper = _out(n); lower = _out(n)
- _check(_lib.qtl_apchannel(_ptr(h), _ptr(l), n, _ptr(upper), _ptr(lower), float(length)))
- return _wrap_multi({f"APCU_{length}": upper, f"APCL_{length}": lower}, idx, "channels", offset)
-'''
-
-
-# ── volatility.py ──
-volatility_additions = '''
-
-def tr(high: object, low: object, close: object, offset: int = 0, **kwargs) -> object:
- """True Range."""
- h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close)
- n = len(h); dst = _out(n)
- _check(_lib.qtl_tr(_ptr(h), _ptr(l), _ptr(c), n, _ptr(dst)))
- return _wrap(dst, idx, "TR", "volatility", int(offset))
-
-
-def bbw(close: object, length: int = 20, mult: float = 2.0,
- offset: int = 0, **kwargs) -> object:
- """Bollinger Band Width."""
- length = int(length); mult = float(mult); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bbw(_ptr(src), n, _ptr(dst), length, mult))
- return _wrap(dst, idx, f"BBW_{length}", "volatility", offset)
-
-
-def bbwn(close: object, length: int = 20, mult: float = 2.0,
- lookback: int = 252, offset: int = 0, **kwargs) -> object:
- """Bollinger Band Width Normalized."""
- length = int(length); mult = float(mult); lookback = int(lookback); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bbwn(_ptr(src), n, _ptr(dst), length, mult, lookback))
- return _wrap(dst, idx, f"BBWN_{length}", "volatility", offset)
-
-
-def bbwp(close: object, length: int = 20, mult: float = 2.0,
- lookback: int = 252, offset: int = 0, **kwargs) -> object:
- """Bollinger Band Width Percentile."""
- length = int(length); mult = float(mult); lookback = int(lookback); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bbwp(_ptr(src), n, _ptr(dst), length, mult, lookback))
- return _wrap(dst, idx, f"BBWP_{length}", "volatility", offset)
-
-
-def stddev(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Standard Deviation."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_stddev(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"STDDEV_{length}", "volatility", offset)
-
-
-def variance(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Variance."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_variance(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"VAR_{length}", "volatility", offset)
-
-
-def etherm(high: object, low: object, length: int = 14,
- offset: int = 0, **kwargs) -> object:
- """Elder Thermometer."""
- length = int(length)
- h, idx = _arr(high); l, _ = _arr(low)
- n = len(h); dst = _out(n)
- _check(_lib.qtl_etherm(_ptr(h), _ptr(l), n, _ptr(dst), length))
- return _wrap(dst, idx, f"ETHERM_{length}", "volatility", int(offset))
-
-
-def ccv(close: object, short_period: int = 20, long_period: int = 1,
- offset: int = 0, **kwargs) -> object:
- """Close-to-Close Volatility."""
- short_period = int(short_period); long_period = int(long_period); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ccv(_ptr(src), n, _ptr(dst), short_period, long_period))
- return _wrap(dst, idx, f"CCV_{short_period}", "volatility", offset)
-
-
-def cv(close: object, length: int = 20, min_vol: float = 0.2,
- max_vol: float = 0.7, offset: int = 0, **kwargs) -> object:
- """Coefficient of Variation."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cv(_ptr(src), n, _ptr(dst), length, float(min_vol), float(max_vol)))
- return _wrap(dst, idx, f"CV_{length}", "volatility", offset)
-
-
-def cvi(close: object, ema_period: int = 10, roc_period: int = 10,
- offset: int = 0, **kwargs) -> object:
- """Chaikin Volatility Index."""
- ema_period = int(ema_period); roc_period = int(roc_period); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cvi(_ptr(src), n, _ptr(dst), ema_period, roc_period))
- return _wrap(dst, idx, f"CVI_{ema_period}", "volatility", offset)
-
-
-def ewma(close: object, length: int = 20, is_pop: int = 1,
- ann_factor: int = 252, offset: int = 0, **kwargs) -> object:
- """Exponentially Weighted Moving Average (volatility)."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ewma(_ptr(src), n, _ptr(dst), length, int(is_pop), int(ann_factor)))
- return _wrap(dst, idx, f"EWMA_{length}", "volatility", offset)
-'''
-
-
-# ── volume.py ──
-volume_additions = '''
-
-def obv(close: object, volume: object, offset: int = 0, **kwargs) -> object:
- """On-Balance Volume."""
- offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_obv(_ptr(c), _ptr(v), n, _ptr(dst)))
- return _wrap(dst, idx, "OBV", "volume", offset)
-
-
-def pvt(close: object, volume: object, offset: int = 0, **kwargs) -> object:
- """Price Volume Trend."""
- offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_pvt(_ptr(c), _ptr(v), n, _ptr(dst)))
- return _wrap(dst, idx, "PVT", "volume", offset)
-
-
-def pvr(close: object, volume: object, offset: int = 0, **kwargs) -> object:
- """Price Volume Rank."""
- offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_pvr(_ptr(c), _ptr(v), n, _ptr(dst)))
- return _wrap(dst, idx, "PVR", "volume", offset)
-
-
-def vf(close: object, volume: object, offset: int = 0, **kwargs) -> object:
- """Volume Flow."""
- offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_vf(_ptr(c), _ptr(v), n, _ptr(dst)))
- return _wrap(dst, idx, "VF", "volume", offset)
-
-
-def nvi(close: object, volume: object, offset: int = 0, **kwargs) -> object:
- """Negative Volume Index."""
- offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_nvi(_ptr(c), _ptr(v), n, _ptr(dst)))
- return _wrap(dst, idx, "NVI", "volume", offset)
-
-
-def pvi(close: object, volume: object, offset: int = 0, **kwargs) -> object:
- """Positive Volume Index."""
- offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_pvi(_ptr(c), _ptr(v), n, _ptr(dst)))
- return _wrap(dst, idx, "PVI", "volume", offset)
-
-
-def tvi(close: object, volume: object, length: int = 14,
- offset: int = 0, **kwargs) -> object:
- """Trade Volume Index."""
- length = int(length); offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_tvi(_ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"TVI_{length}", "volume", offset)
-
-
-def pvd(close: object, volume: object, length: int = 14,
- offset: int = 0, **kwargs) -> object:
- """Price Volume Divergence."""
- length = int(length); offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_pvd(_ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"PVD_{length}", "volume", offset)
-
-
-def vwma(close: object, volume: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Volume Weighted Moving Average."""
- length = int(length); offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_vwma(_ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"VWMA_{length}", "volume", offset)
-
-
-def evwma(close: object, volume: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Elastic Volume Weighted Moving Average."""
- length = int(length); offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_evwma(_ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"EVWMA_{length}", "volume", offset)
-
-
-def efi(close: object, volume: object, length: int = 13,
- offset: int = 0, **kwargs) -> object:
- """Elder Force Index."""
- length = int(length); offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); dst = _out(n)
- _check(_lib.qtl_efi(_ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"EFI_{length}", "volume", offset)
-
-
-def aobv(close: object, volume: object, offset: int = 0, **kwargs) -> object:
- """Archer OBV -> (fast, slow) or DataFrame."""
- offset = int(offset)
- c, idx = _arr(close); v, _ = _arr(volume)
- n = len(c); obv_out = _out(n); sig = _out(n)
- _check(_lib.qtl_aobv(_ptr(c), _ptr(v), n, _ptr(obv_out), _ptr(sig)))
- return _wrap_multi({"AOBV": obv_out, "AOBV_SIG": sig}, idx, "volume", offset)
-
-
-def mfi(high: object, low: object, close: object, volume: object,
- length: int = 14, offset: int = 0, **kwargs) -> object:
- """Money Flow Index."""
- length = int(length); offset = int(offset)
- h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close); v, _ = _arr(volume)
- n = len(h); dst = _out(n)
- _check(_lib.qtl_mfi(_ptr(h), _ptr(l), _ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"MFI_{length}", "volume", offset)
-
-
-def cmf(high: object, low: object, close: object, volume: object,
- length: int = 20, offset: int = 0, **kwargs) -> object:
- """Chaikin Money Flow."""
- length = int(length); offset = int(offset)
- h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close); v, _ = _arr(volume)
- n = len(h); dst = _out(n)
- _check(_lib.qtl_cmf(_ptr(h), _ptr(l), _ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"CMF_{length}", "volume", offset)
-
-
-def eom(high: object, low: object, volume: object,
- length: int = 14, offset: int = 0, **kwargs) -> object:
- """Ease of Movement."""
- length = int(length); offset = int(offset)
- h, idx = _arr(high); l, _ = _arr(low); v, _ = _arr(volume)
- n = len(h); dst = _out(n)
- _check(_lib.qtl_eom(_ptr(h), _ptr(l), _ptr(v), n, _ptr(dst), length, 1e9))
- return _wrap(dst, idx, f"EOM_{length}", "volume", offset)
-
-
-def pvo(volume: object, fast: int = 12, slow: int = 26, signal: int = 9,
- offset: int = 0, **kwargs) -> object:
- """Percentage Volume Oscillator -> (pvo, signal, histogram) or DataFrame."""
- fast = int(fast); slow = int(slow); signal = int(signal); offset = int(offset)
- v, idx = _arr(volume); n = len(v)
- pvo_out = _out(n); sig = _out(n); hist = _out(n)
- _check(_lib.qtl_pvo(_ptr(v), n, _ptr(pvo_out), _ptr(sig), _ptr(hist), fast, slow, signal))
- return _wrap_multi(
- {f"PVO_{fast}_{slow}_{signal}": pvo_out, f"PVOs_{fast}_{slow}_{signal}": sig, f"PVOh_{fast}_{slow}_{signal}": hist},
- idx, "volume", offset)
-'''
-
-
-# ── statistics.py ──
-statistics_additions = '''
-
-def zscore(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Z-Score."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_zscore(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"ZSCORE_{length}", "statistics", offset)
-
-
-def cma(close: object, offset: int = 0, **kwargs) -> object:
- """Cumulative Moving Average."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cma(_ptr(src), n, _ptr(dst)))
- return _wrap(dst, idx, "CMA", "statistics", offset)
-
-
-def entropy(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Shannon Entropy."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_entropy(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"ENTROPY_{length}", "statistics", offset)
-
-
-def correlation(x: object, y: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Pearson Correlation."""
- length = int(length); offset = int(offset)
- xarr, idx = _arr(x); yarr, _ = _arr(y)
- n = len(xarr); dst = _out(n)
- _check(_lib.qtl_correlation(_ptr(xarr), _ptr(yarr), n, _ptr(dst), length))
- return _wrap(dst, idx, f"CORR_{length}", "statistics", offset)
-
-
-def covariance(x: object, y: object, length: int = 20,
- is_sample: bool = True, offset: int = 0, **kwargs) -> object:
- """Covariance."""
- length = int(length); offset = int(offset)
- xarr, idx = _arr(x); yarr, _ = _arr(y)
- n = len(xarr); dst = _out(n)
- _check(_lib.qtl_covariance(_ptr(xarr), _ptr(yarr), n, _ptr(dst), length, int(is_sample)))
- return _wrap(dst, idx, f"COV_{length}", "statistics", offset)
-
-
-def cointegration(x: object, y: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Cointegration."""
- length = int(length); offset = int(offset)
- xarr, idx = _arr(x); yarr, _ = _arr(y)
- n = len(xarr); dst = _out(n)
- _check(_lib.qtl_cointegration(_ptr(xarr), _ptr(yarr), n, _ptr(dst), length))
- return _wrap(dst, idx, f"COINT_{length}", "statistics", offset)
-'''
-
-
-# ── errors.py ──
-errors_additions = '''
-
-def mse(actual: object, predicted: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Mean Squared Error."""
- length = int(length); offset = int(offset)
- a, idx = _arr(actual); p, _ = _arr(predicted)
- n = len(a); dst = _out(n)
- _check(_lib.qtl_mse(_ptr(a), _ptr(p), n, _ptr(dst), length))
- return _wrap(dst, idx, f"MSE_{length}", "errors", offset)
-
-
-def rmse(actual: object, predicted: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Root Mean Squared Error."""
- length = int(length); offset = int(offset)
- a, idx = _arr(actual); p, _ = _arr(predicted)
- n = len(a); dst = _out(n)
- _check(_lib.qtl_rmse(_ptr(a), _ptr(p), n, _ptr(dst), length))
- return _wrap(dst, idx, f"RMSE_{length}", "errors", offset)
-
-
-def mae(actual: object, predicted: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Mean Absolute Error."""
- length = int(length); offset = int(offset)
- a, idx = _arr(actual); p, _ = _arr(predicted)
- n = len(a); dst = _out(n)
- _check(_lib.qtl_mae(_ptr(a), _ptr(p), n, _ptr(dst), length))
- return _wrap(dst, idx, f"MAE_{length}", "errors", offset)
-
-
-def mape(actual: object, predicted: object, length: int = 20,
- offset: int = 0, **kwargs) -> object:
- """Mean Absolute Percentage Error."""
- length = int(length); offset = int(offset)
- a, idx = _arr(actual); p, _ = _arr(predicted)
- n = len(a); dst = _out(n)
- _check(_lib.qtl_mape(_ptr(a), _ptr(p), n, _ptr(dst), length))
- return _wrap(dst, idx, f"MAPE_{length}", "errors", offset)
-'''
-
-
-# ── filters.py ──
-filters_additions = '''
-
-def bessel(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Bessel Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bessel(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"BESSEL_{length}", "filters", offset)
-
-
-def butter2(close: object, length: int = 14, gain: float = 1.0,
- offset: int = 0, **kwargs) -> object:
- """2nd-order Butterworth."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_butter2(_ptr(src), n, _ptr(dst), length, float(gain)))
- return _wrap(dst, idx, f"BUTTER2_{length}", "filters", offset)
-
-
-def butter3(close: object, length: int = 14, gain: float = 1.0,
- offset: int = 0, **kwargs) -> object:
- """3rd-order Butterworth."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_butter3(_ptr(src), n, _ptr(dst), length, float(gain)))
- return _wrap(dst, idx, f"BUTTER3_{length}", "filters", offset)
-
-
-def cheby1(close: object, length: int = 14, ripple: float = 0.5,
- offset: int = 0, **kwargs) -> object:
- """Chebyshev Type I."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cheby1(_ptr(src), n, _ptr(dst), length, float(ripple)))
- return _wrap(dst, idx, f"CHEBY1_{length}", "filters", offset)
-
-
-def cheby2(close: object, length: int = 14, ripple: float = 0.5,
- offset: int = 0, **kwargs) -> object:
- """Chebyshev Type II."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cheby2(_ptr(src), n, _ptr(dst), length, float(ripple)))
- return _wrap(dst, idx, f"CHEBY2_{length}", "filters", offset)
-
-
-def elliptic(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Elliptic (Cauer) Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_elliptic(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"ELLIPTIC_{length}", "filters", offset)
-
-
-def edcf(close: object, length: int = 14, offset: int = 0, **kwargs) -> object:
- """Ehlers Distance Coefficient Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_edcf(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"EDCF_{length}", "filters", offset)
-
-
-def bpf(close: object, length: int = 14, bandwidth: int = 5,
- offset: int = 0, **kwargs) -> object:
- """Bandpass Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bpf(_ptr(src), n, _ptr(dst), length, int(bandwidth)))
- return _wrap(dst, idx, f"BPF_{length}", "filters", offset)
-
-
-def alaguerre(close: object, length: int = 20, order: int = 5,
- offset: int = 0, **kwargs) -> object:
- """Adaptive Laguerre Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_alaguerre(_ptr(src), n, _ptr(dst), length, int(order)))
- return _wrap(dst, idx, f"ALAGUERRE_{length}", "filters", offset)
-
-
-def bilateral(close: object, length: int = 14, sigma_s: float = 0.5,
- sigma_r: float = 1.0, offset: int = 0, **kwargs) -> object:
- """Bilateral Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_bilateral(_ptr(src), n, _ptr(dst), length, float(sigma_s), float(sigma_r)))
- return _wrap(dst, idx, f"BILATERAL_{length}", "filters", offset)
-
-
-def baxterking(close: object, length: int = 12, min_period: int = 6,
- max_period: int = 32, offset: int = 0, **kwargs) -> object:
- """Baxter-King Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_baxterking(_ptr(src), n, _ptr(dst), length, int(min_period), int(max_period)))
- return _wrap(dst, idx, f"BAXTERKING_{length}", "filters", offset)
-
-
-def cfitz(close: object, length: int = 6, bw_period: int = 32,
- offset: int = 0, **kwargs) -> object:
- """Christiano-Fitzgerald Filter."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cfitz(_ptr(src), n, _ptr(dst), length, int(bw_period)))
- return _wrap(dst, idx, f"CFITZ_{length}", "filters", offset)
-'''
-
-
-# ── cycles.py ──
-cycles_additions = '''
-
-def cg(close: object, length: int = 10, offset: int = 0, **kwargs) -> object:
- """Center of Gravity."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cg(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"CG_{length}", "cycles", offset)
-
-
-def dsp(close: object, length: int = 20, offset: int = 0, **kwargs) -> object:
- """Dominant Cycle Period (DSP)."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dsp(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"DSP_{length}", "cycles", offset)
-
-
-def ccor(close: object, length: int = 20, alpha: float = 0.07,
- offset: int = 0, **kwargs) -> object:
- """Circular Correlation."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ccor(_ptr(src), n, _ptr(dst), length, float(alpha)))
- return _wrap(dst, idx, f"CCOR_{length}", "cycles", offset)
-
-
-def ebsw(close: object, hp_length: int = 40, ssf_length: int = 10,
- offset: int = 0, **kwargs) -> object:
- """Even Better Sinewave."""
- hp_length = int(hp_length); ssf_length = int(ssf_length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_ebsw(_ptr(src), n, _ptr(dst), hp_length, ssf_length))
- return _wrap(dst, idx, f"EBSW_{hp_length}", "cycles", offset)
-
-
-def eacp(close: object, min_period: int = 8, max_period: int = 48,
- avg_length: int = 3, enhance: int = 1,
- offset: int = 0, **kwargs) -> object:
- """Ehlers Autocorrelation Periodogram."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_eacp(_ptr(src), n, _ptr(dst), int(min_period), int(max_period), int(avg_length), int(enhance)))
- return _wrap(dst, idx, f"EACP_{min_period}_{max_period}", "cycles", offset)
-'''
-
-
-# ── numerics.py ──
-numerics_additions = '''
-
-def change(close: object, length: int = 1, offset: int = 0, **kwargs) -> object:
- """Price Change."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_change(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"CHANGE_{length}", "numerics", offset)
-
-
-def exptrans(close: object, offset: int = 0, **kwargs) -> object:
- """Exponential Transform."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_exptrans(_ptr(src), n, _ptr(dst)))
- return _wrap(dst, idx, "EXPTRANS", "numerics", offset)
-
-
-def betadist(close: object, length: int = 50, alpha: float = 2.0,
- beta: float = 2.0, offset: int = 0, **kwargs) -> object:
- """Beta Distribution."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_betadist(_ptr(src), n, _ptr(dst), length, float(alpha), float(beta)))
- return _wrap(dst, idx, f"BETADIST_{length}", "numerics", offset)
-
-
-def expdist(close: object, length: int = 50, lam: float = 3.0,
- offset: int = 0, **kwargs) -> object:
- """Exponential Distribution."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_expdist(_ptr(src), n, _ptr(dst), length, float(lam)))
- return _wrap(dst, idx, f"EXPDIST_{length}", "numerics", offset)
-
-
-def binomdist(close: object, length: int = 50, trials: int = 20,
- threshold: int = 10, offset: int = 0, **kwargs) -> object:
- """Binomial Distribution."""
- length = int(length); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_binomdist(_ptr(src), n, _ptr(dst), length, int(trials), int(threshold)))
- return _wrap(dst, idx, f"BINOMDIST_{length}", "numerics", offset)
-
-
-def cwt(close: object, scale: float = 10.0, omega: float = 6.0,
- offset: int = 0, **kwargs) -> object:
- """Continuous Wavelet Transform."""
- offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_cwt(_ptr(src), n, _ptr(dst), float(scale), float(omega)))
- return _wrap(dst, idx, "CWT", "numerics", offset)
-
-
-def dwt(close: object, length: int = 4, levels: int = 0,
- offset: int = 0, **kwargs) -> object:
- """Discrete Wavelet Transform."""
- length = int(length); levels = int(levels); offset = int(offset)
- src, idx = _arr(close); n = len(src); dst = _out(n)
- _check(_lib.qtl_dwt(_ptr(src), n, _ptr(dst), length, levels))
- return _wrap(dst, idx, f"DWT_{length}", "numerics", offset)
-'''
-
-
-# Now apply all additions
-additions = {
- "core.py": (core_additions, ["avgprice", "medprice", "typprice", "midbody"]),
- "momentum.py": (momentum_additions, ["rsi", "roc", "mom", "cmo", "tsi", "apo", "bias", "cfo", "cfb", "asi"]),
- "oscillators.py": (oscillators_additions, ["fisher", "fisher04", "dpo", "trix", "inertia", "rsx", "er", "cti", "reflex", "trendflex", "kri", "psl", "deco", "dosc", "dymoi", "crsi", "bbb", "bbi", "dem", "brar"]),
- "trends_fir.py": (trends_fir_additions, ["sma", "wma", "hma", "trima", "swma", "dwma", "blma", "alma", "lsma", "sgma", "sinema", "hanma", "parzen", "tsf", "conv", "bwma", "crma", "sp15", "tukey_w", "rain", "afirma"]),
- "trends_iir.py": (trends_iir_additions, ["ema", "ema_alpha", "dema", "dema_alpha", "tema", "lema", "hema", "ahrens", "decycler", "dsma", "gdema", "coral", "agc", "ccyc"]),
- "channels.py": (channels_additions, ["bbands", "aberr", "atrbands", "apchannel"]),
- "volatility.py": (volatility_additions, ["tr", "bbw", "bbwn", "bbwp", "stddev", "variance", "etherm", "ccv", "cv", "cvi", "ewma"]),
- "volume.py": (volume_additions, ["obv", "pvt", "pvr", "vf", "nvi", "pvi", "tvi", "pvd", "vwma", "evwma", "efi", "aobv", "mfi", "cmf", "eom", "pvo"]),
- "statistics.py": (statistics_additions, ["zscore", "cma", "entropy", "correlation", "covariance", "cointegration"]),
- "errors.py": (errors_additions, ["mse", "rmse", "mae", "mape"]),
- "filters.py": (filters_additions, ["bessel", "butter2", "butter3", "cheby1", "cheby2", "elliptic", "edcf", "bpf", "alaguerre", "bilateral", "baxterking", "cfitz"]),
- "cycles.py": (cycles_additions, ["cg", "dsp", "ccor", "ebsw", "eacp"]),
- "numerics.py": (numerics_additions, ["change", "exptrans", "betadist", "expdist", "binomdist", "cwt", "dwt"]),
-}
-
-total_added = 0
-for filename, (code, funcnames) in additions.items():
- filepath = os.path.join(PKG, filename)
- content = read(filepath)
-
- # Check which functions are already defined
- missing = [f for f in funcnames if f"\ndef {f}(" not in content]
- if not missing:
- print(f" {filename}: all {len(funcnames)} functions already present")
- continue
-
- # Update __all__ to include new functions
- # Find __all__ closing bracket
- import re
- all_match = re.search(r'__all__\s*=\s*\[([^\]]*)\]', content, re.DOTALL)
- if all_match:
- existing_all = all_match.group(1)
- existing_names = [s.strip().strip('"').strip("'") for s in existing_all.split(",") if s.strip().strip('"').strip("'")]
- new_names = [f for f in funcnames if f not in existing_names]
- if new_names:
- all_entries = existing_names + new_names
- new_all = "__all__ = [\n" + "".join(f' "{n}",\n' for n in all_entries) + "]"
- content = content[:all_match.start()] + new_all + content[all_match.end():]
-
- # Append the wrapper code
- content += "\n" + code.strip() + "\n"
-
- write(filepath, content)
- total_added += len(missing)
- print(f" {filename}: added {len(missing)} functions: {', '.join(missing)}")
-
-print(f"\n Total wrappers added: {total_added}")
-
-
-# ═══════════════════════════════════════════════════════════════════════════
-# Step 3: Rewrite indicators.py as thin re-export
-# ═══════════════════════════════════════════════════════════════════════════
-print("\nStep 3: Rewriting indicators.py as re-export module ...")
-
-CATEGORY_MODULES = [
- "channels", "core", "cycles", "dynamics", "errors", "filters",
- "momentum", "numerics", "oscillators", "reversals", "statistics",
- "trends_fir", "trends_iir", "volatility", "volume",
-]
-
-indicators_content = '''"""High-level indicator wrappers for quantalib.
-
-This module re-exports all indicator functions from per-category submodules.
-Each function accepts numpy arrays (or pandas Series / DataFrame) and
-returns the same type.
-
-Category submodules:
- quantalib.channels — Bollinger Bands, Keltner, Donchian, etc.
- quantalib.core — Price transforms (avgprice, medprice, etc.)
- quantalib.cycles — Hilbert, Sinewave, CG, DSP, etc.
- quantalib.dynamics — ADX, Ichimoku, Supertrend, etc.
- quantalib.errors — MSE, RMSE, MAE, MAPE, Huber, etc.
- quantalib.filters — Butterworth, Chebyshev, Kalman, etc.
- quantalib.momentum — RSI, MACD, ROC, MOM, etc.
- quantalib.numerics — FFT, sigmoid, slope, distributions, etc.
- quantalib.oscillators — Stochastic, Fisher, Williams %R, etc.
- quantalib.reversals — Pivot points, PSAR, fractals, etc.
- quantalib.statistics — Z-score, correlation, linreg, etc.
- quantalib.trends_fir — SMA, WMA, HMA, ALMA, etc.
- quantalib.trends_iir — EMA, DEMA, TEMA, JMA, KAMA, etc.
- quantalib.volatility — ATR, TR, Bollinger Width, etc.
- quantalib.volume — OBV, VWAP, MFI, CMF, etc.
-"""
-from __future__ import annotations
-
-'''
-
-for mod in CATEGORY_MODULES:
- indicators_content += f"from .{mod} import * # noqa: F401, F403\n"
-
-write(os.path.join(PKG, "indicators.py"), indicators_content)
-
-
-# ═══════════════════════════════════════════════════════════════════════════
-# Step 4: Update __init__.py
-# ═══════════════════════════════════════════════════════════════════════════
-print("\nStep 4: Updating __init__.py ...")
-
-init_content = '''"""quantalib — Python wrapper for QuanTAlib NativeAOT exports.
-
-Usage::
-
- import quantalib as qtl
-
- result = qtl.sma(close_array, length=14)
- result = qtl.bbands(close_array, length=20, std=2.0)
-"""
-from __future__ import annotations
-
-from pathlib import Path
-
-from ._loader import load_native_library
-from . import indicators
-from .indicators import * # noqa: F401, F403 — re-export all indicator functions
-
-# Re-export per-category submodules for direct access
-from . import ( # noqa: F401
- channels,
- core,
- cycles,
- dynamics,
- errors,
- filters,
- momentum,
- numerics,
- oscillators,
- reversals,
- statistics,
- trends_fir,
- trends_iir,
- volatility,
- volume,
-)
-
-from ._compat import ALIASES, get_compat
-from ._bridge import (
- QtlError,
- QtlNullPointerError,
- QtlInvalidLengthError,
- QtlInvalidParamError,
- QtlInternalError,
-)
-
-__all__ = [
- "load_native_library",
- "indicators",
- "channels",
- "core",
- "cycles",
- "dynamics",
- "errors",
- "filters",
- "momentum",
- "numerics",
- "oscillators",
- "reversals",
- "statistics",
- "trends_fir",
- "trends_iir",
- "volatility",
- "volume",
- "ALIASES",
- "get_compat",
- "QtlError",
- "QtlNullPointerError",
- "QtlInvalidLengthError",
- "QtlInvalidParamError",
- "QtlInternalError",
-]
-
-
-def _resolve_version() -> str:
- version_file = Path(__file__).resolve().parents[2] / "lib" / "VERSION"
- if version_file.exists():
- version = version_file.read_text(encoding="utf-8").strip()
- if version:
- return version
- return "0.0.0"
-
-
-__version__ = _resolve_version()
-'''
-
-write(os.path.join(PKG, "__init__.py"), init_content)
-
-
-# ═══════════════════════════════════════════════════════════════════════════
-# Step 5: Count & verify
-# ═══════════════════════════════════════════════════════════════════════════
-print("\nStep 5: Verification counts ...")
-
-import re as re2
-
-total_funcs = 0
-for mod in CATEGORY_MODULES:
- filepath = os.path.join(PKG, f"{mod}.py")
- content = read(filepath)
- funcs = re2.findall(r'^def (\w+)\(', content, re2.MULTILINE)
- # Exclude private helpers
- public = [f for f in funcs if not f.startswith('_')]
- total_funcs += len(public)
- print(f" {mod:15s}: {len(public):3d} functions")
-
-print(f" {'TOTAL':15s}: {total_funcs:3d} functions")
-print("\nDone!")
diff --git a/python/tools/count_wrappers.py b/python/tools/count_wrappers.py
deleted file mode 100644
index 0772d615..00000000
--- a/python/tools/count_wrappers.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import re, os
-total = 0
-for f in sorted(os.listdir('python/quantalib')):
- if f.endswith('.py') and not f.startswith('_') and f != 'indicators.py':
- content = open(f'python/quantalib/{f}', encoding='utf-8').read()
- defs = re.findall(r'^def (\w+)\(', content, re.MULTILINE)
- total += len(defs)
- print(f'{f}: {len(defs)} functions - {", ".join(defs[:10])}{"..." if len(defs) > 10 else ""}')
-print(f'\nTotal: {total} wrapper functions across {len([f for f in os.listdir("python/quantalib") if f.endswith(".py") and not f.startswith("_") and f != "indicators.py"])} files')
diff --git a/python/tools/diff_check.py b/python/tools/diff_check.py
deleted file mode 100644
index 30f145f4..00000000
--- a/python/tools/diff_check.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import re, os
-
-# Collect all function names from new category files
-new_fns = set()
-for f in sorted(os.listdir('python/quantalib')):
- if f.endswith('.py') and not f.startswith('_') and f != 'indicators.py':
- content = open(f'python/quantalib/{f}', encoding='utf-8').read()
- new_fns.update(re.findall(r'^def (\w+)\(', content, re.MULTILINE))
-
-# Collect from old indicators.py
-old_content = open('python/quantalib/indicators.py', encoding='utf-8').read()
-old_fns = set(re.findall(r'^def (\w+)\(', old_content, re.MULTILINE))
-old_fns = {f for f in old_fns if not f.startswith('_')}
-
-missing = sorted(old_fns - new_fns)
-extra = sorted(new_fns - old_fns)
-
-with open('python/tools/diff_report.txt', 'w') as out:
- out.write(f'Old indicators.py: {len(old_fns)} public functions\n')
- out.write(f'New category files: {len(new_fns)} functions\n\n')
- out.write(f'Missing from new ({len(missing)}):\n')
- for m in missing:
- out.write(f' {m}\n')
- out.write(f'\nNew indicators not in old ({len(extra)}):\n')
- for e in extra:
- out.write(f' {e}\n')
-
-print('Done - see python/tools/diff_report.txt')
diff --git a/python/tools/extract_sigs.py b/python/tools/extract_sigs.py
deleted file mode 100644
index 30ea6206..00000000
--- a/python/tools/extract_sigs.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Extract C# export signatures for category module generation."""
-import re
-
-cs = open('python/src/Exports.Generated.cs', encoding='utf-8').read()
-
-# Extract each function: entry point name + full C# parameter list
-pattern = r'\[UnmanagedCallersOnly\(EntryPoint\s*=\s*"qtl_(\w+)"\)\]\s+public static int \w+\(([^)]+)\)'
-matches = re.findall(pattern, cs)
-
-for name, params in matches:
- # Parse param types + names
- parts = []
- for p in params.split(','):
- p = p.strip()
- tokens = p.split()
- if len(tokens) >= 2:
- parts.append(f"{tokens[0]} {tokens[1]}")
- print(f"qtl_{name}|{'|'.join(parts)}")
diff --git a/python/tools/fix_length_to_period.py b/python/tools/fix_length_to_period.py
deleted file mode 100644
index cec957a0..00000000
--- a/python/tools/fix_length_to_period.py
+++ /dev/null
@@ -1,194 +0,0 @@
-#!/usr/bin/env python3
-"""Transform new-style functions from `length` → `period` (primary) with `length` as kwargs alias.
-
-Target: all category module .py files in python/quantalib/ that have functions
-using bare `length` as a parameter name.
-
-Rules:
- 1. `def foo(close, length: int = X, ...)` → `def foo(close, period: int = X, ...)`
- 2. `length = int(length)` (standalone assignment) → `period = int(kwargs.get("length", period))`
- 3. Any remaining standalone `length` in the function body → `period`
- 4. Compound names like hpLength, ssLength, minLength, etc. are NOT touched.
- 5. `lengths` (plural) is NOT touched.
- 6. `default_length` in _helpers.py pattern helpers is NOT touched (separate ticket).
-
-Also fixes _helpers.py pattern helpers (_pa, _pf, _pg2, _ph) the same way.
-"""
-from __future__ import annotations
-
-import re
-import sys
-from pathlib import Path
-
-QUANTALIB = Path(__file__).resolve().parent.parent / "quantalib"
-
-# Files to process (category modules + _helpers.py)
-TARGET_FILES = [
- "channels.py",
- "core.py",
- "cycles.py",
- "dynamics.py",
- "errors.py",
- "filters.py",
- "momentum.py",
- "numerics.py",
- "oscillators.py",
- "reversals.py",
- "statistics.py",
- "trends_fir.py",
- "trends_iir.py",
- "volatility.py",
- "volume.py",
- "_helpers.py",
-]
-
-# Pattern: standalone `length` as a word — NOT preceded or followed by
-# alphanumeric or underscore (i.e., not part of hpLength, minLength, etc.)
-# Also NOT `lengths` (plural).
-STANDALONE_LENGTH = re.compile(r'(?`
-SIG_PATTERN = re.compile(r'(? bool:
- """Check if a def line has standalone `length` as a parameter."""
- if not line.lstrip().startswith("def "):
- return False
- # Must have `length` as standalone word (not part of compound)
- return bool(STANDALONE_LENGTH.search(line))
-
-
-def transform_file(filepath: Path) -> tuple[int, int]:
- """Transform a single file. Returns (functions_changed, lines_changed)."""
- text = filepath.read_text(encoding="utf-8")
- lines = text.split("\n")
- new_lines: list[str] = []
- in_target_func = False
- func_indent = ""
- funcs_changed = 0
- lines_changed = 0
- assignment_done = False
-
- i = 0
- while i < len(lines):
- line = lines[i]
- stripped = line.lstrip()
-
- # Detect start of a function with `length` parameter
- if stripped.startswith("def ") and has_standalone_length_param(line):
- in_target_func = True
- func_indent = line[: len(line) - len(stripped)]
- assignment_done = False
- funcs_changed += 1
-
- # Handle multi-line def (continuation lines)
- full_def = line
- while i < len(lines) - 1 and line.rstrip().endswith(","):
- # Replace standalone length in this line
- new_line = STANDALONE_LENGTH.sub("period", line)
- if new_line != line:
- lines_changed += 1
- new_lines.append(new_line)
- i += 1
- line = lines[i]
-
- # Last line of def (or single-line def)
- new_line = STANDALONE_LENGTH.sub("period", line)
- if new_line != line:
- lines_changed += 1
- new_lines.append(new_line)
- i += 1
- continue
-
- # Inside a target function?
- if in_target_func:
- # Detect end of function: non-empty line at or less than func indent,
- # or a new def/class
- if stripped and not line.startswith(func_indent + " ") and not line.startswith(func_indent + "\t"):
- if not stripped.startswith('"""') and not stripped.startswith("'"):
- # Could be the docstring continuation; check differently
- if stripped.startswith("def ") or stripped.startswith("class ") or (len(line) - len(stripped) <= len(func_indent) and stripped and not stripped.startswith('#')):
- in_target_func = False
-
- if in_target_func:
- # Check for _helpers.py style assignment:
- # `length = int(length) if length is not None else default_length`
- m_helpers = HELPERS_ASSIGN.match(line)
- if m_helpers and not assignment_done:
- indent = m_helpers.group(1)
- new_line = f"{indent}period = int(kwargs.get(\"length\", period)) if period is not None else default_length"
- new_lines.append(new_line)
- lines_changed += 1
- assignment_done = True
- i += 1
- continue
-
- # Check for standard assignment: `length = int(length);`
- m_assign = ASSIGN_PATTERN.match(line)
- if m_assign and not assignment_done:
- indent = m_assign.group(1)
- # Preserve anything after the semicolon on the same line
- rest_of_line = ASSIGN_PATTERN.sub("", line)
- # Check if there's more after (e.g., "; offset = int(offset)")
- remaining = line[m_assign.end():]
- new_assignment = f'{indent}period = int(kwargs.get("length", period))'
- if remaining.strip():
- new_assignment += "; " + remaining.strip()
- new_lines.append(new_assignment)
- lines_changed += 1
- assignment_done = True
- i += 1
- continue
-
- # Replace any remaining standalone `length` references
- new_line = STANDALONE_LENGTH.sub("period", line)
- if new_line != line:
- lines_changed += 1
- new_lines.append(new_line)
- i += 1
- continue
-
- new_lines.append(line)
- i += 1
-
- new_text = "\n".join(new_lines)
- if new_text != text:
- filepath.write_text(new_text, encoding="utf-8")
-
- return funcs_changed, lines_changed
-
-
-def main() -> None:
- total_funcs = 0
- total_lines = 0
-
- for fname in TARGET_FILES:
- fpath = QUANTALIB / fname
- if not fpath.exists():
- print(f" SKIP {fname} (not found)")
- continue
-
- funcs, lines = transform_file(fpath)
- if funcs > 0:
- print(f" {fname}: {funcs} functions, {lines} lines changed")
- total_funcs += funcs
- total_lines += lines
- else:
- print(f" {fname}: no changes")
-
- print(f"\nTotal: {total_funcs} functions, {total_lines} lines changed")
-
-
-if __name__ == "__main__":
- main()
diff --git a/python/tools/generate_category_modules.py b/python/tools/generate_category_modules.py
deleted file mode 100644
index 88d3dd05..00000000
--- a/python/tools/generate_category_modules.py
+++ /dev/null
@@ -1,767 +0,0 @@
-#!/usr/bin/env python3
-"""Generate per-category Python indicator modules from Exports.Generated.cs.
-
-Reads the C# exports file and the lib/ directory structure to produce:
- - python/quantalib/_helpers.py (shared wrapper infrastructure)
- - python/quantalib/_bridge.py (ALL ctypes bindings)
- - python/quantalib/{category}.py (one per lib/ category)
- - python/quantalib/indicators.py (re-exports everything)
- - python/quantalib/__init__.py (package root)
-
-Usage:
- python python/tools/generate_category_modules.py
-"""
-from __future__ import annotations
-
-import os
-import re
-import sys
-from dataclasses import dataclass, field
-from pathlib import Path
-
-REPO_ROOT = Path(__file__).resolve().parents[2]
-LIB_DIR = REPO_ROOT / "lib"
-EXPORTS_CS = REPO_ROOT / "python" / "src" / "Exports.Generated.cs"
-OUT_DIR = REPO_ROOT / "python" / "quantalib"
-
-# ---------------------------------------------------------------------------
-# Category mapping: lib/ subdirectory → Python module name
-# ---------------------------------------------------------------------------
-CATEGORY_PY_NAME: dict[str, str] = {
- "channels": "channels",
- "core": "core",
- "cycles": "cycles",
- "dynamics": "dynamics",
- "errors": "errors",
- "filters": "filters",
- "forecasts": "forecasts",
- "momentum": "momentum",
- "numerics": "numerics",
- "oscillators": "oscillators",
- "reversals": "reversals",
- "statistics": "statistics_", # avoid shadowing stdlib 'statistics'
- "trends_FIR": "trends_fir",
- "trends_IIR": "trends_iir",
- "volatility": "volatility",
- "volume": "volume",
-}
-
-# Subdirectories in lib/core/ that are NOT indicators (infrastructure)
-CORE_SKIP = {
- "collections", "ringbuffer", "simd", "tbar", "tbarseries",
- "tests", "tseries", "tvalue", "_index.md",
-}
-
-# Export names that don't map cleanly to a lib/ indicator dir
-EXPORT_RENAMES: dict[str, str] = {
- "htdcperiod": "ht_dcperiod",
- "htdcphase": "ht_dcphase",
- "htphasor": "ht_phasor",
- "htsine": "ht_sine",
- "httrendmode": "ht_trendmode",
- "htit": "htit",
- "ttmsqueeze": "ttm_squeeze",
- "ttmtrend": "ttm_trend",
- "ttmscalper": "ttm_scalper",
- "ttmwave": "ttm_wave",
- "ttmlrc": "ttm_lrc",
-}
-
-
-# ---------------------------------------------------------------------------
-# Data model
-# ---------------------------------------------------------------------------
-@dataclass
-class ExportInfo:
- """Parsed info for a single [UnmanagedCallersOnly] export."""
- entry_name: str # e.g. "qtl_sma"
- func_name: str # e.g. "sma"
- cs_params: list[tuple[str, str]] # [(type, name), ...]
- category: str = "" # resolved lib/ category
- lib_indicator: str = "" # indicator dir name in lib/
-
- @property
- def py_module(self) -> str:
- return CATEGORY_PY_NAME.get(self.category, self.category)
-
-
-# ---------------------------------------------------------------------------
-# Step 1: Build category lookup {indicator_name → category}
-# ---------------------------------------------------------------------------
-def build_category_map() -> dict[str, str]:
- """Scan lib/ subdirectories to build indicator→category mapping."""
- cat_map: dict[str, str] = {}
-
- for cat_dir in sorted(LIB_DIR.iterdir()):
- if not cat_dir.is_dir():
- continue
- cat_name = cat_dir.name
- if cat_name in ("bin", "obj", "feeds") or cat_name.startswith("_") or cat_name.startswith("."):
- continue
-
- for ind_dir in sorted(cat_dir.iterdir()):
- if not ind_dir.is_dir():
- continue
- ind_name = ind_dir.name
- if ind_name.startswith("_") or ind_name.startswith("."):
- continue
- if cat_name == "core" and ind_name in CORE_SKIP:
- continue
-
- # Normalize: indicator directory names are lowercase
- cat_map[ind_name.lower()] = cat_name
-
- return cat_map
-
-
-# ---------------------------------------------------------------------------
-# Step 2: Parse Exports.Generated.cs
-# ---------------------------------------------------------------------------
-RE_ENTRY = re.compile(
- r'\[UnmanagedCallersOnly\(EntryPoint\s*=\s*"(qtl_\w+)"\)\]'
-)
-RE_FUNC = re.compile(
- r'public\s+static\s+int\s+\w+\(([^)]*)\)'
-)
-
-
-def parse_cs_param(raw: str) -> tuple[str, str]:
- """Parse 'double* source' → ('double*', 'source')."""
- raw = raw.strip()
- parts = raw.rsplit(None, 1)
- if len(parts) == 2:
- return (parts[0], parts[1])
- return (raw, "")
-
-
-def parse_exports(cs_path: Path) -> list[ExportInfo]:
- """Parse all [UnmanagedCallersOnly] exports from the C# file."""
- text = cs_path.read_text(encoding="utf-8")
- lines = text.splitlines()
- exports: list[ExportInfo] = []
-
- i = 0
- while i < len(lines):
- m = RE_ENTRY.search(lines[i])
- if m:
- entry_name = m.group(1) # e.g. "qtl_sma"
- func_name = entry_name[4:] # strip "qtl_"
-
- # Find the function signature (may be on next line)
- for j in range(i + 1, min(i + 5, len(lines))):
- fm = RE_FUNC.search(lines[j])
- if fm:
- raw_params = fm.group(1)
- params = [parse_cs_param(p) for p in raw_params.split(",")]
- exports.append(ExportInfo(
- entry_name=entry_name,
- func_name=func_name,
- cs_params=params,
- ))
- break
- i = j + 1
- else:
- i += 1
-
- return exports
-
-
-# ---------------------------------------------------------------------------
-# Step 3: Resolve categories
-# ---------------------------------------------------------------------------
-def resolve_categories(exports: list[ExportInfo], cat_map: dict[str, str]) -> None:
- """Assign each export to its lib/ category."""
- for exp in exports:
- name = exp.func_name
-
- # Check rename mapping first
- mapped = EXPORT_RENAMES.get(name, name)
-
- if mapped in cat_map:
- exp.category = cat_map[mapped]
- exp.lib_indicator = mapped
- else:
- # Try underscore variants
- for variant in [mapped.replace("_", ""), mapped]:
- if variant in cat_map:
- exp.category = cat_map[variant]
- exp.lib_indicator = variant
- break
-
- # Special cases
- if name == "ema_alpha":
- exp.category = "trends_IIR"
- exp.lib_indicator = "ema"
- elif name == "dema_alpha":
- exp.category = "trends_IIR"
- exp.lib_indicator = "dema"
- elif name in ("wclprice", "midpoint", "midprice", "medprice",
- "avgprice", "typprice", "midbody", "ha"):
- exp.category = "core"
- exp.lib_indicator = name
- elif name == "skeleton_noop":
- exp.category = "_internal"
-
- if not exp.category and name != "skeleton_noop":
- print(f" WARNING: No category for export '{name}'", file=sys.stderr)
-
-
-# ---------------------------------------------------------------------------
-# Step 4: Classify parameter patterns for ctypes/Python wrappers
-# ---------------------------------------------------------------------------
-
-def classify_params(exp: ExportInfo) -> dict:
- """Classify the export's parameter pattern for code generation."""
- params = exp.cs_params
- ptypes = [p[0] for p in params]
- pnames = [p[1] for p in params]
-
- info: dict = {
- "inputs": [], # list of (cs_type, name, py_name)
- "outputs": [], # list of (cs_type, name, py_name)
- "int_params": [], # list of (name, py_name, default)
- "double_params": [], # list of (name, py_name, default)
- "n_param": None, # name of the length param
- "pattern": "custom",
- "argtypes": [],
- }
-
- # Identify inputs (double*) that appear before outputs
- # Heuristic: inputs come before 'n', outputs after
- n_idx = None
- for i, (t, n) in enumerate(params):
- if t == "int" and n in ("n", "length") and n_idx is None:
- # Special: some have 'n' later
- pass
- if n == "n" and t == "int":
- n_idx = i
- break
-
- if n_idx is None:
- # n might be at different position, find it
- for i, (t, n) in enumerate(params):
- if t == "int" and n == "n":
- n_idx = i
- break
-
- return info
-
-
-# ---------------------------------------------------------------------------
-# Step 5: Generate ctypes argtypes string
-# ---------------------------------------------------------------------------
-
-def cs_type_to_ctypes(cs_type: str) -> str:
- """Convert C# parameter type to ctypes constant."""
- mapping = {
- "double*": "_dp",
- "int": "_ci",
- "double": "_cd",
- "int*": "_ip",
- "long*": "_lp",
- }
- return mapping.get(cs_type, f"# UNKNOWN: {cs_type}")
-
-
-def gen_argtypes(exp: ExportInfo) -> str:
- """Generate the ctypes argtypes list for a binding."""
- parts = [cs_type_to_ctypes(t) for t, _ in exp.cs_params]
- return "[" + ", ".join(parts) + "]"
-
-
-# ---------------------------------------------------------------------------
-# Step 6: Generate _bridge.py
-# ---------------------------------------------------------------------------
-
-def gen_bridge(exports: list[ExportInfo], by_cat: dict[str, list[ExportInfo]]) -> str:
- """Generate the complete _bridge.py file."""
-
- lines = [
- '"""Low-level ctypes bindings for every quantalib NativeAOT export.',
- '',
- 'Auto-generated by generate_category_modules.py — DO NOT EDIT.',
- '',
- 'Each native function is bound via ``_bind`` at module load. If the shared',
- 'library was compiled without a particular export the binding is silently',
- 'skipped (the corresponding ``HAS_*`` flag stays False).',
- '"""',
- 'from __future__ import annotations',
- '',
- 'import ctypes',
- 'from ctypes import c_double, c_int, POINTER',
- 'from typing import Final',
- '',
- 'from ._loader import load_native_library',
- '',
- '# ---------------------------------------------------------------------------',
- '# Status codes (mirror StatusCodes.cs)',
- '# ---------------------------------------------------------------------------',
- 'QTL_OK: Final[int] = 0',
- 'QTL_ERR_NULL_PTR: Final[int] = 1',
- 'QTL_ERR_INVALID_LENGTH: Final[int] = 2',
- 'QTL_ERR_INVALID_PARAM: Final[int] = 3',
- 'QTL_ERR_INTERNAL: Final[int] = 4',
- '',
- '',
- 'class QtlError(Exception):',
- ' """Base exception for quantalib native errors."""',
- '',
- '',
- 'class QtlNullPointerError(QtlError):',
- ' pass',
- '',
- '',
- 'class QtlInvalidLengthError(QtlError):',
- ' pass',
- '',
- '',
- 'class QtlInvalidParamError(QtlError):',
- ' pass',
- '',
- '',
- 'class QtlInternalError(QtlError):',
- ' pass',
- '',
- '',
- '_STATUS_MAP: dict[int, type[QtlError]] = {',
- ' QTL_ERR_NULL_PTR: QtlNullPointerError,',
- ' QTL_ERR_INVALID_LENGTH: QtlInvalidLengthError,',
- ' QTL_ERR_INVALID_PARAM: QtlInvalidParamError,',
- ' QTL_ERR_INTERNAL: QtlInternalError,',
- '}',
- '',
- '',
- 'def _check(status: int) -> None:',
- ' """Raise if *status* is not QTL_OK."""',
- ' if status == QTL_OK:',
- ' return',
- ' exc_type = _STATUS_MAP.get(status, QtlError)',
- ' raise exc_type(f"quantalib native call failed (status={status})")',
- '',
- '',
- '# ---------------------------------------------------------------------------',
- '# Load native library',
- '# ---------------------------------------------------------------------------',
- '_lib = load_native_library()',
- '',
- '# Shorthand type aliases',
- '_dp = POINTER(c_double) # double*',
- '_ip = POINTER(c_int) # int*',
- '_lp = POINTER(ctypes.c_long) # long*',
- '_ci = c_int',
- '_cd = c_double',
- '',
- '',
- 'def _bind(name: str, argtypes: list[object]) -> bool:',
- ' """Bind a single native function. Returns True if found."""',
- ' fn = getattr(_lib, name, None)',
- ' if fn is None:',
- ' return False',
- ' fn.argtypes = argtypes',
- ' fn.restype = _ci',
- ' return True',
- '',
- '',
- '# ---------------------------------------------------------------------------',
- '# Health check',
- '# ---------------------------------------------------------------------------',
- 'HAS_SKELETON = _bind("qtl_skeleton_noop", [_dp, _ci, _dp])',
- '',
- ]
-
- # Category order
- CAT_ORDER = [
- "core", "momentum", "oscillators", "trends_FIR", "trends_IIR",
- "channels", "volatility", "volume", "statistics", "errors",
- "filters", "cycles", "dynamics", "numerics", "reversals", "forecasts",
- ]
-
- cat_labels = {
- "core": "Core",
- "momentum": "Momentum",
- "oscillators": "Oscillators",
- "trends_FIR": "Trends — FIR",
- "trends_IIR": "Trends — IIR",
- "channels": "Channels",
- "volatility": "Volatility",
- "volume": "Volume",
- "statistics": "Statistics",
- "errors": "Errors",
- "filters": "Filters",
- "cycles": "Cycles",
- "dynamics": "Dynamics",
- "numerics": "Numerics",
- "reversals": "Reversals",
- "forecasts": "Forecasts",
- }
-
- for cat in CAT_ORDER:
- if cat not in by_cat:
- continue
- exps = by_cat[cat]
- label = cat_labels.get(cat, cat)
- lines.append(f'# {"═" * 75}')
- lines.append(f'# {label}')
- lines.append(f'# {"═" * 75}')
-
- for exp in sorted(exps, key=lambda e: e.func_name):
- varname = f"HAS_{exp.func_name.upper()}"
- argtypes = gen_argtypes(exp)
- lines.append(f'{varname} = _bind("{exp.entry_name}", {argtypes})')
-
- lines.append('')
-
- return "\n".join(lines)
-
-
-# ---------------------------------------------------------------------------
-# Step 7: Generate _helpers.py
-# ---------------------------------------------------------------------------
-def gen_helpers() -> str:
- return '''"""Shared wrapper helpers for quantalib indicator modules.
-
-Auto-generated by generate_category_modules.py — DO NOT EDIT.
-"""
-from __future__ import annotations
-
-import numpy as np
-from numpy.typing import NDArray
-
-from ._bridge import _lib, _check, _dp, _ci, _cd
-
-# Optional pandas support
-try:
- import pandas as pd # type: ignore[import-untyped]
-except ImportError: # pragma: no cover
- pd = None # type: ignore[assignment]
-
-# ---------------------------------------------------------------------------
-# Internal helpers
-# ---------------------------------------------------------------------------
-_F64 = np.float64
-
-
-def _arr(x: object) -> tuple[NDArray[np.float64], object]:
- """Return (contiguous float64 array, original_index_or_None)."""
- idx = None
- if pd is not None and isinstance(x, pd.Series):
- idx = x.index
- x = x.to_numpy(dtype=_F64, copy=False)
- elif pd is not None and isinstance(x, pd.DataFrame):
- idx = x.index
- x = x.iloc[:, 0].to_numpy(dtype=_F64, copy=False)
- return np.ascontiguousarray(x, dtype=_F64), idx # type: ignore[arg-type]
-
-
-def _ptr(a: NDArray[np.float64]): # noqa: ANN202
- """Get ctypes double* from array."""
- return a.ctypes.data_as(_dp)
-
-
-def _out(n: int) -> NDArray[np.float64]:
- """Allocate output array."""
- return np.empty(n, dtype=_F64)
-
-
-def _offset(arr: NDArray[np.float64], off: int) -> NDArray[np.float64]:
- """Apply offset (roll + NaN fill)."""
- if off and off != 0:
- arr = np.roll(arr, off)
- if off > 0:
- arr[:off] = np.nan
- else:
- arr[off:] = np.nan
- return arr
-
-
-def _wrap(
- arr: NDArray[np.float64],
- idx: object,
- name: str,
- category: str,
- offset: int = 0,
-):
- """Wrap result: apply offset, optionally convert to pd.Series."""
- arr = _offset(arr, offset)
- if idx is not None and pd is not None:
- s = pd.Series(arr, index=idx, name=name)
- s.category = category
- return s
- return arr
-
-
-def _wrap_multi(
- arrays: dict[str, NDArray[np.float64]],
- idx: object,
- category: str,
- offset: int = 0,
-):
- """Wrap multi-output result into tuple or DataFrame."""
- for k in arrays:
- arrays[k] = _offset(arrays[k], offset)
- if idx is not None and pd is not None:
- df = pd.DataFrame(arrays, index=idx)
- df.category = category
- return df
- return tuple(arrays.values())
-
-
-# ═══════════════════════════════════════════════════════════════════════════
-# Generic pattern helpers
-# ═══════════════════════════════════════════════════════════════════════════
-
-def _pa(
- fn_name: str, close: object, length: int, offset: int,
- default_length: int, label: str, category: str,
-) -> object:
- """Generic Pattern A wrapper: single-input + period."""
- length = int(length) if length is not None else default_length
- offset = int(offset) if offset is not None else 0
- src, idx = _arr(close)
- n = len(src)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(_ptr(src), n, _ptr(dst), length))
- return _wrap(dst, idx, f"{label}_{length}", category, offset)
-
-
-def _pa3(
- fn_name: str, close: object, offset: int,
- label: str, category: str,
-) -> object:
- """Generic Pattern A3 wrapper: single-input, no params."""
- offset = int(offset) if offset is not None else 0
- src, idx = _arr(close)
- n = len(src)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(_ptr(src), n, _ptr(dst)))
- return _wrap(dst, idx, label, category, offset)
-
-
-def _pf(
- fn_name: str, actual: object, predicted: object,
- length: int, offset: int, default_length: int,
- label: str, category: str,
-) -> object:
- """Generic Pattern F wrapper: actual+predicted+period."""
- length = int(length) if length is not None else default_length
- offset = int(offset) if offset is not None else 0
- a, idx = _arr(actual)
- p, _ = _arr(predicted)
- n = len(a)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(_ptr(a), _ptr(p), n, _ptr(dst), length))
- return _wrap(dst, idx, f"{label}_{length}", category, offset)
-
-
-def _pg(
- fn_name: str, close: object, volume: object,
- offset: int, label: str, category: str,
-) -> object:
- """Pattern G: source+volume, no period."""
- offset = int(offset) if offset is not None else 0
- c, idx = _arr(close)
- v, _ = _arr(volume)
- n = len(c)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(_ptr(c), _ptr(v), n, _ptr(dst)))
- return _wrap(dst, idx, label, category, offset)
-
-
-def _pg2(
- fn_name: str, close: object, volume: object, length: int,
- offset: int, default_length: int, label: str, category: str,
-) -> object:
- """Pattern G2: source+volume+period."""
- length = int(length) if length is not None else default_length
- offset = int(offset) if offset is not None else 0
- c, idx = _arr(close)
- v, _ = _arr(volume)
- n = len(c)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(_ptr(c), _ptr(v), n, _ptr(dst), length))
- return _wrap(dst, idx, f"{label}_{length}", category, offset)
-
-
-def _ph(
- fn_name: str, x: object, y: object, length: int,
- offset: int, default_length: int, label: str, category: str,
-) -> object:
- """Pattern H: X+Y+period."""
- length = int(length) if length is not None else default_length
- offset = int(offset) if offset is not None else 0
- xarr, idx = _arr(x)
- yarr, _ = _arr(y)
- n = len(xarr)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(_ptr(xarr), _ptr(yarr), n, _ptr(dst), length))
- return _wrap(dst, idx, f"{label}_{length}", category, offset)
-
-
-def _ohlcv_bars_period(
- fn_name: str, open: object, high: object, low: object,
- close: object, volume: object, period: int,
- offset: int, default_period: int, label: str, category: str,
-) -> object:
- """OHLCV bars + period → single output (BuildBars pattern)."""
- period = int(period) if period is not None else default_period
- offset = int(offset) if offset is not None else 0
- o, idx = _arr(open)
- h, _ = _arr(high)
- l, _ = _arr(low)
- c, _ = _arr(close)
- v, _ = _arr(volume)
- n = len(o)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(
- _ptr(o), _ptr(h), _ptr(l), _ptr(c), _ptr(v), period, n, _ptr(dst)))
- return _wrap(dst, idx, f"{label}_{period}", category, offset)
-
-
-def _hlc_period(
- fn_name: str, high: object, low: object, close: object,
- period: int, offset: int, default_period: int,
- label: str, category: str,
-) -> object:
- """HLC + period → single output."""
- period = int(period) if period is not None else default_period
- offset = int(offset) if offset is not None else 0
- h, idx = _arr(high)
- l, _ = _arr(low)
- c, _ = _arr(close)
- n = len(h)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(
- _ptr(h), _ptr(l), _ptr(c), period, n, _ptr(dst)))
- return _wrap(dst, idx, f"{label}_{period}", category, offset)
-
-
-def _src_period(
- fn_name: str, source: object, period: int,
- offset: int, default_period: int, label: str, category: str,
-) -> object:
- """source + period → single output (BuildSeries pattern, src,period,n,dst)."""
- period = int(period) if period is not None else default_period
- offset = int(offset) if offset is not None else 0
- src, idx = _arr(source)
- n = len(src)
- dst = _out(n)
- _check(getattr(_lib, fn_name)(_ptr(src), period, n, _ptr(dst)))
- return _wrap(dst, idx, f"{label}_{period}", category, offset)
-'''
-
-
-# ---------------------------------------------------------------------------
-# Step 8: Build per-category wrapper functions
-# ---------------------------------------------------------------------------
-# We derive wrapper signatures from the C# export signatures.
-
-# Manual mappings for export names that need specific Python wrapper treatment
-# This defines the "known" wrappers. Anything not here gets auto-generated.
-
-# Map of lib/ directory name → default period for Pattern A indicators
-DEFAULT_PERIODS: dict[str, int] = {
- # Trends FIR
- "sma": 10, "wma": 10, "hma": 9, "trima": 10, "swma": 10, "dwma": 10,
- "blma": 10, "lsma": 25, "sgma": 10, "sinema": 10, "hanma": 10,
- "parzen": 10, "tsf": 14, "sp15": 15, "tukey_w": 10, "rain": 10,
- "fwma": 10, "gwma": 10, "hamma": 10, "hend": 10, "ilrs": 10,
- "kaiser": 10, "lanczos": 10, "nlma": 10, "nyqma": 10, "pma": 10,
- "pwma": 10, "qrma": 10, "rwma": 10, "bwma": 10,
- # Trends IIR
- "ema": 10, "dema": 10, "tema": 10, "lema": 10, "hema": 10,
- "ahrens": 10, "decycler": 20, "frama": 10, "hwma": 10,
- "jma": 10, "kama": 10, "ltma": 10, "mama": 10, "mavp": 10,
- "mcnma": 10, "mgdi": 10, "mma": 10, "nma": 10, "qema": 10,
- "rema": 10, "rgma": 10, "rma": 10, "t3": 10, "trama": 10,
- "vidya": 10, "zldema": 10, "zlema": 10, "zltema": 10,
- "adxvma": 14, "vama": 14, "yzvama": 14,
- # Momentum
- "rsi": 14, "roc": 10, "mom": 10, "cmo": 14, "bias": 26,
- "cfo": 14, "rsx": 14, "pmo": 35,
- "rocp": 10, "rocr": 10, "vel": 10,
- # Oscillators
- "fisher": 9, "fisher04": 9, "dpo": 20, "trix": 18, "inertia": 20,
- "er": 10, "cti": 12, "reflex": 20, "trendflex": 20, "kri": 20,
- "psl": 12, "lrsi": 14,
- # Volatility
- "bbw": 20, "stddev": 20, "variance": 20, "natr": 14, "massi": 14,
- "ui": 14, "jvolty": 14, "jvoltyn": 14, "rsv": 14, "rv": 14,
- "rvi": 14, "vov": 14, "vr": 14,
- # Cycles
- "cg": 10, "dsp": 20, "ccor": 20,
- # Statistics
- "zscore": 20, "entropy": 10, "geomean": 10, "harmean": 10,
- "hurst": 100, "iqr": 20, "kurtosis": 20, "linreg": 14,
- "meandev": 20, "median": 20, "mode": 20, "percentile": 20,
- "polyfit": 20, "quantile": 20, "skew": 20, "spearman": 20,
- "stddev": 20, "stderr": 20, "sum": 20, "theil": 20,
- "trim": 20, "wavg": 20, "wins": 20, "ztest": 20,
- "kendall": 20, "pacf": 20,
- # Filters
- "bessel": 14, "butter2": 14, "butter3": 14, "cheby1": 14,
- "cheby2": 14, "elliptic": 14, "edcf": 14, "bpf": 14,
- "loess": 14, "nw": 14, "rmed": 14, "sgf": 14, "spbf": 14,
- "ssf2": 14, "ssf3": 14, "usf": 14, "voss": 14,
- "wavelet": 14, "wiener": 14,
- # Numerics
- "change": 1, "highest": 14, "lowest": 14, "slope": 14,
- "accel": 0, "jerk": 0,
- # Errors (all pattern F, default 20)
- "mse": 20, "rmse": 20, "mae": 20, "mape": 20, "smape": 20,
- "msle": 20, "rmsle": 20, "me": 20, "mpe": 20, "mrae": 20,
- "rse": 20, "rae": 20, "rsquared": 20, "wmape": 20, "wrmse": 20,
- "mdae": 20, "mdape": 20, "mase": 20, "maape": 20, "mapd": 20,
- "huber": 20, "logcosh": 20, "pseudohuber": 20, "tukeybiweight": 20,
- "quantileloss": 20, "theilu": 20,
-}
-
-
-def main() -> None:
- print("=== Generating quantalib per-category Python modules ===")
-
- # Step 1: Build category map
- cat_map = build_category_map()
- print(f" Found {len(cat_map)} indicators across {len(set(cat_map.values()))} categories")
-
- # Step 2: Parse exports
- exports = parse_exports(EXPORTS_CS)
- print(f" Parsed {len(exports)} exports from Exports.Generated.cs")
-
- # Step 3: Resolve categories
- resolve_categories(exports, cat_map)
-
- # Group by category
- by_cat: dict[str, list[ExportInfo]] = {}
- uncategorized: list[ExportInfo] = []
- for exp in exports:
- if exp.category and exp.category != "_internal":
- by_cat.setdefault(exp.category, []).append(exp)
- elif exp.category != "_internal":
- uncategorized.append(exp)
-
- for cat in sorted(by_cat):
- inds = sorted(e.func_name for e in by_cat[cat])
- print(f" {cat}: {len(inds)} indicators")
-
- if uncategorized:
- print(f" UNCATEGORIZED: {[e.func_name for e in uncategorized]}")
-
- # Step 4: Generate _helpers.py
- helpers_path = OUT_DIR / "_helpers.py"
- helpers_path.write_text(gen_helpers(), encoding="utf-8")
- print(f" Wrote {helpers_path}")
-
- # Step 5: Generate _bridge.py
- bridge_path = OUT_DIR / "_bridge.py"
- bridge_path.write_text(gen_bridge(exports, by_cat), encoding="utf-8")
- print(f" Wrote {bridge_path}")
-
- # Step 6-8: will print summary
- print("\n=== Summary ===")
- print(f" Total exports: {len(exports)}")
- print(f" Categorized: {sum(len(v) for v in by_cat.values())}")
- print(f" Categories: {len(by_cat)}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/python/tools/generate_missing_exports.py b/python/tools/generate_missing_exports.py
deleted file mode 100644
index e805a1cc..00000000
--- a/python/tools/generate_missing_exports.py
+++ /dev/null
@@ -1,419 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-
-import re
-from dataclasses import dataclass
-from pathlib import Path
-
-RE_EXPORT_CALL = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\.Batch\(")
-RE_CLASS = re.compile(r"\bpublic\s+(?:sealed\s+|abstract\s+|partial\s+)*class\s+([A-Za-z_][A-Za-z0-9_]*)")
-RE_BATCH_HEAD = re.compile(r"\bpublic\s+static\s+([A-Za-z0-9_<>,\.\?\[\]\(\)\s]+?)\s+Batch\s*\(")
-
-
-@dataclass
-class Param:
- type_name: str
- name: str
- has_default: bool
-
-
-@dataclass
-class Overload:
- return_type: str
- params: list[Param]
-
-
-def split_top_level(s: str) -> list[str]:
- out: list[str] = []
- cur: list[str] = []
- depth_angle = 0
- depth_paren = 0
- for ch in s:
- if ch == "<":
- depth_angle += 1
- elif ch == ">":
- depth_angle = max(0, depth_angle - 1)
- elif ch == "(":
- depth_paren += 1
- elif ch == ")":
- depth_paren = max(0, depth_paren - 1)
-
- if ch == "," and depth_angle == 0 and depth_paren == 0:
- part = "".join(cur).strip()
- if part:
- out.append(part)
- cur = []
- else:
- cur.append(ch)
- part = "".join(cur).strip()
- if part:
- out.append(part)
- return out
-
-
-def parse_params(params_text: str) -> list[Param]:
- params: list[Param] = []
- if not params_text.strip():
- return params
- for raw in split_top_level(params_text):
- has_default = "=" in raw
- left = raw.split("=", 1)[0].strip()
- tokens = left.split()
- if len(tokens) < 2:
- continue
- name = tokens[-1].strip()
- type_name = " ".join(tokens[:-1]).replace("in ", "").replace("ref ", "").strip()
- params.append(Param(type_name=type_name, name=name, has_default=has_default))
- return params
-
-
-def extract_batch_overloads(text: str) -> list[Overload]:
- overloads: list[Overload] = []
- for m in RE_BATCH_HEAD.finditer(text):
- ret = " ".join(m.group(1).split())
- i = m.end()
- depth = 1
- j = i
- while j < len(text) and depth > 0:
- if text[j] == "(":
- depth += 1
- elif text[j] == ")":
- depth -= 1
- j += 1
- if depth != 0:
- continue
- params_text = text[i : j - 1]
- overloads.append(Overload(return_type=ret, params=parse_params(params_text)))
- return overloads
-
-
-def load_exported_indicators(repo_root: Path) -> set[str]:
- # Baseline only: compare against hand-authored Exports.cs.
- # Generated file is overwritten each run and must not affect diff input.
- exported: set[str] = set()
- p = repo_root / "python" / "src" / "Exports.cs"
- if p.exists():
- txt = p.read_text(encoding="utf-8", errors="ignore")
- exported.update(RE_EXPORT_CALL.findall(txt))
- return exported
-
-
-def load_lib_indicators(repo_root: Path) -> dict[str, Path]:
- lib = repo_root / "lib"
- indicators: dict[str, Path] = {}
- for p in lib.rglob("*.cs"):
- parts = {x.lower() for x in p.parts}
- if "bin" in parts or "obj" in parts:
- continue
- if p.name.endswith(".Tests.cs"):
- continue
- txt = p.read_text(encoding="utf-8", errors="ignore")
- if "public static" not in txt or "Batch(" not in txt:
- continue
- m = RE_CLASS.search(txt)
- if not m:
- continue
- indicators[m.group(1)] = p
- return indicators
-
-
-def is_supported_scalar(type_name: str) -> bool:
- if type_name in {"int", "double", "bool"}:
- return True
- if "BatchOutputs" in type_name:
- return False
- if type_name in {"TSeries", "TBarSeries", "Span", "ReadOnlySpan", "Span", "ReadOnlySpan"}:
- return False
- # enum-like names
- return bool(re.fullmatch(r"[A-Za-z_][A-Za-z0-9_\.]*\??", type_name))
-
-
-def is_tuple_return_of_tseries(ret: str) -> bool:
- if not (ret.startswith("(") and ret.endswith(")")):
- return False
- parts = split_top_level(ret[1:-1])
- if not parts:
- return False
- for p in parts:
- t = p.strip().split()[0]
- if t != "TSeries":
- return False
- return True
-
-
-def tuple_fields(ret: str) -> list[str]:
- parts = split_top_level(ret[1:-1])
- fields: list[str] = []
- for p in parts:
- toks = p.strip().split()
- if len(toks) >= 2:
- fields.append(toks[1])
- return fields
-
-
-def choose_overload(overloads: list[Overload]) -> Overload | None:
- def supported(o: Overload) -> bool:
- for p in o.params:
- t = p.type_name
- if t in {"ReadOnlySpan", "Span", "TSeries", "TBarSeries"}:
- continue
- if not is_supported_scalar(t):
- return False
- return True
-
- span_candidates: list[Overload] = []
- series_candidates: list[Overload] = []
- tuple_candidates: list[Overload] = []
-
- for o in overloads:
- if not supported(o):
- continue
- has_in_span = any(p.type_name == "ReadOnlySpan" for p in o.params)
- has_out_span = any(p.type_name == "Span" for p in o.params)
- has_series_obj = any(p.type_name in {"TSeries", "TBarSeries"} for p in o.params)
-
- if o.return_type == "void" and has_in_span and has_out_span:
- span_candidates.append(o)
- elif o.return_type == "TSeries" and has_series_obj:
- series_candidates.append(o)
- elif is_tuple_return_of_tseries(o.return_type) and has_series_obj:
- tuple_candidates.append(o)
-
- # priority: span overloads (supports multi-output), then single TSeries, then tuple TSeries
- if span_candidates:
- span_candidates.sort(key=lambda o: (sum(1 for p in o.params if p.type_name == "Span"), -len(o.params)), reverse=True)
- return span_candidates[0]
- if series_candidates:
- series_candidates.sort(key=lambda o: -len(o.params))
- return series_candidates[0]
- if tuple_candidates:
- tuple_candidates.sort(key=lambda o: -len(o.params))
- return tuple_candidates[0]
- return None
-
-
-def map_scalar_to_abi(type_name: str) -> str:
- if type_name == "double":
- return "double"
- # int, bool, enums, nullable enums -> int ABI
- return "int"
-
-
-def map_scalar_call(type_name: str, arg_name: str) -> str:
- if type_name == "double":
- return arg_name
- if type_name == "int":
- return arg_name
- if type_name == "bool":
- return f"{arg_name} != 0"
- # enum or nullable enum
- return f"({type_name.rstrip('?')}){arg_name}"
-
-
-def build_wrapper(class_name: str, ov: Overload) -> str:
- entry = f"qtl_{class_name.lower()}"
- method = f"Qtl{class_name}"
-
- sig_parts: list[str] = []
- null_checks: list[str] = []
- setup_lines: list[str] = []
- call_args: list[str] = []
-
- has_n = False
-
- # span-based overload
- if ov.return_type == "void" and any(p.type_name == "ReadOnlySpan" for p in ov.params):
- for p in ov.params:
- t = p.type_name
- if t == "ReadOnlySpan":
- sig_parts.append(f"double* {p.name}")
- null_checks.append(f"{p.name} == null")
- call_args.append(f"Src({p.name}, n)")
- has_n = True
- elif t == "Span":
- sig_parts.append(f"double* {p.name}")
- null_checks.append(f"{p.name} == null")
- call_args.append(f"Dst({p.name}, n)")
- has_n = True
- else:
- abi_t = map_scalar_to_abi(t)
- sig_parts.append(f"{abi_t} {p.name}")
- call_args.append(map_scalar_call(t, p.name))
- if has_n:
- sig_parts.insert(sum(1 for s in sig_parts if s.startswith('double* ')), "int n")
-
- null_expr = " || ".join(null_checks) if null_checks else "false"
- lines = [
- f' [UnmanagedCallersOnly(EntryPoint = "{entry}")]',
- f" public static int {method}({', '.join(sig_parts)})",
- " {",
- f" if ({null_expr}) return StatusCodes.QTL_ERR_NULL_PTR;",
- " if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;",
- " try",
- " {",
- f" {class_name}.Batch({', '.join(call_args)});",
- " return StatusCodes.QTL_OK;",
- " }",
- " catch { return StatusCodes.QTL_ERR_INTERNAL; }",
- " }",
- ]
- return "\n".join(lines)
-
- # object-based overloads
- for p in ov.params:
- t = p.type_name
- if t == "TSeries":
- sig_parts.append(f"double* {p.name}")
- null_checks.append(f"{p.name} == null")
- has_n = True
- setup_lines.append(f" var {p.name}Series = BuildSeries({p.name}, n);")
- call_args.append(f"{p.name}Series")
- elif t == "TBarSeries":
- for fld in ("Open", "High", "Low", "Close", "Volume"):
- nm = f"{p.name}{fld}"
- sig_parts.append(f"double* {nm}")
- null_checks.append(f"{nm} == null")
- has_n = True
- setup_lines.append(
- f" var {p.name}Bars = BuildBars({p.name}Open, {p.name}High, {p.name}Low, {p.name}Close, {p.name}Volume, n);"
- )
- call_args.append(f"{p.name}Bars")
- else:
- abi_t = map_scalar_to_abi(t)
- sig_parts.append(f"{abi_t} {p.name}")
- call_args.append(map_scalar_call(t, p.name))
-
- if has_n:
- sig_parts.append("int n")
-
- if ov.return_type == "TSeries":
- sig_parts.append("double* dst")
- null_checks.append("dst == null")
- elif is_tuple_return_of_tseries(ov.return_type):
- for f in tuple_fields(ov.return_type):
- dn = f"dst{f}"
- sig_parts.append(f"double* {dn}")
- null_checks.append(f"{dn} == null")
-
- null_expr = " || ".join(null_checks) if null_checks else "false"
- lines = [
- f' [UnmanagedCallersOnly(EntryPoint = "{entry}")]',
- f" public static int {method}({', '.join(sig_parts)})",
- " {",
- f" if ({null_expr}) return StatusCodes.QTL_ERR_NULL_PTR;",
- ]
- if has_n:
- lines.append(" if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;")
- lines.extend([" try", " {"])
- lines.extend(setup_lines)
-
- call = f"{class_name}.Batch({', '.join(call_args)})"
- if ov.return_type == "TSeries":
- lines.extend(
- [
- f" var result = {call};",
- " var values = result.Values;",
- " if (values.Length > n) return StatusCodes.QTL_ERR_INVALID_LENGTH;",
- " var outSpan = Dst(dst, n);",
- " outSpan.Fill(double.NaN);",
- " values.CopyTo(outSpan);",
- " return StatusCodes.QTL_OK;",
- ]
- )
- elif is_tuple_return_of_tseries(ov.return_type):
- fields = tuple_fields(ov.return_type)
- lines.append(f" var result = {call};")
- for f in fields:
- dn = f"dst{f}"
- lines.extend(
- [
- f" var values{f} = result.{f}.Values;",
- f" if (values{f}.Length > n) return StatusCodes.QTL_ERR_INVALID_LENGTH;",
- f" var outSpan{f} = Dst({dn}, n);",
- f" outSpan{f}.Fill(double.NaN);",
- f" values{f}.CopyTo(outSpan{f});",
- ]
- )
- lines.append(" return StatusCodes.QTL_OK;")
- else:
- lines.append(f" {call};")
- lines.append(" return StatusCodes.QTL_OK;")
-
- lines.extend([" }", " catch { return StatusCodes.QTL_ERR_INTERNAL; }", " }"])
- return "\n".join(lines)
-
-
-def generate(repo_root: Path) -> str:
- exported = load_exported_indicators(repo_root)
- lib_indicators = load_lib_indicators(repo_root)
- missing = sorted(set(lib_indicators.keys()) - exported)
-
- wrappers: list[str] = []
- skipped: list[str] = []
-
- for cls in missing:
- p = lib_indicators[cls]
- text = p.read_text(encoding="utf-8", errors="ignore")
- ovs = extract_batch_overloads(text)
- ov = choose_overload(ovs)
- if ov is None:
- skipped.append(cls)
- continue
- wrappers.append(build_wrapper(cls, ov))
-
- header = """//
-#nullable enable
-using System;
-using System.Runtime.InteropServices;
-using QuanTAlib;
-
-namespace QuanTAlib.Python;
-
-public static unsafe partial class Exports
-{
- private static TSeries BuildSeries(double* src, int n)
- {
- var t = new long[n];
- var v = new double[n];
- new ReadOnlySpan(src, n).CopyTo(v);
- for (int i = 0; i < n; i++) t[i] = i;
- return new TSeries(t, v);
- }
-
- private static TBarSeries BuildBars(double* open, double* high, double* low, double* close, double* volume, int n)
- {
- var t = new long[n];
- var o = new double[n];
- var h = new double[n];
- var l = new double[n];
- var c = new double[n];
- var v = new double[n];
- new ReadOnlySpan(open, n).CopyTo(o);
- new ReadOnlySpan(high, n).CopyTo(h);
- new ReadOnlySpan(low, n).CopyTo(l);
- new ReadOnlySpan(close, n).CopyTo(c);
- new ReadOnlySpan(volume, n).CopyTo(v);
- for (int i = 0; i < n; i++) t[i] = i;
- var bars = new TBarSeries(n);
- bars.AddRange(t, o, h, l, c, v);
- return bars;
- }
-
-"""
- skipped_block = ""
- if skipped:
- skipped_block = "\n// Skipped (no supported overload found):\n" + "\n".join(f"// - {s}" for s in skipped) + "\n"
-
- return header + "\n\n".join(wrappers) + skipped_block + "\n}\n"
-
-
-def main() -> int:
- repo_root = Path(__file__).resolve().parents[2]
- out_file = repo_root / "python" / "src" / "Exports.Generated.cs"
- out_file.write_text(generate(repo_root), encoding="utf-8")
- print(f"Wrote {out_file}")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
\ No newline at end of file
diff --git a/python/tools/generate_wrappers.py b/python/tools/generate_wrappers.py
deleted file mode 100644
index ad0145ce..00000000
--- a/python/tools/generate_wrappers.py
+++ /dev/null
@@ -1,927 +0,0 @@
-#!/usr/bin/env python3
-"""Generate per-category Python wrapper modules from C# export signatures.
-
-Reads Exports.Generated.cs, maps exports to lib/ categories,
-and generates one .py file per category under python/quantalib/.
-
-Run from repo root:
- python python/tools/generate_wrappers.py
-"""
-from __future__ import annotations
-import os
-import re
-import textwrap
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parent.parent.parent
-CS_FILE = ROOT / "python" / "src" / "Exports.Generated.cs"
-LIB_DIR = ROOT / "lib"
-OUT_DIR = ROOT / "python" / "quantalib"
-
-# ── Category mapping ──────────────────────────────────────────────────────
-# Scan lib/ subdirs to build export→category map
-def build_category_map() -> dict[str, str]:
- """Map indicator name (lowercase) → category folder name."""
- m: dict[str, str] = {}
- for cat_dir in sorted(LIB_DIR.iterdir()):
- if not cat_dir.is_dir() or cat_dir.name.startswith("."):
- continue
- cat = cat_dir.name
- for ind_dir in sorted(cat_dir.iterdir()):
- if ind_dir.is_dir() and not ind_dir.name.startswith("_"):
- m[ind_dir.name.lower()] = cat
- return m
-
-CAT_MAP = build_category_map()
-
-# Manual overrides for export names that differ from lib/ dir names
-EXPORT_TO_LIB = {
- "abber": "aberr",
- "htdcperiod": "ht_dcperiod",
- "htdcphase": "ht_dcphase",
- "htphasor": "ht_phasor",
- "htsine": "ht_sine",
- "httrendmode": "ht_trendmode",
- "htit": "htit",
- "ttmlrc": "ttm_lrc",
- "ttmscalper": "ttm_scalper",
- "ttmsqueeze": "ttm_squeeze",
- "ttmtrend": "ttm_trend",
- "ttmwave": "ttm_wave",
-}
-
-def get_category(export_name: str) -> str:
- """Return category for an export name."""
- lib_name = EXPORT_TO_LIB.get(export_name, export_name)
- if lib_name in CAT_MAP:
- return CAT_MAP[lib_name]
- # Some exports have _ removed vs lib dir (e.g. td_seq → tdseq)
- for k, v in CAT_MAP.items():
- if k.replace("_", "") == export_name.replace("_", ""):
- return v
- return "uncategorized"
-
-
-# ── Parse C# exports ─────────────────────────────────────────────────────
-def parse_exports() -> list[dict]:
- """Parse all exports from Exports.Generated.cs."""
- cs = CS_FILE.read_text(encoding="utf-8")
- pattern = r'\[UnmanagedCallersOnly\(EntryPoint\s*=\s*"qtl_(\w+)"\)\]\s+public static int \w+\(([^)]+)\)'
- exports = []
- for name, params_str in re.findall(pattern, cs):
- params = []
- for p in params_str.split(","):
- p = p.strip()
- tokens = p.split()
- if len(tokens) >= 2:
- ptype = tokens[0]
- pname = tokens[1]
- params.append({"type": ptype, "name": pname})
- exports.append({
- "name": name,
- "params": params,
- "category": get_category(name),
- })
- return exports
-
-
-# ── Classify param roles ─────────────────────────────────────────────────
-def classify_params(params):
- """Identify inputs, outputs, scalars in a param list."""
- inputs = []
- outputs = []
- n_idx = None
- scalars = []
-
- for i, p in enumerate(params):
- name = p["name"]
- ptype = p["type"]
-
- if name == "n":
- n_idx = i
- continue
-
- if ptype == "double*":
- # Heuristic: if name contains output/dst/destination/Out/middle/upper/lower etc.
- out_names = {"output", "dst", "destination", "middle", "upper", "lower",
- "haOpenOut", "haHighOut", "haLowOut", "haCloseOut",
- "dstMiddle", "dstUpper", "dstLower", "dstTenkan", "dstKijun",
- "dstSenkouA", "dstSenkouB", "dstChikou",
- "kOut", "dOut", "jOut", "kstOut", "sigOut",
- "rvgiOutput", "signalOutput", "signalOutput",
- "momOut", "sqOut", "trend", "strength",
- "sine", "leadSine", "inPhase", "quadrature", "ppOutput",
- "upOutput", "downOutput", "highOutput", "lowOutput",
- "pmaOutput", "triggerOutput", "famaOutput",
- "upper1", "lower1", "upper2", "lower2", "vwap", "stdDev",
- "viPlus", "viMinus", "midline",
- "signal"}
- if name in out_names or name.endswith("Out") or name.endswith("Output"):
- outputs.append(p)
- else:
- inputs.append(p)
- elif ptype == "int" or ptype == "double":
- scalars.append(p)
-
- return inputs, outputs, n_idx, scalars
-
-
-# ── Generate wrapper function ────────────────────────────────────────────
-
-# Description map for well-known indicators
-DESCRIPTIONS = {
- # Core
- "avgprice": "Average Price = (O+H+L+C)/4",
- "ha": "Heikin-Ashi Candles",
- "medprice": "Median Price = (H+L)/2",
- "midbody": "Mid Body = (O+C)/2",
- "midpoint": "Midpoint = src[i] over period",
- "midprice": "Mid Price = (High+Low)/2 over period",
- "typprice": "Typical Price = (H+L+C)/3",
- "wclprice": "Weighted Close Price = (H+L+2*C)/4",
- # Momentum
- "asi": "Accumulative Swing Index",
- "bias": "Bias Indicator",
- "bop": "Balance of Power",
- "cci": "Commodity Channel Index",
- "cfb": "Composite Fractal Behavior",
- "cmo": "Chande Momentum Oscillator",
- "macd": "Moving Average Convergence Divergence",
- "mom": "Momentum",
- "pmo": "Price Momentum Oscillator",
- "ppo": "Percentage Price Oscillator",
- "prs": "Price Relative Strength",
- "roc": "Rate of Change",
- "rocp": "Rate of Change (Percentage)",
- "rocr": "Rate of Change (Ratio)",
- "rsi": "Relative Strength Index",
- "rsx": "Relative Strength Xtra",
- "sam": "Simple Alpha Momentum",
- "tsi": "True Strength Index",
- "vel": "Velocity",
- # Oscillators
- "ac": "Accelerator Oscillator",
- "ao": "Awesome Oscillator",
- "apo": "Absolute Price Oscillator",
- "bbb": "Bollinger Band Bounce",
- "bbi": "Bull Bear Index",
- "bbs": "Bollinger Band Squeeze",
- "brar": "Bull-Bear Ratio",
- "cfo": "Chande Forecast Oscillator",
- "coppock": "Coppock Curve",
- "crsi": "Connors RSI",
- "cti": "Correlation Trend Indicator",
- "deco": "DECO Oscillator",
- "dem": "DeMarker",
- "dosc": "Derivative Oscillator",
- "dpo": "Detrended Price Oscillator",
- "dymoi": "Dynamic Momentum Index",
- "er": "Efficiency Ratio",
- "eri": "Elder Ray Index",
- "fi": "Force Index",
- "fisher": "Fisher Transform",
- "fisher04": "Fisher Transform (0.4 variant)",
- "gator": "Gator Oscillator",
- "imi": "Intraday Momentum Index",
- "inertia": "Inertia",
- "kdj": "KDJ Indicator",
- "kri": "Kairi Relative Index",
- "kst": "Know Sure Thing",
- "lrsi": "Laguerre RSI",
- "marketfi": "Market Facilitation Index",
- "mstoch": "Modified Stochastic",
- "pgo": "Pretty Good Oscillator",
- "psl": "Psychological Line",
- "qqe": "Quantitative Qualitative Estimation",
- "reflex": "Reflex",
- "reverseema": "Reverse EMA",
- "rvgi": "Relative Vigor Index",
- "smi": "Stochastic Momentum Index",
- "squeeze": "Squeeze Momentum",
- "stc": "Schaff Trend Cycle",
- "stoch": "Stochastic Oscillator",
- "stochf": "Fast Stochastic",
- "stochrsi": "Stochastic RSI",
- "td_seq": "Tom DeMark Sequential",
- "trendflex": "Trendflex",
- "trix": "Triple EMA Rate of Change",
- "ttmwave": "TTM Wave",
- "ultosc": "Ultimate Oscillator",
- "willr": "Williams %R",
- # Trends FIR
- "alma": "Arnaud Legoux Moving Average",
- "blma": "Blackman Moving Average",
- "bwma": "Butterworth-weighted Moving Average",
- "conv": "Convolution Filter",
- "crma": "Cosine-Ramp Moving Average",
- "dwma": "Double Weighted Moving Average",
- "fwma": "Fibonacci Weighted Moving Average",
- "gwma": "Gaussian Weighted Moving Average",
- "hamma": "Hamming Moving Average",
- "hanma": "Hann Moving Average",
- "hend": "Henderson Moving Average",
- "hma": "Hull Moving Average",
- "ilrs": "Integral of Linear Regression Slope",
- "kaiser": "Kaiser Window Moving Average",
- "lanczos": "Lanczos Moving Average",
- "lsma": "Least Squares Moving Average",
- "nlma": "Non-Lag Moving Average",
- "nyqma": "Nyquist Moving Average",
- "parzen": "Parzen Moving Average",
- "pma": "Predictive Moving Average",
- "pwma": "Pascal Weighted Moving Average",
- "qrma": "Quick Reaction Moving Average",
- "rain": "RAIN Moving Average",
- "rwma": "Range Weighted Moving Average",
- "sgma": "Savitzky-Golay Moving Average",
- "sinema": "Sine Weighted Moving Average",
- "sma": "Simple Moving Average",
- "sp15": "SP-15 Moving Average",
- "swma": "Symmetric Weighted Moving Average",
- "trima": "Triangular Moving Average",
- "tsf": "Time Series Forecast",
- "tukey_w": "Tukey-windowed Moving Average",
- "wma": "Weighted Moving Average",
- # Trends IIR
- "adxvma": "ADX Variable Moving Average",
- "ahrens": "Ahrens Moving Average",
- "coral": "CORAL Trend",
- "decycler": "Simple Decycler",
- "dema": "Double Exponential Moving Average",
- "dsma": "Deviation-Scaled Moving Average",
- "ema": "Exponential Moving Average",
- "frama": "Fractal Adaptive Moving Average",
- "gdema": "Generalized Double EMA",
- "hema": "Henderson EMA",
- "holt": "Holt Exponential Smoothing",
- "htit": "Hilbert Transform Instantaneous Trendline",
- "hwma": "Holt-Winter Moving Average",
- "jma": "Jurik Moving Average",
- "kama": "Kaufman Adaptive Moving Average",
- "lema": "Laguerre EMA",
- "ltma": "Low-Lag Triple Moving Average",
- "mama": "MESA Adaptive Moving Average",
- "mavp": "Moving Average Variable Period",
- "mcnma": "McNicholl Moving Average",
- "mgdi": "McGinley Dynamic",
- "mma": "Modified Moving Average",
- "nma": "Normalized Moving Average",
- "qema": "Quadruple EMA",
- "rema": "Regularized EMA",
- "rgma": "Recursive Gaussian Moving Average",
- "rma": "Rolling Moving Average",
- "t3": "Tillson T3",
- "tema": "Triple Exponential Moving Average",
- "trama": "Triangular Adaptive Moving Average",
- "vama": "Volume Adjusted Moving Average",
- "vidya": "Variable Index Dynamic Average",
- "yzvama": "Yang Zhang Volatility Adaptive MA",
- "zldema": "Zero-Lag Double EMA",
- "zlema": "Zero-Lag EMA",
- "zltema": "Zero-Lag Triple EMA",
- # Channels
- "abber": "Aberration Bands",
- "accbands": "Acceleration Bands",
- "apchannel": "Average Price Channel",
- "apz": "Adaptive Price Zone",
- "atrbands": "ATR Bands",
- "bbands": "Bollinger Bands",
- "dchannel": "Donchian Channel",
- "decaychannel": "Decay Channel",
- "fcb": "Fractal Chaos Bands",
- "jbands": "J-Line Bands",
- "kchannel": "Keltner Channel",
- "maenv": "Moving Average Envelope",
- "mmchannel": "Min-Max Channel",
- "pchannel": "Price Channel",
- "regchannel": "Regression Channel",
- "sdchannel": "Standard Deviation Channel",
- "starchannel": "Stoller Average Range Channel (STARC)",
- "stbands": "SuperTrend Bands",
- "ttmlrc": "TTM Linear Regression Channel",
- "ubands": "Upper/Lower Bands",
- "uchannel": "Ulcer Channel",
- "vwapbands": "VWAP Bands",
- "vwapsd": "VWAP Standard Deviation",
- # Volatility
- "adr": "Average Daily Range",
- "atr": "Average True Range",
- "atrn": "Normalized ATR",
- "bbw": "Bollinger Band Width",
- "bbwn": "Bollinger Band Width Normalized",
- "bbwp": "Bollinger Band Width Percentile",
- "ccv": "Close-to-Close Volatility",
- "cv": "Coefficient of Variation",
- "cvi": "Chaikin Volatility Index",
- "etherm": "Elder Thermometer",
- "ewma": "Exponentially Weighted Moving Average Volatility",
- "gkv": "Garman-Klass Volatility",
- "hlv": "High-Low Volatility",
- "hv": "Historical Volatility",
- "jvolty": "Jurik Volatility",
- "jvoltyn": "Jurik Volatility Normalized",
- "massi": "Mass Index",
- "natr": "Normalized ATR",
- "rsv": "Rogers-Satchell Volatility",
- "rv": "Realized Volatility",
- "rvi": "Relative Volatility Index",
- "tr": "True Range",
- "ui": "Ulcer Index",
- "vov": "Volatility of Volatility",
- "vr": "Volatility Ratio",
- "yzv": "Yang-Zhang Volatility",
- # Volume
- "adl": "Accumulation/Distribution Line",
- "adosc": "Accumulation/Distribution Oscillator",
- "aobv": "Archer On-Balance Volume",
- "cmf": "Chaikin Money Flow",
- "efi": "Elder Force Index",
- "eom": "Ease of Movement",
- "evwma": "Elastic Volume Weighted Moving Average",
- "iii": "Intraday Intensity Index",
- "kvo": "Klinger Volume Oscillator",
- "mfi": "Money Flow Index",
- "nvi": "Negative Volume Index",
- "obv": "On-Balance Volume",
- "pvd": "Price Volume Divergence",
- "pvi": "Positive Volume Index",
- "pvo": "Percentage Volume Oscillator",
- "pvr": "Price Volume Rank",
- "pvt": "Price Volume Trend",
- "tvi": "Trade Volume Index",
- "twap": "Time Weighted Average Price",
- "va": "Volume Accumulation",
- "vf": "Volume Flow",
- "vo": "Volume Oscillator",
- "vroc": "Volume Rate of Change",
- "vwad": "Volume Weighted Accumulation/Distribution",
- "vwap": "Volume Weighted Average Price",
- "vwma": "Volume Weighted Moving Average",
- "wad": "Williams Accumulation/Distribution",
- # Statistics
- "acf": "Autocorrelation Function",
- "beta": "Beta Coefficient",
- "cma": "Cumulative Moving Average",
- "cointegration": "Cointegration",
- "correlation": "Pearson Correlation",
- "covariance": "Covariance",
- "entropy": "Shannon Entropy",
- "geomean": "Geometric Mean",
- "granger": "Granger Causality",
- "harmean": "Harmonic Mean",
- "hurst": "Hurst Exponent",
- "iqr": "Interquartile Range",
- "jb": "Jarque-Bera Test",
- "kendall": "Kendall Rank Correlation",
- "kurtosis": "Kurtosis",
- "linreg": "Linear Regression",
- "meandev": "Mean Deviation",
- "median": "Rolling Median",
- "mode": "Rolling Mode",
- "pacf": "Partial Autocorrelation Function",
- "percentile": "Rolling Percentile",
- "polyfit": "Polynomial Fit",
- "quantile": "Rolling Quantile",
- "skew": "Skewness",
- "spearman": "Spearman Rank Correlation",
- "stddev": "Standard Deviation",
- "stderr": "Standard Error",
- "sum": "Rolling Sum",
- "theil": "Theil U Statistic",
- "trim": "Trimmed Mean",
- "variance": "Variance",
- "wavg": "Weighted Average",
- "wins": "Winsorized Mean",
- "zscore": "Z-Score",
- "ztest": "Z-Test",
- # Errors
- "huber": "Huber Loss",
- "logcosh": "Log-Cosh Loss",
- "maape": "Mean Arctangent Absolute Percentage Error",
- "mae": "Mean Absolute Error",
- "mapd": "Mean Absolute Percentage Deviation",
- "mape": "Mean Absolute Percentage Error",
- "mase": "Mean Absolute Scaled Error",
- "mdae": "Median Absolute Error",
- "mdape": "Median Absolute Percentage Error",
- "me": "Mean Error",
- "mpe": "Mean Percentage Error",
- "mrae": "Mean Relative Absolute Error",
- "mse": "Mean Squared Error",
- "msle": "Mean Squared Logarithmic Error",
- "pseudohuber": "Pseudo-Huber Loss",
- "quantileloss": "Quantile Loss (Pinball Loss)",
- "rae": "Relative Absolute Error",
- "rmse": "Root Mean Squared Error",
- "rmsle": "Root Mean Squared Logarithmic Error",
- "rse": "Relative Squared Error",
- "rsquared": "R-Squared (Coefficient of Determination)",
- "smape": "Symmetric Mean Absolute Percentage Error",
- "theilu": "Theil U Statistic (Error)",
- "tukeybiweight": "Tukey Biweight Loss",
- "wmape": "Weighted Mean Absolute Percentage Error",
- "wrmse": "Weighted Root Mean Squared Error",
- # Filters
- "agc": "Automatic Gain Control",
- "alaguerre": "Adaptive Laguerre Filter",
- "baxterking": "Baxter-King Filter",
- "bessel": "Bessel Filter",
- "bilateral": "Bilateral Filter",
- "bpf": "Bandpass Filter",
- "butter2": "2nd-Order Butterworth Filter",
- "butter3": "3rd-Order Butterworth Filter",
- "cfitz": "Christiano-Fitzgerald Filter",
- "cheby1": "Chebyshev Type I Filter",
- "cheby2": "Chebyshev Type II Filter",
- "edcf": "Ehlers Distance Coefficient Filter",
- "elliptic": "Elliptic (Cauer) Filter",
- "gauss": "Gaussian Filter",
- "hann": "Hann Filter",
- "hp": "Hodrick-Prescott Filter",
- "hpf": "High-Pass Filter",
- "kalman": "Kalman Filter",
- "laguerre": "Laguerre Filter",
- "lms": "Least Mean Squares Filter",
- "loess": "LOESS Smoother",
- "modf": "Modified Filter",
- "notch": "Notch Filter",
- "nw": "Nadaraya-Watson Filter",
- "oneeuro": "1€ Filter",
- "rls": "Recursive Least Squares Filter",
- "rmed": "Running Median Filter",
- "roofing": "Roofing Filter",
- "sgf": "Savitzky-Golay Filter",
- "spbf": "Short-Period Bandpass Filter",
- "ssf2": "Super Smoother (2-pole)",
- "ssf3": "Super Smoother (3-pole)",
- "usf": "Universal Smoother Filter",
- "voss": "Voss Predictor",
- "wavelet": "Wavelet Filter",
- "wiener": "Wiener Filter",
- # Cycles
- "ccor": "Circular Correlation",
- "ccyc": "Cyber Cycle",
- "cg": "Center of Gravity",
- "dsp": "Dominant Cycle Period",
- "eacp": "Ehlers Autocorrelation Periodogram",
- "ebsw": "Even Better Sinewave",
- "homod": "Homodyne Discriminator",
- "ht_dcperiod": "Hilbert Transform Dominant Cycle Period",
- "ht_dcphase": "Hilbert Transform Dominant Cycle Phase",
- "ht_phasor": "Hilbert Transform Phasor",
- "ht_sine": "Hilbert Transform Sine",
- "lunar": "Lunar Cycle",
- "solar": "Solar Cycle",
- "ssfdsp": "Supersmoother DSP",
- # Dynamics
- "adx": "Average Directional Index",
- "adxr": "ADX Rating",
- "alligator": "Williams Alligator",
- "amat": "Archer Moving Average Trends",
- "aroon": "Aroon",
- "aroonosc": "Aroon Oscillator",
- "chop": "Choppiness Index",
- "dmx": "Directional Movement Extended",
- "dx": "Directional Movement Index",
- "ghla": "Gann Hi-Lo Activator",
- "ht_trendmode": "Hilbert Transform Trend Mode",
- "ichimoku": "Ichimoku Cloud",
- "impulse": "Elder Impulse System",
- "pfe": "Polarized Fractal Efficiency",
- "qstick": "QStick",
- "ravi": "Range Action Verification Index",
- "super": "SuperTrend",
- "ttmsqueeze": "TTM Squeeze",
- "ttmtrend": "TTM Trend",
- "vhf": "Vertical Horizontal Filter",
- "vortex": "Vortex Indicator",
- # Reversals
- "chandelier": "Chandelier Exit",
- "ckstop": "Chuck LeBeau Stop",
- "fractals": "Williams Fractals",
- "pivot": "Pivot Points (Traditional)",
- "pivotcam": "Camarilla Pivot Points",
- "pivotdem": "DeMark Pivot Points",
- "pivotext": "Extended Pivot Points",
- "pivotfib": "Fibonacci Pivot Points",
- "pivotwood": "Woodie Pivot Points",
- "psar": "Parabolic SAR",
- "swings": "Swing High/Low",
- "ttmscalper": "TTM Scalper",
- # Forecasts
- "afirma": "Adaptive FIR Moving Average",
- # Numerics
- "accel": "Acceleration",
- "betadist": "Beta Distribution",
- "binomdist": "Binomial Distribution",
- "change": "Price Change",
- "cwt": "Continuous Wavelet Transform",
- "dwt": "Discrete Wavelet Transform",
- "expdist": "Exponential Distribution",
- "exptrans": "Exponential Transform",
- "fdist": "F-Distribution",
- "fft": "Fast Fourier Transform",
- "gammadist": "Gamma Distribution",
- "highest": "Highest Value",
- "ifft": "Inverse FFT",
- "jerk": "Jerk (3rd derivative)",
- "lineartrans": "Linear Transform",
- "lognormdist": "Log-Normal Distribution",
- "logtrans": "Logarithmic Transform",
- "lowest": "Lowest Value",
- "normalize": "Normalization",
- "normdist": "Normal Distribution",
- "poissondist": "Poisson Distribution",
- "relu": "ReLU Activation",
- "sigmoid": "Sigmoid Transform",
- "slope": "Slope (1st derivative)",
- "sqrttrans": "Square Root Transform",
- "tdist": "Student's t-Distribution",
- "weibulldist": "Weibull Distribution",
-}
-
-# Python function name overrides (export_name → python_name)
-PY_NAME = {
- "abber": "aberr", # fix typo in C# export
- "htdcperiod": "ht_dcperiod",
- "htdcphase": "ht_dcphase",
- "htphasor": "ht_phasor",
- "htsine": "ht_sine",
- "httrendmode": "ht_trendmode",
- "ttmlrc": "ttm_lrc",
- "ttmscalper": "ttm_scalper",
- "ttmsqueeze": "ttm_squeeze",
- "ttmtrend": "ttm_trend",
- "ttmwave": "ttm_wave",
-}
-
-
-def gen_wrapper(export: dict) -> str | None:
- """Generate a Python wrapper function for one export."""
- name = export["name"]
- params = export["params"]
- py_name = PY_NAME.get(name, name)
- label = py_name.upper()
- cat = export["category"]
- desc = DESCRIPTIONS.get(name, DESCRIPTIONS.get(py_name, f"{label} indicator"))
-
- inputs, outputs, n_idx, scalars = classify_params(params)
-
- # Build Python function signature and body
- lines = []
-
- # Determine input pattern and generate accordingly
- input_names = [p["name"] for p in inputs]
- output_names = [p["name"] for p in outputs]
- scalar_specs = [(p["name"], p["type"]) for p in scalars]
-
- # Build Python params
- py_params = []
- py_body = []
-
- # Categorize input types
- has_ohlcv = all(x in [p["name"] for p in inputs] for x in ["sourceOpen", "sourceHigh", "sourceLow", "sourceClose", "sourceVolume"])
- has_ohlc = all(x in [p["name"] for p in inputs] for x in ["open", "high", "low", "close"]) and not has_ohlcv
- has_hlc = all(x in [p["name"] for p in inputs] for x in ["high", "low", "close"]) and not has_ohlc and not has_ohlcv
- has_hl = {"high", "low"}.issubset(set(input_names)) and "close" not in input_names and not has_ohlcv
- has_actual_predicted = {"actual", "predicted"}.issubset(set(input_names))
- has_xy = {"seriesX", "seriesY"}.issubset(set(input_names)) or {"x", "y"}.issubset(set(input_names))
- has_src_vol = (len(inputs) == 2 and any("volume" in p["name"].lower() or p["name"] == "volume" for p in inputs))
- has_price_vol = (len(inputs) == 2 and any(p["name"] == "price" for p in inputs) and any(p["name"] == "volume" for p in inputs))
- single_src = len(inputs) == 1 and inputs[0]["type"] == "double*"
-
- # Generate function
- # Decide function signature
- sig_params = []
-
- # Add input params
- if has_ohlcv:
- sig_params.extend([
- "open: object", "high: object", "low: object",
- "close: object", "volume: object",
- ])
- elif has_ohlc:
- sig_params.extend([
- "open: object", "high: object", "low: object", "close: object",
- ])
- elif has_hlc:
- sig_params.extend(["high: object", "low: object", "close: object"])
- elif has_hl:
- sig_params.extend(["high: object", "low: object"])
- elif has_actual_predicted:
- sig_params.extend(["actual: object", "predicted: object"])
- elif has_xy:
- sig_params.extend(["x: object", "y: object"])
- elif has_price_vol:
- sig_params.extend(["price: object", "volume: object"])
- elif has_src_vol:
- # Figure out which is source, which is volume
- src_name = [p["name"] for p in inputs if p["name"] != "volume"][0] if inputs else "source"
- sig_params.extend([f"close: object", "volume: object"])
- elif single_src:
- src_name = inputs[0]["name"] if inputs else "source"
- py_input_name = "close" if src_name in ("source", "src", "prices", "price") else src_name
- sig_params.append(f"{py_input_name}: object")
- elif len(inputs) == 2:
- # Two inputs (e.g. prs: baseSeries, compSeries)
- for p in inputs:
- pn = p["name"]
- if pn.startswith("source") or pn.startswith("base"):
- pn = "x"
- elif pn.startswith("comp"):
- pn = "y"
- sig_params.append(f"{pn}: object")
- elif len(inputs) == 0 and len(outputs) == 0:
- # Weird case
- return None
- else:
- for p in inputs:
- sig_params.append(f"{p['name']}: object")
-
- # Add scalar params with defaults
- scalar_defaults = {
- "period": 14, "length": 14, "hpLength": 40, "ssLength": 10,
- "fastPeriod": 12, "slowPeriod": 26, "acPeriod": 5,
- "bbPeriod": 20, "bbMult": 2.0, "kcPeriod": 10, "kcMult": 1.5,
- "multiplier": 2.0, "factor": 2.0, "sigma": 6.0,
- "rsiPeriod": 14, "smoothFactor": 5, "qqeFactor": 4.236,
- "kPeriod": 14, "dPeriod": 3, "kSmooth": 3, "dSmooth": 3,
- "kLength": 14, "windowSize": 256, "minPeriod": 6, "maxPeriod": 48,
- "longRoc": 14, "shortRoc": 11, "wmaPeriod": 10,
- "r1": 10, "r2": 15, "r3": 20, "r4": 30,
- "s1": 10, "s2": 10, "s3": 10, "s4": 15, "sigPeriod": 9,
- "jawPeriod": 13, "jawShift": 8, "jawOffset": 8,
- "teethPeriod": 8, "teethShift": 5, "teethOffset": 5,
- "lipsPeriod": 5, "lipsShift": 3, "lipsOffset": 3,
- "tenkanPeriod": 9, "kijunPeriod": 26, "senkouBPeriod": 52, "displacement": 26,
- "emaPeriod": 13, "macdFast": 12, "macdSlow": 26, "macdSignal": 9,
- "signalPeriod": 9, "signal": 3,
- "numHarmonics": 10,
- "atrPeriod": 22, "stopPeriod": 3,
- "alpha": 2.0, "beta": 2.0, "gamma": 0.7, "k": 2.0,
- "lambda": 1600.0, "mu": 0.01, "mu0": 0.0,
- "delta": 1.35, "c": 4.685,
- "q": 0.3, "r": 1.0,
- "vfactor": 0.7, "vovPeriod": 20, "volatilityPeriod": 20,
- "d1": 10, "d2": 20, "nu": 10,
- "order": 3, "polyOrder": 3, "feedback": 0, "fbWeight": 0.5,
- "annualize": 1, "annualPeriods": 252, "isPopulation": 0,
- "predict": 3, "bandwidth": 0.25,
- "nanValue": 0.0, "initialLastValid": 0.0, "initialLast": 0.0,
- "x0": 0.0, "intercept": 0.0, "slope_val": 1.0,
- "minCutoff": 1.0, "dCutoff": 1.0,
- "method": 0, "maType": 0,
- "percentage": 2.5, "percent": 50.0,
- "quantileLevel": 0.5, "quantile": 0.5,
- "trimPct": 0.1, "winPct": 0.05,
- "offset": 0,
- "shortPeriod": 12, "longPeriod": 26, "sumLength": 25,
- "emaLength": 9, "rmaLength": 14, "stdevLength": 10,
- "stochLength": 14, "rsiLength": 14,
- "fastLength": 23, "slowLength": 50, "smoothing": 10,
- "lookback": 5, "useCloses": 0,
- "levels": 4, "threshMult": 1.0, "smoothPeriod": 5,
- "blau": 3, "phase": 0, "power": 1.0,
- "rmsPeriod": 20,
- "nyquistPeriod": 2, "passes": 3,
- "cumulative": 0, "usePercent": 1, "useEma": 0,
- "base": 2.0, "degree": 2,
- "minLength": 5, "maxLength": 50,
- "yzvShortPeriod": 10, "yzvLongPeriod": 100, "percentileLookback": 252,
- "baseLength": 20, "shortAtrPeriod": 14, "longAtrPeriod": 50,
- "strPeriod": 14, "centerPeriod": 20,
- "stPeriod": 14, "momPeriod": 12,
- "scale": 10.0, "omega": 6.0,
- "trials": 20, "threshold": 10,
- "lam": 3.0, "afStart": 0.02, "afIncrement": 0.02, "afMax": 0.2,
- "cutoff": 10, "fastLimit": 0.5, "slowLimit": 0.05,
- "minVol": 0.2, "maxVol": 0.7,
- "friction": 0.4,
- "avgLength": 3, "enhance": 1,
- "numDevs": 2.0,
- "window_type": 0, "use_simd": 0,
- "hpLength_val": 40, "ssfLength": 10,
- }
-
- for sname, stype in scalar_specs:
- # Get reasonable default
- default = scalar_defaults.get(sname)
- if default is None:
- # Try to infer
- if "period" in sname.lower() or "length" in sname.lower():
- default = 14
- elif "mult" in sname.lower() or "factor" in sname.lower():
- default = 2.0
- elif stype == "double":
- default = 1.0
- else:
- default = 10
-
- if stype == "double":
- sig_params.append(f"{sname}: float = {default}")
- else:
- sig_params.append(f"{sname}: int = {int(default)}")
-
- sig_params.append("offset: int = 0")
- sig_params.append("**kwargs")
-
- # Build function body
- body = []
-
- # Sanitize scalars
- for sname, stype in scalar_specs:
- if stype == "double":
- body.append(f" {sname} = float({sname})")
- else:
- body.append(f" {sname} = int({sname})")
- body.append(" offset = int(offset)")
-
- # Convert inputs
- if has_ohlcv:
- body.append(" o, idx = _arr(open); h, _ = _arr(high); l, _ = _arr(low)")
- body.append(" c, _ = _arr(close); v, _ = _arr(volume)")
- body.append(" n = len(o)")
- elif has_ohlc:
- body.append(" o, idx = _arr(open); h, _ = _arr(high); l, _ = _arr(low); c, _ = _arr(close)")
- body.append(" n = len(o)")
- elif has_hlc:
- body.append(" h, idx = _arr(high); l, _ = _arr(low); c, _ = _arr(close)")
- body.append(" n = len(h)")
- elif has_hl:
- body.append(" h, idx = _arr(high); l, _ = _arr(low)")
- body.append(" n = len(h)")
- elif has_actual_predicted:
- body.append(" a, idx = _arr(actual); p, _ = _arr(predicted)")
- body.append(" n = len(a)")
- elif has_xy:
- body.append(" xarr, idx = _arr(x); yarr, _ = _arr(y)")
- body.append(" n = len(xarr)")
- elif has_price_vol:
- body.append(" pr, idx = _arr(price); v, _ = _arr(volume)")
- body.append(" n = len(pr)")
- elif has_src_vol:
- body.append(" src, idx = _arr(close); v, _ = _arr(volume)")
- body.append(" n = len(src)")
- elif single_src:
- py_input_name = "close" if inputs[0]["name"] in ("source", "src", "prices", "price") else inputs[0]["name"]
- body.append(f" src, idx = _arr({py_input_name})")
- body.append(" n = len(src)")
- elif len(inputs) == 2:
- body.append(f" xarr, idx = _arr(x); yarr, _ = _arr(y)")
- body.append(" n = len(xarr)")
-
- # Allocate outputs
- for p in outputs:
- body.append(f" {p['name']} = _out(n)")
-
- # Build native call arguments in original order
- call_args = []
- for p in params:
- pname = p["name"]
- ptype = p["type"]
- if pname == "n":
- call_args.append("n")
- elif ptype == "double*":
- if p in outputs:
- call_args.append(f"_ptr({pname})")
- else:
- # Map to our local var names
- if has_ohlcv:
- vmap = {"sourceOpen": "o", "sourceHigh": "h", "sourceLow": "l", "sourceClose": "c", "sourceVolume": "v"}
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- elif has_ohlc:
- vmap = {"open": "o", "high": "h", "low": "l", "close": "c"}
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- elif has_hlc:
- vmap = {"high": "h", "low": "l", "close": "c"}
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- elif has_hl:
- vmap = {"high": "h", "low": "l"}
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- elif has_actual_predicted:
- vmap = {"actual": "a", "predicted": "p"}
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- elif has_xy:
- vmap = {"seriesX": "xarr", "seriesY": "yarr", "x": "xarr", "y": "yarr"}
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- elif has_price_vol:
- vmap = {"price": "pr", "volume": "v"}
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- elif has_src_vol:
- if pname == "volume":
- call_args.append("_ptr(v)")
- else:
- call_args.append("_ptr(src)")
- elif single_src:
- call_args.append("_ptr(src)")
- elif len(inputs) == 2:
- vmap = {}
- for ip in inputs:
- if ip["name"].startswith("source") or ip["name"].startswith("base"):
- vmap[ip["name"]] = "xarr"
- else:
- vmap[ip["name"]] = "yarr"
- call_args.append(f"_ptr({vmap.get(pname, pname)})")
- else:
- call_args.append(f"_ptr({pname})")
- else:
- call_args.append(pname)
-
- call_str = ", ".join(call_args)
- body.append(f' _check(_lib.qtl_{name}({call_str}))')
-
- # Wrap output
- if len(outputs) == 1:
- out_name = outputs[0]["name"]
- # Decide label
- has_period_scalar = any("period" in s[0].lower() or "length" in s[0].lower() for s in scalar_specs)
- if has_period_scalar:
- # Use first period-like scalar for label
- period_var = next(s[0] for s in scalar_specs if "period" in s[0].lower() or "length" in s[0].lower())
- body.append(f' return _wrap({out_name}, idx, f"{label}_{{{period_var}}}", "{cat}", offset)')
- else:
- body.append(f' return _wrap({out_name}, idx, "{label}", "{cat}", offset)')
- elif len(outputs) > 1:
- # Multi-output
- out_dict_parts = []
- for p in outputs:
- out_dict_parts.append(f'"{p["name"]}": {p["name"]}')
- out_dict = ", ".join(out_dict_parts)
- body.append(f' return _wrap_multi({{{out_dict}}}, idx, "{cat}", offset)')
- else:
- body.append(" return None # no output detected")
-
- # Assemble
- sig = ", ".join(sig_params)
-
- func = f'def {py_name}({sig}) -> object:\n'
- func += f' """{desc}."""\n'
- func += "\n".join(body) + "\n"
-
- return func
-
-
-def generate_category_file(category: str, exports: list[dict]) -> str:
- """Generate a full category module."""
- # Map category to Python module name
- mod_name = category.replace("-", "_")
-
- header = f'"""quantalib {category} indicators.\n\nAuto-generated — DO NOT EDIT.\n"""\n'
- header += "from __future__ import annotations\n\n"
- header += "from ._helpers import _arr, _ptr, _out, _wrap, _wrap_multi, _check, _lib\n\n\n"
-
- functions = []
- all_names = []
-
- for exp in sorted(exports, key=lambda e: e["name"]):
- func = gen_wrapper(exp)
- if func:
- py_name = PY_NAME.get(exp["name"], exp["name"])
- all_names.append(py_name)
- functions.append(func)
-
- # __all__
- all_str = "__all__ = [\n"
- for n in all_names:
- all_str += f' "{n}",\n'
- all_str += "]\n"
-
- return header + all_str + "\n\n" + "\n\n".join(functions)
-
-
-def main():
- exports = parse_exports()
-
- # Group by category
- by_cat: dict[str, list[dict]] = {}
- for exp in exports:
- cat = exp["category"]
- by_cat.setdefault(cat, []).append(exp)
-
- print(f"Parsed {len(exports)} exports in {len(by_cat)} categories:")
- for cat, exps in sorted(by_cat.items()):
- print(f" {cat}: {len(exps)} indicators")
-
- # Generate files
- for cat, exps in sorted(by_cat.items()):
- if cat == "uncategorized":
- continue
- mod_name = cat.replace("-", "_")
- # Map category dirs to Python module names
- py_mod = {
- "trends_FIR": "trends_fir",
- "trends_IIR": "trends_iir",
- }.get(mod_name, mod_name)
-
- outpath = OUT_DIR / f"{py_mod}.py"
- content = generate_category_file(cat, exps)
- outpath.write_text(content, encoding="utf-8")
- print(f" Generated {outpath.name} ({len(exps)} indicators)")
-
- # List uncategorized
- if "uncategorized" in by_cat:
- print(f"\n UNCATEGORIZED: {[e['name'] for e in by_cat['uncategorized']]}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/python/tools/validate_exports.py b/python/tools/validate_exports.py
deleted file mode 100644
index cdb0f093..00000000
--- a/python/tools/validate_exports.py
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-
-import argparse
-import re
-import sys
-from pathlib import Path
-
-
-EXPORT_BATCH_PATTERN = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\.Batch\(")
-PUBLIC_CLASS_PATTERN = re.compile(
- r"\bpublic\s+(?:sealed\s+|abstract\s+|partial\s+)*class\s+([A-Za-z_][A-Za-z0-9_]*)"
-)
-PUBLIC_STATIC_BATCH_PATTERN = re.compile(r"\bpublic\s+static\b[\s\S]{0,1200}?\bBatch\s*\(")
-
-
-def collect_exported_indicators(exports_dir: Path) -> set[str]:
- exported: set[str] = set()
- for p in exports_dir.glob("Exports*.cs"):
- text = p.read_text(encoding="utf-8", errors="ignore")
- exported.update(EXPORT_BATCH_PATTERN.findall(text))
- return exported
-
-
-def collect_lib_indicators(lib_dir: Path) -> set[str]:
- indicators: set[str] = set()
-
- for cs_file in lib_dir.rglob("*.cs"):
- parts = {p.lower() for p in cs_file.parts}
- if "bin" in parts or "obj" in parts:
- continue
- if cs_file.name.endswith(".Tests.cs"):
- continue
-
- text = cs_file.read_text(encoding="utf-8", errors="ignore")
- if "Batch(" not in text or "public static" not in text:
- continue
-
- class_names = PUBLIC_CLASS_PATTERN.findall(text)
- if not class_names:
- continue
-
- if not PUBLIC_STATIC_BATCH_PATTERN.search(text):
- continue
-
- indicators.update(class_names)
-
- return indicators
-
-
-def main() -> int:
- parser = argparse.ArgumentParser(
- description="Validate that python/src/Exports.cs covers all indicator classes in lib/ with public static Batch methods."
- )
- parser.add_argument("--repo-root", type=Path, default=None)
- parser.add_argument("--max-print", type=int, default=200)
- args = parser.parse_args()
-
- repo_root = args.repo_root or Path(__file__).resolve().parents[2]
- exports_dir = repo_root / "python" / "src"
- lib_dir = repo_root / "lib"
-
- if not exports_dir.exists():
- print(f"ERROR: missing dir: {exports_dir}")
- return 2
- if not lib_dir.exists():
- print(f"ERROR: missing dir: {lib_dir}")
- return 2
-
- exported = collect_exported_indicators(exports_dir)
- lib_indicators = collect_lib_indicators(lib_dir)
-
- missing = sorted(lib_indicators - exported)
- extra = sorted(exported - lib_indicators)
-
- print(f"EXPORTED_COUNT={len(exported)}")
- print(f"LIB_INDICATOR_COUNT={len(lib_indicators)}")
- print(f"MISSING_COUNT={len(missing)}")
- print(f"EXTRA_COUNT={len(extra)}")
-
- if missing:
- print("\nMISSING_EXPORTS:")
- for name in missing[: args.max_print]:
- print(name)
- if len(missing) > args.max_print:
- print(f"... ({len(missing) - args.max_print} more)")
-
- if extra:
- print("\nEXTRA_EXPORT_REFERENCES:")
- for name in extra[: args.max_print]:
- print(name)
- if len(extra) > args.max_print:
- print(f"... ({len(extra) - args.max_print} more)")
-
- if missing:
- print(
- "\nFAILED: Exports.cs is missing indicators found in /lib. "
- "Add exports or intentionally exclude in validator policy."
- )
- return 1
-
- print("\nOK: Exports.cs covers all detected /lib indicators.")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
\ No newline at end of file