feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings

This commit is contained in:
Miha Kralj
2026-03-09 13:45:46 -07:00
parent 8e43d62cbb
commit 031f1b5fe6
491 changed files with 6156 additions and 5590 deletions
+6 -17
View File
@@ -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.
+7
View File
@@ -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)
+7
View File
@@ -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 |
+16 -16
View File
@@ -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 |
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+109 -101
View File
@@ -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
/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(
ReadOnlySpan<double> source,
Span<double> 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<double> 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<double>.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<double>.Shared.Return(rentedWindow);
}
double stdDev = Math.Sqrt(sumResiduals2 / n);
double band = multiplier * stdDev;
middle[i] = regression;
upper[i] = regression + band;
lower[i] = regression - band;
}
}
+1 -1
View File
@@ -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)
+126 -64
View File
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -325,7 +326,11 @@ public sealed class Sdchannel : ITValuePublisher
/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(
ReadOnlySpan<double> source,
Span<double> 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<double> window;
if (period <= StackAllocThreshold)
{
int count = Math.Min(i + 1, period);
int start = i - count + 1;
window = stackalloc double[period];
}
else
{
rentedWindow = ArrayPool<double>.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<double>.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);
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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]
+2 -2
View File
@@ -141,12 +141,12 @@ public sealed class RingBuffer : IEnumerable<double>
/// <summary>
/// Average of all elements in the buffer.
/// Returns 0 if buffer is empty.
/// Returns <see langword="double.NaN"/> if buffer is empty.
/// </summary>
public double Average
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _count > 0 ? _sum / _count : 0;
get => _count > 0 ? _sum / _count : double.NaN;
}
/// <summary>
+7 -7
View File
@@ -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;
}
+6
View File
@@ -213,6 +213,12 @@ public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
/// <remarks>
/// <b>Synthetic timestamps:</b> Each element receives a fabricated timestamp starting at
/// <see cref="DateTime.UtcNow"/> (captured once at call time) and incrementing by one minute
/// per item. These timestamps do not reflect real market time. Use the typed overloads
/// (<see cref="Add(TValue)"/>, <see cref="Add(DateTime,double,bool)"/>) when real timestamps matter.
/// </remarks>
public void Add(IEnumerable<double> values)
{
long t = DateTime.UtcNow.Ticks;
+28
View File
@@ -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
}
+1 -1
View File
@@ -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));
}
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+28
View File
@@ -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
}
+7 -2
View File
@@ -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;
}
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
@@ -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);
}
}
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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);
}
@@ -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);
}
}
-2
View File
@@ -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;
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
@@ -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);
}
}
+2 -1
View File
@@ -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;
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+4
View File
@@ -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. |
+72
View File
@@ -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<double>();
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<double>();
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<double>();
var diMinusResults = new List<double>();
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()
{
+14
View File
@@ -62,6 +62,16 @@ public sealed class Adx : ITValuePublisher
/// </summary>
public TValue DiMinus { get; private set; }
/// <summary>
/// Current smoothed +DM value (RMA-smoothed raw plus directional movement, before TR normalization).
/// </summary>
public TValue DmPlus { get; private set; }
/// <summary>
/// Current smoothed -DM value (RMA-smoothed raw minus directional movement, before TR normalization).
/// </summary>
public TValue DmMinus { get; private set; }
/// <summary>
/// True if the ADX has warmed up and is providing valid results.
/// </summary>
@@ -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 });
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+46
View File
@@ -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<double>();
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<double>();
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()
{
+16
View File
@@ -53,6 +53,18 @@ public sealed class Dx : ITValuePublisher
/// </summary>
public TValue DiMinus { get; private set; }
/// <summary>
/// Current smoothed +DM value (Wilder-smoothed raw plus directional movement, before TR normalization).
/// Equivalent to TA-Lib PLUS_DM.
/// </summary>
public TValue DmPlus { get; private set; }
/// <summary>
/// Current smoothed -DM value (Wilder-smoothed raw minus directional movement, before TR normalization).
/// Equivalent to TA-Lib MINUS_DM.
/// </summary>
public TValue DmMinus { get; private set; }
/// <summary>
/// True if the DX has warmed up and is providing valid results.
/// </summary>
@@ -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 });
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
@@ -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);
}
}
+1 -1
View File
@@ -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)
@@ -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);
}
}
+1 -1
View File
@@ -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)
+121
View File
@@ -0,0 +1,121 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// MINUS_DI: Minus Directional Indicator (Wilder, 1978)
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[SkipLocalsInit]
public sealed class MinusDi : ITValuePublisher
{
private readonly Dx _dx;
/// <summary>Display name for the indicator.</summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>Current -DI value.</summary>
public TValue Last { get; private set; }
/// <summary>True when the indicator has warmed up.</summary>
public bool IsHot => _dx.IsHot;
/// <summary>Bars required for warmup.</summary>
public int WarmupPeriod => _dx.WarmupPeriod;
/// <summary>The period parameter.</summary>
public int Period => _dx.Period;
/// <summary>
/// Creates MinusDi with specified period.
/// </summary>
/// <param name="period">Wilder smoothing period (must be &gt; 0)</param>
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})";
}
/// <summary>
/// Creates MinusDi and immediately processes the bar series.
/// </summary>
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;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
public void Prime(TBarSeries source)
{
foreach (var bar in source)
{
Update(bar);
}
}
public void Prime(ReadOnlySpan<double> 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;
}
}
+31
View File
@@ -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)
+121
View File
@@ -0,0 +1,121 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// MINUS_DM: Minus Directional Movement (Wilder, 1978)
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[SkipLocalsInit]
public sealed class MinusDm : ITValuePublisher
{
private readonly Dx _dx;
/// <summary>Display name for the indicator.</summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>Current smoothed -DM value.</summary>
public TValue Last { get; private set; }
/// <summary>True when the indicator has warmed up.</summary>
public bool IsHot => _dx.IsHot;
/// <summary>Bars required for warmup.</summary>
public int WarmupPeriod => _dx.WarmupPeriod;
/// <summary>The period parameter.</summary>
public int Period => _dx.Period;
/// <summary>
/// Creates MinusDm with specified period.
/// </summary>
/// <param name="period">Wilder smoothing period (must be &gt; 0)</param>
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})";
}
/// <summary>
/// Creates MinusDm and immediately processes the bar series.
/// </summary>
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;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
public void Prime(TBarSeries source)
{
foreach (var bar in source)
{
Update(bar);
}
}
public void Prime(ReadOnlySpan<double> 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;
}
}
+28
View File
@@ -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)
+1 -1
View File
@@ -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)
+121
View File
@@ -0,0 +1,121 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// PLUS_DI: Plus Directional Indicator (Wilder, 1978)
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[SkipLocalsInit]
public sealed class PlusDi : ITValuePublisher
{
private readonly Dx _dx;
/// <summary>Display name for the indicator.</summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>Current +DI value.</summary>
public TValue Last { get; private set; }
/// <summary>True when the indicator has warmed up.</summary>
public bool IsHot => _dx.IsHot;
/// <summary>Bars required for warmup.</summary>
public int WarmupPeriod => _dx.WarmupPeriod;
/// <summary>The period parameter.</summary>
public int Period => _dx.Period;
/// <summary>
/// Creates PlusDi with specified period.
/// </summary>
/// <param name="period">Wilder smoothing period (must be &gt; 0)</param>
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})";
}
/// <summary>
/// Creates PlusDi and immediately processes the bar series.
/// </summary>
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;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
public void Prime(TBarSeries source)
{
foreach (var bar in source)
{
Update(bar);
}
}
public void Prime(ReadOnlySpan<double> 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;
}
}
+31
View File
@@ -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)
+121
View File
@@ -0,0 +1,121 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// PLUS_DM: Plus Directional Movement (Wilder, 1978)
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[SkipLocalsInit]
public sealed class PlusDm : ITValuePublisher
{
private readonly Dx _dx;
/// <summary>Display name for the indicator.</summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>Current smoothed +DM value.</summary>
public TValue Last { get; private set; }
/// <summary>True when the indicator has warmed up.</summary>
public bool IsHot => _dx.IsHot;
/// <summary>Bars required for warmup.</summary>
public int WarmupPeriod => _dx.WarmupPeriod;
/// <summary>The period parameter.</summary>
public int Period => _dx.Period;
/// <summary>
/// Creates PlusDm with specified period.
/// </summary>
/// <param name="period">Wilder smoothing period (must be &gt; 0)</param>
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})";
}
/// <summary>
/// Creates PlusDm and immediately processes the bar series.
/// </summary>
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;
}
/// <summary>
/// Initializes the indicator state using the provided bar series history.
/// </summary>
public void Prime(TBarSeries source)
{
foreach (var bar in source)
{
Update(bar);
}
}
public void Prime(ReadOnlySpan<double> 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;
}
}
+28
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("SuperTrend", "SUPER", overlay=true)
@@ -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
}
+14 -43
View File
@@ -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;
}
}
+1 -1
View File
@@ -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)
+1 -16
View File
@@ -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<double>)closeBuf).MinMaxSIMD();
double numerator = hi - lo;
if (diffSum > 1e-10)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Huber Loss (HUBER)", "HUBER")
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+26
View File
@@ -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);
}
}
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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")

Some files were not shown because too many files have changed in this diff Show More