diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index 1e4afae9..fe858258 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -107,6 +107,17 @@ jobs: path: .sarif/resharper.sarif retention-days: 7 + - name: Upload Roslyn SARIF artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: sarif-roslyn + path: | + .sarif/*.sarif + !.sarif/resharper.sarif + retention-days: 7 + if-no-files-found: warn + # ============================================================================== # 2) Snyk Security Scan -> SARIF artifact # ============================================================================== @@ -438,20 +449,8 @@ jobs: path: coverage-merged/ retention-days: 7 - - name: List Roslyn SARIF files - if: always() - run: | - echo "Roslyn SARIF files generated (per-project, SARIF 2.1):" - ls -la .sarif/*.sarif 2>/dev/null || echo "No SARIF files found" - - - name: Upload Roslyn SARIF Artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: sarif-roslyn - path: .sarif/*.sarif - retention-days: 7 - if-no-files-found: warn + # Roslyn SARIF artifacts are uploaded by ReSharper_Analysis job + # (which builds without SonarScanner wrapper that can suppress ErrorLog output) # ============================================================================== # 5) GitHub Code Scanning Upload (SARIF → Security tab) diff --git a/LICENSE b/LICENSE index 261eeb9e..1795092c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ - Apache License +Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -48,7 +48,7 @@ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner + submitted to the Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent @@ -60,7 +60,7 @@ designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and + on behalf of whom a Contribution has been received by the Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of @@ -106,7 +106,7 @@ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not + within such NOTICE file, excluding any notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or @@ -175,18 +175,7 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2024 Miha Kralj Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,4 +187,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. + limitations under the License. \ No newline at end of file diff --git a/_sidebar.md b/_sidebar.md index fabfa20d..29b2ef40 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -197,11 +197,15 @@ * [CHOP - Choppiness Index](/lib/dynamics/chop/Chop.md) * [DMX - Jurik Directional Movement Index](/lib/dynamics/dmx/Dmx.md) * [DX - Directional Movement Index](/lib/dynamics/dx/Dx.md) + * [MINUS_DI - Minus Directional Indicator](/lib/dynamics/minus_di/MinusDi.md) + * [MINUS_DM - Minus Directional Movement](/lib/dynamics/minus_dm/MinusDm.md) * [GHLA - Gann High-Low Activator](/lib/dynamics/ghla/Ghla.md) * [HT_TRENDMODE - Ehlers Hilbert Transform Trend vs Cycle Mode](/lib/dynamics/ht_trendmode/HtTrendmode.md) * [ICHIMOKU - Ichimoku Cloud](/lib/dynamics/ichimoku/Ichimoku.md) * [IMPULSE - Elder Impulse System](/lib/dynamics/impulse/Impulse.md) * [PFE - Polarized Fractal Efficiency](/lib/dynamics/pfe/Pfe.md) + * [PLUS_DI - Plus Directional Indicator](/lib/dynamics/plus_di/PlusDi.md) + * [PLUS_DM - Plus Directional Movement](/lib/dynamics/plus_dm/PlusDm.md) * [QSTICK - Qstick Indicator](/lib/dynamics/qstick/Qstick.md) * [RAVI - Chande Range Action Verification Index](/lib/dynamics/ravi/Ravi.md) * [SUPER - SuperTrend](/lib/dynamics/super/Super.md) @@ -369,12 +373,14 @@ * [FFT - Fast Fourier Transform](/lib/numerics/fft/Fft.md) * [GAMMADIST - Gamma Distribution](/lib/numerics/gammadist/Gammadist.md) * [HIGHEST - Rolling Maximum](/lib/numerics/highest/Highest.md) + * [MAXINDEX - Index of Highest Value](/lib/numerics/maxindex/Maxindex.md) * [IFFT - Inverse Fast Fourier Transform](/lib/numerics/ifft/Ifft.md) * [JERK - Jerk](/lib/numerics/jerk/Jerk.md) * [LINEARTRANS - Linear Transform](/lib/numerics/lineartrans/Lineartrans.md) * [LOGNORMDIST - Log-Normal Distribution](/lib/numerics/lognormdist/Lognormdist.md) * [LOGTRANS - Logarithmic Transform](/lib/numerics/logtrans/Logtrans.md) * [LOWEST - Rolling Minimum](/lib/numerics/lowest/Lowest.md) + * [MININDEX - Index of Lowest Value](/lib/numerics/minindex/Minindex.md) * [NORMDIST - Normal Distribution](/lib/numerics/normdist/Normdist.md) * [NORMALIZE - Min-Max Normalization](/lib/numerics/normalize/Normalize.md) * [POISSONDIST - Poisson Distribution](/lib/numerics/poissondist/Poissondist.md) @@ -447,6 +453,7 @@ * [PIVOTFIB - Fibonacci Pivot Points](/lib/reversals/pivotfib/Pivotfib.md) * [PIVOTWOOD - Woodie's Pivot Points](/lib/reversals/pivotwood/Pivotwood.md) * [PSAR - Parabolic Stop And Reverse](/lib/reversals/psar/Psar.md) + * [SAREXT - Parabolic SAR Extended](/lib/reversals/sarext/Sarext.md) * [SWINGS - Swing High/Low Detection](/lib/reversals/swings/Swings.md) * [TTM_SCALPER - TTM Scalper Alert](/lib/reversals/ttm_scalper/TtmScalper.md) diff --git a/docs/indicators.md b/docs/indicators.md index a936de60..2e388c36 100644 --- a/docs/indicators.md +++ b/docs/indicators.md @@ -194,12 +194,16 @@ Indicators measuring trend strength, regime, and directional movement quality. | [**CHOP**](../lib/dynamics/chop/Chop.md) | Choppiness Index | ATR sum vs range; trending vs choppy | | [**DMX**](../lib/dynamics/dmx/Dmx.md) | Jurik DMX | Enhanced directional movement | | [**DX**](../lib/dynamics/dx/Dx.md) | Directional Movement Index | Raw directional strength | +| [**MINUS_DI**](../lib/dynamics/minusdi/MinusDi.md) | Minus Directional Indicator | Downward DI (0-100) | +| [**MINUS_DM**](../lib/dynamics/minusdm/MinusDm.md) | Minus Directional Movement | Smoothed downward DM | | [**GHLA**](../lib/dynamics/ghla/Ghla.md) | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover | | [**HT_TRENDMODE**](../lib/dynamics/ht_trendmode/HtTrendmode.md) | Ehlers Hilbert Transform Trend vs Cycle Mode | Cycle vs trend regime detection | | [**ICHIMOKU**](../lib/dynamics/ichimoku/Ichimoku.md) | Ichimoku Cloud | Multi-component trend system | | [**IMPULSE**](../lib/dynamics/impulse/Impulse.md) | Elder Impulse System | EMA + MACD-H trend/momentum fusion | | [**QSTICK**](../lib/dynamics/qstick/Qstick.md) | Qstick | Average close-open difference | | [**PFE**](../lib/dynamics/pfe/Pfe.md) | Polarized Fractal Efficiency | Fractal path efficiency as trend strength | +| [**PLUS_DI**](../lib/dynamics/plusdi/PlusDi.md) | Plus Directional Indicator | Upward DI (0-100) | +| [**PLUS_DM**](../lib/dynamics/plusdm/PlusDm.md) | Plus Directional Movement | Smoothed upward DM | | [**RAVI**](../lib/dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dual-SMA divergence as trend strength | | [**SUPER**](../lib/dynamics/super/Super.md) | SuperTrend | ATR-based trend bands | | [**TTM_SQUEEZE**](../lib/dynamics/ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | BB inside KC squeeze with momentum | @@ -429,14 +433,18 @@ Mathematical transformations and derivative indicators. Building blocks for anal | [**WEIBULLDIST**](../lib/numerics/weibulldist/Weibulldist.md) | Weibull Distribution (CDF) | Rolling Weibull CDF — two-parameter closed-form probability transform via pow + exp | | [**CHANGE**](../lib/numerics/change/Change.md) | Percentage Change | Relative price movement | | [**CWT**](../lib/numerics/cwt/Cwt.md) | Continuous Wavelet Transform | Morlet CWT magnitude at a specified scale — time-frequency decomposition | +| [**DECAY**](../lib/numerics/decay/Decay.md) | Linear Decay | Peak envelope with linear degradation | | [**DWT**](../lib/numerics/dwt/Dwt.md) | Discrete Wavelet Transform | À trous Haar stationary DWT — multi-resolution approximation + detail, WarmupPeriod = 2^levels | +| [**EDECAY**](../lib/numerics/edecay/Edecay.md) | Exponential Decay | Peak envelope with exponential degradation | | [**EXPTRANS**](../lib/numerics/exptrans/Exptrans.md) | Exponential Transform | e^x for log-space reversal | | [**HIGHEST**](../lib/numerics/highest/Highest.md) | Rolling Maximum | O(1) via monotonic deque | +| [**MAXINDEX**](../lib/numerics/maxindex/Maxindex.md) | Rolling Maximum Index | Position of max in window | | [**JERK**](../lib/numerics/jerk/Jerk.md) | Jerk (3rd Derivative) | Change in acceleration | | [**LINEARTRANS**](../lib/numerics/lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling | | [**LOGNORMDIST**](../lib/numerics/lognormdist/Lognormdist.md) | Log-Normal Distribution | Log-normal CDF via min-max normalization | | [**LOGTRANS**](../lib/numerics/logtrans/Logtrans.md) | Logarithmic Transform | Natural log for percentage analysis | | [**LOWEST**](../lib/numerics/lowest/Lowest.md) | Rolling Minimum | O(1) via monotonic deque | +| [**MININDEX**](../lib/numerics/minindex/Minindex.md) | Rolling Minimum Index | Position of min in window | | [**NORMALIZE**](../lib/numerics/normalize/Normalize.md) | Min-Max Normalization | Scale to [0,1] via rolling min/max | | [**RELU**](../lib/numerics/relu/Relu.md) | Rectified Linear Unit | max(0, x) activation | | [**SIGMOID**](../lib/numerics/sigmoid/Sigmoid.md) | Logistic Function | 1/(1+e^-x) bounded [0,1] | @@ -492,6 +500,7 @@ Reversal indicators identify potential turning points, compute adaptive stop lev | [**PIVOTFIB**](../lib/reversals/pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Fibonacci pivots: 7 levels (PP, R1-R3, S1-S3); ratios 0.382/0.618/1.000 applied to range | | [**PIVOTWOOD**](../lib/reversals/pivotwood/Pivotwood.md) | Woodie's Pivot Points | Close-weighted pivots: 7 levels (PP, R1-R3, S1-S3); PP = (H+L+2C)/4 biased toward close | | [**PSAR**](../lib/reversals/psar/Psar.md) | Parabolic Stop And Reverse | Accelerating trailing stop; SAR dots flip on reversal; Welles Wilder (1978) | +| [**SAREXT**](../lib/reversals/sarext/Sarext.md) | Parabolic SAR Extended | Asymmetric AF PSAR; sign-encoded output | | [**SWINGS**](../lib/reversals/swings/Swings.md) | Swing High/Low Detection | Configurable-lookback pattern detector; dual SwingHigh/SwingLow with persistent levels | | [**TTM_SCALPER**](../lib/reversals/ttm_scalper/TtmScalper.md) | TTM Scalper Alert | 3-bar pivot high/low detection for scalping entries; John Carter | diff --git a/lib/_index.md b/lib/_index.md index 6d6c824d..6a24a347 100644 --- a/lib/_index.md +++ b/lib/_index.md @@ -59,7 +59,7 @@ | [BAXTERKING](filters/baxterking/BaxterKing.md) | Baxter-King Band-Pass Filter | Filters | | [BBANDS](channels/bbands/Bbands.md) | Bollinger Bands | Channels | | [BBB](oscillators/bbb/Bbb.md) | Bollinger %B | Oscillators | -| [BBI](oscillators/bbi/Bbi.md) | Bulls Bears Index | Oscillators | +| [BBI](oscillators/bbi/Bbi.md) | Bulls Bears Index | Oscillators | | [BBS](oscillators/bbs/Bbs.md) | Bollinger Band Squeeze | Oscillators | | [BBW](volatility/bbw/Bbw.md) | Bollinger Band Width | Volatility | | [BBWN](volatility/bbwn/Bbwn.md) | BB Width Normalized | Volatility | @@ -72,7 +72,7 @@ | [BINOMDIST](numerics/binomdist/Binomdist.md) | Binomial Distribution | Numerics | | [BLMA](trends_FIR/blma/Blma.md) | Blackman MA | Trends (FIR) | | [BOP](momentum/bop/Bop.md) | Balance of Power | Momentum | -| [BPF](filters/bpf/Bpf.md) | BandPass Filter | Filters | +| [BPF](filters/bpf/Bpf.md) | BandPass Filter | Filters | | [BRAR](oscillators/brar/Brar.md) | Bull-Bear Power Ratio | Oscillators | | [BUTTER2](filters/butter2/Butter2.md) | Ehlers 2-Pole Butterworth Filter | Filters | | [BUTTER3](filters/butter3/Butter3.md) | Ehlers 3-Pole Butterworth Filter | Filters | @@ -114,9 +114,9 @@ | [DEMA](trends_IIR/dema/Dema.md) | Double Exponential MA | Trends (IIR) | | [DMX](dynamics/dmx/Dmx.md) | Jurik Directional Movement Index | Dynamics | | [DOSC](oscillators/dosc/Dosc.md) | Derivative Oscillator | Oscillators | -| [DPO](oscillators/dpo/Dpo.md) | Detrended Price Oscillator | Oscillators | -| [DYMOI](oscillators/dymoi/Dymoi.md) | Dynamic Momentum Index | Oscillators | -| [DSMA](trends_IIR/dsma/Dsma.md) | Deviation-Scaled MA | Trends (IIR) | +| [DPO](oscillators/dpo/Dpo.md) | Detrended Price Oscillator | Oscillators | +| [DYMOI](oscillators/dymoi/Dymoi.md) | Dynamic Momentum Index | Oscillators | +| [DSMA](trends_IIR/dsma/Dsma.md) | Deviation-Scaled MA | Trends (IIR) | | [DSP](cycles/dsp/Dsp.md) | Ehlers Detrended Synthetic Price | Cycles | | [DWMA](trends_FIR/dwma/Dwma.md) | Double Weighted MA | Trends (FIR) | | [DWT](numerics/dwt/Dwt.md) | Discrete Wavelet Transform | Numerics | @@ -130,7 +130,7 @@ | [ENTROPY](statistics/entropy/Entropy.md) | Shannon Entropy | Statistics | | [EOM](volume/eom/Eom.md) | Ease of Movement | Volume | | [ER](oscillators/er/Er.md) | Efficiency Ratio | Oscillators | -| [ERI](oscillators/eri/Eri.md) | Elder Ray Index | Oscillators | +| [ERI](oscillators/eri/Eri.md) | Elder Ray Index | Oscillators | | [ETHERM](volatility/etherm/Etherm.md) | Elder's Thermometer | Volatility | | [EVWMA](volume/evwma/Evwma.md) | Elastic Volume Weighted MA | Volume | | [EWMA](volatility/ewma/Ewma.md) | EWMA Volatility | Volatility | @@ -148,7 +148,7 @@ | [GAMMADIST](numerics/gammadist/Gammadist.md) | Gamma Distribution | Numerics | | [GATOR](oscillators/gator/Gator.md) | Williams Gator Oscillator | Oscillators | | [GAUSS](filters/gauss/Gauss.md) | Gaussian Filter | Filters | -| [GDEMA](trends_IIR/gdema/Gdema.md) | Generalized DEMA | Trends (IIR) | +| [GDEMA](trends_IIR/gdema/Gdema.md) | Generalized DEMA | Trends (IIR) | | [GEOMEAN](statistics/geomean/Geomean.md) | Geometric Mean | Statistics | | [GHLA](dynamics/ghla/Ghla.md) | Gann High-Low Activator | Dynamics | | [GKV](volatility/gkv/Gkv.md) | Garman-Klass Volatility | Volatility | @@ -169,11 +169,11 @@ | [HP](filters/hp/Hp.md) | Hodrick-Prescott | Filters | | [HPF](filters/hpf/Hpf.md) | Ehlers Highpass Filter | Filters | | [HTIT](trends_IIR/htit/Htit.md) | Ehlers Hilbert Transform Instantaneous Trend (aka HT_TRENDLINE) | Trends (IIR) | -| [HT_DCPERIOD](cycles/ht_dcperiod/Htdcperiod.md) | Ehlers HT Dominant Cycle Period | Cycles | -| [HT_DCPHASE](cycles/ht_dcphase/Htdcphase.md) | Ehlers HT Dominant Cycle Phase | Cycles | -| [HT_PHASOR](cycles/ht_phasor/Htphasor.md) | Ehlers HT Phasor Components | Cycles | -| [HT_SINE](cycles/ht_sine/Htsine.md) | Ehlers HT SineWave | Cycles | -| [HT_TRENDMODE](dynamics/ht_trendmode/Httrendmode.md) | Ehlers HT Trend vs Cycle | Dynamics | +| [HT_DCPERIOD](cycles/ht_dcperiod/Htdcperiod.md) | Ehlers HT Dominant Cycle Period | Cycles | +| [HT_DCPHASE](cycles/ht_dcphase/Htdcphase.md) | Ehlers HT Dominant Cycle Phase | Cycles | +| [HT_PHASOR](cycles/ht_phasor/Htphasor.md) | Ehlers HT Phasor Components | Cycles | +| [HT_SINE](cycles/ht_sine/Htsine.md) | Ehlers HT SineWave | Cycles | +| [HT_TRENDMODE](dynamics/ht_trendmode/Httrendmode.md) | Ehlers HT Trend vs Cycle | Dynamics | | [HUBER](errors/huber/Huber.md) | Huber Loss | Errors | | [HURST](statistics/hurst/Hurst.md) | Hurst Exponent | Statistics | | [HV](volatility/hv/Hv.md) | Historical Volatility | Volatility | @@ -227,7 +227,7 @@ | [MASE](errors/mase/Mase.md) | Mean Absolute Scaled Error | Errors | | [MASSI](volatility/massi/Massi.md) | Mass Index | Volatility | | [MAVP](trends_IIR/mavp/Mavp.md) | Moving Average Variable Period | Trends (IIR) | -| [MCNMA](trends_IIR/mcnma/Mcnma.md) | McNicholl EMA | Trends (IIR) | +| [MCNMA](trends_IIR/mcnma/Mcnma.md) | McNicholl EMA | Trends (IIR) | | [MDAE](errors/mdae/Mdae.md) | Median Absolute Error | Errors | | [MDAPE](errors/mdape/Mdape.md) | Median Absolute % Error | Errors | | [ME](errors/me/Me.md) | Mean Error | Errors | @@ -256,7 +256,7 @@ | [NOTCH](filters/notch/Notch.md) | Notch Filter | Filters | | [NVI](volume/nvi/Nvi.md) | Negative Volume Index | Volume | | [NW](filters/nw/Nw.md) | Nadaraya-Watson Kernel Regression | Filters | -| [NYQMA](trends_FIR/nyqma/Nyqma.md) | Nyquist MA | Trends (FIR) | +| [NYQMA](trends_FIR/nyqma/Nyqma.md) | Nyquist MA | Trends (FIR) | | [OBV](volume/obv/Obv.md) | On Balance Volume | Volume | | [ONEEURO](filters/oneeuro/OneEuro.md) | One Euro Filter | Filters | | [PACF](statistics/pacf/Pacf.md) | Partial Autocorrelation Function | Statistics | @@ -274,7 +274,7 @@ | [PMA](trends_FIR/pma/Pma.md) | Predictive Moving Average | Trends (FIR) | | [PMO](momentum/pmo/Pmo.md) | Price Momentum Oscillator | Momentum | | [POISSONDIST](numerics/poissondist/Poissondist.md) | Poisson Distribution | Numerics | -| [POLYFIT](statistics/polyfit/Polyfit.md) | Polynomial Fitting | Statistics | +| [POLYFIT](statistics/polyfit/Polyfit.md) | Polynomial Fitting | Statistics | | [PPO](momentum/ppo/Ppo.md) | Percentage Price Oscillator | Momentum | | [PRS](momentum/prs/Prs.md) | Price Relative Strength | Momentum | | [PSAR](reversals/psar/Psar.md) | Parabolic Stop And Reverse | Reversals | @@ -294,7 +294,7 @@ | [QUANTILELOSS](errors/quantileloss/QuantileLoss.md) | Quantile Loss | Errors | | [RAE](errors/rae/Rae.md) | Relative Absolute Error | Errors | | [RAIN](trends_FIR/rain/Rain.md) | Rainbow MA | Trends (FIR) | -| [RAVI](dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dynamics | +| [RAVI](dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dynamics | | [REFLEX](oscillators/reflex/Reflex.md) | Ehlers Reflex Indicator | Oscillators | | [REGCHANNEL](channels/regchannel/Regchannel.md) | Regression Channels | Channels | | [RELU](numerics/relu/Relu.md) | Rectified Linear Unit | Numerics | diff --git a/lib/channels/aberr/aberr.pine b/lib/channels/aberr/aberr.pine index 468f909d..87c0bad7 100644 --- a/lib/channels/aberr/aberr.pine +++ b/lib/channels/aberr/aberr.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Aberration (ABERR)", "ABERR", overlay=true) diff --git a/lib/channels/accbands/accbands.pine b/lib/channels/accbands/accbands.pine index 3cf0f7a1..f85326d7 100644 --- a/lib/channels/accbands/accbands.pine +++ b/lib/channels/accbands/accbands.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Acceleration Bands (ACCBANDS)", "ACCBANDS", overlay=true) diff --git a/lib/channels/apchannel/apchannel.md b/lib/channels/apchannel/apchannel.md index 4886da06..b58a86c2 100644 --- a/lib/channels/apchannel/apchannel.md +++ b/lib/channels/apchannel/apchannel.md @@ -4,17 +4,17 @@ | ---------------- | -------------------------------- | | **Category** | Channel | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | None | -| **Outputs** | Single series (Apchannel) | +| **Parameters** | `alpha` (default 0.2) | +| **Outputs** | Multiple series (Upper, Lower) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `⌈3/alpha⌉` bars (default 15) | ### TL;DR - APCHANNEL applies exponential smoothing independently to price highs and lows, creating a dynamic envelope that "remembers" significant extremes wh... -- No configurable parameters; computation is stateless per bar. +- Parameterized by `alpha` (default 0.2). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `⌈3/alpha⌉` bars (default 15) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. APCHANNEL applies exponential smoothing independently to price highs and lows, creating a dynamic envelope that "remembers" significant extremes while gradually fading their influence over time. Unlike rigid Donchian channels that drop price extremes abruptly when they exit the lookback window (the "cliff effect"), APCHANNEL decays them smoothly through leaky integration. The result is a channel with continuously sloping boundaries that responds to volatility without the discontinuous jumps that plague fixed-window approaches. The algorithm is $O(1)$ per bar with only two state variables and no buffers. diff --git a/lib/channels/apchannel/apchannel.pine b/lib/channels/apchannel/apchannel.pine index cc0ca753..a9f67394 100644 --- a/lib/channels/apchannel/apchannel.pine +++ b/lib/channels/apchannel/apchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Adaptive Price Channel (APCHANNEL)", "APCHANNEL", overlay=true) diff --git a/lib/channels/apz/apz.pine b/lib/channels/apz/apz.pine index 418d9514..0b2f79bb 100644 --- a/lib/channels/apz/apz.pine +++ b/lib/channels/apz/apz.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Adaptive Price Zone", "APZ", overlay=true) diff --git a/lib/channels/atrbands/atrbands.pine b/lib/channels/atrbands/atrbands.pine index 127f8afd..515872c0 100644 --- a/lib/channels/atrbands/atrbands.pine +++ b/lib/channels/atrbands/atrbands.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("ATR Bands (ATRBANDS)", "ATRBANDS", overlay=true) diff --git a/lib/channels/bbands/bbands.pine b/lib/channels/bbands/bbands.pine index cc86e738..80b3c52d 100644 --- a/lib/channels/bbands/bbands.pine +++ b/lib/channels/bbands/bbands.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Bollinger Bands (BBANDS)", "BBANDS", overlay=true) diff --git a/lib/channels/dchannel/dchannel.pine b/lib/channels/dchannel/dchannel.pine index 6dc69281..2da080b0 100644 --- a/lib/channels/dchannel/dchannel.pine +++ b/lib/channels/dchannel/dchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Donchian Channels (DCHANNEL)", "DCHANNEL", overlay=true) diff --git a/lib/channels/decaychannel/decaychannel.pine b/lib/channels/decaychannel/decaychannel.pine index 1ac60957..2020db47 100644 --- a/lib/channels/decaychannel/decaychannel.pine +++ b/lib/channels/decaychannel/decaychannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Decay Min-Max Channel (DECAYCHANNEL)", "DECAYCHANNEL", overlay=true) diff --git a/lib/channels/fcb/fcb.pine b/lib/channels/fcb/fcb.pine index 0f394dd1..a369e02f 100644 --- a/lib/channels/fcb/fcb.pine +++ b/lib/channels/fcb/fcb.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Fractal Chaos Bands (FCB)", "FCB", overlay=true) diff --git a/lib/channels/jbands/Jbands.pine b/lib/channels/jbands/Jbands.pine index 04807578..d146c277 100644 --- a/lib/channels/jbands/Jbands.pine +++ b/lib/channels/jbands/Jbands.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Jurik Adaptive Envelope Bands", "JBANDS", overlay=true) diff --git a/lib/channels/jbands/jbands.md b/lib/channels/jbands/jbands.md index c72e18e3..534e13b5 100644 --- a/lib/channels/jbands/jbands.md +++ b/lib/channels/jbands/jbands.md @@ -7,14 +7,14 @@ | **Parameters** | `period`, `phase` (default 0) | | **Outputs** | Multiple series (Upper, Lower) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `⌈20 + 80 × period^0.36⌉` bars | ### TL;DR - JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new pri... - Parameterized by `period`, `phase` (default 0). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `⌈20 + 80 × period^0.36⌉` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new price extremes and decay exponentially during consolidation. Unlike standard volatility bands (Bollinger, Keltner) which maintain symmetric width around a center line, JBANDS feature "snap-and-decay" hysteresis: expansion is instantaneous (plasticity), contraction is gradual (elasticity). The decay rate is dynamically modulated by a two-stage volatility estimator — a 10-bar SMA feeding a 128-bar trimmed mean — making the bands tight during quiet markets and expansive during trends. The center line is the full JMA: a 2-pole IIR filter with phase control and adaptive alpha. diff --git a/lib/channels/kchannel/kchannel.pine b/lib/channels/kchannel/kchannel.pine index 1d05b214..96262305 100644 --- a/lib/channels/kchannel/kchannel.pine +++ b/lib/channels/kchannel/kchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Keltner Channel (KCHANNEL)", "KCHANNEL", overlay=true) diff --git a/lib/channels/maenv/maenv.pine b/lib/channels/maenv/maenv.pine index 54575383..a0ab3939 100644 --- a/lib/channels/maenv/maenv.pine +++ b/lib/channels/maenv/maenv.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("MA Envelope (MAE)", "MAE", overlay=true) diff --git a/lib/channels/mmchannel/mmchannel.pine b/lib/channels/mmchannel/mmchannel.pine index 2c84b785..28fea159 100644 --- a/lib/channels/mmchannel/mmchannel.pine +++ b/lib/channels/mmchannel/mmchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Min-Max Channel (MMCHANNEL)", "MMCHANNEL", overlay=true) diff --git a/lib/channels/pchannel/pchannel.pine b/lib/channels/pchannel/pchannel.pine index 159cc821..3c04be73 100644 --- a/lib/channels/pchannel/pchannel.pine +++ b/lib/channels/pchannel/pchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Price Channel (PCHANNEL)", "PCHANNEL", overlay=true) diff --git a/lib/channels/regchannel/Regchannel.cs b/lib/channels/regchannel/Regchannel.cs index 7181bdc5..73a6d8f3 100644 --- a/lib/channels/regchannel/Regchannel.cs +++ b/lib/channels/regchannel/Regchannel.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -206,7 +207,7 @@ public sealed class Regchannel : ITValuePublisher for (int i = 0; i < count; i++) { sumY += values[i]; - sumXY += i * values[i]; + sumXY = Math.FusedMultiplyAdd((double)i, values[i], sumXY); } double n = count; @@ -325,7 +326,11 @@ public sealed class Regchannel : ITValuePublisher /// /// Batch calculation using spans. + /// O(period) per bar: sums recomputed from circular buffer each bar for numerical + /// consistency with the streaming path, plus closed-form residual variance: + /// sumResiduals² = sumY² − intercept·sumY − slope·sumXY, eliminating a second O(period) pass. /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Batch( ReadOnlySpan source, Span middle, @@ -360,121 +365,124 @@ public sealed class Regchannel : ITValuePublisher double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0; double denomFull = period * sumX2Full - sumXFull * sumXFull; - // Track last valid value for NaN substitution - double lastValid = double.NaN; - - for (int i = 0; i < len; i++) + // Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences. + const int StackAllocThreshold = 256; + double[]? rentedWindow = null; + scoped Span window; + if (period <= StackAllocThreshold) { - // Get valid value with last-valid substitution - double currentValue = source[i]; - if (double.IsFinite(currentValue)) - { - lastValid = currentValue; - } - else - { - currentValue = lastValid; - } + window = stackalloc double[period]; + } + else + { + rentedWindow = ArrayPool.Shared.Rent(period); + window = rentedWindow.AsSpan(0, period); + } - // If still NaN (no valid value seen yet), output NaN - if (!double.IsFinite(currentValue)) + try + { + window.Clear(); + + double lastValid = double.NaN; + int head = 0, count = 0; + + for (int i = 0; i < len; i++) { - middle[i] = double.NaN; - upper[i] = double.NaN; - lower[i] = double.NaN; - continue; - } - - int count = Math.Min(i + 1, period); - int start = i - count + 1; - - if (count <= 1) - { - middle[i] = currentValue; - upper[i] = currentValue; - lower[i] = currentValue; - continue; - } - - // Calculate sums for linear regression with NaN handling - double sumY = 0; - double sumXY = 0; - double lastValidInWindow = double.NaN; - - for (int j = 0; j < count; j++) - { - double rawY = source[start + j]; - double y; - if (double.IsFinite(rawY)) + // NaN substitution + double y = source[i]; + if (double.IsFinite(y)) { - lastValidInWindow = rawY; - y = rawY; + lastValid = y; } else { - y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0; + y = lastValid; } - sumY += y; - sumXY += j * y; - } - double n = count; - double sx, denom; - - if (count < period) - { - sx = 0.5 * n * (n - 1); - double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0; - denom = n * sx2 - sx * sx; - } - else - { - sx = sumXFull; - denom = denomFull; - } - - double slope, intercept, regression; - - if (Math.Abs(denom) < 1e-10) - { - slope = 0; - intercept = sumY / n; - regression = intercept; - } - else - { - slope = (n * sumXY - sx * sumY) / denom; - intercept = (sumY - slope * sx) / n; - regression = Math.FusedMultiplyAdd(slope, count - 1, intercept); - } - - // Calculate standard deviation of residuals with NaN handling - double sumResiduals2 = 0; - lastValidInWindow = double.NaN; - for (int j = 0; j < count; j++) - { - double rawY = source[start + j]; - double y; - if (double.IsFinite(rawY)) + // No valid value seen yet + if (!double.IsFinite(y)) { - lastValidInWindow = rawY; - y = rawY; + middle[i] = double.NaN; + upper[i] = double.NaN; + lower[i] = double.NaN; + continue; + } + + // Store in circular buffer + window[head] = y; + head = (head + 1) % period; + if (count < period) + { + count++; + } + + if (count <= 1) + { + middle[i] = y; + upper[i] = y; + lower[i] = y; + continue; + } + + // Recompute sums fresh from circular buffer (oldest-to-newest). + // This matches the streaming Update() path numerically. + double sumY = 0, sumXY = 0, sumY2 = 0; + int oldestIdx = (head - count + period) % period; + for (int k = 0; k < count; k++) + { + double wk = window[(oldestIdx + k) % period]; + sumY += wk; + sumXY = Math.FusedMultiplyAdd((double)k, wk, sumXY); + sumY2 = Math.FusedMultiplyAdd(wk, wk, sumY2); + } + + double n = count; + double sx, denom; + + if (count < period) + { + sx = 0.5 * n * (n - 1); + double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0; + denom = n * sx2 - sx * sx; } else { - y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0; + sx = sumXFull; + denom = denomFull; } - double predicted = Math.FusedMultiplyAdd(slope, j, intercept); - double residual = y - predicted; - sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2); + + double slope, intercept, regression; + + if (Math.Abs(denom) < 1e-10) + { + slope = 0; + intercept = sumY / n; + regression = intercept; + } + else + { + slope = (n * sumXY - sx * sumY) / denom; + intercept = (sumY - slope * sx) / n; + regression = Math.FusedMultiplyAdd(slope, count - 1, intercept); + } + + // Closed-form residual variance (normal-equation identity): + // sumResiduals² = sumY² − intercept·sumY − slope·sumXY + double sumResiduals2 = Math.Max(0.0, sumY2 - intercept * sumY - slope * sumXY); + double stdDev = Math.Sqrt(sumResiduals2 / n); + double band = multiplier * stdDev; + + middle[i] = regression; + upper[i] = regression + band; + lower[i] = regression - band; + } + } + finally + { + if (rentedWindow != null) + { + ArrayPool.Shared.Return(rentedWindow); } - - double stdDev = Math.Sqrt(sumResiduals2 / n); - double band = multiplier * stdDev; - - middle[i] = regression; - upper[i] = regression + band; - lower[i] = regression - band; } } diff --git a/lib/channels/regchannel/regchannel.pine b/lib/channels/regchannel/regchannel.pine index d02ac098..b456dd22 100644 --- a/lib/channels/regchannel/regchannel.pine +++ b/lib/channels/regchannel/regchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Regression Channels (REGCHANNEL)", "REGCHANNEL", overlay=true) diff --git a/lib/channels/sdchannel/Sdchannel.cs b/lib/channels/sdchannel/Sdchannel.cs index 2fcb9ba7..90f8d23d 100644 --- a/lib/channels/sdchannel/Sdchannel.cs +++ b/lib/channels/sdchannel/Sdchannel.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -325,7 +326,11 @@ public sealed class Sdchannel : ITValuePublisher /// /// Batch calculation using spans. + /// O(period) per bar: sums and residuals recomputed from circular buffer each bar for numerical + /// consistency with the streaming path. Uses the same plain arithmetic as Update() for sumXY + /// and the same FMA residual loop to ensure streaming/batch agreement within 1e-9. /// + [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Batch( ReadOnlySpan source, Span middle, @@ -360,75 +365,130 @@ public sealed class Sdchannel : ITValuePublisher double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0; double denomFull = period * sumX2Full - sumXFull * sumXFull; - for (int i = 0; i < len; i++) + // Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences. + const int StackAllocThreshold = 256; + double[]? rentedWindow = null; + scoped Span window; + if (period <= StackAllocThreshold) { - int count = Math.Min(i + 1, period); - int start = i - count + 1; + window = stackalloc double[period]; + } + else + { + rentedWindow = ArrayPool.Shared.Rent(period); + window = rentedWindow.AsSpan(0, period); + } - if (count <= 1) + try + { + window.Clear(); + + double lastValid = double.NaN; + int head = 0, count = 0; + + for (int i = 0; i < len; i++) { - middle[i] = source[i]; - upper[i] = source[i]; - lower[i] = source[i]; - continue; + // NaN substitution matching GetValid() in streaming Update() + double y = source[i]; + if (double.IsFinite(y)) + { + lastValid = y; + } + else + { + y = lastValid; + } + + // No valid value seen yet + if (!double.IsFinite(y)) + { + middle[i] = double.NaN; + upper[i] = double.NaN; + lower[i] = double.NaN; + continue; + } + + // Store in circular buffer + window[head] = y; + head = (head + 1) % period; + if (count < period) + { + count++; + } + + if (count <= 1) + { + middle[i] = y; + upper[i] = y; + lower[i] = y; + continue; + } + + // Recompute sums fresh from circular buffer (oldest-to-newest). + // Plain arithmetic for sumXY matches streaming Update() path numerically. + double sumY = 0, sumXY = 0; + int oldestIdx = (head - count + period) % period; + for (int k = 0; k < count; k++) + { + double wk = window[(oldestIdx + k) % period]; + sumY += wk; + sumXY += (double)k * wk; + } + + double n = count; + double sx, denom; + + if (count < period) + { + sx = 0.5 * n * (n - 1); + double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0; + denom = n * sx2 - sx * sx; + } + else + { + sx = sumXFull; + denom = denomFull; + } + + double slope, intercept, regression; + + if (Math.Abs(denom) < 1e-10) + { + slope = 0; + intercept = sumY / n; + regression = intercept; + } + else + { + slope = (n * sumXY - sx * sumY) / denom; + intercept = (sumY - slope * sx) / n; + regression = Math.FusedMultiplyAdd(slope, count - 1, intercept); + } + + // Explicit residual loop matching streaming Update() path numerically. + double sumResiduals2 = 0; + for (int k = 0; k < count; k++) + { + double wk = window[(oldestIdx + k) % period]; + double predicted = Math.FusedMultiplyAdd(slope, k, intercept); + double residual = wk - predicted; + sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2); + } + + double stdDev = Math.Sqrt(sumResiduals2 / n); + double band = multiplier * stdDev; + + middle[i] = regression; + upper[i] = regression + band; + lower[i] = regression - band; } - - // Calculate sums for linear regression - double sumY = 0; - double sumXY = 0; - - for (int j = 0; j < count; j++) + } + finally + { + if (rentedWindow != null) { - double y = source[start + j]; - sumY += y; - sumXY += j * y; + ArrayPool.Shared.Return(rentedWindow); } - - double n = count; - double sx, denom; - - if (count < period) - { - sx = 0.5 * n * (n - 1); - double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0; - denom = n * sx2 - sx * sx; - } - else - { - sx = sumXFull; - denom = denomFull; - } - - double slope, intercept, regression; - - if (Math.Abs(denom) < 1e-10) - { - slope = 0; - intercept = sumY / n; - regression = intercept; - } - else - { - slope = (n * sumXY - sx * sumY) / denom; - intercept = (sumY - slope * sx) / n; - regression = Math.FusedMultiplyAdd(slope, count - 1, intercept); - } - - // Calculate standard deviation of residuals - double sumResiduals2 = 0; - for (int j = 0; j < count; j++) - { - double predicted = Math.FusedMultiplyAdd(slope, j, intercept); - double residual = source[start + j] - predicted; - sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2); - } - - double stdDev = Math.Sqrt(sumResiduals2 / n); - double band = multiplier * stdDev; - - middle[i] = regression; - upper[i] = regression + band; - lower[i] = regression - band; } } @@ -464,7 +524,9 @@ public sealed class Sdchannel : ITValuePublisher public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Sdchannel Indicator) Calculate(TSeries source, int period = 20, double multiplier = 2.0) { - var indicator = new Sdchannel(source, period, multiplier); + // Use parameterless constructor to avoid double-processing: new Sdchannel(source, ...) calls Prime(source), + // then Update(source) would call Prime again. + var indicator = new Sdchannel(period, multiplier); var results = indicator.Update(source); return (results, indicator); } diff --git a/lib/channels/sdchannel/sdchannel.pine b/lib/channels/sdchannel/sdchannel.pine index eb76dcda..c4739026 100644 --- a/lib/channels/sdchannel/sdchannel.pine +++ b/lib/channels/sdchannel/sdchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Standard Deviation Channel (SDCHANNEL)", "SDCHANNEL", overlay=true) diff --git a/lib/channels/starchannel/starchannel.pine b/lib/channels/starchannel/starchannel.pine index 9ce0d868..a7a9539a 100644 --- a/lib/channels/starchannel/starchannel.pine +++ b/lib/channels/starchannel/starchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Stoller Average Range Channel (STARCHANNEL)", "STARCHANNEL", overlay=true) diff --git a/lib/channels/stbands/stbands.pine b/lib/channels/stbands/stbands.pine index d45c62bf..5f90fd2f 100644 --- a/lib/channels/stbands/stbands.pine +++ b/lib/channels/stbands/stbands.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Super Trend Bands (STBANDS)", "STBANDS", overlay=true) diff --git a/lib/channels/ubands/ubands.pine b/lib/channels/ubands/ubands.pine index e8cc4c5f..2346196d 100644 --- a/lib/channels/ubands/ubands.pine +++ b/lib/channels/ubands/ubands.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 // Ultimate Bands logic based on work by John F. Ehlers (c) 2024 diff --git a/lib/channels/uchannel/uchannel.md b/lib/channels/uchannel/uchannel.md index c82672b9..cda324df 100644 --- a/lib/channels/uchannel/uchannel.md +++ b/lib/channels/uchannel/uchannel.md @@ -7,14 +7,14 @@ | **Parameters** | `strPeriod` (default DefaultStrPeriod), `centerPeriod` (default DefaultCenterPeriod), `multiplier` (default DefaultMultiplier) | | **Outputs** | Multiple series (Upper, Middle, Lower, STR) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `Math.Max(strPeriod, centerPeriod)` bars | ### TL;DR - Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band widt... - Parameterized by `strperiod` (default defaultstrperiod), `centerperiod` (default defaultcenterperiod), `multiplier` (default defaultmultiplier). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `Math.Max(strPeriod, centerPeriod)` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band width, creating a channel where both the trend estimate and the volatility measure share the same low-lag, zero-overshoot filter characteristics. Unlike UBANDS which uses RMS of price residuals, UCHANNEL uses Smoothed True Range (STR) for band width, making it responsive to gap-inclusive volatility. Separate period parameters allow independent tuning of centerline smoothness and band-width responsiveness. diff --git a/lib/channels/uchannel/uchannel.pine b/lib/channels/uchannel/uchannel.pine index 9a04fa8e..780e23ee 100644 --- a/lib/channels/uchannel/uchannel.pine +++ b/lib/channels/uchannel/uchannel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 // Ultimate Channel logic based on work by John F. Ehlers (c) 2024 diff --git a/lib/channels/vwapbands/vwapbands.pine b/lib/channels/vwapbands/vwapbands.pine index 1b8f2446..2da9fd51 100644 --- a/lib/channels/vwapbands/vwapbands.pine +++ b/lib/channels/vwapbands/vwapbands.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("VWAP Bands (VWAPBANDS)", "VWAPBANDS", overlay=true) diff --git a/lib/channels/vwapsd/vwapsd.pine b/lib/channels/vwapsd/vwapsd.pine index 00d6110c..09c10547 100644 --- a/lib/channels/vwapsd/vwapsd.pine +++ b/lib/channels/vwapsd/vwapsd.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("VWAP with Standard Deviation Bands", "VWAPSD", overlay=true) diff --git a/lib/core/ringbuffer/RingBuffer.Tests.cs b/lib/core/ringbuffer/RingBuffer.Tests.cs index 27b7df8a..6795d2d6 100644 --- a/lib/core/ringbuffer/RingBuffer.Tests.cs +++ b/lib/core/ringbuffer/RingBuffer.Tests.cs @@ -12,7 +12,7 @@ public class RingBufferTests Assert.Equal(0, buffer.Count); Assert.False(buffer.IsFull); Assert.Equal(0, buffer.Sum); - Assert.Equal(0, buffer.Average); + Assert.True(double.IsNaN(buffer.Average)); } [Fact] @@ -192,7 +192,7 @@ public class RingBufferTests Assert.Equal(0, buffer.Count); Assert.Equal(0, buffer.Sum); - Assert.Equal(0, buffer.Average); + Assert.True(double.IsNaN(buffer.Average)); Assert.False(buffer.IsFull); } @@ -453,11 +453,11 @@ public class RingBufferTests } [Fact] - public void Average_EmptyBuffer_ReturnsZero() + public void Average_EmptyBuffer_ReturnsNaN() { var buffer = new RingBuffer(5); - Assert.Equal(0, buffer.Average); + Assert.True(double.IsNaN(buffer.Average)); } [Fact] diff --git a/lib/core/ringbuffer/RingBuffer.cs b/lib/core/ringbuffer/RingBuffer.cs index 0e85ee94..38a0e067 100644 --- a/lib/core/ringbuffer/RingBuffer.cs +++ b/lib/core/ringbuffer/RingBuffer.cs @@ -141,12 +141,12 @@ public sealed class RingBuffer : IEnumerable /// /// Average of all elements in the buffer. - /// Returns 0 if buffer is empty. + /// Returns if buffer is empty. /// public double Average { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _count > 0 ? _sum / _count : 0; + get => _count > 0 ? _sum / _count : double.NaN; } /// diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs index 0f2451a1..c84158ee 100644 --- a/lib/core/simd/SimdExtensions.cs +++ b/lib/core/simd/SimdExtensions.cs @@ -90,7 +90,7 @@ public static class SimdExtensions for (int i = 0; i < span.Length; i++) { double diff = span[i] - mean; - sumSquares += diff * diff; + sumSquares = Math.FusedMultiplyAdd(diff, diff, sumSquares); } return sumSquares / (span.Length - 1); } @@ -438,7 +438,7 @@ public static class SimdExtensions for (; i < span.Length; i++) { double diff = span[i] - m; - result += diff * diff; + result = Math.FusedMultiplyAdd(diff, diff, result); } return result / (span.Length - 1); @@ -682,17 +682,17 @@ public static class SimdExtensions // Unroll scalar loop with 4 accumulators to break dependency chains for (; i <= len - 4; i += 4) { - s1 += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); - s2 += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1); - s3 += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2); - s4 += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3); + s1 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i), Unsafe.Add(ref br, i), s1); + s2 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 1), Unsafe.Add(ref br, i + 1), s2); + s3 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 2), Unsafe.Add(ref br, i + 2), s3); + s4 = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i + 3), Unsafe.Add(ref br, i + 3), s4); } double s = s1 + s2 + s3 + s4; for (; i < len; i++) { - s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); + s = Math.FusedMultiplyAdd(Unsafe.Add(ref ar, i), Unsafe.Add(ref br, i), s); } return s; } diff --git a/lib/core/tseries/tseries.cs b/lib/core/tseries/tseries.cs index 6ac28d7c..746dc8fe 100644 --- a/lib/core/tseries/tseries.cs +++ b/lib/core/tseries/tseries.cs @@ -213,6 +213,12 @@ public class TSeries : IReadOnlyList, ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew); + /// + /// Synthetic timestamps: Each element receives a fabricated timestamp starting at + /// (captured once at call time) and incrementing by one minute + /// per item. These timestamps do not reflect real market time. Use the typed overloads + /// (, ) when real timestamps matter. + /// public void Add(IEnumerable values) { long t = DateTime.UtcNow.Ticks; diff --git a/lib/cycles/ccor/Ccor.Validation.Tests.cs b/lib/cycles/ccor/Ccor.Validation.Tests.cs index 0ca9cc84..32f98f77 100644 --- a/lib/cycles/ccor/Ccor.Validation.Tests.cs +++ b/lib/cycles/ccor/Ccor.Validation.Tests.cs @@ -364,5 +364,33 @@ public class CcorValidationTests "Different thresholds should produce different market state classifications"); } + [Fact] + public void Ccor_Correction_Recomputes() + { + var ind = new Ccor(period: 20); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + ind.Update(new TValue(t0.AddMinutes(i), + 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorPrice = 105.5; + ind.Update(new TValue(anchorTime, anchorPrice), isNew: true); + double anchorResult = ind.Last.Value; + + // Correction with a dramatically different price — recompute must yield different result + ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original price — must exactly restore original result + ind.Update(new TValue(anchorTime, anchorPrice), isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, Tolerance); + } + #endregion } diff --git a/lib/cycles/ccor/Ccor.cs b/lib/cycles/ccor/Ccor.cs index d992dffc..14046cc2 100644 --- a/lib/cycles/ccor/Ccor.cs +++ b/lib/cycles/ccor/Ccor.cs @@ -231,7 +231,7 @@ public sealed class Ccor : AbstractBase { foreach (double value in source) { - Update(new TValue(DateTime.UtcNow, value)); + Update(new TValue(DateTime.MinValue, value)); } } diff --git a/lib/cycles/ccor/ccor.pine b/lib/cycles/ccor/ccor.pine index 12a02baa..ae3ca750 100644 --- a/lib/cycles/ccor/ccor.pine +++ b/lib/cycles/ccor/ccor.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Correlation Cycle (CCOR)", "CCOR", overlay=false) diff --git a/lib/cycles/ccyc/ccyc.pine b/lib/cycles/ccyc/ccyc.pine index 21842d3d..8810086e 100644 --- a/lib/cycles/ccyc/ccyc.pine +++ b/lib/cycles/ccyc/ccyc.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Cyber Cycle (CCYC)", "CCYC", overlay=false) diff --git a/lib/cycles/cg/Cg.cs b/lib/cycles/cg/Cg.cs index 9652e94a..fb1e31d8 100644 --- a/lib/cycles/cg/Cg.cs +++ b/lib/cycles/cg/Cg.cs @@ -295,7 +295,7 @@ public sealed class Cg : AbstractBase int idx = (effectiveStart + j) % period; double price = buffer[idx]; int weight = j + 1; // 1-based weighting - weightedSum += weight * price; + weightedSum = Math.FusedMultiplyAdd(weight, price, weightedSum); sum += price; } diff --git a/lib/cycles/cg/cg.pine b/lib/cycles/cg/cg.pine index dab426dd..2c52116f 100644 --- a/lib/cycles/cg/cg.pine +++ b/lib/cycles/cg/cg.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Center of Gravity (CG)", "CG", overlay=false) diff --git a/lib/cycles/dsp/dsp.pine b/lib/cycles/dsp/dsp.pine index 88c07afb..981c7df1 100644 --- a/lib/cycles/dsp/dsp.pine +++ b/lib/cycles/dsp/dsp.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Detrended Synthetic Price (DSP)", "DSP", overlay=false) diff --git a/lib/cycles/eacp/Eacp.Validation.Tests.cs b/lib/cycles/eacp/Eacp.Validation.Tests.cs index 0fb55c84..ad48adf9 100644 --- a/lib/cycles/eacp/Eacp.Validation.Tests.cs +++ b/lib/cycles/eacp/Eacp.Validation.Tests.cs @@ -418,5 +418,33 @@ public class EacpValidationTests Assert.True(diff > 5, $"Should detect different cycles: {eacp1.DominantCycle} vs {eacp2.DominantCycle}"); } + [Fact] + public void Eacp_Correction_Recomputes() + { + var ind = new Eacp(8, 48, 3, true); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + ind.Update(new TValue(t0.AddMinutes(i), + 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorPrice = 105.5; + ind.Update(new TValue(anchorTime, anchorPrice), isNew: true); + double anchorResult = ind.Last.Value; + + // Correction with a dramatically different price — recompute must yield different result + ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original price — must exactly restore original result + ind.Update(new TValue(anchorTime, anchorPrice), isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, Tolerance); + } + #endregion } diff --git a/lib/cycles/eacp/Eacp.cs b/lib/cycles/eacp/Eacp.cs index 225b7003..78bf5f72 100644 --- a/lib/cycles/eacp/Eacp.cs +++ b/lib/cycles/eacp/Eacp.cs @@ -48,6 +48,7 @@ public sealed class Eacp : AbstractBase private readonly double[] _corr; private readonly double[] _power; private readonly double[] _smooth; + private readonly double[] _p_smooth; // State for filters and output [StructLayout(LayoutKind.Auto)] @@ -122,6 +123,7 @@ public sealed class Eacp : AbstractBase _corr = new double[size]; _power = new double[size]; _smooth = new double[size]; + _p_smooth = new double[size]; _filtHistory = new RingBuffer(size + maxPeriod); Name = $"Eacp({minPeriod},{maxPeriod})"; @@ -156,11 +158,13 @@ public sealed class Eacp : AbstractBase { _ps = _s; _filtHistory.Snapshot(); + Array.Copy(_smooth, _p_smooth, _smooth.Length); } else { _s = _ps; _filtHistory.Restore(); + Array.Copy(_p_smooth, _smooth, _smooth.Length); } var s = _s; @@ -407,6 +411,7 @@ public sealed class Eacp : AbstractBase Array.Clear(_corr); Array.Clear(_power); Array.Clear(_smooth); + Array.Clear(_p_smooth); Last = default; } @@ -414,7 +419,7 @@ public sealed class Eacp : AbstractBase { foreach (double value in source) { - Update(new TValue(DateTime.UtcNow, value)); + Update(new TValue(DateTime.MinValue, value)); } } @@ -459,7 +464,7 @@ public sealed class Eacp : AbstractBase var eacp = new Eacp(minPeriod, maxPeriod, avgLength, enhance); for (int i = 0; i < len; i++) { - var result = eacp.Update(new TValue(DateTime.UtcNow, source[i])); + var result = eacp.Update(new TValue(DateTime.MinValue, source[i])); output[i] = result.Value; } } diff --git a/lib/cycles/eacp/eacp.pine b/lib/cycles/eacp/eacp.pine index 0902232b..def587bc 100644 --- a/lib/cycles/eacp/eacp.pine +++ b/lib/cycles/eacp/eacp.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Autocorrelation Periodogram (EACP)","EACP",overlay=false) diff --git a/lib/cycles/ebsw/ebsw.md b/lib/cycles/ebsw/ebsw.md index f2859512..65bead80 100644 --- a/lib/cycles/ebsw/ebsw.md +++ b/lib/cycles/ebsw/ebsw.md @@ -7,14 +7,14 @@ | **Parameters** | `hpLength` (default 40), `ssfLength` (default 10) | | **Outputs** | Single series (Ebsw) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `Math.Max(hpLength, ssfLength) + 3` bars (default 43) | ### TL;DR - EBSW is a refined cycle oscillator that combines a high-pass filter (trend removal), a Super-Smoother filter (noise removal), and Automatic Gain Co... - Parameterized by `hplength` (default 40), `ssflength` (default 10). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `Math.Max(hpLength, ssfLength) + 3` bars (default 43) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. EBSW is a refined cycle oscillator that combines a high-pass filter (trend removal), a Super-Smoother filter (noise removal), and Automatic Gain Control to produce a normalized $[-1, +1]$ output representing the current position within the dominant market cycle. Developed by John Ehlers as an improvement over the original Hilbert Transform SineWave, it provides cleaner turning point detection without requiring complex phase extraction mathematics. diff --git a/lib/cycles/ebsw/ebsw.pine b/lib/cycles/ebsw/ebsw.pine index fe7b2620..a7d005e1 100644 --- a/lib/cycles/ebsw/ebsw.pine +++ b/lib/cycles/ebsw/ebsw.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Even Better Sinewave (EBSW)", "EBSW", overlay=false) diff --git a/lib/cycles/homod/homod.md b/lib/cycles/homod/homod.md index 7366c1cb..40800f5d 100644 --- a/lib/cycles/homod/homod.md +++ b/lib/cycles/homod/homod.md @@ -7,14 +7,14 @@ | **Parameters** | `minPeriod` (default 6.0), `maxPeriod` (default 50.0) | | **Outputs** | Single series (Homod) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `maxPeriod * 2` bars (default 100) | ### TL;DR - HOMOD estimates the dominant cycle period of a market using homodyne mixing, a technique from radio engineering where a signal is multiplied by a d... - Parameterized by `minperiod` (default 6.0), `maxperiod` (default 50.0). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `maxPeriod * 2` bars (default 100) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. HOMOD estimates the dominant cycle period of a market using homodyne mixing, a technique from radio engineering where a signal is multiplied by a delayed copy of itself to expose the angular phase change between samples. The output is a continuously varying period measurement (in bars) that tracks the market's instantaneous cycle length, enabling adaptive indicator tuning. Developed by John Ehlers, it offers better noise rejection and stability than the raw Hilbert Transform period estimator. diff --git a/lib/cycles/homod/homod.pine b/lib/cycles/homod/homod.pine index f1a40ae5..6c93b96a 100644 --- a/lib/cycles/homod/homod.pine +++ b/lib/cycles/homod/homod.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Homodyne Discriminator (HOMOD)","HOMOD",overlay=false) diff --git a/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs b/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs index a0a50b67..a81c1c32 100644 --- a/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs +++ b/lib/cycles/ht_dcperiod/HtDcperiod.Validation.Tests.cs @@ -92,4 +92,32 @@ public sealed class HtDcperiodValidationTests : IDisposable var q = new HtDcperiod(); Assert.Equal(talibLookback, q.WarmupPeriod); } + + [Fact] + public void HtDcperiod_Correction_Recomputes() + { + var ind = new HtDcperiod(); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + ind.Update(new TValue(t0.AddMinutes(i), + 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorPrice = 105.5; + ind.Update(new TValue(anchorTime, anchorPrice), isNew: true); + double anchorResult = ind.Last.Value; + + // Correction with a dramatically different price — recompute must yield different result + ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original price — must exactly restore original result + ind.Update(new TValue(anchorTime, anchorPrice), isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, 1e-9); + } } diff --git a/lib/cycles/ht_dcperiod/ht_dcperiod.pine b/lib/cycles/ht_dcperiod/ht_dcperiod.pine index 08a90716..03fc68d5 100644 --- a/lib/cycles/ht_dcperiod/ht_dcperiod.pine +++ b/lib/cycles/ht_dcperiod/ht_dcperiod.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Hilbert Transform Dominant Cycle Period (HT_DCPERIOD)", "HT_DCPERIOD", overlay=false) diff --git a/lib/cycles/ht_dcphase/HtDcphase.Tests.cs b/lib/cycles/ht_dcphase/HtDcphase.Tests.cs index 71887d53..98f2b601 100644 --- a/lib/cycles/ht_dcphase/HtDcphase.Tests.cs +++ b/lib/cycles/ht_dcphase/HtDcphase.Tests.cs @@ -139,8 +139,8 @@ public class HtDcphaseTests // First update (new bar) var result1 = ht.Update(new TValue(now.AddMinutes(70), 105), isNew: true); - // Same bar update - var result2 = ht.Update(new TValue(now.AddMinutes(70), 106), isNew: false); + // Same bar update with the same price — must yield identical result + var result2 = ht.Update(new TValue(now.AddMinutes(70), 105), isNew: false); Assert.Equal(result1.Value, result2.Value); } diff --git a/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs b/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs index 2c50481b..1153b810 100644 --- a/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs +++ b/lib/cycles/ht_dcphase/HtDcphase.Validation.Tests.cs @@ -92,4 +92,32 @@ public sealed class HtDcphaseValidationTests : IDisposable var q = new HtDcphase(); Assert.Equal(talibLookback, q.WarmupPeriod); } + + [Fact] + public void HtDcphase_Correction_Recomputes() + { + var ind = new HtDcphase(); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + ind.Update(new TValue(t0.AddMinutes(i), + 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorPrice = 105.5; + ind.Update(new TValue(anchorTime, anchorPrice), isNew: true); + double anchorResult = ind.Last.Value; + + // Correction with a dramatically different price — recompute must yield different result + ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original price — must exactly restore original result + ind.Update(new TValue(anchorTime, anchorPrice), isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, 1e-9); + } } diff --git a/lib/cycles/ht_dcphase/HtDcphase.cs b/lib/cycles/ht_dcphase/HtDcphase.cs index f2baf2b9..21f471ea 100644 --- a/lib/cycles/ht_dcphase/HtDcphase.cs +++ b/lib/cycles/ht_dcphase/HtDcphase.cs @@ -217,12 +217,10 @@ public sealed class HtDcphase : AbstractBase } else { - // Same-bar update: restore previous state and return cached result from Last _state = _p_state; Array.Copy(_p_circBuffer, _circBuffer, CIRC_BUFFER_SIZE); Array.Copy(_p_smoothPrice, _smoothPrice, SMOOTH_PRICE_SIZE); Array.Copy(_p_priceHistory, _priceHistory, PRICE_HISTORY_SIZE); - return Last.Value; } var s = _state; diff --git a/lib/cycles/ht_dcphase/ht_dcphase.pine b/lib/cycles/ht_dcphase/ht_dcphase.pine index 5b2fb9b6..e5a4f00c 100644 --- a/lib/cycles/ht_dcphase/ht_dcphase.pine +++ b/lib/cycles/ht_dcphase/ht_dcphase.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Hilbert Transform Dominant Cycle Phase (HT_DCPHASE)", "HT_DCPHASE", overlay=false) diff --git a/lib/cycles/ht_phasor/phasor.pine b/lib/cycles/ht_phasor/phasor.pine index 2efa687e..3dc0782c 100644 --- a/lib/cycles/ht_phasor/phasor.pine +++ b/lib/cycles/ht_phasor/phasor.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Hilbert Transform Phasor Components (HT_PHASOR)", "HT_PHASOR", overlay=false) diff --git a/lib/cycles/ht_sine/HtSine.Validation.Tests.cs b/lib/cycles/ht_sine/HtSine.Validation.Tests.cs index 7db0a6d1..42f6255d 100644 --- a/lib/cycles/ht_sine/HtSine.Validation.Tests.cs +++ b/lib/cycles/ht_sine/HtSine.Validation.Tests.cs @@ -122,4 +122,34 @@ public sealed class HtSineValidationTests : IDisposable Assert.Equal(talibLookback, htSine.WarmupPeriod); } + + [Fact] + public void HtSine_Correction_Recomputes() + { + var ind = new HtSine(); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + ind.Update(new TValue(t0.AddMinutes(i), + 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorPrice = 105.5; + ind.Update(new TValue(anchorTime, anchorPrice), isNew: true); + double anchorSine = ind.Last.Value; + double anchorLeadSine = ind.LeadSine; + + // Correction with a dramatically different price — recompute must yield different results + ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false); + Assert.NotEqual(anchorSine, ind.Last.Value); + + // Correction back to original price — must exactly restore original results + ind.Update(new TValue(anchorTime, anchorPrice), isNew: false); + Assert.Equal(anchorSine, ind.Last.Value, 1e-9); + Assert.Equal(anchorLeadSine, ind.LeadSine, 1e-9); + } } diff --git a/lib/cycles/ht_sine/HtSine.cs b/lib/cycles/ht_sine/HtSine.cs index 74d51ee6..bd81f0c6 100644 --- a/lib/cycles/ht_sine/HtSine.cs +++ b/lib/cycles/ht_sine/HtSine.cs @@ -295,7 +295,6 @@ public sealed class HtSine : AbstractBase Array.Copy(_circBuffer, _p_circBuffer, CIRC_BUFFER_SIZE); Array.Copy(_smoothPrice, _p_smoothPrice, SMOOTH_PRICE_SIZE); Array.Copy(_priceHistory, _p_priceHistory, PRICE_HISTORY_SIZE); - _state.Today++; } else { @@ -305,6 +304,8 @@ public sealed class HtSine : AbstractBase Array.Copy(_p_priceHistory, _priceHistory, PRICE_HISTORY_SIZE); } + _state.Today++; + // Local copy of state for struct promotion (AGENTS.md §2.5) var s = _state; diff --git a/lib/cycles/ht_sine/ht_sine.pine b/lib/cycles/ht_sine/ht_sine.pine index 8a686602..a9fe9ed3 100644 --- a/lib/cycles/ht_sine/ht_sine.pine +++ b/lib/cycles/ht_sine/ht_sine.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Hilbert Transform SineWave (HT_SINE)", "HT_SINE", overlay=false) diff --git a/lib/cycles/lunar/lunar.pine b/lib/cycles/lunar/lunar.pine index a4427f05..52ff7132 100644 --- a/lib/cycles/lunar/lunar.pine +++ b/lib/cycles/lunar/lunar.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Lunar Phase (LUNAR)", "LUNAR", overlay=false) diff --git a/lib/cycles/solar/solar.pine b/lib/cycles/solar/solar.pine index f0bab0c7..87f0d29a 100644 --- a/lib/cycles/solar/solar.pine +++ b/lib/cycles/solar/solar.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Solar Cycle (SOLAR)", "SOLAR", overlay=false) diff --git a/lib/cycles/ssfdsp/ssfdsp.pine b/lib/cycles/ssfdsp/ssfdsp.pine index 457917d2..5031dbbe 100644 --- a/lib/cycles/ssfdsp/ssfdsp.pine +++ b/lib/cycles/ssfdsp/ssfdsp.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers SSF Detrended Synthetic Price (SSFDSP)", "SSF-DSP", overlay=false) diff --git a/lib/dynamics/_index.md b/lib/dynamics/_index.md index a0bf8a0d..5eb2d6b7 100644 --- a/lib/dynamics/_index.md +++ b/lib/dynamics/_index.md @@ -17,6 +17,8 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu | [CHOP](chop/Chop.md) | Choppiness Index | Trendiness measure. High values = choppy. Low = trending. | | [DMX](dmx/Dmx.md) | Jurik DMX | Smoothed bipolar DMI using Jurik smoothing. Low noise. | | [DX](dx/Dx.md) | Directional Movement Index | Raw directional strength. Unsmoothed ADX component. | +| [MINUS_DI](minusdi/MinusDi.md) | Minus Directional Indicator | Downward directional movement as % of true range. 0-100. | +| [MINUS_DM](minusdm/MinusDm.md) | Minus Directional Movement | Wilder-smoothed downward directional movement. Price units. | | [HT_TRENDMODE](ht_trendmode/Httrendmode.md) | Ehlers Hilbert Transform Trend vs Cycle Mode | Ehlers Hilbert Transform. Binary trend/cycle mode detection. | | [ICHIMOKU](ichimoku/Ichimoku.md) | Ichimoku Cloud | Five-line system. Cloud defines support/resistance zones. | | [IMPULSE](impulse/Impulse.md) | Elder Impulse System | EMA + MACD histogram alignment. Color-coded trend/momentum filter. | @@ -27,5 +29,7 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu | [VORTEX](vortex/Vortex.md) | Vortex Indicator | VI+ and VI- measure positive/negative trend movement. | | [GHLA](ghla/Ghla.md) | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover. | | [PFE](pfe/Pfe.md) | Polarized Fractal Efficiency | Trend efficiency: straight-line / total path distance. | +| [PLUS_DI](plusdi/PlusDi.md) | Plus Directional Indicator | Upward directional movement as % of true range. 0-100. | +| [PLUS_DM](plusdm/PlusDm.md) | Plus Directional Movement | Wilder-smoothed upward directional movement. Price units. | | [RAVI](ravi/Ravi.md) | Chande Range Action Verification Index | \|SMA(short) − SMA(long)\| / SMA(long) × 100. | | [VHF](vhf/Vhf.md) | Vertical Horizontal Filter | Max-min range / sum of absolute changes. | diff --git a/lib/dynamics/adx/Adx.Validation.Tests.cs b/lib/dynamics/adx/Adx.Validation.Tests.cs index 36fdc7af..1f98c88c 100644 --- a/lib/dynamics/adx/Adx.Validation.Tests.cs +++ b/lib/dynamics/adx/Adx.Validation.Tests.cs @@ -91,6 +91,78 @@ public sealed class AdxValidationTests : IDisposable ValidationHelper.VerifyData(results, tulipResults, lookback: offset); } + [Fact] + public void DiPlus_MatchesTalib() + { + var adx = new Adx(14); + var diPlusResults = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + adx.Update(_data.Bars[i]); + diPlusResults.Add(adx.DiPlus.Value); + } + + double[] hData = _data.Bars.High.Select(x => x.Value).ToArray(); + double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray(); + double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray(); + double[] outReal = new double[_data.Bars.Count]; + + var retCode = Functions.PlusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14); + Assert.Equal(TALib.Core.RetCode.Success, retCode); + + int lookback = Functions.PlusDILookback(14); + ValidationHelper.VerifyData(diPlusResults, outReal, outRange, lookback); + } + + [Fact] + public void DiMinus_MatchesTalib() + { + var adx = new Adx(14); + var diMinusResults = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + adx.Update(_data.Bars[i]); + diMinusResults.Add(adx.DiMinus.Value); + } + + double[] hData = _data.Bars.High.Select(x => x.Value).ToArray(); + double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray(); + double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray(); + double[] outReal = new double[_data.Bars.Count]; + + var retCode = Functions.MinusDI(hData, lData, cData, 0..^0, outReal, out var outRange, 14); + Assert.Equal(TALib.Core.RetCode.Success, retCode); + + int lookback = Functions.MinusDILookback(14); + ValidationHelper.VerifyData(diMinusResults, outReal, outRange, lookback); + } + + [Fact] + public void MatchesSkender_DiValues() + { + var adx = new Adx(14); + var diPlusResults = new List(); + var diMinusResults = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + adx.Update(_data.Bars[i]); + diPlusResults.Add(adx.DiPlus.Value); + diMinusResults.Add(adx.DiMinus.Value); + } + + // Skender's GetAdx returns ADX with +DI and -DI values + var skenderResults = _data.SkenderQuotes.GetAdx(14).ToList(); + + // Verify +DI + ValidationHelper.VerifyData(diPlusResults, skenderResults, x => x.Pdi); + + // Verify -DI + ValidationHelper.VerifyData(diMinusResults, skenderResults, x => x.Mdi); + } + [Fact] public void MatchesOoples() { diff --git a/lib/dynamics/adx/Adx.cs b/lib/dynamics/adx/Adx.cs index 6c491489..957b6257 100644 --- a/lib/dynamics/adx/Adx.cs +++ b/lib/dynamics/adx/Adx.cs @@ -62,6 +62,16 @@ public sealed class Adx : ITValuePublisher /// public TValue DiMinus { get; private set; } + /// + /// Current smoothed +DM value (RMA-smoothed raw plus directional movement, before TR normalization). + /// + public TValue DmPlus { get; private set; } + + /// + /// Current smoothed -DM value (RMA-smoothed raw minus directional movement, before TR normalization). + /// + public TValue DmMinus { get; private set; } + /// /// True if the ADX has warmed up and is providing valid results. /// @@ -116,6 +126,8 @@ public sealed class Adx : ITValuePublisher Last = default; DiPlus = default; DiMinus = default; + DmPlus = default; + DmMinus = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -325,6 +337,8 @@ public sealed class Adx : ITValuePublisher DiPlus = new TValue(input.Time, diPlus); DiMinus = new TValue(input.Time, diMinus); + DmPlus = new TValue(input.Time, _samples >= _period ? _dmPlusSmooth : 0); + DmMinus = new TValue(input.Time, _samples >= _period ? _dmMinusSmooth : 0); Last = new TValue(input.Time, finalAdx); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); diff --git a/lib/dynamics/adx/adx.pine b/lib/dynamics/adx/adx.pine index c3c8db0b..12eac1ff 100644 --- a/lib/dynamics/adx/adx.pine +++ b/lib/dynamics/adx/adx.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Average Directional Movement Index (ADX)", "ADX", overlay=false) diff --git a/lib/dynamics/adxr/adxr.pine b/lib/dynamics/adxr/adxr.pine index 5dbc3992..dbe3099f 100644 --- a/lib/dynamics/adxr/adxr.pine +++ b/lib/dynamics/adxr/adxr.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Average Directional Movement Index Rating (ADXR)", "ADXR", overlay=false) diff --git a/lib/dynamics/alligator/alligator.pine b/lib/dynamics/alligator/alligator.pine index f7e1be3a..5714b6cd 100644 --- a/lib/dynamics/alligator/alligator.pine +++ b/lib/dynamics/alligator/alligator.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Williams Alligator", "ALLIGATOR", overlay=true) diff --git a/lib/dynamics/amat/amat.pine b/lib/dynamics/amat/amat.pine index 454814cd..48c67c73 100644 --- a/lib/dynamics/amat/amat.pine +++ b/lib/dynamics/amat/amat.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Archer Moving Averages Trends (AMAT)", "AMAT", overlay=false) diff --git a/lib/dynamics/aroon/aroon.pine b/lib/dynamics/aroon/aroon.pine index b19b704a..992beae6 100644 --- a/lib/dynamics/aroon/aroon.pine +++ b/lib/dynamics/aroon/aroon.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Aroon (AROON)", "AROON", overlay=false) diff --git a/lib/dynamics/aroonosc/aroonosc.pine b/lib/dynamics/aroonosc/aroonosc.pine index 01dfbdea..ff41b982 100644 --- a/lib/dynamics/aroonosc/aroonosc.pine +++ b/lib/dynamics/aroonosc/aroonosc.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Aroon Oscillator", "AROONOSC", overlay=false) diff --git a/lib/dynamics/chop/chop.pine b/lib/dynamics/chop/chop.pine index fb92f272..848a9175 100644 --- a/lib/dynamics/chop/chop.pine +++ b/lib/dynamics/chop/chop.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Choppiness Index", "CHOP", overlay=false) diff --git a/lib/dynamics/dmx/dmx.pine b/lib/dynamics/dmx/dmx.pine index 1dbad938..fdbb89e5 100644 --- a/lib/dynamics/dmx/dmx.pine +++ b/lib/dynamics/dmx/dmx.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Jurik Directional Movement Index (DMX)", "DMX", overlay=false) diff --git a/lib/dynamics/dx/Dx.Validation.Tests.cs b/lib/dynamics/dx/Dx.Validation.Tests.cs index 40711525..3130b97e 100644 --- a/lib/dynamics/dx/Dx.Validation.Tests.cs +++ b/lib/dynamics/dx/Dx.Validation.Tests.cs @@ -102,6 +102,52 @@ public sealed class DxValidationTests : IDisposable ValidationHelper.VerifyData(diMinusResults, outReal, outRange, lookback); } + [Fact] + public void DmPlus_MatchesTalib() + { + var dx = new Dx(14); + var dmPlusResults = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + dx.Update(_data.Bars[i]); + dmPlusResults.Add(dx.DmPlus.Value); + } + + double[] hData = _data.Bars.High.Select(x => x.Value).ToArray(); + double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray(); + double[] outReal = new double[_data.Bars.Count]; + + var retCode = Functions.PlusDM(hData, lData, 0..^0, outReal, out var outRange, 14); + Assert.Equal(TALib.Core.RetCode.Success, retCode); + + int lookback = Functions.PlusDMLookback(14); + ValidationHelper.VerifyData(dmPlusResults, outReal, outRange, lookback); + } + + [Fact] + public void DmMinus_MatchesTalib() + { + var dx = new Dx(14); + var dmMinusResults = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + dx.Update(_data.Bars[i]); + dmMinusResults.Add(dx.DmMinus.Value); + } + + double[] hData = _data.Bars.High.Select(x => x.Value).ToArray(); + double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray(); + double[] outReal = new double[_data.Bars.Count]; + + var retCode = Functions.MinusDM(hData, lData, 0..^0, outReal, out var outRange, 14); + Assert.Equal(TALib.Core.RetCode.Success, retCode); + + int lookback = Functions.MinusDMLookback(14); + ValidationHelper.VerifyData(dmMinusResults, outReal, outRange, lookback); + } + [Fact] public void MatchesSkender_DiValues() { diff --git a/lib/dynamics/dx/Dx.cs b/lib/dynamics/dx/Dx.cs index db5c1beb..df8a675a 100644 --- a/lib/dynamics/dx/Dx.cs +++ b/lib/dynamics/dx/Dx.cs @@ -53,6 +53,18 @@ public sealed class Dx : ITValuePublisher /// public TValue DiMinus { get; private set; } + /// + /// Current smoothed +DM value (Wilder-smoothed raw plus directional movement, before TR normalization). + /// Equivalent to TA-Lib PLUS_DM. + /// + public TValue DmPlus { get; private set; } + + /// + /// Current smoothed -DM value (Wilder-smoothed raw minus directional movement, before TR normalization). + /// Equivalent to TA-Lib MINUS_DM. + /// + public TValue DmMinus { get; private set; } + /// /// True if the DX has warmed up and is providing valid results. /// @@ -106,6 +118,8 @@ public sealed class Dx : ITValuePublisher Last = default; DiPlus = default; DiMinus = default; + DmPlus = default; + DmMinus = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -276,6 +290,8 @@ public sealed class Dx : ITValuePublisher DiPlus = new TValue(input.Time, diPlus); DiMinus = new TValue(input.Time, diMinus); + DmPlus = new TValue(input.Time, _samples >= _period ? _dmPlusSmooth : 0); + DmMinus = new TValue(input.Time, _samples >= _period ? _dmMinusSmooth : 0); Last = new TValue(input.Time, dx); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); diff --git a/lib/dynamics/dx/dx.pine b/lib/dynamics/dx/dx.pine index 112b7036..d32b4ae4 100644 --- a/lib/dynamics/dx/dx.pine +++ b/lib/dynamics/dx/dx.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Directional Movement Index (DX)", "DX", overlay=false) diff --git a/lib/dynamics/ghla/ghla.pine b/lib/dynamics/ghla/ghla.pine index 656264c9..1f44609c 100644 --- a/lib/dynamics/ghla/ghla.pine +++ b/lib/dynamics/ghla/ghla.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Gann High-Low Activator", "GHLA", overlay=true) diff --git a/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs b/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs index cb83ae72..6283ff47 100644 --- a/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs +++ b/lib/dynamics/ht_trendmode/HtTrendmode.Validation.Tests.cs @@ -197,4 +197,32 @@ public sealed class HtTrendmodeValidationTests : IDisposable Assert.Equal(results1[i], results2[i]); } } + + [Fact] + public void HtTrendmode_Correction_Recomputes() + { + var ind = new HtTrendmode(); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + ind.Update(new TValue(t0.AddMinutes(i), + 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorPrice = 105.5; + ind.Update(new TValue(anchorTime, anchorPrice), isNew: true); + double anchorSmooth = ind.SmoothPeriod; + + // Correction with a dramatically different price — SmoothPeriod must change + ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false); + Assert.NotEqual(anchorSmooth, ind.SmoothPeriod); + + // Correction back to original price — must exactly restore original SmoothPeriod + ind.Update(new TValue(anchorTime, anchorPrice), isNew: false); + Assert.Equal(anchorSmooth, ind.SmoothPeriod, 1e-9); + } } diff --git a/lib/dynamics/ht_trendmode/ht_trendmode.pine b/lib/dynamics/ht_trendmode/ht_trendmode.pine index 6c1e801d..954db3ed 100644 --- a/lib/dynamics/ht_trendmode/ht_trendmode.pine +++ b/lib/dynamics/ht_trendmode/ht_trendmode.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Hilbert Transform Trend vs Cycle Mode (HT_TRENDMODE)", "HT_TRENDMODE", overlay=false) diff --git a/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs b/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs index 613efc76..4565387c 100644 --- a/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs +++ b/lib/dynamics/ichimoku/Ichimoku.Validation.Tests.cs @@ -679,4 +679,32 @@ public sealed class IchimokuValidationTests : IDisposable } #endregion + + [Fact] + public void Ichimoku_Correction_Recomputes() + { + var ind = new Ichimoku(); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + double p = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0); + ind.Update(new TBar(t0.AddMinutes(i), p, p + 2, p - 2, p, 1000), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorClose = 105.5; + ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: true); + double anchorTenkan = ind.Tenkan.Value; + + // Correction with a dramatically different price — Tenkan must change + ind.Update(new TBar(anchorTime, anchorClose * 10, (anchorClose + 2) * 10, (anchorClose - 2) * 10, anchorClose * 10, 1000), isNew: false); + Assert.NotEqual(anchorTenkan, ind.Tenkan.Value); + + // Correction back to original price — must exactly restore original Tenkan + ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: false); + Assert.Equal(anchorTenkan, ind.Tenkan.Value, 1e-9); + } } diff --git a/lib/dynamics/ichimoku/ichimoku.pine b/lib/dynamics/ichimoku/ichimoku.pine index bd1debba..406782f1 100644 --- a/lib/dynamics/ichimoku/ichimoku.pine +++ b/lib/dynamics/ichimoku/ichimoku.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ichimoku Cloud", "ICHIMOKU", overlay=true) diff --git a/lib/dynamics/impulse/Impulse.md b/lib/dynamics/impulse/Impulse.md index a5f77a54..9859a266 100644 --- a/lib/dynamics/impulse/Impulse.md +++ b/lib/dynamics/impulse/Impulse.md @@ -7,14 +7,14 @@ | **Parameters** | `emaPeriod` (default 13), `macdFast` (default 12), `macdSlow` (default 26), `macdSignal` (default 9) | | **Outputs** | Single series (Impulse) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `Math.Max(emaPeriod, macdSlow) + macdSignal - 1` bars (default 34) | ### TL;DR - The Elder Impulse System combines a 13-period EMA (trend inertia) with the MACD(12,26,9) histogram (momentum acceleration) to classify each bar as ... - Parameterized by `emaperiod` (default 13), `macdfast` (default 12), `macdslow` (default 26), `macdsignal` (default 9). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `Math.Max(emaPeriod, macdSlow) + macdSignal - 1` bars (default 34) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The Impulse System identifies inflection points where a trend speeds up or slows down." -- Alexander Elder, *Come Into My Trading Room* diff --git a/lib/dynamics/minusdi/MinusDi.cs b/lib/dynamics/minusdi/MinusDi.cs new file mode 100644 index 00000000..cf01d35e --- /dev/null +++ b/lib/dynamics/minusdi/MinusDi.cs @@ -0,0 +1,121 @@ +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// MINUS_DI: Minus Directional Indicator (Wilder, 1978) +/// +/// +/// Measures downward directional movement as a percentage of true range. +/// Extracted from the DX calculation: -DI = Smoothed(-DM) / Smoothed(TR) × 100. +/// Range: 0 to 100. Higher values indicate stronger downward movement. +/// +[SkipLocalsInit] +public sealed class MinusDi : ITValuePublisher +{ + private readonly Dx _dx; + + /// Display name for the indicator. + public string Name { get; } + + public event TValuePublishedHandler? Pub; + + /// Current -DI value. + public TValue Last { get; private set; } + + /// True when the indicator has warmed up. + public bool IsHot => _dx.IsHot; + + /// Bars required for warmup. + public int WarmupPeriod => _dx.WarmupPeriod; + + /// The period parameter. + public int Period => _dx.Period; + + /// + /// Creates MinusDi with specified period. + /// + /// Wilder smoothing period (must be > 0) + public MinusDi(int period = 14) + { + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + _dx = new Dx(period); + Name = $"MinusDi({period})"; + } + + /// + /// Creates MinusDi and immediately processes the bar series. + /// + public MinusDi(TBarSeries source, int period = 14) : this(period) + { + var result = Batch(source, period); + Last = result[^1]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar input, bool isNew = true) + { + _dx.Update(input, isNew); + Last = _dx.DiMinus; + if (isNew) + { + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + } + return Last; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue input, bool isNew = true) + { + // DI requires OHLC data — scalar update not meaningful + return Last; + } + + public TSeries Update(TBarSeries source) + { + var result = new TSeries(source.Count); + foreach (var bar in source) + { + result.Add(Update(bar)); + } + return result; + } + + /// + /// Initializes the indicator state using the provided bar series history. + /// + public void Prime(TBarSeries source) + { + foreach (var bar in source) + { + Update(bar); + } + } + + public void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + // Not applicable — DI requires OHLC bar data, not scalar values + } + + public static TSeries Batch(TBarSeries source, int period = 14) + { + var indicator = new MinusDi(period); + return indicator.Update(source); + } + + public static (TSeries Results, MinusDi Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new MinusDi(period); + return (indicator.Update(source), indicator); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _dx.Reset(); + Last = default; + } +} diff --git a/lib/dynamics/minusdi/MinusDi.md b/lib/dynamics/minusdi/MinusDi.md new file mode 100644 index 00000000..9ad88342 --- /dev/null +++ b/lib/dynamics/minusdi/MinusDi.md @@ -0,0 +1,31 @@ +# MINUS_DI: Minus Directional Indicator + +### TL;DR +Measures downward directional movement strength as a percentage (0-100). + +## Introduction +The Minus Directional Indicator (-DI) measures the strength of downward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr. + +When -DI is rising, downward price pressure is increasing. When -DI crosses above +DI, it signals a potential bearish trend. The -DI line is commonly plotted alongside +DI to visualize directional balance. + +## Calculation +-DI = Smoothed(-DM) / Smoothed(TR) × 100 + +Where: +- -DM (Minus Directional Movement) = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0 +- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|) +- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input + +## Parameters +| Parameter | Default | Range | Description | +| :--- | :--- | :--- | :--- | +| Period | 14 | 2-∞ | Wilder smoothing period | + +## Interpretation +- **Rising -DI:** Strengthening downward movement +- **-DI > +DI:** Bears dominate; potential downtrend +- **-DI crossover above +DI:** Bearish signal +- **High -DI (>40):** Strong downward momentum + +## References +- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978) diff --git a/lib/dynamics/minusdm/MinusDm.cs b/lib/dynamics/minusdm/MinusDm.cs new file mode 100644 index 00000000..d8308b12 --- /dev/null +++ b/lib/dynamics/minusdm/MinusDm.cs @@ -0,0 +1,121 @@ +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// MINUS_DM: Minus Directional Movement (Wilder, 1978) +/// +/// +/// Wilder-smoothed downward directional movement in price units. +/// Extracted from the DX calculation: Smoothed(-DM) using Wilder's method. +/// Values ≥ 0 in price units. Higher values indicate stronger downward movement magnitude. +/// +[SkipLocalsInit] +public sealed class MinusDm : ITValuePublisher +{ + private readonly Dx _dx; + + /// Display name for the indicator. + public string Name { get; } + + public event TValuePublishedHandler? Pub; + + /// Current smoothed -DM value. + public TValue Last { get; private set; } + + /// True when the indicator has warmed up. + public bool IsHot => _dx.IsHot; + + /// Bars required for warmup. + public int WarmupPeriod => _dx.WarmupPeriod; + + /// The period parameter. + public int Period => _dx.Period; + + /// + /// Creates MinusDm with specified period. + /// + /// Wilder smoothing period (must be > 0) + public MinusDm(int period = 14) + { + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + _dx = new Dx(period); + Name = $"MinusDm({period})"; + } + + /// + /// Creates MinusDm and immediately processes the bar series. + /// + public MinusDm(TBarSeries source, int period = 14) : this(period) + { + var result = Batch(source, period); + Last = result[^1]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar input, bool isNew = true) + { + _dx.Update(input, isNew); + Last = _dx.DmMinus; + if (isNew) + { + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + } + return Last; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue input, bool isNew = true) + { + // DM requires OHLC data — scalar update not meaningful + return Last; + } + + public TSeries Update(TBarSeries source) + { + var result = new TSeries(source.Count); + foreach (var bar in source) + { + result.Add(Update(bar)); + } + return result; + } + + /// + /// Initializes the indicator state using the provided bar series history. + /// + public void Prime(TBarSeries source) + { + foreach (var bar in source) + { + Update(bar); + } + } + + public void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + // Not applicable — DM requires OHLC bar data, not scalar values + } + + public static TSeries Batch(TBarSeries source, int period = 14) + { + var indicator = new MinusDm(period); + return indicator.Update(source); + } + + public static (TSeries Results, MinusDm Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new MinusDm(period); + return (indicator.Update(source), indicator); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _dx.Reset(); + Last = default; + } +} diff --git a/lib/dynamics/minusdm/MinusDm.md b/lib/dynamics/minusdm/MinusDm.md new file mode 100644 index 00000000..721dc068 --- /dev/null +++ b/lib/dynamics/minusdm/MinusDm.md @@ -0,0 +1,28 @@ +# MINUS_DM: Minus Directional Movement + +### TL;DR +Wilder-smoothed downward directional movement in price units (≥0). + +## Introduction +Minus Directional Movement (-DM) measures the magnitude of downward price movement, smoothed using Wilder's method. Unlike -DI which normalizes by true range to produce a percentage, -DM outputs raw smoothed values in price units. + +-DM captures when the previous bar's low exceeds the current bar's low by more than the current bar's high exceeds the previous bar's high. It is the raw building block of the Directional Movement System. + +## Calculation +-DM = max(PrevLow - Low, 0) when PrevLow - Low > High - PrevHigh, else 0 + +Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input + +## Parameters +| Parameter | Default | Range | Description | +| :--- | :--- | :--- | :--- | +| Period | 14 | 2-∞ | Wilder smoothing period | + +## Interpretation +- **Rising -DM:** Increasing downward price extension +- **-DM > +DM:** Downward movement exceeds upward movement +- **Zero -DM:** No downward directional movement on the bar +- Values are in price units and scale with the instrument + +## References +- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978) diff --git a/lib/dynamics/pfe/pfe.pine b/lib/dynamics/pfe/pfe.pine index 56dfbefd..0ee77dbc 100644 --- a/lib/dynamics/pfe/pfe.pine +++ b/lib/dynamics/pfe/pfe.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("PFE: Polarized Fractal Efficiency", "PFE", overlay=false) diff --git a/lib/dynamics/plusdi/PlusDi.cs b/lib/dynamics/plusdi/PlusDi.cs new file mode 100644 index 00000000..72db2ded --- /dev/null +++ b/lib/dynamics/plusdi/PlusDi.cs @@ -0,0 +1,121 @@ +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// PLUS_DI: Plus Directional Indicator (Wilder, 1978) +/// +/// +/// Measures upward directional movement as a percentage of true range. +/// Extracted from the DX calculation: +DI = Smoothed(+DM) / Smoothed(TR) × 100. +/// Range: 0 to 100. Higher values indicate stronger upward movement. +/// +[SkipLocalsInit] +public sealed class PlusDi : ITValuePublisher +{ + private readonly Dx _dx; + + /// Display name for the indicator. + public string Name { get; } + + public event TValuePublishedHandler? Pub; + + /// Current +DI value. + public TValue Last { get; private set; } + + /// True when the indicator has warmed up. + public bool IsHot => _dx.IsHot; + + /// Bars required for warmup. + public int WarmupPeriod => _dx.WarmupPeriod; + + /// The period parameter. + public int Period => _dx.Period; + + /// + /// Creates PlusDi with specified period. + /// + /// Wilder smoothing period (must be > 0) + public PlusDi(int period = 14) + { + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + _dx = new Dx(period); + Name = $"PlusDi({period})"; + } + + /// + /// Creates PlusDi and immediately processes the bar series. + /// + public PlusDi(TBarSeries source, int period = 14) : this(period) + { + var result = Batch(source, period); + Last = result[^1]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar input, bool isNew = true) + { + _dx.Update(input, isNew); + Last = _dx.DiPlus; + if (isNew) + { + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + } + return Last; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue input, bool isNew = true) + { + // DI requires OHLC data — scalar update not meaningful + return Last; + } + + public TSeries Update(TBarSeries source) + { + var result = new TSeries(source.Count); + foreach (var bar in source) + { + result.Add(Update(bar)); + } + return result; + } + + /// + /// Initializes the indicator state using the provided bar series history. + /// + public void Prime(TBarSeries source) + { + foreach (var bar in source) + { + Update(bar); + } + } + + public void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + // Not applicable — DI requires OHLC bar data, not scalar values + } + + public static TSeries Batch(TBarSeries source, int period = 14) + { + var indicator = new PlusDi(period); + return indicator.Update(source); + } + + public static (TSeries Results, PlusDi Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new PlusDi(period); + return (indicator.Update(source), indicator); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _dx.Reset(); + Last = default; + } +} diff --git a/lib/dynamics/plusdi/PlusDi.md b/lib/dynamics/plusdi/PlusDi.md new file mode 100644 index 00000000..e3b79167 --- /dev/null +++ b/lib/dynamics/plusdi/PlusDi.md @@ -0,0 +1,31 @@ +# PLUS_DI: Plus Directional Indicator + +### TL;DR +Measures upward directional movement strength as a percentage (0-100). + +## Introduction +The Plus Directional Indicator (+DI) measures the strength of upward price movement relative to the true range. It is one of the components of the Directional Movement System developed by J. Welles Wilder Jr. + +When +DI is rising, upward price pressure is increasing. When +DI crosses above -DI, it signals a potential bullish trend. The +DI line is commonly plotted alongside -DI to visualize directional balance. + +## Calculation ++DI = Smoothed(+DM) / Smoothed(TR) × 100 + +Where: +- +DM (Plus Directional Movement) = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0 +- TR (True Range) = max(High - Low, |High - PrevClose|, |Low - PrevClose|) +- Smoothing uses Wilder's method: Smooth = Smooth - Smooth/N + Input + +## Parameters +| Parameter | Default | Range | Description | +| :--- | :--- | :--- | :--- | +| Period | 14 | 2-∞ | Wilder smoothing period | + +## Interpretation +- **Rising +DI:** Strengthening upward movement +- **+DI > -DI:** Bulls dominate; potential uptrend +- **+DI crossover above -DI:** Bullish signal +- **High +DI (>40):** Strong upward momentum + +## References +- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978) diff --git a/lib/dynamics/plusdm/PlusDm.cs b/lib/dynamics/plusdm/PlusDm.cs new file mode 100644 index 00000000..0ed40180 --- /dev/null +++ b/lib/dynamics/plusdm/PlusDm.cs @@ -0,0 +1,121 @@ +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// PLUS_DM: Plus Directional Movement (Wilder, 1978) +/// +/// +/// Wilder-smoothed upward directional movement in price units. +/// Extracted from the DX calculation: Smoothed(+DM) using Wilder's method. +/// Values ≥ 0 in price units. Higher values indicate stronger upward movement magnitude. +/// +[SkipLocalsInit] +public sealed class PlusDm : ITValuePublisher +{ + private readonly Dx _dx; + + /// Display name for the indicator. + public string Name { get; } + + public event TValuePublishedHandler? Pub; + + /// Current smoothed +DM value. + public TValue Last { get; private set; } + + /// True when the indicator has warmed up. + public bool IsHot => _dx.IsHot; + + /// Bars required for warmup. + public int WarmupPeriod => _dx.WarmupPeriod; + + /// The period parameter. + public int Period => _dx.Period; + + /// + /// Creates PlusDm with specified period. + /// + /// Wilder smoothing period (must be > 0) + public PlusDm(int period = 14) + { + if (period <= 0) + { + throw new ArgumentException("Period must be greater than 0", nameof(period)); + } + _dx = new Dx(period); + Name = $"PlusDm({period})"; + } + + /// + /// Creates PlusDm and immediately processes the bar series. + /// + public PlusDm(TBarSeries source, int period = 14) : this(period) + { + var result = Batch(source, period); + Last = result[^1]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar input, bool isNew = true) + { + _dx.Update(input, isNew); + Last = _dx.DmPlus; + if (isNew) + { + Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); + } + return Last; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue input, bool isNew = true) + { + // DM requires OHLC data — scalar update not meaningful + return Last; + } + + public TSeries Update(TBarSeries source) + { + var result = new TSeries(source.Count); + foreach (var bar in source) + { + result.Add(Update(bar)); + } + return result; + } + + /// + /// Initializes the indicator state using the provided bar series history. + /// + public void Prime(TBarSeries source) + { + foreach (var bar in source) + { + Update(bar); + } + } + + public void Prime(ReadOnlySpan source, TimeSpan? step = null) + { + // Not applicable — DM requires OHLC bar data, not scalar values + } + + public static TSeries Batch(TBarSeries source, int period = 14) + { + var indicator = new PlusDm(period); + return indicator.Update(source); + } + + public static (TSeries Results, PlusDm Indicator) Calculate(TBarSeries source, int period = 14) + { + var indicator = new PlusDm(period); + return (indicator.Update(source), indicator); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + _dx.Reset(); + Last = default; + } +} diff --git a/lib/dynamics/plusdm/PlusDm.md b/lib/dynamics/plusdm/PlusDm.md new file mode 100644 index 00000000..fc035a08 --- /dev/null +++ b/lib/dynamics/plusdm/PlusDm.md @@ -0,0 +1,28 @@ +# PLUS_DM: Plus Directional Movement + +### TL;DR +Wilder-smoothed upward directional movement in price units (≥0). + +## Introduction +Plus Directional Movement (+DM) measures the magnitude of upward price movement, smoothed using Wilder's method. Unlike +DI which normalizes by true range to produce a percentage, +DM outputs raw smoothed values in price units. + ++DM captures when the current bar's high exceeds the previous bar's high by more than the previous bar's low exceeds the current bar's low. It is the raw building block of the Directional Movement System. + +## Calculation ++DM = max(High - PrevHigh, 0) when High - PrevHigh > PrevLow - Low, else 0 + +Smoothed using Wilder's method: Smooth = Smooth - Smooth/N + Input + +## Parameters +| Parameter | Default | Range | Description | +| :--- | :--- | :--- | :--- | +| Period | 14 | 2-∞ | Wilder smoothing period | + +## Interpretation +- **Rising +DM:** Increasing upward price extension +- **+DM > -DM:** Upward movement exceeds downward movement +- **Zero +DM:** No upward directional movement on the bar +- Values are in price units and scale with the instrument + +## References +- Wilder, J. Welles Jr. "New Concepts in Technical Trading Systems" (1978) diff --git a/lib/dynamics/qstick/qstick.pine b/lib/dynamics/qstick/qstick.pine index ddb2e9b2..52735b6b 100644 --- a/lib/dynamics/qstick/qstick.pine +++ b/lib/dynamics/qstick/qstick.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Qstick Indicator", "QSTICK", overlay=false) diff --git a/lib/dynamics/ravi/Ravi.md b/lib/dynamics/ravi/Ravi.md index 99f84928..43f31501 100644 --- a/lib/dynamics/ravi/Ravi.md +++ b/lib/dynamics/ravi/Ravi.md @@ -7,14 +7,14 @@ | **Parameters** | `shortPeriod` (default 7), `longPeriod` (default 65) | | **Outputs** | Single series (Ravi) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `longPeriod` bars (default 65) | ### TL;DR - RAVI (Range Action Verification Index) measures trend strength by computing the absolute percentage divergence between a short-period SMA and a lon... - Parameterized by `shortperiod` (default 7), `longperiod` (default 65). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `longPeriod` bars (default 65) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The simplest question in technical analysis is also the most important: is this market trending or not? RAVI answers it with two moving averages and a division." diff --git a/lib/dynamics/ravi/ravi.pine b/lib/dynamics/ravi/ravi.pine index d2e5657e..1596f864 100644 --- a/lib/dynamics/ravi/ravi.pine +++ b/lib/dynamics/ravi/ravi.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("RAVI: Chande Range Action Verification Index", "RAVI", overlay=false) diff --git a/lib/dynamics/super/super.pine b/lib/dynamics/super/super.pine index e34ff8ed..c7f22934 100644 --- a/lib/dynamics/super/super.pine +++ b/lib/dynamics/super/super.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("SuperTrend", "SUPER", overlay=true) diff --git a/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs b/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs index 9058adb4..ee397900 100644 --- a/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs +++ b/lib/dynamics/ttm_squeeze/TtmSqueeze.Validation.Tests.cs @@ -325,5 +325,33 @@ public class TtmSqueezeValidationTests Assert.InRange(squeeze.ColorCode, 0, 3); } + [Fact] + public void TtmSqueeze_Correction_Recomputes() + { + var ind = new TtmSqueeze(); + var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc); + + // Build state well past warmup + for (int i = 0; i < 100; i++) + { + double p = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0); + ind.Update(new TBar(t0.AddMinutes(i), p, p + 2, p - 2, p, 1000), isNew: true); + } + + // Anchor bar + var anchorTime = t0.AddMinutes(100); + const double anchorClose = 105.5; + ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: true); + double anchorMomentum = ind.Momentum.Value; + + // Correction with a dramatically different price — Momentum must change + ind.Update(new TBar(anchorTime, anchorClose * 10, (anchorClose + 2) * 10, (anchorClose - 2) * 10, anchorClose * 10, 1000), isNew: false); + Assert.NotEqual(anchorMomentum, ind.Momentum.Value); + + // Correction back to original price — must exactly restore original Momentum + ind.Update(new TBar(anchorTime, anchorClose, anchorClose + 2, anchorClose - 2, anchorClose, 1000), isNew: false); + Assert.Equal(anchorMomentum, ind.Momentum.Value, 1e-9); + } + #endregion } diff --git a/lib/dynamics/ttm_squeeze/TtmSqueeze.cs b/lib/dynamics/ttm_squeeze/TtmSqueeze.cs index 258b9fa0..65c4a73b 100644 --- a/lib/dynamics/ttm_squeeze/TtmSqueeze.cs +++ b/lib/dynamics/ttm_squeeze/TtmSqueeze.cs @@ -322,7 +322,7 @@ public sealed class TtmSqueeze : ITValuePublisher _priceSum -= oldest; _priceSumSquares -= oldest * oldest; } - _priceBuffer.Add(close, isNew); + _priceBuffer.Add(close); _priceSum += close; _priceSumSquares += close * close; @@ -365,11 +365,11 @@ public sealed class TtmSqueeze : ITValuePublisher _prevSqueezeOn = squeezeOn; // === Donchian Midline === - _highBuffer.Add(high, isNew); - _lowBuffer.Add(low, isNew); + _highBuffer.Add(high); + _lowBuffer.Add(low); - double donchianHigh = GetMax(_highBuffer); - double donchianLow = GetMin(_lowBuffer); + double donchianHigh = _highBuffer.Max(); + double donchianLow = _lowBuffer.Min(); double donchianMid = (donchianHigh + donchianLow) / 2; // === Momentum (Linear Regression) === @@ -383,7 +383,7 @@ public sealed class TtmSqueeze : ITValuePublisher _momentumSumXY = _momentumSumXY + prevSumY - _momPeriod * oldest; _momentumSumY -= oldest; } - _momentumBuffer.Add(deviation, isNew); + _momentumBuffer.Add(deviation); _momentumSumY += deviation; // Recalculate sumXY during warmup (non-O(1), but short duration) @@ -535,6 +535,10 @@ public sealed class TtmSqueeze : ITValuePublisher _saved_prevMomentum = _prevMomentum; _saved_prevSqueezeOn = _prevSqueezeOn; _saved_barCount = _barCount; + _priceBuffer.Snapshot(); + _highBuffer.Snapshot(); + _lowBuffer.Snapshot(); + _momentumBuffer.Snapshot(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -552,43 +556,10 @@ public sealed class TtmSqueeze : ITValuePublisher _prevMomentum = _saved_prevMomentum; _prevSqueezeOn = _saved_prevSqueezeOn; _barCount = _saved_barCount; + _priceBuffer.Restore(); + _highBuffer.Restore(); + _lowBuffer.Restore(); + _momentumBuffer.Restore(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double GetMax(RingBuffer buffer) - { - if (buffer.Count == 0) - { - return 0; - } - var span = buffer.GetSpan(); - double max = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] > max) - { - max = span[i]; - } - } - return max; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double GetMin(RingBuffer buffer) - { - if (buffer.Count == 0) - { - return 0; - } - var span = buffer.GetSpan(); - double min = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] < min) - { - min = span[i]; - } - } - return min; - } } diff --git a/lib/dynamics/ttm_trend/TtmTrend.pine b/lib/dynamics/ttm_trend/TtmTrend.pine index cca20600..c3f0ef23 100644 --- a/lib/dynamics/ttm_trend/TtmTrend.pine +++ b/lib/dynamics/ttm_trend/TtmTrend.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("TTM Trend", "TTM_TREND", overlay=true) diff --git a/lib/dynamics/vhf/Vhf.cs b/lib/dynamics/vhf/Vhf.cs index feaef26d..83f267e2 100644 --- a/lib/dynamics/vhf/Vhf.cs +++ b/lib/dynamics/vhf/Vhf.cs @@ -412,22 +412,7 @@ public sealed class Vhf : AbstractBase // Calculate VHF if (closeFilled >= closeBufSize && diffFilled >= period) { - // Scan for max/min over close buffer - double hi = double.MinValue; - double lo = double.MaxValue; - for (int k = 0; k < closeBufSize; k++) - { - double cv = closeBuf[k]; - if (cv > hi) - { - hi = cv; - } - if (cv < lo) - { - lo = cv; - } - } - + var (lo, hi) = ((ReadOnlySpan)closeBuf).MinMaxSIMD(); double numerator = hi - lo; if (diffSum > 1e-10) diff --git a/lib/dynamics/vhf/vhf.pine b/lib/dynamics/vhf/vhf.pine index a2aacd1d..50628f56 100644 --- a/lib/dynamics/vhf/vhf.pine +++ b/lib/dynamics/vhf/vhf.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("VHF: Vertical Horizontal Filter", "VHF", overlay=false) diff --git a/lib/dynamics/vortex/vortex.pine b/lib/dynamics/vortex/vortex.pine index 786a21aa..1dac4b56 100644 --- a/lib/dynamics/vortex/vortex.pine +++ b/lib/dynamics/vortex/vortex.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Vortex Indicator", "VORTEX", overlay=false) diff --git a/lib/errors/huber/Huber.md b/lib/errors/huber/Huber.md index 047e477c..ee47fb1d 100644 --- a/lib/errors/huber/Huber.md +++ b/lib/errors/huber/Huber.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period`, `delta` (default 1.345) | | **Outputs** | Single series (Huber) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/huber/huber.pine b/lib/errors/huber/huber.pine index 75b6b001..e98de7c6 100644 --- a/lib/errors/huber/huber.pine +++ b/lib/errors/huber/huber.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Huber Loss (HUBER)", "HUBER") diff --git a/lib/errors/logcosh/LogCosh.md b/lib/errors/logcosh/LogCosh.md index 6b740e21..27317c74 100644 --- a/lib/errors/logcosh/LogCosh.md +++ b/lib/errors/logcosh/LogCosh.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | -| **Outputs** | Single series (UNKNOWN) | +| **Outputs** | Single series (LogCosh) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/logcosh/logcosh.pine b/lib/errors/logcosh/logcosh.pine index c3c7421c..099545b9 100644 --- a/lib/errors/logcosh/logcosh.pine +++ b/lib/errors/logcosh/logcosh.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Log-Cosh Loss", "LogCosh", overlay=false) diff --git a/lib/errors/maape/Maape.md b/lib/errors/maape/Maape.md index 850c738d..86282b14 100644 --- a/lib/errors/maape/Maape.md +++ b/lib/errors/maape/Maape.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAAPE) | -| **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Output range** | $[0, \pi/2]$ | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/maape/maape.pine b/lib/errors/maape/maape.pine index e92da552..07722aa0 100644 --- a/lib/errors/maape/maape.pine +++ b/lib/errors/maape/maape.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Arctangent Absolute Percentage Error", "MAAPE", overlay=false, format=format.percent) diff --git a/lib/errors/mae/Mae.Validation.Tests.cs b/lib/errors/mae/Mae.Validation.Tests.cs index 0b8ee3f2..6457e94b 100644 --- a/lib/errors/mae/Mae.Validation.Tests.cs +++ b/lib/errors/mae/Mae.Validation.Tests.cs @@ -71,4 +71,30 @@ public sealed class MaeValidationTests : IDisposable } } } + + [Fact] + public void Mae_Correction_Recomputes() + { + var ind = new Mae(20); + + // Build state well past warmup + for (int i = 0; i < 50; i++) + { + ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5); + } + + // Anchor bar + const double anchorActual = 125.0; + const double anchorPredicted = 123.0; + ind.Update(anchorActual, anchorPredicted, isNew: true); + double anchorResult = ind.Last.Value; + + // Correction with dramatically different values — recompute must yield different result + ind.Update(anchorActual * 10, anchorPredicted * 10, isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original — must exactly restore original result + ind.Update(anchorActual, anchorPredicted, isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, 1e-9); + } } diff --git a/lib/errors/mae/Mae.md b/lib/errors/mae/Mae.md index b8d4d4a0..29133e10 100644 --- a/lib/errors/mae/Mae.md +++ b/lib/errors/mae/Mae.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mae/mae.pine b/lib/errors/mae/mae.pine index 31265815..4420cb77 100644 --- a/lib/errors/mae/mae.pine +++ b/lib/errors/mae/mae.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Absolute Error (MAE)", "MAE") diff --git a/lib/errors/mapd/Mapd.md b/lib/errors/mapd/Mapd.md index 57604b0c..84350f33 100644 --- a/lib/errors/mapd/Mapd.md +++ b/lib/errors/mapd/Mapd.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAPD) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mapd/mapd.pine b/lib/errors/mapd/mapd.pine index 27edd405..dbc38c8c 100644 --- a/lib/errors/mapd/mapd.pine +++ b/lib/errors/mapd/mapd.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Absolute %Deviation (MAPD)", "MAPD") diff --git a/lib/errors/mape/Mape.Validation.Tests.cs b/lib/errors/mape/Mape.Validation.Tests.cs index a152611c..4ac02ee7 100644 --- a/lib/errors/mape/Mape.Validation.Tests.cs +++ b/lib/errors/mape/Mape.Validation.Tests.cs @@ -84,4 +84,31 @@ public sealed class MapeValidationTests : IDisposable return sum / actual.Length; } + + [Fact] + public void Mape_Correction_Recomputes() + { + var ind = new Mape(20); + + // Build state well past warmup (actual always > 0 so MAPE denominator is valid) + for (int i = 0; i < 50; i++) + { + ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5); + } + + // Anchor bar + const double anchorActual = 125.0; + const double anchorPredicted = 123.0; + ind.Update(anchorActual, anchorPredicted, isNew: true); + double anchorResult = ind.Last.Value; + + // MAPE is scale-invariant: ×10 on both actual and predicted leaves ratio unchanged. + // Change only predicted to dramatically alter the error percentage. + ind.Update(anchorActual, 10.0, isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original — must exactly restore original result + ind.Update(anchorActual, anchorPredicted, isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, 1e-9); + } } \ No newline at end of file diff --git a/lib/errors/mape/Mape.md b/lib/errors/mape/Mape.md index 3a938cf8..797900b0 100644 --- a/lib/errors/mape/Mape.md +++ b/lib/errors/mape/Mape.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MAPE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mape/mape.pine b/lib/errors/mape/mape.pine index d9cfd433..b10beefe 100644 --- a/lib/errors/mape/mape.pine +++ b/lib/errors/mape/mape.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Absolute %Error (MAPE)", "MAPE") diff --git a/lib/errors/mase/Mase.cs b/lib/errors/mase/Mase.cs index e05199bb..e9e34dc0 100644 --- a/lib/errors/mase/Mase.cs +++ b/lib/errors/mase/Mase.cs @@ -152,7 +152,7 @@ public sealed class Mase : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew); + return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/mase/Mase.md b/lib/errors/mase/Mase.md index c4b0ab81..24e6f96b 100644 --- a/lib/errors/mase/Mase.md +++ b/lib/errors/mase/Mase.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Mase) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/mase/mase.pine b/lib/errors/mase/mase.pine index 98cedc41..782fde38 100644 --- a/lib/errors/mase/mase.pine +++ b/lib/errors/mase/mase.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Absolute Scaled Error (MASE)", "MASE") diff --git a/lib/errors/mdae/Mdae.cs b/lib/errors/mdae/Mdae.cs index ea91d3b1..bb8212db 100644 --- a/lib/errors/mdae/Mdae.cs +++ b/lib/errors/mdae/Mdae.cs @@ -104,7 +104,7 @@ public sealed class Mdae : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew); + return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/mdae/Mdae.md b/lib/errors/mdae/Mdae.md index 9e58dcc3..8357ea96 100644 --- a/lib/errors/mdae/Mdae.md +++ b/lib/errors/mdae/Mdae.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Mdae) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/mdae/mdae.pine b/lib/errors/mdae/mdae.pine index fd85189b..28bb4c0a 100644 --- a/lib/errors/mdae/mdae.pine +++ b/lib/errors/mdae/mdae.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Median Absolute Error", "MdAE", overlay=false) diff --git a/lib/errors/mdape/Mdape.cs b/lib/errors/mdape/Mdape.cs index e53e895b..a1edcb37 100644 --- a/lib/errors/mdape/Mdape.cs +++ b/lib/errors/mdape/Mdape.cs @@ -58,7 +58,7 @@ public sealed class Mdape : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return UpdateCore(DateTime.UtcNow, actual, predicted, isNew); + return UpdateCore(DateTime.MinValue, actual, predicted, isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/mdape/Mdape.md b/lib/errors/mdape/Mdape.md index be37b67b..4733e197 100644 --- a/lib/errors/mdape/Mdape.md +++ b/lib/errors/mdape/Mdape.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Mdape) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/mdape/mdape.pine b/lib/errors/mdape/mdape.pine index ce85f6ad..7e526d7e 100644 --- a/lib/errors/mdape/mdape.pine +++ b/lib/errors/mdape/mdape.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Median Absolute Percentage Error", "MdAPE", overlay=false, format=format.percent) diff --git a/lib/errors/me/Me.md b/lib/errors/me/Me.md index 6c090ab3..7f205cbb 100644 --- a/lib/errors/me/Me.md +++ b/lib/errors/me/Me.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (ME) | -| **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Output range** | Any (positive or negative) | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/me/me.pine b/lib/errors/me/me.pine index 69eaceac..e808b79f 100644 --- a/lib/errors/me/me.pine +++ b/lib/errors/me/me.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Error (ME)", "ME") diff --git a/lib/errors/mpe/Mpe.md b/lib/errors/mpe/Mpe.md index f4bfd5e4..fcc119ed 100644 --- a/lib/errors/mpe/Mpe.md +++ b/lib/errors/mpe/Mpe.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MPE) | -| **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Output range** | Any (positive or negative) | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mpe/mpe.pine b/lib/errors/mpe/mpe.pine index 2ccb5707..cda1d0a2 100644 --- a/lib/errors/mpe/mpe.pine +++ b/lib/errors/mpe/mpe.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean %Error (MPE)", "MPE") diff --git a/lib/errors/mrae/Mrae.md b/lib/errors/mrae/Mrae.md index 58f16d00..0d84ffb7 100644 --- a/lib/errors/mrae/Mrae.md +++ b/lib/errors/mrae/Mrae.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MRAE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mrae/mrae.pine b/lib/errors/mrae/mrae.pine index 9b5d55e0..7943a2bf 100644 --- a/lib/errors/mrae/mrae.pine +++ b/lib/errors/mrae/mrae.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Relative Absolute Error", "MRAE", overlay=false) diff --git a/lib/errors/mse/Mse.Validation.Tests.cs b/lib/errors/mse/Mse.Validation.Tests.cs index 7a47f20a..0b82dbf7 100644 --- a/lib/errors/mse/Mse.Validation.Tests.cs +++ b/lib/errors/mse/Mse.Validation.Tests.cs @@ -71,4 +71,30 @@ public sealed class MseValidationTests : IDisposable } } } + + [Fact] + public void Mse_Correction_Recomputes() + { + var ind = new Mse(20); + + // Build state well past warmup + for (int i = 0; i < 50; i++) + { + ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5); + } + + // Anchor bar + const double anchorActual = 125.0; + const double anchorPredicted = 123.0; + ind.Update(anchorActual, anchorPredicted, isNew: true); + double anchorResult = ind.Last.Value; + + // Correction with dramatically different values — recompute must yield different result + ind.Update(anchorActual * 10, anchorPredicted * 10, isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original — must exactly restore original result + ind.Update(anchorActual, anchorPredicted, isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, 1e-9); + } } \ No newline at end of file diff --git a/lib/errors/mse/Mse.md b/lib/errors/mse/Mse.md index 168e5dfc..34c4236f 100644 --- a/lib/errors/mse/Mse.md +++ b/lib/errors/mse/Mse.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MSE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/mse/mse.pine b/lib/errors/mse/mse.pine index cf1957aa..16ca3819 100644 --- a/lib/errors/mse/mse.pine +++ b/lib/errors/mse/mse.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Squared Error (MSE)", "MSE") diff --git a/lib/errors/msle/Msle.md b/lib/errors/msle/Msle.md index 86b35540..e2b1fdc2 100644 --- a/lib/errors/msle/Msle.md +++ b/lib/errors/msle/Msle.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (MSLE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/msle/msle.pine b/lib/errors/msle/msle.pine index 3286927c..07c73b75 100644 --- a/lib/errors/msle/msle.pine +++ b/lib/errors/msle/msle.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Mean Squared Logarithmic Error (MSLE)", "MSLE") diff --git a/lib/errors/pseudohuber/PseudoHuber.md b/lib/errors/pseudohuber/PseudoHuber.md index 1cdc332e..704e9633 100644 --- a/lib/errors/pseudohuber/PseudoHuber.md +++ b/lib/errors/pseudohuber/PseudoHuber.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period`, `delta` (default 1.0) | -| **Outputs** | Single series (UNKNOWN) | +| **Outputs** | Single series (PseudoHuber) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Pseudo-Huber Loss (also called Charbonnier Loss) is a smooth approximation to the Huber loss function. - Parameterized by `period`, `delta` (default 1.0). - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "All the robustness of Huber, none of the discontinuities." diff --git a/lib/errors/pseudohuber/pseudohuber.pine b/lib/errors/pseudohuber/pseudohuber.pine index db78d082..d0ab747d 100644 --- a/lib/errors/pseudohuber/pseudohuber.pine +++ b/lib/errors/pseudohuber/pseudohuber.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Pseudo-Huber Loss", "PseudoHuber", overlay=false) diff --git a/lib/errors/quantileloss/QuantileLoss.md b/lib/errors/quantileloss/QuantileLoss.md index ff024fad..830963de 100644 --- a/lib/errors/quantileloss/QuantileLoss.md +++ b/lib/errors/quantileloss/QuantileLoss.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period`, `quantile` (default 0.5) | -| **Outputs** | Single series (Quantile) | +| **Outputs** | Single series (QuantileLoss) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/quantileloss/quantileloss.pine b/lib/errors/quantileloss/quantileloss.pine index ff1f2f2f..705eeed4 100644 --- a/lib/errors/quantileloss/quantileloss.pine +++ b/lib/errors/quantileloss/quantileloss.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Quantile Loss (Pinball Loss)", "QuantileLoss", overlay=false) diff --git a/lib/errors/rae/Rae.cs b/lib/errors/rae/Rae.cs index 898ad6f4..809ab3c4 100644 --- a/lib/errors/rae/Rae.cs +++ b/lib/errors/rae/Rae.cs @@ -149,7 +149,7 @@ public sealed class Rae : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew); + return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/rae/Rae.md b/lib/errors/rae/Rae.md index cd0c3796..464114f9 100644 --- a/lib/errors/rae/Rae.md +++ b/lib/errors/rae/Rae.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (Rae) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/rae/rae.pine b/lib/errors/rae/rae.pine index 93469a22..708b6611 100644 --- a/lib/errors/rae/rae.pine +++ b/lib/errors/rae/rae.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Relative Absolute Error (RAE)", "RAE") diff --git a/lib/errors/rmse/Rmse.Validation.Tests.cs b/lib/errors/rmse/Rmse.Validation.Tests.cs index 062dd7a2..386233f6 100644 --- a/lib/errors/rmse/Rmse.Validation.Tests.cs +++ b/lib/errors/rmse/Rmse.Validation.Tests.cs @@ -73,4 +73,30 @@ public sealed class RmseValidationTests : IDisposable } } } + + [Fact] + public void Rmse_Correction_Recomputes() + { + var ind = new Rmse(20); + + // Build state well past warmup + for (int i = 0; i < 50; i++) + { + ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5); + } + + // Anchor bar + const double anchorActual = 125.0; + const double anchorPredicted = 123.0; + ind.Update(anchorActual, anchorPredicted, isNew: true); + double anchorResult = ind.Last.Value; + + // Correction with dramatically different values — recompute must yield different result + ind.Update(anchorActual * 10, anchorPredicted * 10, isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original — must exactly restore original result + ind.Update(anchorActual, anchorPredicted, isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, 1e-9); + } } diff --git a/lib/errors/rmse/Rmse.md b/lib/errors/rmse/Rmse.md index 904c4d72..45c7972a 100644 --- a/lib/errors/rmse/Rmse.md +++ b/lib/errors/rmse/Rmse.md @@ -3,11 +3,11 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual, Predicted (dual series) | | **Parameters** | `period` | | **Outputs** | Single series (RMSE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR diff --git a/lib/errors/rmse/rmse.pine b/lib/errors/rmse/rmse.pine index 5428aaa9..0de3272b 100644 --- a/lib/errors/rmse/rmse.pine +++ b/lib/errors/rmse/rmse.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Root Mean Squared Error (RMSE)", "RMSE") diff --git a/lib/errors/rmsle/Rmsle.md b/lib/errors/rmsle/Rmsle.md index be0a42b5..c6b2ccb5 100644 --- a/lib/errors/rmsle/Rmsle.md +++ b/lib/errors/rmsle/Rmsle.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (RMSLE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Root Mean Squared Logarithmic Error is the square root of MSLE, providing an error metric in log-scale units. - Parameterized by `period`. - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "RMSLE: because sometimes your errors need to be measured in decades, not dollars." diff --git a/lib/errors/rmsle/rmsle.pine b/lib/errors/rmsle/rmsle.pine index 1e67d158..01a95ade 100644 --- a/lib/errors/rmsle/rmsle.pine +++ b/lib/errors/rmsle/rmsle.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Root Mean Squared Logarithmic Error (RMSLE)", "RMSLE") diff --git a/lib/errors/rse/Rse.cs b/lib/errors/rse/Rse.cs index 8037f11e..b3d7c8b4 100644 --- a/lib/errors/rse/Rse.cs +++ b/lib/errors/rse/Rse.cs @@ -156,7 +156,7 @@ public sealed class Rse : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew); + return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/rse/Rse.md b/lib/errors/rse/Rse.md index 82c0d7a0..04c097dd 100644 --- a/lib/errors/rse/Rse.md +++ b/lib/errors/rse/Rse.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (Rse) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/rse/rse.pine b/lib/errors/rse/rse.pine index 6c0f27b6..ad4b4503 100644 --- a/lib/errors/rse/rse.pine +++ b/lib/errors/rse/rse.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Relative Squared Error (RSE)", "RSE") diff --git a/lib/errors/rsquared/Rsquared.Validation.Tests.cs b/lib/errors/rsquared/Rsquared.Validation.Tests.cs index 5c60349a..83881a82 100644 --- a/lib/errors/rsquared/Rsquared.Validation.Tests.cs +++ b/lib/errors/rsquared/Rsquared.Validation.Tests.cs @@ -194,4 +194,30 @@ public sealed class RsquaredValidationTests : IDisposable Assert.True(double.IsFinite(rsq.Last.Value), "QuanTAlib R² last must be finite"); Assert.True(rsq.Last.Value <= 1.0 + 1e-9, $"QuanTAlib R² should be ≤ 1, got {rsq.Last.Value}"); } + + [Fact] + public void Rsquared_Correction_Recomputes() + { + var ind = new Rsquared(20); + + // Build state well past warmup + for (int i = 0; i < 50; i++) + { + ind.Update(100.0 + i * 0.5, 98.0 + i * 0.5); + } + + // Anchor bar + const double anchorActual = 125.0; + const double anchorPredicted = 123.0; + ind.Update(anchorActual, anchorPredicted, isNew: true); + double anchorResult = ind.Last.Value; + + // R² is scale-invariant: change only predicted (not ×10 both) to break R² + ind.Update(anchorActual, 10.0, isNew: false); + Assert.NotEqual(anchorResult, ind.Last.Value); + + // Correction back to original — must exactly restore original result + ind.Update(anchorActual, anchorPredicted, isNew: false); + Assert.Equal(anchorResult, ind.Last.Value, 1e-9); + } } diff --git a/lib/errors/rsquared/Rsquared.cs b/lib/errors/rsquared/Rsquared.cs index addb7218..8d036333 100644 --- a/lib/errors/rsquared/Rsquared.cs +++ b/lib/errors/rsquared/Rsquared.cs @@ -150,7 +150,7 @@ public sealed class Rsquared : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew); + return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/rsquared/Rsquared.md b/lib/errors/rsquared/Rsquared.md index b7cbb693..e03e638e 100644 --- a/lib/errors/rsquared/Rsquared.md +++ b/lib/errors/rsquared/Rsquared.md @@ -3,17 +3,17 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | -| **Outputs** | Single series (R) | -| **Output range** | $\geq 0$ | +| **Outputs** | Single series (R²) | +| **Output range** | $(-\infty, 1]$ | | **Warmup** | `period` bars | ### TL;DR - The Coefficient of Determination (R²) measures the proportion of variance in the actual values that is predictable from the predicted values. - Parameterized by `period`. -- Output range: $\geq 0$. +- Output range: $(-\infty, 1]$. - Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/errors/rsquared/rsquared.pine b/lib/errors/rsquared/rsquared.pine index 7f319561..6dfcf675 100644 --- a/lib/errors/rsquared/rsquared.pine +++ b/lib/errors/rsquared/rsquared.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("R² Coefficient of Determination (RSQUARED)", "RSQUARED") diff --git a/lib/errors/smape/Smape.md b/lib/errors/smape/Smape.md index 7880f283..84d88e21 100644 --- a/lib/errors/smape/Smape.md +++ b/lib/errors/smape/Smape.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (SMAPE) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Symmetric Mean Absolute Percentage Error addresses a fundamental asymmetry in MAPE: the fact that over-predictions and under-predictions of the sam... - Parameterized by `period`. - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "MAPE punishes based on who's right; SMAPE punishes based on how different they are." diff --git a/lib/errors/smape/smape.pine b/lib/errors/smape/smape.pine index 0eac678e..f75a7e02 100644 --- a/lib/errors/smape/smape.pine +++ b/lib/errors/smape/smape.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Symmetric Mean Absolute %Error (SMAPE)", "SMAPE") diff --git a/lib/errors/theilu/TheilU.cs b/lib/errors/theilu/TheilU.cs index b21ca932..851971f5 100644 --- a/lib/errors/theilu/TheilU.cs +++ b/lib/errors/theilu/TheilU.cs @@ -65,7 +65,7 @@ public sealed class TheilU : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return UpdateCore(DateTime.UtcNow, actual, predicted, isNew); + return UpdateCore(DateTime.MinValue, actual, predicted, isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/theilu/TheilU.md b/lib/errors/theilu/TheilU.md index de47b848..fce391b5 100644 --- a/lib/errors/theilu/TheilU.md +++ b/lib/errors/theilu/TheilU.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (TheilU) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/theilu/theilu.pine b/lib/errors/theilu/theilu.pine index e24e5080..ca6e6834 100644 --- a/lib/errors/theilu/theilu.pine +++ b/lib/errors/theilu/theilu.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Theil's U Statistic", "TheilU", overlay=false) diff --git a/lib/errors/tukeybiweight/TukeyBiweight.md b/lib/errors/tukeybiweight/TukeyBiweight.md index e390b192..351e322c 100644 --- a/lib/errors/tukeybiweight/TukeyBiweight.md +++ b/lib/errors/tukeybiweight/TukeyBiweight.md @@ -3,18 +3,18 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | -| **Parameters** | `period`, `c` (default DefaultC) | -| **Outputs** | Single series (UNKNOWN) | +| **Inputs** | Actual vs Predicted (dual input) | +| **Parameters** | `period`, `c` (default 4.685) | +| **Outputs** | Single series (TukeyBiweight) | | **Output range** | $\geq 0$ | -| **Warmup** | 1 bar | +| **Warmup** | `period` bars | ### TL;DR - Tukey's Biweight (also called Bisquare) is a redescending M-estimator that completely ignores errors beyond a threshold. - Parameterized by `period`, `c` (default defaultc). - Output range: $\geq 0$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `period` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "When outliers need to be silenced, not just quieted." diff --git a/lib/errors/tukeybiweight/tukeybiweight.pine b/lib/errors/tukeybiweight/tukeybiweight.pine index 6dba23dc..c882d7fb 100644 --- a/lib/errors/tukeybiweight/tukeybiweight.pine +++ b/lib/errors/tukeybiweight/tukeybiweight.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Tukey's Biweight Loss", "TukeyBiweight", overlay=false) diff --git a/lib/errors/wmape/Wmape.cs b/lib/errors/wmape/Wmape.cs index 12fdcd56..a68fb068 100644 --- a/lib/errors/wmape/Wmape.cs +++ b/lib/errors/wmape/Wmape.cs @@ -128,7 +128,7 @@ public sealed class Wmape : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, bool isNew = true) { - return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), isNew); + return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew); } public override TValue Update(TValue input, bool isNew = true) diff --git a/lib/errors/wmape/Wmape.md b/lib/errors/wmape/Wmape.md index 2dd10a86..4c9b6546 100644 --- a/lib/errors/wmape/Wmape.md +++ b/lib/errors/wmape/Wmape.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (Wmape) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/wmape/wmape.pine b/lib/errors/wmape/wmape.pine index 12664772..9ccd99d3 100644 --- a/lib/errors/wmape/wmape.pine +++ b/lib/errors/wmape/wmape.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Weighted Mean Absolute Percentage Error", "WMAPE", overlay=false, format=format.percent) diff --git a/lib/errors/wrmse/Wrmse.cs b/lib/errors/wrmse/Wrmse.cs index cb6f70c9..a70cc88f 100644 --- a/lib/errors/wrmse/Wrmse.cs +++ b/lib/errors/wrmse/Wrmse.cs @@ -176,7 +176,7 @@ public sealed class Wrmse : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double actual, double predicted, double weight, bool isNew = true) { - return Update(new TValue(DateTime.UtcNow, actual), new TValue(DateTime.UtcNow, predicted), weight, isNew); + return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), weight, isNew); } /// diff --git a/lib/errors/wrmse/Wrmse.md b/lib/errors/wrmse/Wrmse.md index 1981e738..f67f22eb 100644 --- a/lib/errors/wrmse/Wrmse.md +++ b/lib/errors/wrmse/Wrmse.md @@ -3,7 +3,7 @@ | Property | Value | | ---------------- | -------------------------------- | | **Category** | Error Metric | -| **Inputs** | Source (close) | +| **Inputs** | Actual vs Predicted (dual input) | | **Parameters** | `period` | | **Outputs** | Single series (Wrmse) | | **Output range** | $\geq 0$ | diff --git a/lib/errors/wrmse/wrmse.pine b/lib/errors/wrmse/wrmse.pine index f994829d..0b818370 100644 --- a/lib/errors/wrmse/wrmse.pine +++ b/lib/errors/wrmse/wrmse.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Weighted Root Mean Squared Error", "WRMSE", overlay=false) diff --git a/lib/feeds/IFeed.md b/lib/feeds/IFeed.md index af56bfd9..02ab510b 100644 --- a/lib/feeds/IFeed.md +++ b/lib/feeds/IFeed.md @@ -60,3 +60,4 @@ When implementing `IFeed`: * **`GBM`**: Geometric Brownian Motion generator (Synthetic). * **`CsvFeed`**: Reads OHLCV data from CSV files (Historical). +* **`AlphaVantage`**: Fetches OHLCV data from the Alpha Vantage REST API (Historical/Live). diff --git a/lib/feeds/csvfeed/CsvFeed.Tests.cs b/lib/feeds/csvfeed/CsvFeed.Tests.cs index a8e3d19a..fc914a7c 100644 --- a/lib/feeds/csvfeed/CsvFeed.Tests.cs +++ b/lib/feeds/csvfeed/CsvFeed.Tests.cs @@ -20,6 +20,7 @@ public sealed class CsvFeedTests : IDisposable private static readonly string[] NegativeValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,-100,50,-150,-50,1000"]; private static readonly string[] ScientificNotationData = ["timestamp,open,high,low,close,volume", "2023-01-01,1.5e2,2e2,1e2,1.75e2,1e6"]; private static readonly string[] GapDataReversed = ["timestamp,open,high,low,close,volume", "2023-01-05,103,104,102,103,1000", "2023-01-04,102,103,101,102,1000", "2023-01-02,101,102,100,101,1000", "2023-01-01,100,101,99,100,1000"]; + private static readonly string[] TwoBarsWithGapData = ["timestamp,open,high,low,close,volume", "2023-01-03,101,102,100,101,1000", "2023-01-01,100,101,99,100,1000"]; private static readonly string[] WhitespaceData = ["timestamp,open,high,low,close,volume", " 2023-01-01 , 100 , 101 , 99 , 100 , 1000 "]; private static readonly string[] ZeroValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,0,0,0,0,0"]; private static readonly string[] LargeValuesData = ["timestamp,open,high,low,close,volume", "2023-01-01,999999999.99,1000000000.01,999999999.00,999999999.50,9999999999999"]; @@ -337,6 +338,26 @@ public sealed class CsvFeedTests : IDisposable Assert.True(bar2.Time > bar1.Time); } + [Fact] + public void CsvFeed_ObsoleteOverload_SilentlyMissesEndOfStream() + { + string tempCsv = CreateTempCsv(SingleBarData); + var feed = new CsvFeed(tempCsv); + + // Value overload cannot detect end — silently returns last bar + feed.Next(isNew: true); // bar 1 + var postEnd = feed.Next(isNew: true); // past end — no exception, no signal + + // Must use HasMore to detect end when using value overload + Assert.False(feed.HasMore); + Assert.Equal(100.0, postEnd.Close); // last bar returned again + + // Ref overload detects end correctly + bool isNew = true; + feed.Next(ref isNew); + Assert.False(isNew); // ref overload signals end + } + #endregion #region Fetch Method Tests @@ -410,6 +431,26 @@ public sealed class CsvFeedTests : IDisposable Assert.False(feed.HasCurrentBar); } + [Fact] + public void CsvFeed_FetchThenNext_StreamsFromFetchStartNotEnd() + { + var feed = new CsvFeed(TestCsvPath); + + var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks; + var interval = TimeSpan.FromDays(1); + var series = feed.Fetch(5, startTime, interval); + + // After Fetch, streaming replays from the START of the fetched window + // (not from after the last returned bar) + for (int i = 0; i < series.Count; i++) + { + bool isNew = true; + var bar = feed.Next(ref isNew); + Assert.Equal(series[i].Time, bar.Time); + Assert.True(isNew); + } + } + #endregion #region Reset Method Tests @@ -888,5 +929,20 @@ public sealed class CsvFeedTests : IDisposable } } + [Fact] + public void CsvFeed_Fetch_Tolerance_GapBarSkipped() + { + // Verify gap-skip behavior: Jan 2 is absent, Fetch re-aligns to Jan 3 + string tempCsv = CreateTempCsv(TwoBarsWithGapData); + var feed = new CsvFeed(tempCsv); + var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks; + var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1)); + + // Jan 1 and Jan 3 present; Jan 2 absent → Fetch includes both with gap + Assert.Equal(2, series.Count); + Assert.Equal(startTime, series[0].Time); + Assert.Equal(startTime + 2 * TimeSpan.FromDays(1).Ticks, series[1].Time); + } + #endregion } diff --git a/lib/feeds/gbm/Gbm.Tests.cs b/lib/feeds/gbm/Gbm.Tests.cs index 484572b1..457699ad 100644 --- a/lib/feeds/gbm/Gbm.Tests.cs +++ b/lib/feeds/gbm/Gbm.Tests.cs @@ -664,6 +664,36 @@ public class GBMTests Assert.False(gbm.HasCurrentBar); } + [Fact] + public void GBM_FetchThenNext_PriceContinuity() + { + var gbm = new GBM(startPrice: 100.0, seed: 42); + long startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks; + var interval = TimeSpan.FromMinutes(1); + + var series = gbm.Fetch(5, startTime, interval); + double lastBatchClose = series[4].Close; + + // Next bar after Fetch must open at the last batch close (price continuity) + var nextBar = gbm.Next(isNew: true); + Assert.Equal(lastBatchClose, nextBar.Open, 1e-10); + } + + [Fact] + public void GBM_NextThenFetch_PriceContinuity() + { + var gbm = new GBM(startPrice: 100.0, seed: 42); + + _ = gbm.Next(isNew: true); + var bar2 = gbm.Next(isNew: true); // _lastPrice = bar2.Close + + long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks; + var series = gbm.Fetch(3, startTime, TimeSpan.FromMinutes(1)); + + // First bar of Fetch must open at bar2.Close + Assert.Equal(bar2.Close, series[0].Open, 1e-10); + } + #endregion #region IFeed Interface Tests diff --git a/lib/feeds/gbm/gbm.cs b/lib/feeds/gbm/gbm.cs index 02781ad0..0708864d 100644 --- a/lib/feeds/gbm/gbm.cs +++ b/lib/feeds/gbm/gbm.cs @@ -120,6 +120,10 @@ public sealed class GBM : IFeed /// /// Resets the generator to its initial state. /// + /// + /// Sets the internal time anchor to . For deterministic + /// time sequences use with an explicit start time. + /// public void Reset() { _lastPrice = StartPrice; @@ -284,6 +288,15 @@ public sealed class GBM : IFeed /// A TBarSeries containing the generated bars /// Thrown when count is not positive /// Thrown when interval is not positive + /// + /// Price continuity: batch[0].Open equals _lastPrice at call time, so the + /// batch begins exactly where the previous call left off. + /// After the call, _lastPrice and _lastTime are updated to the end of the + /// generated batch, enabling seamless continuation via subsequent + /// calls. need not follow the previous _lastTime — + /// this allows replaying a window or generating a non-contiguous batch while preserving + /// price continuity. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TBarSeries Fetch(int count, long startTime, TimeSpan interval) { diff --git a/lib/filters/agc/Agc.md b/lib/filters/agc/Agc.md index 3895e088..249382f8 100644 --- a/lib/filters/agc/Agc.md +++ b/lib/filters/agc/Agc.md @@ -6,15 +6,15 @@ | **Inputs** | Source (close) | | **Parameters** | `decay` (default 0.991) | | **Outputs** | Single series (AGC) | -| **Output range** | Tracks input | +| **Output range** | [-1, +1] (normalized) | | **Warmup** | `1` bars | ### TL;DR - The Automatic Gain Control normalizes any oscillating signal to the \[-1, +1\] range through exponential peak tracking. - Parameterized by `decay` (default 0.991). -- Output range: Tracks input. -- Requires `1` bars of warmup before first valid output (IsHot = true). +- Output range: [-1, +1] (normalized amplitude). +- Requires `1` bar of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The purpose of the AGC is to normalize the amplitude of any indicator to unity." — John F. Ehlers, TASC January 2015 @@ -86,26 +86,25 @@ Peak initializes to $10^{-10}$ (tiny positive) to avoid division by zero on the ### Operation Count (Streaming Mode) -AGC (Adaptive Gain Control) applies a slow EMA to estimate signal level, then scales the signal by the inverse of that level. O(1) per bar. +AGC uses exponential peak decay with a ratchet-up mechanism, then divides the signal by the tracked peak. O(1) per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Level EMA (FMA) | 1 | ~4 cy | ~4 cy | -| Gain = 1 / level (division) | 1 | ~10 cy | ~10 cy | -| Output multiply | 1 | ~3 cy | ~3 cy | -| **Total** | **3** | — | **~17 cycles** | +| Peak decay (multiply) | 1 | ~3 cy | ~3 cy | +| Abs + compare (ratchet) | 1 | ~2 cy | ~2 cy | +| Normalize (division) | 1 | ~10 cy | ~10 cy | +| **Total** | **3** | — | **~15 cycles** | -O(1) per bar. The division dominates; precomputing gain incrementally saves it but adds state. ~17 cycles/bar. +O(1) per bar. The division dominates. ~15 cycles/bar. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Level EMA recursion | No | Sequential IIR dependency | -| Gain division | No | Depends on current EMA | -| Output multiply | N/A | Single scalar multiply | +| Peak decay + ratchet | No | Data-dependent branching (max of decay vs abs) | +| Normalize (division) | No | Depends on current peak | -Fully recursive. Batch throughput: ~17 cy/bar. +Fully sequential due to data-dependent peak tracking. Batch throughput: ~15 cy/bar. | Metric | Value | |---|---| diff --git a/lib/filters/agc/agc.pine b/lib/filters/agc/agc.pine index 003e3046..23e1e031 100644 --- a/lib/filters/agc/agc.pine +++ b/lib/filters/agc/agc.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 // Indicator algorithm (C) 2015 John F. Ehlers diff --git a/lib/filters/alaguerre/ALaguerre.md b/lib/filters/alaguerre/ALaguerre.md index 54e76edf..4c1b9365 100644 --- a/lib/filters/alaguerre/ALaguerre.md +++ b/lib/filters/alaguerre/ALaguerre.md @@ -7,7 +7,7 @@ | **Parameters** | `length` (default 20), `medianLength` (default 5) | | **Outputs** | Single series (ALaguerre) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `max(4, length)` bars | | **Signature** | [alaguerre_signature](alaguerre_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - The Adaptive Laguerre Filter extends Ehlers' four-element all-pass cascade by replacing the fixed damping factor with a per-bar adaptive alpha deri... - Parameterized by `length` (default 20), `medianlength` (default 5). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `max(4, length)` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The best filter is one that knows when to listen closely and when to smooth aggressively." -- John F. Ehlers (paraphrased) @@ -111,24 +111,28 @@ The filter requires $\max(4, N)$ bars before producing reliable output. The firs ### Operation Count (Streaming Mode) -Laguerre filter uses 4 cascaded Laguerre stages L0..L3, each an O(1) gamma-parameterized FMA, plus a final weighted combination. +Adaptive Laguerre combines HH/LL tracking error normalization (O(N) scan), median smoothing (O(M log M) sort), and 4-stage Laguerre cascade (O(1)). | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | +| Tracking error computation | 1 | ~3 cy | ~3 cy | +| HH/LL scan over N-element buffer | N | ~2 cy | ~40 cy (N=20) | +| Median via insertion sort (M elements) | M log M | ~3 cy | ~25 cy (M=5) | | Laguerre state update x4 (each: 2 FMA) | 8 | ~4 cy | ~32 cy | -| Weighted output combination (3 adds) | 3 | ~2 cy | ~6 cy | -| **Total** | **11** | — | **~38 cycles** | +| Weighted output combination | 3 | ~2 cy | ~6 cy | +| **Total** | — | — | **~106 cycles** (N=20, M=5) | -O(1) per bar. Four recursive stages with precomputed gamma constant. ~38 cycles/bar. +O(N + M log M) per bar due to HH/LL scan and median sort. Dominated by the lookback scan for large N. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Laguerre stage recursion | No | Each stage `L[k][n]` depends on `L[k-1][n]` and `L[k][n-1]` | -| Weighted combination | No | Only 4 terms; SIMD overhead not worthwhile | +| HH/LL scan | Partial | Min/max over contiguous buffer amenable to `Vector` | +| Median sort | No | Comparison-based sort, data-dependent | +| Laguerre cascade | No | Each stage depends on previous stage and previous bar | -Cascaded IIR stages cannot be vectorized. Batch throughput: ~38 cy/bar. +Adaptive overhead limits SIMD gains. Batch throughput: ~106 cy/bar (N=20, M=5). | Metric | Value | Notes | |--------|-------|-------| diff --git a/lib/filters/alaguerre/alaguerre.pine b/lib/filters/alaguerre/alaguerre.pine index dd7a3401..95204c57 100644 --- a/lib/filters/alaguerre/alaguerre.pine +++ b/lib/filters/alaguerre/alaguerre.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 // Indicator algorithm (C) 2004 John F. Ehlers diff --git a/lib/filters/baxterking/BaxterKing.cs b/lib/filters/baxterking/BaxterKing.cs index 6089a88e..9d91246b 100644 --- a/lib/filters/baxterking/BaxterKing.cs +++ b/lib/filters/baxterking/BaxterKing.cs @@ -198,13 +198,14 @@ public sealed class BaxterKing : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ComputeFilter() { - // Apply symmetric FIR: sum of weights[i] * buffer[i] - // Buffer[0] is oldest (K bars ago from center), buffer[2K] is newest + // Apply symmetric FIR: dot product of weights with buffer (oldest to newest) // The "center" is buffer[K], which represents time t-K (delayed output) - double sum = 0.0; - for (int i = 0; i < _filterLen; i++) + _buffer.GetSequencedSpans(out var first, out var second); + ReadOnlySpan w = _weights; + double sum = w.Slice(0, first.Length).DotProduct(first); + if (!second.IsEmpty) { - sum += _weights[i] * _buffer[i]; + sum += w.Slice(first.Length).DotProduct(second); } return sum; } @@ -270,6 +271,7 @@ public sealed class BaxterKing : AbstractBase ComputeWeights(weights, pLow, pHigh, k); int n = source.Length; + ReadOnlySpan w = weights; for (int i = 0; i < n; i++) { @@ -279,12 +281,7 @@ public sealed class BaxterKing : AbstractBase } else { - double sum = 0.0; - for (int j = 0; j < filterLen; j++) - { - sum += weights[j] * source[i - filterLen + 1 + j]; - } - output[i] = sum; + output[i] = w.DotProduct(source.Slice(i - filterLen + 1, filterLen)); } } } diff --git a/lib/filters/baxterking/BaxterKing.md b/lib/filters/baxterking/BaxterKing.md index 170588b6..ca5e77dc 100644 --- a/lib/filters/baxterking/BaxterKing.md +++ b/lib/filters/baxterking/BaxterKing.md @@ -6,16 +6,15 @@ | **Inputs** | Source (close) | | **Parameters** | `pLow` (default 6), `pHigh` (default 32), `k` (default 12) | | **Outputs** | Single series (BaxterKing) | -| **Output range** | Tracks input | -| **Warmup** | 1 bar | -| **Signature** | [baxterking_signature](baxterking_signature.md) | +| **Output range** | Oscillates around zero | +| **Warmup** | `2K+1` bars (default 25) | ### TL;DR - The **Baxter-King Band-Pass Filter** is a symmetric finite impulse response (FIR) filter that approximates the ideal spectral band-pass by truncati... - Parameterized by `plow` (default 6), `phigh` (default 32), `k` (default 12). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero (extracts cyclical component). +- Requires `2K+1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The business cycle is whatever remains after you strip away the trend and the noise. Baxter and King figured out the stripping." diff --git a/lib/filters/baxterking/baxterking.pine b/lib/filters/baxterking/baxterking.pine index 57325f07..8428ea2c 100644 --- a/lib/filters/baxterking/baxterking.pine +++ b/lib/filters/baxterking/baxterking.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Baxter-King Band-Pass Filter (BAXTERKING)", "BAXTERKING", overlay=false) diff --git a/lib/filters/baxterking/baxterking_signature.md b/lib/filters/baxterking/baxterking_signature.md deleted file mode 100644 index 1661f0cf..00000000 --- a/lib/filters/baxterking/baxterking_signature.md +++ /dev/null @@ -1,3 +0,0 @@ -# BAXTERKING Signature - -![BAXTERKING Signature](baxterking_signature.svg) \ No newline at end of file diff --git a/lib/filters/baxterking/baxterking_signature.svg b/lib/filters/baxterking/baxterking_signature.svg deleted file mode 100644 index 911bb505..00000000 --- a/lib/filters/baxterking/baxterking_signature.svg +++ /dev/null @@ -1,19710 +0,0 @@ - - - - - - - - 2026-03-02T14:19:18.917924 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/filters/bessel/Bessel.md b/lib/filters/bessel/Bessel.md index 4d250783..a8692548 100644 --- a/lib/filters/bessel/Bessel.md +++ b/lib/filters/bessel/Bessel.md @@ -7,7 +7,7 @@ | **Parameters** | `length` | | **Outputs** | Single series (Bessel) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `length` bars | | **Signature** | [bessel_signature](bessel_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - The Bessel Filter is a 2nd-order low-pass IIR filter designed to preserve the **shape** and **timing** of price moves. - Parameterized by `length`. - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `length` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > When you care more about *when* the market turns than how aggressively you can torture the noise, you reach for a Bessel. diff --git a/lib/filters/bessel/bessel.pine b/lib/filters/bessel/bessel.pine index b339e530..4cded7ea 100644 --- a/lib/filters/bessel/bessel.pine +++ b/lib/filters/bessel/bessel.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Bessel 2nd Order Filter (BESSEL)", "BESSEL", overlay=true) diff --git a/lib/filters/bilateral/Bilateral.cs b/lib/filters/bilateral/Bilateral.cs index 0c46e767..a1ca235b 100644 --- a/lib/filters/bilateral/Bilateral.cs +++ b/lib/filters/bilateral/Bilateral.cs @@ -114,10 +114,10 @@ public sealed class Bilateral : AbstractBase { double val = GetValidValue(source[i]); double removed = _buffer.Add(val); - _state.SumSq += (val * val); + _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq); if (_buffer.IsFull) { - _state.SumSq -= (removed * removed); + _state.SumSq = Math.FusedMultiplyAdd(-removed, removed, _state.SumSq); } } @@ -187,10 +187,10 @@ public sealed class Bilateral : AbstractBase double val = GetValidValue(input.Value); double removed = _buffer.Add(val); - _state.SumSq += (val * val); + _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq); if (_buffer.IsFull) { - _state.SumSq -= (removed * removed); + _state.SumSq = Math.FusedMultiplyAdd(-removed, removed, _state.SumSq); } } else @@ -207,15 +207,15 @@ public sealed class Bilateral : AbstractBase if (_buffer.Count == 0) { _buffer.Add(val); - _state.SumSq += (val * val); + _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq); } else { double oldNewest = _buffer.Newest; // Get current newest before overwriting _buffer.UpdateNewest(val); - _state.SumSq -= (oldNewest * oldNewest); - _state.SumSq += (val * val); + _state.SumSq = Math.FusedMultiplyAdd(-oldNewest, oldNewest, _state.SumSq); + _state.SumSq = Math.FusedMultiplyAdd(val, val, _state.SumSq); } } @@ -421,12 +421,12 @@ public sealed class Bilateral : AbstractBase { removed = window[windowIdx]; sum -= removed; - sumSq -= removed * removed; + sumSq = Math.FusedMultiplyAdd(-removed, removed, sumSq); } window[windowIdx] = val; sum += val; - sumSq += val * val; + sumSq = Math.FusedMultiplyAdd(val, val, sumSq); int currentNewestIdx = windowIdx; windowIdx = (windowIdx + 1) % period; diff --git a/lib/filters/bilateral/Bilateral.pine b/lib/filters/bilateral/Bilateral.pine index 91176c8c..b69ef886 100644 --- a/lib/filters/bilateral/Bilateral.pine +++ b/lib/filters/bilateral/Bilateral.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Bilateral Filter (BILATERAL)", "BILATERAL", overlay=true) diff --git a/lib/filters/bpf/Bpf.md b/lib/filters/bpf/Bpf.md index e9f90d08..9d03cd08 100644 --- a/lib/filters/bpf/Bpf.md +++ b/lib/filters/bpf/Bpf.md @@ -6,15 +6,14 @@ | **Inputs** | Source (close) | | **Parameters** | `lowerPeriod`, `upperPeriod` | | **Outputs** | Single series (BPF) | -| **Output range** | Tracks input | +| **Output range** | Oscillates around zero | | **Warmup** | `Math.Max(lowerPeriod, upperPeriod)` bars | -| **Signature** | [bpf_signature](bpf_signature.md) | ### TL;DR - The **BPF** (BandPass Filter) is a second-order IIR architecture designed to surgically excise specific frequency components from a time series. - Parameterized by `lowerperiod`, `upperperiod`. -- Output range: Tracks input. +- Output range: Oscillates around zero (bandpass extracts cyclic component). - Requires `Math.Max(lowerPeriod, upperPeriod)` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/filters/bpf/bpf.pine b/lib/filters/bpf/bpf.pine index 9ea900f0..67b1e8a2 100644 --- a/lib/filters/bpf/bpf.pine +++ b/lib/filters/bpf/bpf.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Bandpass Filter (BPF)", "BPF", overlay=true) diff --git a/lib/filters/bpf/bpf_signature.md b/lib/filters/bpf/bpf_signature.md deleted file mode 100644 index 5add5eb2..00000000 --- a/lib/filters/bpf/bpf_signature.md +++ /dev/null @@ -1,3 +0,0 @@ -# BPF Signature - -![BPF Signature](bpf_signature.svg) \ No newline at end of file diff --git a/lib/filters/bpf/bpf_signature.svg b/lib/filters/bpf/bpf_signature.svg deleted file mode 100644 index a5d4e1ba..00000000 --- a/lib/filters/bpf/bpf_signature.svg +++ /dev/null @@ -1,19881 +0,0 @@ - - - - - - - - 2026-03-02T14:19:23.147479 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/filters/butter2/butter2.pine b/lib/filters/butter2/butter2.pine index 3e5c8e8f..8ab7956e 100644 --- a/lib/filters/butter2/butter2.pine +++ b/lib/filters/butter2/butter2.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers 2-Pole Butterworth Filter (BUTTER2)", "BUTTER2", overlay=true) diff --git a/lib/filters/butter3/butter3.pine b/lib/filters/butter3/butter3.pine index 2807386e..8c929455 100644 --- a/lib/filters/butter3/butter3.pine +++ b/lib/filters/butter3/butter3.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 // Indicator algorithm (C) 2004-2024 John F. Ehlers diff --git a/lib/filters/cfitz/Cfitz.md b/lib/filters/cfitz/Cfitz.md index 70f04c1d..b1b21549 100644 --- a/lib/filters/cfitz/Cfitz.md +++ b/lib/filters/cfitz/Cfitz.md @@ -6,15 +6,14 @@ | **Inputs** | Source (close) | | **Parameters** | `pLow` (default 6), `pHigh` (default 32) | | **Outputs** | Single series (Cfitz) | -| **Output range** | Tracks input | +| **Output range** | Oscillates around zero | | **Warmup** | `2` bars | -| **Signature** | [cfitz_signature](cfitz_signature.md) | ### TL;DR - The **Christiano-Fitzgerald Band-Pass Filter** is an asymmetric full-sample filter that approximates the ideal spectral band-pass by using time-var... - Parameterized by `plow` (default 6), `phigh` (default 32). -- Output range: Tracks input. +- Output range: Oscillates around zero (extracts cyclical component). - Requires `2` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/filters/cfitz/cfitz.pine b/lib/filters/cfitz/cfitz.pine index 87cd831a..122acae8 100644 --- a/lib/filters/cfitz/cfitz.pine +++ b/lib/filters/cfitz/cfitz.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Christiano-Fitzgerald Band-Pass Filter (CFITZ)", "CFITZ", overlay=false, max_bars_back=5000) diff --git a/lib/filters/cfitz/cfitz_signature.md b/lib/filters/cfitz/cfitz_signature.md deleted file mode 100644 index 328bf330..00000000 --- a/lib/filters/cfitz/cfitz_signature.md +++ /dev/null @@ -1,3 +0,0 @@ -# CFITZ Signature - -![CFITZ Signature](cfitz_signature.svg) \ No newline at end of file diff --git a/lib/filters/cfitz/cfitz_signature.svg b/lib/filters/cfitz/cfitz_signature.svg deleted file mode 100644 index 84b043a2..00000000 --- a/lib/filters/cfitz/cfitz_signature.svg +++ /dev/null @@ -1,19647 +0,0 @@ - - - - - - - - 2026-03-02T14:19:27.388136 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/filters/cheby1/cheby1.pine b/lib/filters/cheby1/cheby1.pine index 12ddf593..0b09b8ee 100644 --- a/lib/filters/cheby1/cheby1.pine +++ b/lib/filters/cheby1/cheby1.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Chebyshev Type I Filter (CHEBY1)", "CHEBY1", overlay=true) diff --git a/lib/filters/cheby2/cheby2.pine b/lib/filters/cheby2/cheby2.pine index c177ad24..b9cd342f 100644 --- a/lib/filters/cheby2/cheby2.pine +++ b/lib/filters/cheby2/cheby2.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Chebyshev Type II Filter (CHEBY2)", "CHEBY2", overlay=true) diff --git a/lib/filters/edcf/Edcf.md b/lib/filters/edcf/Edcf.md index 6c49cd23..8ada382f 100644 --- a/lib/filters/edcf/Edcf.md +++ b/lib/filters/edcf/Edcf.md @@ -7,7 +7,7 @@ | **Parameters** | `length` (default 15) | | **Outputs** | Single series (Edcf) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `length` bars | | **Signature** | [edcf_signature](edcf_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - The **Ehlers Distance Coefficient Filter (EDCF)** is a nonlinear adaptive FIR filter created by John F. - Parameterized by `length` (default 15). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `length` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. ## Overview diff --git a/lib/filters/edcf/edcf.pine b/lib/filters/edcf/edcf.pine index 521e4585..0b47072f 100644 --- a/lib/filters/edcf/edcf.pine +++ b/lib/filters/edcf/edcf.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("EDCF: Ehlers Distance Coefficient Filter", "EDCF", overlay = true) diff --git a/lib/filters/elliptic/elliptic.pine b/lib/filters/elliptic/elliptic.pine index b23e3aa3..ab8b346b 100644 --- a/lib/filters/elliptic/elliptic.pine +++ b/lib/filters/elliptic/elliptic.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Elliptic 2nd Order Filter (ELLIPTIC)", "ELLIPTIC", overlay=true) diff --git a/lib/filters/gauss/Gauss.md b/lib/filters/gauss/Gauss.md index 4a4082cd..9184562f 100644 --- a/lib/filters/gauss/Gauss.md +++ b/lib/filters/gauss/Gauss.md @@ -8,13 +8,15 @@ | **Outputs** | Single series (Gauss) | | **Output range** | Tracks input | | **Warmup** | 1 bar | +| **Signature** | [gauss_signature](gauss_signature.md) | + ### TL;DR - Gauss (Gaussian Filter) is a smoothing filter that applies a Gaussian kernel to time series data. - Parameterized by `sigma` (default 1.0). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `2⌈3σ⌉+1` bars of warmup before first valid output (IsHot = true). Default: **7 bars** (σ=1.0). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "SMA smears data like cheap paint. Gaussian filtering respects the signal's soul." diff --git a/lib/filters/gauss/gauss.pine b/lib/filters/gauss/gauss.pine index fc13e893..7a958d7d 100644 --- a/lib/filters/gauss/gauss.pine +++ b/lib/filters/gauss/gauss.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Gaussian Filter (GAUSS)", "GAUSS", overlay=true) diff --git a/lib/filters/gauss/gauss_signature.md b/lib/filters/gauss/gauss_signature.md new file mode 100644 index 00000000..a8b6f344 --- /dev/null +++ b/lib/filters/gauss/gauss_signature.md @@ -0,0 +1,3 @@ +# ELLIPTIC Signature + +![GAUSS signature](gauss_signature.svg) \ No newline at end of file diff --git a/lib/filters/hann/Hann.md b/lib/filters/hann/Hann.md index 50e5023e..a135dc14 100644 --- a/lib/filters/hann/Hann.md +++ b/lib/filters/hann/Hann.md @@ -15,7 +15,7 @@ - Hann (Hann Filter) is a Finite Impulse Response (FIR) smoothing filter that applies a Hann window to time series data. - Parameterized by `length`. - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `length` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The Hanning window whispers where the Boxcar screams. Smoothness is not just an aesthetic; it's a mathematical necessity." diff --git a/lib/filters/hann/hann.pine b/lib/filters/hann/hann.pine index 9306debb..97ec9717 100644 --- a/lib/filters/hann/hann.pine +++ b/lib/filters/hann/hann.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Hann FIR Filter (HANN)", "HANN", overlay=true) diff --git a/lib/filters/hp/Hp.md b/lib/filters/hp/Hp.md index a07fcdf8..2588944b 100644 --- a/lib/filters/hp/Hp.md +++ b/lib/filters/hp/Hp.md @@ -8,13 +8,15 @@ | **Outputs** | Single series (HP) | | **Output range** | Tracks input | | **Warmup** | 1 bar | +| **Signature** | [hp_signature](hp_signature.md) | + ### TL;DR - The Hodrick-Prescott (HP) filter is a widely used tool in macroeconomics for separating the cyclical component of a time series from raw data. - Parameterized by `lambda` (default 1600.0). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `⌈2√λ⌉` bars of warmup before first valid output (IsHot = true). Default: **~80 bars** (λ=1600). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Trends are not lines; they are curves that we simplify for our sanity, often at the cost of reality." diff --git a/lib/filters/hp/hp.pine b/lib/filters/hp/hp.pine index 6b829902..5dbdf3e5 100644 --- a/lib/filters/hp/hp.pine +++ b/lib/filters/hp/hp.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Hodrick-Prescott Filter (HP)", "HP", overlay=false) diff --git a/lib/filters/hp/hp_signature.md b/lib/filters/hp/hp_signature.md new file mode 100644 index 00000000..f086fab4 --- /dev/null +++ b/lib/filters/hp/hp_signature.md @@ -0,0 +1,3 @@ +# ELLIPTIC Signature + +![HP Signature](hp_signature.svg) \ No newline at end of file diff --git a/lib/filters/hpf/Hpf.md b/lib/filters/hpf/Hpf.md index 6f22de54..f44e6bbb 100644 --- a/lib/filters/hpf/Hpf.md +++ b/lib/filters/hpf/Hpf.md @@ -8,14 +8,13 @@ | **Outputs** | Single series (HPF) | | **Output range** | Tracks input | | **Warmup** | 1 bar | -| **Signature** | [hpf_signature](hpf_signature.md) | ### TL;DR - The 2-Pole Highpass Filter (HPF) is designed to separate high-frequency components (like cycles and noise) from the underlying trend. - Parameterized by `length` (default 40). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero (detrended signal). +- Requires `length` bars of warmup before first valid output (IsHot = true). Default: **40 bars**. - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Noise is just signal you haven't figured out how to filter yet. Or maybe, it's the only signal that matters." diff --git a/lib/filters/hpf/hpf.pine b/lib/filters/hpf/hpf.pine index 6750e046..984881ed 100644 --- a/lib/filters/hpf/hpf.pine +++ b/lib/filters/hpf/hpf.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Ehlers Highpass Filter (HPF)", "HPF", overlay=true) diff --git a/lib/filters/hpf/hpf_signature.md b/lib/filters/hpf/hpf_signature.md deleted file mode 100644 index 9e17f05c..00000000 --- a/lib/filters/hpf/hpf_signature.md +++ /dev/null @@ -1,3 +0,0 @@ -# HPF Signature - -![HPF Signature](hpf_signature.svg) \ No newline at end of file diff --git a/lib/filters/hpf/hpf_signature.svg b/lib/filters/hpf/hpf_signature.svg deleted file mode 100644 index acf0015d..00000000 --- a/lib/filters/hpf/hpf_signature.svg +++ /dev/null @@ -1,19689 +0,0 @@ - - - - - - - - 2026-03-02T14:19:35.053870 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/filters/kalman/Kalman.md b/lib/filters/kalman/Kalman.md index acd879a1..9dd240ca 100644 --- a/lib/filters/kalman/Kalman.md +++ b/lib/filters/kalman/Kalman.md @@ -8,6 +8,8 @@ | **Outputs** | Single series (Kalman) | | **Output range** | Tracks input | | **Warmup** | `10` bars | +| **Signature** | [kalman_signature](kalman_signature.md) | + ### TL;DR diff --git a/lib/filters/kalman/kalman.pine b/lib/filters/kalman/kalman.pine index f14405bf..40f401d7 100644 --- a/lib/filters/kalman/kalman.pine +++ b/lib/filters/kalman/kalman.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Kalman Filter (KALMAN)", "KALMAN", overlay=true) diff --git a/lib/filters/kalman/kalman_signature.md b/lib/filters/kalman/kalman_signature.md new file mode 100644 index 00000000..1f959745 --- /dev/null +++ b/lib/filters/kalman/kalman_signature.md @@ -0,0 +1,3 @@ +# ELLIPTIC Signature + +![KALMAN Signature](kalman_signature.svg) \ No newline at end of file diff --git a/lib/filters/laguerre/Laguerre.md b/lib/filters/laguerre/Laguerre.md index 391b255e..add129d3 100644 --- a/lib/filters/laguerre/Laguerre.md +++ b/lib/filters/laguerre/Laguerre.md @@ -8,6 +8,8 @@ | **Outputs** | Single series (Laguerre) | | **Output range** | Tracks input | | **Warmup** | `WarmupBars` bars | +| **Signature** | [laguerre_signature](laguerre_signature.md) | + ### TL;DR diff --git a/lib/filters/laguerre/laguerre.pine b/lib/filters/laguerre/laguerre.pine index ce217505..47e681d1 100644 --- a/lib/filters/laguerre/laguerre.pine +++ b/lib/filters/laguerre/laguerre.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 // Indicator algorithm (C) 2004 John F. Ehlers diff --git a/lib/filters/laguerre/laguerre_signature.md b/lib/filters/laguerre/laguerre_signature.md new file mode 100644 index 00000000..81199426 --- /dev/null +++ b/lib/filters/laguerre/laguerre_signature.md @@ -0,0 +1,3 @@ +# ELLIPTIC Signature + +![LAGUERRE Signature](laguerre_signature.svg) \ No newline at end of file diff --git a/lib/filters/lms/lms.pine b/lib/filters/lms/lms.pine index 9f9cac6d..e761621c 100644 --- a/lib/filters/lms/lms.pine +++ b/lib/filters/lms/lms.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("Least Mean Squares Adaptive Filter (LMS)", "LMS", overlay=true) diff --git a/lib/filters/loess/Loess.cs b/lib/filters/loess/Loess.cs index 2dcc9a57..bb959812 100644 --- a/lib/filters/loess/Loess.cs +++ b/lib/filters/loess/Loess.cs @@ -1,4 +1,3 @@ -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -309,48 +308,7 @@ public sealed class Loess : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double DotProduct(ReadOnlySpan a, ReadOnlySpan b) - { - // a and b expected to be same length (slice called in Update ensures this) - int length = a.Length; - if (length == 0) - { - return 0; - } - - int i = 0; - double sum = 0; - - if (Vector.IsHardwareAccelerated && length >= Vector.Count) - { - var vSum = Vector.Zero; - ref double rA = ref MemoryMarshal.GetReference(a); - ref double rB = ref MemoryMarshal.GetReference(b); - - int vectorCount = Vector.Count; - int limit = length - vectorCount; - - for (; i <= limit; i += vectorCount) - { - var vA = Vector.LoadUnsafe(ref rA, (nuint)i); - var vB = Vector.LoadUnsafe(ref rB, (nuint)i); - vSum += vA * vB; - } - - // Reduce vector sum - for (int j = 0; j < vectorCount; j++) - { - sum += vSum[j]; - } - } - - // Remainder - for (; i < length; i++) - { - sum += a[i] * b[i]; - } - - return sum; - } + => a.DotProduct(b); public static (TSeries Results, Loess Indicator) Calculate(TSeries source, int period) { diff --git a/lib/filters/loess/loess.pine b/lib/filters/loess/loess.pine index c67ed154..527b6b18 100644 --- a/lib/filters/loess/loess.pine +++ b/lib/filters/loess/loess.pine @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 indicator("LOESS Filter", "LOESS", overlay=true) diff --git a/lib/filters/loess/loess_signature.svg b/lib/filters/loess/loess_signature.svg index d4e24468..03e8381d 100644 --- a/lib/filters/loess/loess_signature.svg +++ b/lib/filters/loess/loess_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:19:36.373048 + 2026-03-10T19:40:09.112705 image/svg+xml @@ -30,28 +30,313 @@ z - + + + + + + + + + - - + - - + + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - + + + + + - + - + - - - - + @@ -300,126 +840,449 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + - - - - - - @@ -627,233 +1399,1176 @@ z - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - - - - - - - - - - - - - - - + - + + + + - + - - - + + + - + + - + + + + - + - - - + + + - + + - + + + + - + - - - + + + + + + - + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -2574,29 +9407,449 @@ z + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3386,233 +13428,1151 @@ L 1072.948078 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + - - + + + + + - + - - - - - - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3623,698 +14583,3466 @@ L 1433.247957 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + @@ -4627,6 +19442,7 @@ L 716 0 L 716 1209 z " transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.md b/lib/filters/oneeuro/OneEuro.md index 9ef21f53..b731ed2f 100644 --- a/lib/filters/oneeuro/OneEuro.md +++ b/lib/filters/oneeuro/OneEuro.md @@ -8,6 +8,8 @@ | **Outputs** | Single series (OneEuro) | | **Output range** | Tracks input | | **Warmup** | `1` bars | +| **Signature** | [oneeuro_signature](oneeuro_signature.md) | + ### TL;DR 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/oneeuro/oneeuro_signature.md b/lib/filters/oneeuro/oneeuro_signature.md new file mode 100644 index 00000000..cce643e9 --- /dev/null +++ b/lib/filters/oneeuro/oneeuro_signature.md @@ -0,0 +1,3 @@ +# LOESS Signature + +![ONEEURO Signature](oneeuro_signature.svg) \ No newline at end of file diff --git a/lib/filters/rls/Rls.md b/lib/filters/rls/Rls.md index d52e161f..899b4d68 100644 --- a/lib/filters/rls/Rls.md +++ b/lib/filters/rls/Rls.md @@ -8,6 +8,8 @@ | **Outputs** | Single series (RLS) | | **Output range** | Tracks input | | **Warmup** | `order + 1` bars | +| **Signature** | [rls_signature](rls_signature.md) | + ### TL;DR 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/rls/rls_signature.md b/lib/filters/rls/rls_signature.md new file mode 100644 index 00000000..e9163d2b --- /dev/null +++ b/lib/filters/rls/rls_signature.md @@ -0,0 +1,3 @@ +# LOESS Signature + +![RLS Signature](rls_signature.svg) \ No newline at end of file diff --git a/lib/filters/rmed/Rmed.md b/lib/filters/rmed/Rmed.md index f86e12c4..1c7294ec 100644 --- a/lib/filters/rmed/Rmed.md +++ b/lib/filters/rmed/Rmed.md @@ -7,7 +7,7 @@ | **Parameters** | `period` (default 12) | | **Outputs** | Single series (Rmed) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | 5 bars (MedianWindow) | | **Signature** | [rmed_signature](rmed_signature.md) | ### TL;DR @@ -15,7 +15,7 @@ - RMED applies exponential smoothing to a 5-bar running median, creating a nonlinear IIR filter that rejects impulsive spike noise while providing sm... - Parameterized by `period` (default 12). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires **5 bars** of warmup (MedianWindow) before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "John Ehlers combined two tools that rarely meet: the median (nonlinear, spike-resistant) and the EMA (smooth, recursive). The median kills the spikes, the EMA smooths the survivors. Together they produce a filter that is both resistant and smooth." diff --git a/lib/filters/roofing/Roofing.md b/lib/filters/roofing/Roofing.md index 5c29790a..f93e5654 100644 --- a/lib/filters/roofing/Roofing.md +++ b/lib/filters/roofing/Roofing.md @@ -6,15 +6,15 @@ | **Inputs** | Source (close) | | **Parameters** | `hpLength` (default 48), `ssLength` (default 10) | | **Outputs** | Single series (ROOFING) | -| **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Output range** | Oscillates around zero | +| **Warmup** | `hpLength` bars (default 48) | ### TL;DR - The **Roofing Filter** is John Ehlers' bandpass architecture designed specifically for oscillator construction. - Parameterized by `hplength` (default 48), `sslength` (default 10). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero (bandpass behavior). +- Requires `hpLength` bars of warmup before first valid output (IsHot = true). Default: **48 bars**. - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The trend is your friend until it overwhelms the signal. The noise is your enemy until you mistake it for alpha." diff --git a/lib/filters/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/roofing/roofing_signature.svg b/lib/filters/roofing/roofing_signature.svg deleted file mode 100644 index 9fa9c70a..00000000 --- a/lib/filters/roofing/roofing_signature.svg +++ /dev/null @@ -1,19488 +0,0 @@ - - - - - - - - 2026-03-02T15:02:13.896994 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/sgf/sgf_signature.svg b/lib/filters/sgf/sgf_signature.svg index 29a0ac04..af6d58e7 100644 --- a/lib/filters/sgf/sgf_signature.svg +++ b/lib/filters/sgf/sgf_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:19:42.547595 + 2026-03-10T19:40:09.842674 image/svg+xml @@ -30,28 +30,313 @@ z - + + + + + + + + + - - + - - + + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - + + + + + - + - + - - - - + @@ -300,126 +812,449 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + - - - - - - @@ -627,128 +1371,587 @@ z - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - + - + @@ -756,14 +1959,19 @@ z - + + + + - + - + - + @@ -771,14 +1979,19 @@ z - + + + + - + - + - + @@ -786,14 +1999,19 @@ z - + + + + - + - + - + @@ -801,14 +2019,19 @@ z - + + + + - + - + - + @@ -816,14 +2039,19 @@ z - + + + + - + - + - + @@ -831,29 +2059,449 @@ z + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3386,233 +13646,1111 @@ L 1072.948078 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3623,698 +14761,3436 @@ L 1433.247957 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + @@ -4618,6 +19562,7 @@ L 716 0 L 716 1209 z " transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/filters/spbf/Spbf.md b/lib/filters/spbf/Spbf.md index c13c117c..f315aa73 100644 --- a/lib/filters/spbf/Spbf.md +++ b/lib/filters/spbf/Spbf.md @@ -6,15 +6,15 @@ | **Inputs** | Source (close) | | **Parameters** | `shortPeriod` (default 40), `longPeriod` (default 60), `rmsPeriod` (default 50) | | **Outputs** | Single series (SPBF) | -| **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Output range** | Oscillates around zero | +| **Warmup** | `max(longPeriod, rmsPeriod)` bars (default 60) | ### TL;DR - The **Super Passband Filter** is John Ehlers' wide-band bandpass constructed by differencing two z-transformed EMAs with Ehlers-style smoothing ($\... - Parameterized by `shortperiod` (default 40), `longperiod` (default 60), `rmsperiod` (default 50). -- Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Output range: Oscillates around zero. +- Requires `max(longPeriod, rmsPeriod)` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Two EMAs walk into a frequency domain. The difference between them is the only thing worth trading." diff --git a/lib/filters/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/spbf/spbf_signature.svg b/lib/filters/spbf/spbf_signature.svg deleted file mode 100644 index 51d5b789..00000000 --- a/lib/filters/spbf/spbf_signature.svg +++ /dev/null @@ -1,19761 +0,0 @@ - - - - - - - - 2026-03-02T15:02:16.910049 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/filters/ssf2/ssf2.pine b/lib/filters/ssf2/ssf2.pine index b16c44c8..e49d3b55 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 @@ -26,7 +26,7 @@ ssf2(series float src, simple int length) => float ssrc = nz(src, src[1]) float src1 = nz(src[1], ssrc) float src2 = nz(src[2], src1) - ssf_internal := c1 * ssrc + c2 * nz(ssf_internal[1], src1) + c3 * nz(ssf_internal[2], src2) + ssf_internal := c1 * (ssrc + src1) * 0.5 + c2 * nz(ssf_internal[1], src1) + c3 * nz(ssf_internal[2], src2) ssf_internal // ---------- Main loop ---------- diff --git a/lib/filters/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.md b/lib/filters/usf/Usf.md index f2ebc01e..d76a31e4 100644 --- a/lib/filters/usf/Usf.md +++ b/lib/filters/usf/Usf.md @@ -62,25 +62,24 @@ Where: ### Operation Count (Streaming Mode) -Universal Smooth Filter: adaptive-weight FIR that adjusts taps based on signal characteristics. O(N) per bar. +Ehlers Ultimate Smoother Filter (USF): 2-pole IIR low-pass filter with high-pass subtraction for zero-lag smoothing. Five FMA operations per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Adaptive weight derivation | N | ~8 cy | ~240 cy (N=30) | -| Weighted sum FMA | N | ~5 cy | ~150 cy | -| Normalization | 1 | ~3 cy | ~3 cy | -| **Total (N=30)** | **2N+1** | — | **~393 cycles** | +| Input combination (3 taps) | 3 | ~4 cy | ~12 cy | +| Feedback (2 taps) | 2 | ~4 cy | ~8 cy | +| State update | 4 | ~1 cy | ~4 cy | +| **Total** | **9** | — | **~24 cycles** | -O(N) per bar. Adaptive weights computed per bar (no precomputation) because they depend on current signal level. ~393 cycles for N=30. +O(1) per bar. Coefficients derived from period parameter; precomputed. ~24 cycles/bar. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Adaptive weight computation | Partial | Independent per element; vectorizable if no data dependency | -| Weighted sum FMA | Yes | Standard dot product | +| USF recursion | No | y[n] depends on y[n-1] and y[n-2] | -SIMD potential: ~100 cy for N=30 if adaptive weights vectorized. +Batch throughput: ~24 cy/bar. | Metric | Score | Notes | | :--- | :--- | :--- | diff --git a/lib/filters/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.md b/lib/filters/voss/Voss.md index 2684025f..92811890 100644 --- a/lib/filters/voss/Voss.md +++ b/lib/filters/voss/Voss.md @@ -6,9 +6,8 @@ | **Inputs** | Source (close) | | **Parameters** | `period` (default 20), `predict` (default 3), `bandwidth` (default 0.25) | | **Outputs** | Single series (VOSS) | -| **Output range** | Tracks input | +| **Output range** | Oscillates around zero | | **Warmup** | `period` bars | -| **Signature** | [voss_signature](voss_signature.md) | ### TL;DR @@ -86,25 +85,26 @@ The Voss predictor stage is an IIR filter with `Order` feedback taps, each weigh ### Operation Count (Streaming Mode) -Voss-McCartney 1/f noise filter: octave-cascade of N random sources, each updated probabilistically. O(N) per bar worst-case, O(1) amortized. +Ehlers Voss Predictive Filter: two-stage pipeline — 2-pole bandpass filter (Stage 1) + weighted feedback predictor (Stage 2). O(Order) per bar where Order = 3 × Predict. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Bit-scan (which octave to update) | 1 | ~3 cy | ~3 cy | -| RNG sample + accumulate | 1 | ~8 cy | ~8 cy | -| Running sum update | 1 | ~2 cy | ~2 cy | -| **Total (amortized)** | **3** | — | **~13 cycles** | +| BPF: diff + 3-term FMA | 3 | ~4 cy | ~12 cy | +| Voss: weighted sum loop | Order | ~5 cy | ~45 cy (Order=9) | +| Voss: gain × filt − sumC | 2 | ~4 cy | ~8 cy | +| State update (shifts) | 4 | ~1 cy | ~4 cy | +| **Total (Order=9)** | **Order+9** | — | **~69 cycles** | -O(1) amortized per bar. Each bar updates exactly 1 octave source on average. ~13 cycles/bar amortized. +O(Order) per bar. Order = 3 × Predict; at defaults (Predict=3), Order=9. ~69 cycles/bar. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Octave update scheduling | No | Bit-count branching; data-dependent | -| RNG generation | Partial | SIMD RNG (e.g., xoshiro SIMD) available but niche | +| BPF recursion | No | y[n] depends on y[n-1] and y[n-2] | +| Voss weighted sum | Partial | Inner loop vectorizable but short (Order=9) | -Amortized O(1) makes SIMD gains minimal. Batch throughput: ~13 cy/bar. +Batch throughput: ~69 cy/bar at defaults. | Metric | Value | |--------|-------| diff --git a/lib/filters/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/voss/voss_signature.md b/lib/filters/voss/voss_signature.md deleted file mode 100644 index b6a71da9..00000000 --- a/lib/filters/voss/voss_signature.md +++ /dev/null @@ -1,3 +0,0 @@ -# VOSS Signature - -![VOSS Signature](voss_signature.svg) \ No newline at end of file diff --git a/lib/filters/voss/voss_signature.svg b/lib/filters/voss/voss_signature.svg deleted file mode 100644 index fcc2e8a4..00000000 --- a/lib/filters/voss/voss_signature.svg +++ /dev/null @@ -1,19660 +0,0 @@ - - - - - - - - 2026-03-02T14:19:47.660764 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/filters/wavelet/Wavelet.md b/lib/filters/wavelet/Wavelet.md index abec9960..78e889c8 100644 --- a/lib/filters/wavelet/Wavelet.md +++ b/lib/filters/wavelet/Wavelet.md @@ -7,14 +7,16 @@ | **Parameters** | `levels` (default 4), `threshMult` (default 1.0) | | **Outputs** | Single series (Wavelet) | | **Output range** | Tracks input | -| **Warmup** | 1 bar | +| **Warmup** | `2^levels` bars (default 16) | +| **Signature** | [wavelet_signature](wavelet_signature.md) | + ### TL;DR - The Wavelet Denoising Filter applies an *à trous* (with holes) Haar wavelet decomposition with soft thresholding to remove high-frequency noise fro... - Parameterized by `levels` (default 4), `threshmult` (default 1.0). - Output range: Tracks input. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `2^levels` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The wavelet transform is to the Fourier transform what a microscope is to a telescope: same math, different scale." diff --git a/lib/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/wavelet/wavelet_signature.md b/lib/filters/wavelet/wavelet_signature.md new file mode 100644 index 00000000..e1f9529b --- /dev/null +++ b/lib/filters/wavelet/wavelet_signature.md @@ -0,0 +1,3 @@ +# SGF Signature + +![WAVELET Signature](wavelet_signature.svg) \ No newline at end of file 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.md b/lib/forecasts/afirma/Afirma.md index 3bf9d084..f0270ecd 100644 --- a/lib/forecasts/afirma/Afirma.md +++ b/lib/forecasts/afirma/Afirma.md @@ -137,18 +137,18 @@ $$ ### Operation Count (Streaming Mode) -AFIRMA chains AR model estimation with IIR/FIR filtering — O(p) per bar where p = AR order. +Windowed FIR convolution — O(P) per bar where P = period (window length). | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| AR coefficient estimation (p terms) | p | 4 cy | ~4p cy | -| FIR forward pass (p multiplies) | p | 1 cy | ~p cy | -| IIR feedback pass (p multiplies) | p | 1 cy | ~p cy | -| Output computation via FMA | 1 | 1 cy | ~1 cy | -| NaN guard + state update | 1 | 2 cy | ~2 cy | -| **Total (p=8)** | **O(p)** | — | **~49 cy** | +| RingBuffer write | 1 | ~2 cy | ~2 cy | +| FIR convolution (P FMA ops) | P | ~1 cy | ~P cy | +| Weight normalization | 1 | ~1 cy | ~1 cy | +| LS regression (if enabled) | n | ~2 cy | ~2n cy | +| NaN guard + state update | 1 | ~2 cy | ~2 cy | +| **Total (P=20)** | **O(P)** | — | **~25 cy** | -O(p) per bar where p = AR order. AR coefficient estimation dominates; FMA-fused filter passes are cheap. Streaming mode maintains p state variables. +O(P) per bar where P = window length. FMA-fused dot product dominates; LS regression adds O(n) where n = min(⌊(P−1)/2⌋, 50). | Metric | Value | Notes | | :--- | :---: | :--- | diff --git a/lib/forecasts/afirma/afirma.pine b/lib/forecasts/afirma/afirma.pine index 07962748..79fb661f 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) @@ -7,7 +7,7 @@ indicator("Autoregressive FIR Moving Average (AFIRMA)", "AFIRMA", overlay=true) //@param source Series to calculate AFIRMA from //@param period Lookback period - window size //@param windowType Window function type (1:Hanning, 2:Hamming, 3:Blackman, 4:Blackman-Harris) -//@param leastSquares Apply least squares cubic polynomial fitting for autoregressive prediction +//@param leastSquares Apply least squares linear regression fitting for autoregressive prediction //@returns AFIRMA value, calculates from first bar using available data //@optimized Uses windowing functions with O(n) complexity; least squares adds O(n) for polynomial fitting afirma(series float src, simple int period, simple int windowType=4, simple bool leastSquares=false) => @@ -111,7 +111,7 @@ afirma(series float src, simple int period, simple int windowType=4, simple bool i_period = input.int(20, "Period", minval=1) i_source = input.source(close, "Source") i_windowType = input.int(4, "Window Function", minval=1, maxval=4, tooltip="1:Hanning, 2:Hamming, 3:Blackman, 4:Blackman-Harris") -i_leastSquares = input.bool(false, "Least Squares Method", tooltip="Enable cubic polynomial fitting for autoregressive prediction") +i_leastSquares = input.bool(false, "Least Squares Method", tooltip="Enable linear regression fitting for autoregressive prediction") // Calculation afirma_value = afirma(i_source, i_period, i_windowType, i_leastSquares) diff --git a/lib/momentum/asi/Asi.md b/lib/momentum/asi/Asi.md index d00bf7eb..c8deb56e 100644 --- a/lib/momentum/asi/Asi.md +++ b/lib/momentum/asi/Asi.md @@ -7,7 +7,7 @@ | **Parameters** | `limitMove` (default 3.0) | | **Outputs** | Single series (Asi) | | **Output range** | Varies (see docs) | -| **Warmup** | `> 2` bars | +| **Warmup** | 2 bars | ### TL;DR diff --git a/lib/momentum/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.md b/lib/momentum/bop/Bop.md index 4a1a7e50..eda343b2 100644 --- a/lib/momentum/bop/Bop.md +++ b/lib/momentum/bop/Bop.md @@ -7,7 +7,7 @@ | **Parameters** | None | | **Outputs** | Single series (BOP) | | **Output range** | Varies (see docs) | -| **Warmup** | `> 0` bars | +| **Warmup** | 0 bars (always hot) | ### TL;DR diff --git a/lib/momentum/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.md b/lib/momentum/cci/Cci.md index 105e0c44..492fb1b0 100644 --- a/lib/momentum/cci/Cci.md +++ b/lib/momentum/cci/Cci.md @@ -4,17 +4,17 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `period` (default DefaultPeriod) | +| **Parameters** | `period` (default 20) | | **Outputs** | Single series (CCI) | | **Output range** | Varies (see docs) | -| **Warmup** | `> period` bars | +| **Warmup** | `period` bars | ### TL;DR - The Commodity Channel Index (CCI) is a versatile momentum-based oscillator developed by Donald Lambert in 1980. -- Parameterized by `period` (default defaultperiod). +- Parameterized by `period` (default 20). - Output range: Varies (see docs). -- Requires `> period` bars of warmup before first valid output (IsHot = true). +- Requires `period` bars (20 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. ## Overview diff --git a/lib/momentum/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.md b/lib/momentum/cfb/Cfb.md index 74ab8fba..daac5087 100644 --- a/lib/momentum/cfb/Cfb.md +++ b/lib/momentum/cfb/Cfb.md @@ -7,14 +7,14 @@ | **Parameters** | int[]? lengths = null | | **Outputs** | Single series (CFB) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `maxLen` bars (192 default) | ### TL;DR - The Composite Fractal Behavior index measures trend duration by analyzing fractal efficiency across 96 simultaneous lookback periods (2 to 192 bars... - Parameterized by int[]? lengths = null. - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `maxLen` bars (192 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "Mark Jurik's CFB is not a momentum indicator. It is a stopwatch for chaos." diff --git a/lib/momentum/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.md b/lib/momentum/cmo/Cmo.md index 50334c43..7a956d9b 100644 --- a/lib/momentum/cmo/Cmo.md +++ b/lib/momentum/cmo/Cmo.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | Source (close) | -| **Parameters** | `period` (default DefaultPeriod) | +| **Parameters** | `period` (default 14) | | **Outputs** | Single series (Cmo) | | **Output range** | $-100$ to $+100$ | | **Warmup** | `period + 1` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Chande Momentum Oscillator (CMO) is a momentum indicator developed by Tushar Chande. -- Parameterized by `period` (default defaultperiod). +- Parameterized by `period` (default 14). - Output range: $-100$ to $+100$. - Requires `period + 1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/momentum/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.md b/lib/momentum/macd/Macd.md index fa919674..0a3d3c84 100644 --- a/lib/momentum/macd/Macd.md +++ b/lib/momentum/macd/Macd.md @@ -7,14 +7,14 @@ | **Parameters** | `fastPeriod` (default 12), `slowPeriod` (default 26), `signalPeriod` (default 9) | | **Outputs** | Multiple series (Signal, Histogram) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `Max(fast, slow) + signal - 2` bars (33 default) | ### TL;DR - The Moving Average Convergence Divergence measures momentum through the relationship between two exponential moving averages. - Parameterized by `fastperiod` (default 12), `slowperiod` (default 26), `signalperiod` (default 9). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `Max(fast, slow) + signal - 2` bars (33 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "The trend is your friend, until it bends." — Ed Seykota diff --git a/lib/momentum/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.md b/lib/momentum/ppo/Ppo.md index 9a513615..3dcd61c8 100644 --- a/lib/momentum/ppo/Ppo.md +++ b/lib/momentum/ppo/Ppo.md @@ -4,17 +4,17 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | Source (close) | -| **Parameters** | `fastPeriod` (default DefaultFastPeriod), `slowPeriod` (default DefaultSlowPeriod), `signalPeriod` (default DefaultSignalPeriod) | +| **Parameters** | `fastPeriod` (default 12), `slowPeriod` (default 26), `signalPeriod` (default 9) | | **Outputs** | Multiple series (Signal, Histogram) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | `slowPeriod + signalPeriod` bars (35 default) | ### TL;DR - PPO (Percentage Price Oscillator) measures the percentage difference between a fast EMA and a slow EMA. -- Parameterized by `fastperiod` (default defaultfastperiod), `slowperiod` (default defaultslowperiod), `signalperiod` (default defaultsignalperiod). +- Parameterized by `fastPeriod` (default 12), `slowPeriod` (default 26), `signalPeriod` (default 9). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires `slowPeriod + signalPeriod` bars (35 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. > "MACD told you the spread in dollars. PPO tells you the spread in percent. One of those actually works across instruments." diff --git a/lib/momentum/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.md b/lib/momentum/rsx/Rsx.md index 353d56e4..633850ce 100644 --- a/lib/momentum/rsx/Rsx.md +++ b/lib/momentum/rsx/Rsx.md @@ -1,4 +1,4 @@ -# RSX: Relative Strength Quality Index +# RSX: Relative Strength Xtra (Jurik RSX) | Property | Value | | ---------------- | -------------------------------- | diff --git a/lib/momentum/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.md b/lib/momentum/tsi/Tsi.md index 319e6132..2f3125a3 100644 --- a/lib/momentum/tsi/Tsi.md +++ b/lib/momentum/tsi/Tsi.md @@ -4,17 +4,17 @@ | ---------------- | -------------------------------- | | **Category** | Momentum | | **Inputs** | Source (close) | -| **Parameters** | `longPeriod` (default DefaultLongPeriod), `shortPeriod` (default DefaultShortPeriod), `signalPeriod` (default DefaultSignalPeriod) | +| **Parameters** | `longPeriod` (default 25), `shortPeriod` (default 13), `signalPeriod` (default 13) | | **Outputs** | Single series (Tsi) | -| **Output range** | $-1$ to $+1$ | -| **Warmup** | 1 bar | +| **Output range** | $-100$ to $+100$ | +| **Warmup** | `longPeriod + shortPeriod + signalPeriod` bars (51 default) | ### TL;DR - The True Strength Index (TSI) is a momentum oscillator developed by William Blau that uses double-smoothed exponential moving averages of price mom... -- Parameterized by `longperiod` (default defaultlongperiod), `shortperiod` (default defaultshortperiod), `signalperiod` (default defaultsignalperiod). -- Output range: $-1$ to $+1$. -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Parameterized by `longPeriod` (default 25), `shortPeriod` (default 13), `signalPeriod` (default 13). +- Output range: $-100$ to $+100$. +- Requires `longPeriod + shortPeriod + signalPeriod` bars (51 default) of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. The True Strength Index (TSI) is a momentum oscillator developed by William Blau that uses double-smoothed exponential moving averages of price momentum to reduce noise and identify trend strength and direction. diff --git a/lib/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..a347913b 100644 --- a/lib/numerics/_index.md +++ b/lib/numerics/_index.md @@ -10,20 +10,24 @@ Basic mathematical transforms and utility functions for time series. These build | [BETADIST](betadist/Betadist.md) | Beta Distribution | Beta probability distribution transform. | | [BINOMDIST](binomdist/Binomdist.md) | Binomial Distribution | Binomial probability distribution transform. | | [CHANGE](change/Change.md) | Percentage Change | Relative price movement over lookback period. | +| [DECAY](decay/Decay.md) | Linear Decay | Peak envelope with linear degradation; max(input, prev − 1/period). | | [CWT](cwt/Cwt.md) | Continuous Wavelet Transform | Time-frequency decomposition with continuous wavelets. | | [DWT](dwt/Dwt.md) | Discrete Wavelet Transform | À trous Haar stationary DWT; multi-resolution approximation + detail decomposition. | +| [EDECAY](edecay/Edecay.md) | Exponential Decay | Peak envelope with exponential degradation; max(input, prev × (period−1)/period). | | [EXPDIST](expdist/Expdist.md) | Exponential Distribution | Exponential probability distribution transform. | | [EXPTRANS](exptrans/Exptrans.md) | Exponential Transform | e^x transform for log-space conversion reversal. | | [FDIST](fdist/Fdist.md) | F-Distribution | Fisher-Snedecor probability distribution transform. | | [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..af73962e 100644 --- a/lib/numerics/accel/accel.pine +++ b/lib/numerics/accel/accel.pine @@ -1,83 +1,28 @@ -// 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) +indicator("Second Derivative / Acceleration (ACCEL)", "ACCEL", overlay=false, precision=8) -//@function Calculates acceleration (slope of slope) -//@param src Source series to calculate slope from -//@param len Lookback period for calculation -//@returns acceleration -accel(series float src, simple int len1) => - if len1 <= 1 - runtime.error("Length 1 for slope calculation must be greater than 1") - var float sumX1 = 0.0, var float sumY1 = 0.0, var float sumXY1 = 0.0, var float sumX21 = 0.0 - var int validCount1 = 0 - var array x_values1 = array.new_float(len1) - var array y_values1 = array.new_float(len1) - var int head1 = 0 - var int internal_time_counter1 = 0 - if internal_time_counter1 >= len1 - float oldX1 = array.get(x_values1, head1) - float oldY1 = array.get(y_values1, head1) - if not na(oldY1) - sumX1 := sumX1 - oldX1, sumY1 := sumY1 - oldY1 - sumXY1 := sumXY1 - oldX1 * oldY1, sumX21 := sumX21 - oldX1 * oldX1 - validCount1 := validCount1 - 1 - float currentX1 = internal_time_counter1 - float currentY1 = src - array.set(x_values1, head1, currentX1) - array.set(y_values1, head1, currentY1) - if not na(currentY1) - sumX1 := sumX1 + currentX1, sumY1 := sumY1 + currentY1 - sumXY1 := sumXY1 + currentX1 * currentY1, sumX21 := sumX21 + currentX1 * currentX1 - validCount1 := validCount1 + 1 - head1 := (head1 + 1) % len1 - internal_time_counter1 := internal_time_counter1 + 1 - float current_slope = na - if validCount1 >= 2 - float n1 = validCount1 - float divisor1 = n1 * sumX21 - sumX1 * sumX1 - if divisor1 != 0.0 - current_slope := (n1 * sumXY1 - sumX1 * sumY1) / divisor1 - var float sumX2 = 0.0, var float sumY2 = 0.0, var float sumXY2 = 0.0, var float sumX22 = 0.0 - var int validCount2 = 0 - var array x_values2 = array.new_float(len1) - var array y_values2 = array.new_float(len1) - var int head2 = 0 - var int internal_time_counter2 = 0 - if internal_time_counter2 >= len1 - float oldX2 = array.get(x_values2, head2) - float oldY2 = array.get(y_values2, head2) - if not na(oldY2) - sumX2 := sumX2 - oldX2, sumY2 := sumY2 - oldY2 - sumXY2 := sumXY2 - oldX2 * oldY2, sumX22 := sumX22 - oldX2 * oldX2 - validCount2 := validCount2 - 1 - float currentX2 = internal_time_counter2 - float currentY2 = current_slope - array.set(x_values2, head2, currentX2) - array.set(y_values2, head2, currentY2) - if not na(currentY2) - sumX2 := sumX2 + currentX2, sumY2 := sumY2 + currentY2 - sumXY2 := sumXY2 + currentX2 * currentY2, sumX22 := sumX22 + currentX2 * currentX2 - validCount2 := validCount2 + 1 - head2 := (head2 + 1) % len1 - internal_time_counter2 := internal_time_counter2 + 1 - float calculatedAccel = na - if validCount2 >= 2 - float n2 = validCount2 - float divisor2 = n2 * sumX22 - sumX2 * sumX2 - if divisor2 != 0.0 - calculatedAccel := (n2 * sumXY2 - sumX2 * sumY2) / divisor2 - calculatedAccel +//@function Calculates the second finite difference (acceleration): Accel = V[t] - 2*V[t-1] + V[t-2] +//@param src Source series +//@returns Second difference. Returns 0.0 until 3 bars are available. +//@optimized Uses direct history access and FMA-equivalent for zero-allocation streaming. +accel(series float src) => + float v0 = src + float v1 = src[1] + float v2 = src[2] + if na(v1) or na(v2) + 0.0 + else + v0 - 2.0 * v1 + v2 // ---------- Main loop ---------- // Inputs -i_period = input.int(14, "Period", minval=2) i_source = input.source(close, "Source") // Calculation -a = accel(i_source, i_period) +a = accel(i_source) // Plot plot(a, "Accel", color=color.yellow, linewidth=2) diff --git a/lib/numerics/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..72de920d 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) @@ -27,4 +27,4 @@ i_length = input.int(1, "Length", minval = 1) result = change(i_source, i_length) // Plot -plot(result, "Change %", color.blue, color=color.yellow, linewidth=2) +plot(result, "Change %", color=color.yellow, linewidth=2) diff --git a/lib/numerics/cwt/Cwt.md b/lib/numerics/cwt/Cwt.md index 1a59b34f..7749e021 100644 --- a/lib/numerics/cwt/Cwt.md +++ b/lib/numerics/cwt/Cwt.md @@ -7,14 +7,14 @@ | **Parameters** | `scale` (default 10.0), `omega0` (default 6.0) | | **Outputs** | Single series (Cwt) | | **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Warmup** | windowSize (2K+1) bars, where K = round(3 × scale) | ### TL;DR - CWT computes the magnitude of the Continuous Wavelet Transform at a specified scale using the Morlet wavelet, providing a time-frequency decomposit... - Parameterized by `scale` (default 10.0), `omega0` (default 6.0). - Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). +- Requires windowSize (2K+1) bars of warmup before first valid output (IsHot = true), where K = round(3 × scale). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. CWT computes the magnitude of the Continuous Wavelet Transform at a specified scale using the Morlet wavelet, providing a time-frequency decomposition that measures the energy content of a specific frequency band at each point in time. Unlike Fourier analysis which loses time localization, the wavelet transform maintains both time and frequency information simultaneously. The output is a non-negative magnitude series where peaks indicate strong presence of the target frequency (determined by the scale parameter) and troughs indicate absence of that frequency component. diff --git a/lib/numerics/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/decay/Decay.Quantower.Tests.cs b/lib/numerics/decay/Decay.Quantower.Tests.cs new file mode 100644 index 00000000..26ea769c --- /dev/null +++ b/lib/numerics/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/numerics/decay/Decay.Quantower.cs b/lib/numerics/decay/Decay.Quantower.cs new file mode 100644 index 00000000..8aba7297 --- /dev/null +++ b/lib/numerics/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/numerics/decay/Decay.Tests.cs b/lib/numerics/decay/Decay.Tests.cs new file mode 100644 index 00000000..315328e2 --- /dev/null +++ b/lib/numerics/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/numerics/decay/Decay.Validation.Tests.cs b/lib/numerics/decay/Decay.Validation.Tests.cs new file mode 100644 index 00000000..7721a3ef --- /dev/null +++ b/lib/numerics/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/numerics/decay/Decay.cs b/lib/numerics/decay/Decay.cs new file mode 100644 index 00000000..2efdeef3 --- /dev/null +++ b/lib/numerics/decay/Decay.cs @@ -0,0 +1,196 @@ +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 double _scale; + private int _count; + [StructLayout(LayoutKind.Auto)] + private record struct State(double LastValid, double LastOutput); + private State _state, _p_state; + private int _p_count; + 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)); + } + + _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/numerics/decay/Decay.md b/lib/numerics/decay/Decay.md new file mode 100644 index 00000000..73f472f3 --- /dev/null +++ b/lib/numerics/decay/Decay.md @@ -0,0 +1,153 @@ +# DECAY: Linear Decay + +| Property | Value | +| ---------------- | -------------------------------- | +| **Category** | Numerics | +| **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/numerics/decay/decay.pine b/lib/numerics/decay/decay.pine new file mode 100644 index 00000000..653512ef --- /dev/null +++ b/lib/numerics/decay/decay.pine @@ -0,0 +1,32 @@ +// 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) => + 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/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/edecay/Edecay.Quantower.Tests.cs b/lib/numerics/edecay/Edecay.Quantower.Tests.cs new file mode 100644 index 00000000..78f0ad8a --- /dev/null +++ b/lib/numerics/edecay/Edecay.Quantower.Tests.cs @@ -0,0 +1,210 @@ +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib.Tests; + +public class EdecayIndicatorTests +{ + [Fact] + public void EdecayIndicator_Constructor_SetsDefaults() + { + var indicator = new EdecayIndicator(); + + Assert.Equal(5, indicator.Period); + Assert.Equal(SourceType.Close, indicator.Source); + Assert.True(indicator.ShowColdValues); + Assert.Equal("EDECAY - Exponential Decay", indicator.Name); + Assert.False(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + } + + [Fact] + public void EdecayIndicator_MinHistoryDepths_EqualsZero() + { + var indicator = new EdecayIndicator { Period = 20 }; + + Assert.Equal(0, EdecayIndicator.MinHistoryDepths); + Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths); + } + + [Fact] + public void EdecayIndicator_ShortName_IncludesPeriodAndSource() + { + var indicator = new EdecayIndicator { Period = 15 }; + + Assert.Contains("EDECAY", indicator.ShortName, StringComparison.Ordinal); + Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal); + } + + [Fact] + public void EdecayIndicator_Initialize_CreatesLineSeries() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void EdecayIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + + Assert.Equal(1, indicator.LinesSeries[0].Count); + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0))); + } + + [Fact] + public void EdecayIndicator_ProcessUpdate_NewBar_ComputesValue() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106); + + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); + + Assert.Equal(2, indicator.LinesSeries[0].Count); + } + + [Fact] + public void EdecayIndicator_ProcessUpdate_NewTick_ProcessesWithoutError() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 105, 95, 102); + + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick)); + + Assert.Equal(2, indicator.LinesSeries[0].Count); + } + + [Fact] + public void EdecayIndicator_MultipleUpdates_ProducesCorrectSequence() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 20; i++) + { + indicator.HistoricalData.AddBar( + now.AddMinutes(i), + 100 + i * 2, + 105 + i * 2, + 95 + i * 2, + 102 + i * 2); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + Assert.Equal(20, indicator.LinesSeries[0].Count); + + for (int i = 0; i < 20; i++) + { + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i))); + } + } + + [Fact] + public void EdecayIndicator_DifferentSourceTypes_Work() + { + var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 }; + + foreach (var source in sources) + { + var indicator = new EdecayIndicator { Period = 5, Source = source }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + indicator.HistoricalData.AddBar(now, 100, 110, 90, 105); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + + Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)), + $"Source {source} should produce finite value"); + } + } + + [Fact] + public void EdecayIndicator_Period_CanBeChanged() + { + var indicator = new EdecayIndicator { Period = 10 }; + Assert.Equal(10, indicator.Period); + + indicator.Period = 20; + Assert.Equal(20, indicator.Period); + Assert.Equal(0, EdecayIndicator.MinHistoryDepths); + } + + [Fact] + public void EdecayIndicator_Uptrend_OutputFollowsPrice() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + double price = 100 + i * 5; + indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + // In uptrend, edecay output should equal close price (input > decayed) + double lastValue = indicator.LinesSeries[0].GetValue(0); + Assert.Equal(145, lastValue, 1); // last close = 100 + 9*5 = 145 + } + + [Fact] + public void EdecayIndicator_FlatPrices_OutputEqualsInput() + { + var indicator = new EdecayIndicator { Period = 5 }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 5; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + double lastValue = indicator.LinesSeries[0].GetValue(0); + Assert.Equal(100, lastValue, 1); + } + + [Fact] + public void EdecayIndicator_DifferentPeriods_Work() + { + var periods = new[] { 1, 5, 10, 20 }; + + foreach (var period in periods) + { + var indicator = new EdecayIndicator { Period = period }; + indicator.Initialize(); + + var now = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 102 + i, 98 + i, 101 + i); + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); + } + + Assert.Equal(10, indicator.LinesSeries[0].Count); + } + } +} diff --git a/lib/numerics/edecay/Edecay.Quantower.cs b/lib/numerics/edecay/Edecay.Quantower.cs new file mode 100644 index 00000000..1420ef18 --- /dev/null +++ b/lib/numerics/edecay/Edecay.Quantower.cs @@ -0,0 +1,60 @@ +using System.Drawing; +using System.Runtime.CompilerServices; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +/// +/// EDECAY (Exponential Decay) Quantower indicator. +/// Tracks peaks and decays exponentially at a rate of (period-1)/period per bar. +/// Formula: output = max(input, prev_output * (period-1)/period) +/// +[SkipLocalsInit] +public class EdecayIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)] + public int Period { get; set; } = 5; + + [IndicatorExtensions.DataSourceInput] + public SourceType Source { get; set; } = SourceType.Close; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Edecay _edecay = null!; + protected LineSeries Series; + protected string SourceName = null!; + private Func _priceSelector = null!; + + public static int MinHistoryDepths => 0; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public override string ShortName => $"EDECAY {Period}:{SourceName}"; + + public EdecayIndicator() + { + OnBackGround = true; + SeparateWindow = false; + SourceName = Source.ToString(); + Name = "EDECAY - Exponential Decay"; + Description = "Exponential Decay: output = max(input, prev_output * (period-1)/period)"; + Series = new LineSeries(name: $"EDECAY {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid); + AddLineSeries(Series); + } + + protected override void OnInit() + { + _edecay = new Edecay(Period); + SourceName = Source.ToString(); + _priceSelector = Source.GetPriceSelector(); + base.OnInit(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void OnUpdate(UpdateArgs args) + { + var item = HistoricalData[Count - 1, SeekOriginHistory.Begin]; + TValue result = _edecay.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar()); + Series.SetValue(result.Value, _edecay.IsHot, ShowColdValues); + } +} diff --git a/lib/numerics/edecay/Edecay.Tests.cs b/lib/numerics/edecay/Edecay.Tests.cs new file mode 100644 index 00000000..687d0b6d --- /dev/null +++ b/lib/numerics/edecay/Edecay.Tests.cs @@ -0,0 +1,449 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class EdecayTests +{ + private readonly TSeries _gbm; + private const int TestPeriod = 5; + private const int DataPoints = 100; + + public EdecayTests() + { + var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.5, seed: 42); + var bars = gbm.Fetch(DataPoints, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + _gbm = bars.Close; + } + + #region Constructor Tests + + [Fact] + public void Constructor_WithValidPeriod_SetsProperties() + { + var edecay = new Edecay(TestPeriod); + Assert.Equal($"Edecay({TestPeriod})", edecay.Name); + Assert.Equal(1, edecay.WarmupPeriod); + } + + [Fact] + public void Constructor_WithZeroPeriod_ThrowsArgumentException() + { + var ex = Assert.Throws(() => new Edecay(0)); + Assert.Equal("period", ex.ParamName); + } + + [Fact] + public void Constructor_WithNegativePeriod_ThrowsArgumentException() + { + var ex = Assert.Throws(() => new Edecay(-1)); + Assert.Equal("period", ex.ParamName); + } + + [Fact] + public void Constructor_WithSource_SubscribesToEvents() + { + var source = new TSeries(DataPoints); + var edecay = new Edecay(source, TestPeriod); + Assert.NotNull(edecay); + } + + #endregion + + #region Basic Calculation Tests + + [Fact] + public void Update_FirstBar_ReturnsInputValue() + { + var edecay = new Edecay(TestPeriod); + var tv = edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + Assert.Equal(100.0, tv.Value); + } + + [Fact] + public void Update_DecayingValues_OutputDecaysExponentially() + { + var edecay = new Edecay(5); // scale = 4/5 = 0.8 + var time = DateTime.UtcNow; + + // First bar at 1.0 + edecay.Update(new TValue(time, 1.0), true); + + // Next bars at 0.0 — output should decay by ×0.8 per bar + var tv1 = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + Assert.Equal(0.8, tv1.Value, 10); // 1.0 * 0.8 + + var tv2 = edecay.Update(new TValue(time.AddSeconds(2), 0.0), true); + Assert.Equal(0.64, tv2.Value, 10); // 0.8 * 0.8 + + var tv3 = edecay.Update(new TValue(time.AddSeconds(3), 0.0), true); + Assert.Equal(0.512, tv3.Value, 10); // 0.64 * 0.8 + } + + [Fact] + public void Update_RisingInput_FollowsInput() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 100.0), true); + var tv = edecay.Update(new TValue(time.AddSeconds(1), 105.0), true); + Assert.Equal(105.0, tv.Value, 10); // input > decayed, so follows input + } + + [Fact] + public void Last_IsAccessible() + { + var edecay = new Edecay(TestPeriod); + edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + Assert.Equal(100.0, edecay.Last.Value, 10); + } + + [Fact] + public void IsHot_ReturnsFalseBeforeFirstBar() + { + var edecay = new Edecay(TestPeriod); + Assert.False(edecay.IsHot); + } + + [Fact] + public void IsHot_ReturnsTrueAfterFirstBar() + { + var edecay = new Edecay(TestPeriod); + edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + Assert.True(edecay.IsHot); + } + + [Fact] + public void Name_IsAccessible() + { + var edecay = new Edecay(TestPeriod); + Assert.Equal($"Edecay({TestPeriod})", edecay.Name); + } + + #endregion + + #region State Management Tests + + [Fact] + public void Update_WithIsNewTrue_AdvancesState() + { + var edecay = new Edecay(TestPeriod); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 100.0), true); + edecay.Update(new TValue(time.AddSeconds(1), 105.0), true); + edecay.Update(new TValue(time.AddSeconds(2), 110.0), true); + + Assert.NotEqual(default, edecay.Last); + } + + [Fact] + public void Update_WithIsNewFalse_UpdatesCurrentState() + { + var edecay = new Edecay(5); // scale = 0.8 + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var first = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + + // Correct same bar with different value + var corrected = edecay.Update(new TValue(time.AddSeconds(1), 0.5), false); + + // first: max(0.0, 1.0*0.8)=0.8 + Assert.Equal(0.8, first.Value, 10); + // corrected: max(0.5, 1.0*0.8)=0.8 + Assert.Equal(0.8, corrected.Value, 10); + } + + [Fact] + public void Update_IterativeCorrections_RestoresPreviousState() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var baseline = edecay.Update(new TValue(time.AddSeconds(1), 0.5), true); + + // Make several corrections + edecay.Update(new TValue(time.AddSeconds(1), 0.9), false); + edecay.Update(new TValue(time.AddSeconds(1), 0.1), false); + var restored = edecay.Update(new TValue(time.AddSeconds(1), 0.5), false); + + Assert.Equal(baseline.Value, restored.Value, 10); + } + + [Fact] + public void Reset_ClearsStateAndLastValidTracking() + { + var edecay = new Edecay(TestPeriod); + var time = DateTime.UtcNow; + + for (int i = 0; i < 10; i++) + { + edecay.Update(new TValue(time.AddSeconds(i), 100.0 + i)); + } + + edecay.Reset(); + + Assert.Equal(default, edecay.Last); + Assert.False(edecay.IsHot); + } + + #endregion + + #region Robustness Tests + + [Fact] + public void Update_WithNaN_UsesLastValidValue() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var afterNaN = edecay.Update(new TValue(time.AddSeconds(1), double.NaN), true); + + Assert.True(double.IsFinite(afterNaN.Value)); + // NaN uses last valid (1.0), so max(1.0, 1.0*0.8)=1.0 + Assert.Equal(1.0, afterNaN.Value, 10); + } + + [Fact] + public void Update_WithInfinity_UsesLastValidValue() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 1.0), true); + var afterInf = edecay.Update(new TValue(time.AddSeconds(1), double.PositiveInfinity), true); + + Assert.True(double.IsFinite(afterInf.Value)); + } + + [Fact] + public void Update_BatchNaN_HandlesSafely() + { + var edecay = new Edecay(TestPeriod); + var time = DateTime.UtcNow; + + for (int i = 0; i < 20; i++) + { + var value = i % 3 == 0 ? double.NaN : 100.0 + i; + var tv = edecay.Update(new TValue(time.AddSeconds(i), value), true); + Assert.True(double.IsFinite(tv.Value)); + } + } + + #endregion + + #region Consistency Tests (All 4 modes must match) + + [Fact] + public void AllModes_ProduceSameResults() + { + // Mode 1: Batch via TSeries + var batchResult = Edecay.Batch(_gbm, TestPeriod); + + // Mode 2: Streaming + var streamingEdecay = new Edecay(TestPeriod); + var streamingResult = new TSeries(DataPoints); + for (int i = 0; i < _gbm.Count; i++) + { + var tv = streamingEdecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true); + streamingResult.Add(tv, true); + } + + // Mode 3: Span-based + Span spanOutput = stackalloc double[DataPoints]; + Edecay.Batch(_gbm.Values, spanOutput, TestPeriod); + + // Mode 4: Event-driven + var eventEdecay = new Edecay(TestPeriod); + var eventResult = new TSeries(DataPoints); + eventEdecay.Pub += (object? _, in TValueEventArgs e) => eventResult.Add(e.Value, e.IsNew); + for (int i = 0; i < _gbm.Count; i++) + { + eventEdecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true); + } + + int compareCount = Math.Min(100, DataPoints); + for (int i = DataPoints - compareCount; i < DataPoints; i++) + { + Assert.Equal(batchResult[i].Value, streamingResult[i].Value, 10); + Assert.Equal(batchResult[i].Value, spanOutput[i], 10); + Assert.Equal(batchResult[i].Value, eventResult[i].Value, 10); + } + } + + #endregion + + #region Span API Tests + + [Fact] + public void Calculate_Span_ValidatesEmptySource() + { + var ex = Assert.Throws(() => + { + ReadOnlySpan empty = []; + Span output = stackalloc double[1]; + Edecay.Batch(empty, output, TestPeriod); + }); + Assert.Equal("source", ex.ParamName); + } + + [Fact] + public void Calculate_Span_ValidatesOutputLength() + { + var ex = Assert.Throws(() => + { + ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; + Span output = stackalloc double[3]; // too short + Edecay.Batch(source, output, TestPeriod); + }); + Assert.Equal("output", ex.ParamName); + } + + [Fact] + public void Calculate_Span_ValidatesPeriod() + { + var ex = Assert.Throws(() => + { + ReadOnlySpan source = stackalloc double[] { 1, 2, 3, 4, 5 }; + Span output = stackalloc double[5]; + Edecay.Batch(source, output, 0); + }); + Assert.Equal("period", ex.ParamName); + } + + [Fact] + public void Calculate_Span_MatchesTSeries() + { + var batchResult = Edecay.Batch(_gbm, TestPeriod); + + Span spanOutput = stackalloc double[DataPoints]; + Edecay.Batch(_gbm.Values, spanOutput, TestPeriod); + + for (int i = 0; i < DataPoints; i++) + { + Assert.Equal(batchResult[i].Value, spanOutput[i], 10); + } + } + + [Fact] + public void Calculate_Span_LargeData_NoStackOverflow() + { + int largeSize = 10000; + double[] source = new double[largeSize]; + double[] output = new double[largeSize]; + + for (int i = 0; i < largeSize; i++) + { + source[i] = 100.0 + i * 0.1; + } + + Edecay.Batch(source, output, TestPeriod); + + Assert.Equal(largeSize, output.Length); + } + + #endregion + + #region Chainability Tests + + [Fact] + public void Pub_FiresOnUpdate() + { + var edecay = new Edecay(TestPeriod); + bool eventFired = false; + + edecay.Pub += (object? _, in TValueEventArgs e) => eventFired = true; + edecay.Update(new TValue(DateTime.UtcNow, 100.0)); + + Assert.True(eventFired); + } + + [Fact] + public void EventBasedChaining_Works() + { + var source = new TSeries(10); + var edecay = new Edecay(source, 2); + var results = new List(); + + edecay.Pub += (object? _, in TValueEventArgs e) => results.Add(e.Value.Value); + + for (int i = 0; i < 10; i++) + { + source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), true); + } + + Assert.Equal(10, results.Count); + } + + #endregion + + #region Edecay-Specific Tests + + [Fact] + public void Edecay_Period1_DecaysToZero() + { + var edecay = new Edecay(1); // scale = 0/1 = 0.0 + var time = DateTime.UtcNow; + + edecay.Update(new TValue(time, 5.0), true); + var tv = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + // max(0.0, 5.0*0.0) = 0.0 + Assert.Equal(0.0, tv.Value, 10); + } + + [Fact] + public void Edecay_ConstantInput_OutputEqualsInput() + { + var edecay = new Edecay(5); + var time = DateTime.UtcNow; + + for (int i = 0; i < 20; i++) + { + var tv = edecay.Update(new TValue(time.AddSeconds(i), 100.0), true); + Assert.Equal(100.0, tv.Value, 10); + } + } + + [Fact] + public void Edecay_OutputNeverBelowInput() + { + var edecay = new Edecay(10); + var time = DateTime.UtcNow; + var rng = new Random(42); + + for (int i = 0; i < 100; i++) + { + double input = rng.NextDouble() * 200; + var tv = edecay.Update(new TValue(time.AddSeconds(i), input), true); + Assert.True(tv.Value >= input || Math.Abs(tv.Value - input) < 1e-10, + $"Output {tv.Value} should be >= input {input}"); + } + } + + [Fact] + public void Edecay_DiffersFromLinearDecay() + { + var edecay = new Edecay(5); // scale = 0.8 + var decay = new Decay(5); // scale = 0.2 + var time = DateTime.UtcNow; + + // Start both at 100 + edecay.Update(new TValue(time, 100.0), true); + decay.Update(new TValue(time, 100.0), true); + + // Feed 0.0 and compare + var e1 = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true); + var d1 = decay.Update(new TValue(time.AddSeconds(1), 0.0), true); + + // Edecay: max(0, 100*0.8) = 80 + // Decay: max(0, 100-0.2) = 99.8 + Assert.Equal(80.0, e1.Value, 10); + Assert.Equal(99.8, d1.Value, 10); + Assert.NotEqual(e1.Value, d1.Value); + } + + #endregion +} diff --git a/lib/numerics/edecay/Edecay.Validation.Tests.cs b/lib/numerics/edecay/Edecay.Validation.Tests.cs new file mode 100644 index 00000000..38bae1a2 --- /dev/null +++ b/lib/numerics/edecay/Edecay.Validation.Tests.cs @@ -0,0 +1,260 @@ +using Xunit; +using Xunit.Abstractions; + +namespace QuanTAlib.Tests; + +/// +/// Validation tests for EDECAY (Exponential Decay) against the Tulip Indicators algorithm. +/// The Tulip .NET binding does not expose decay/edecay directly, so validation +/// uses manual computation of the Tulip ti_edecay algorithm: +/// output[0] = input[0] +/// output[i] = max(input[i], output[i-1] * (period-1)/period) +/// +public sealed class EdecayValidationTests(ITestOutputHelper output) : IDisposable +{ + private readonly ValidationTestData _testData = new(); + private readonly ITestOutputHelper _output = output; + private bool _disposed; + + private const int TestPeriod = 5; + private const double TulipTolerance = 1e-9; + + public void Dispose() + { + Dispose(disposing: true); + } + + private void Dispose(bool disposing) + { + if (_disposed) { return; } + _disposed = true; + if (disposing) { _testData?.Dispose(); } + } + + /// + /// Reference implementation of Tulip ti_edecay for validation. + /// + private static double[] TulipEdecay(double[] input, int period) + { + double[] output = new double[input.Length]; + double scale = (period - 1.0) / period; + output[0] = input[0]; + for (int i = 1; i < input.Length; i++) + { + double d = output[i - 1] * scale; + output[i] = input[i] > d ? input[i] : d; + } + return output; + } + + #region Tulip Algorithm Validation + + [Fact] + public void Edecay_MatchesTulipEdecay_Batch() + { + double[] input = _testData.RawData.ToArray(); + + var quantResult = Edecay.Batch(_testData.Data, TestPeriod); + double[] tulipResult = TulipEdecay(input, TestPeriod); + + int count = quantResult.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance, + $"Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}"); + } + + _output.WriteLine("Edecay Batch validated successfully against Tulip edecay algorithm"); + } + + [Fact] + public void Edecay_MatchesTulipEdecay_Streaming() + { + double[] input = _testData.RawData.ToArray(); + + var edecay = new Edecay(TestPeriod); + var streamingResults = new List(); + + foreach (var item in _testData.Data) + { + streamingResults.Add(edecay.Update(item).Value); + } + + double[] tulipResult = TulipEdecay(input, TestPeriod); + + int count = streamingResults.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(streamingResults[i] - tulipResult[i]) <= TulipTolerance, + $"Mismatch at index {i}: QuanTAlib={streamingResults[i]:G17}, Tulip={tulipResult[i]:G17}"); + } + + _output.WriteLine("Edecay Streaming validated successfully against Tulip edecay algorithm"); + } + + [Fact] + public void Edecay_MatchesTulipEdecay_Span() + { + double[] input = _testData.RawData.ToArray(); + + var quantOutput = new double[input.Length]; + Edecay.Batch(new ReadOnlySpan(input), quantOutput, TestPeriod); + + double[] tulipResult = TulipEdecay(input, TestPeriod); + + int count = quantOutput.Length; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(quantOutput[i] - tulipResult[i]) <= TulipTolerance, + $"Mismatch at index {i}: QuanTAlib={quantOutput[i]:G17}, Tulip={tulipResult[i]:G17}"); + } + + _output.WriteLine("Edecay Span validated successfully against Tulip edecay algorithm"); + } + + #endregion + + #region Different Periods + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + [InlineData(20)] + [InlineData(50)] + public void Edecay_MatchesTulipEdecay_DifferentPeriods(int period) + { + double[] input = _testData.RawData.ToArray(); + + var quantResult = Edecay.Batch(_testData.Data, period); + double[] tulipResult = TulipEdecay(input, period); + + int count = quantResult.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + + for (int i = start; i < count; i++) + { + Assert.True( + Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance, + $"Period={period}, Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}"); + } + } + + #endregion + + #region Edge Cases + + [Fact] + public void Edecay_HandlesConstantValues() + { + var constantData = new TSeries(100); + for (int i = 0; i < 100; i++) + { + constantData.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0), true); + } + + var result = Edecay.Batch(constantData, TestPeriod); + + // Constant input: output always equals input since input >= decayed + for (int i = 0; i < 100; i++) + { + Assert.Equal(100.0, result[i].Value, TulipTolerance); + } + } + + [Fact] + public void Edecay_HandlesExponentiallyDecreasing() + { + double[] input = new double[20]; + for (int i = 0; i < 20; i++) + { + input[i] = 100.0 * Math.Pow(0.9, i); + } + + var quantOutput = new double[20]; + Edecay.Batch(input, quantOutput, TestPeriod); + + double[] tulipResult = TulipEdecay(input, TestPeriod); + + for (int i = 0; i < 20; i++) + { + Assert.Equal(tulipResult[i], quantOutput[i], TulipTolerance); + } + } + + [Fact] + public void Batch_MatchesStreaming_IdenticalResults() + { + var batchResult = Edecay.Batch(_testData.Data, TestPeriod); + + var edecay = new Edecay(TestPeriod); + var streamingResults = new List(); + foreach (var item in _testData.Data) + { + streamingResults.Add(edecay.Update(item).Value); + } + + int count = _testData.Data.Count; + int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount); + for (int i = start; i < count; i++) + { + Assert.Equal(batchResult[i].Value, streamingResults[i], ValidationHelper.DefaultTolerance); + } + _output.WriteLine("Edecay Batch vs Streaming consistency validated"); + } + + [Fact] + public void Edecay_OutputAlwaysGreaterOrEqualInput() + { + double[] input = _testData.RawData.ToArray(); + var quantOutput = new double[input.Length]; + Edecay.Batch(input, quantOutput, TestPeriod); + + for (int i = 0; i < input.Length; i++) + { + Assert.True(quantOutput[i] >= input[i] - 1e-15, + $"Output {quantOutput[i]} must be >= input {input[i]} at index {i}"); + } + } + + [Fact] + public void Edecay_DecayIsMultiplicative() + { + // With period=5, scale = 4/5 = 0.8 + // After a spike, each subsequent bar without new highs should multiply by 0.8 + double[] input = [100.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + double[] tulipResult = TulipEdecay(input, TestPeriod); + + // output[0] = 100.0 + // output[1] = max(0, 100 * 0.8) = 80.0 + // output[2] = max(0, 80 * 0.8) = 64.0 + // output[3] = max(0, 64 * 0.8) = 51.2 + // output[4] = max(0, 51.2 * 0.8) = 40.96 + // output[5] = max(0, 40.96 * 0.8) = 32.768 + Assert.Equal(100.0, tulipResult[0], TulipTolerance); + Assert.Equal(80.0, tulipResult[1], TulipTolerance); + Assert.Equal(64.0, tulipResult[2], TulipTolerance); + Assert.Equal(51.2, tulipResult[3], TulipTolerance); + Assert.Equal(40.96, tulipResult[4], TulipTolerance); + Assert.Equal(32.768, tulipResult[5], TulipTolerance); + + var quantOutput = new double[6]; + Edecay.Batch(input, quantOutput, TestPeriod); + + for (int i = 0; i < 6; i++) + { + Assert.Equal(tulipResult[i], quantOutput[i], TulipTolerance); + } + } + + #endregion +} diff --git a/lib/numerics/edecay/Edecay.cs b/lib/numerics/edecay/Edecay.cs new file mode 100644 index 00000000..ffd92b48 --- /dev/null +++ b/lib/numerics/edecay/Edecay.cs @@ -0,0 +1,196 @@ +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 double _scale; + private int _count; + [StructLayout(LayoutKind.Auto)] + private record struct State(double LastValid, double LastOutput); + private State _state, _p_state; + private int _p_count; + 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)); + } + + _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/numerics/edecay/Edecay.md b/lib/numerics/edecay/Edecay.md new file mode 100644 index 00000000..540061ea --- /dev/null +++ b/lib/numerics/edecay/Edecay.md @@ -0,0 +1,165 @@ +# EDECAY: Exponential Decay + +| Property | Value | +| ---------------- | -------------------------------- | +| **Category** | Numerics | +| **Inputs** | Source (close) | +| **Parameters** | `period` (default 5) | +| **Outputs** | Single series (Edecay) | +| **Output range** | Same as input (overlay) | +| **Warmup** | `1` bar | + +### TL;DR + +- EDECAY (Exponential Decay) tracks the maximum of the current input and the previous output multiplied by a decay factor of `(period-1)/period`. +- Parameterized by `period` (default 5). +- Output range: Same as input — this is an overlay indicator. +- Requires `1` bar of warmup before first valid output (IsHot = true). +- Validated against Tulip Indicators `ti_edecay` reference algorithm. + +> "A ratchet that only moves down gradually: price can push it up instantly, but gravity pulls it back at an exponential pace — faster when far from zero, slower as it approaches." + +EDECAY implements the exponential decaying function. When price is above the decayed level, output snaps to price. When price falls below, the output decays exponentially by multiplying by `(period-1)/period` per bar, creating a ceiling that gradually descends. Unlike linear DECAY which subtracts a fixed amount, EDECAY's multiplicative factor produces a proportional decay rate. + +## Historical Context + +The exponential decay indicator originates from the Tulip Indicators library, a high-performance C library of technical indicators. It provides a peak-tracking mechanism where the tracked level decays at a proportional rate. The indicator is useful for: + +- **Trailing stops**: The decaying level acts as a trailing stop that descends proportionally. +- **Peak detection**: Identifies when price last reached a new high relative to the decay rate. +- **Signal filtering**: Removes noise by requiring price to exceed the decayed level to register as significant. + +## Architecture & Physics + +### 1. Pure IIR (No Buffer) + +The indicator requires no history buffer — only the previous output value is needed: + +$$ +\text{state} = \{y_{t-1}\} +$$ + +This makes it O(1) in both time and space. + +### 2. Exponential Decay Calculation + +$$ +y_t = \max(x_t, \; y_{t-1} \cdot \frac{p-1}{p}) +$$ + +where: +- $x_t$ = current input value +- $y_{t-1}$ = previous output value +- $p$ = period parameter +- $\frac{p-1}{p}$ = multiplicative decay factor per bar + +### 3. First Bar Initialization + +$$ +y_0 = x_0 +$$ + +The first bar simply passes through the input value. + +### 4. State Management + +The indicator uses state rollback for bar correction: + +``` +if isNew: + save current state as previous +else: + restore previous state +``` + +## Mathematical Foundation + +### Core Formula + +$$ +y_t = \max(x_t, \; y_{t-1} \cdot s) +$$ + +where $s = \frac{p-1}{p}$ is the multiplicative decay factor. + +### Decay Behavior + +After a peak at value $v$, with no new inputs exceeding the decayed level, the output follows: + +$$ +y_{t+k} = v \cdot s^k = v \cdot \left(\frac{p-1}{p}\right)^k +$$ + +The output asymptotically approaches zero but never reaches it ($v > 0$). + +### Comparison with Linear Decay + +| Property | DECAY (Linear) | EDECAY (Exponential) | +|----------|----------------|---------------------| +| Formula | $y - \frac{1}{p}$ | $y \cdot \frac{p-1}{p}$ | +| Decay rate | Constant absolute | Proportional to current value | +| Reaches zero | Yes, in finite time | No, asymptotic approach | +| Scale-invariant | No | Yes | + +### Properties + +| Property | Value | +|----------|-------| +| Lookback | 0 | +| Output ≥ Input | Always (by construction) | +| Decay rate | Proportional $\frac{p-1}{p}$ | +| Monotonic when decaying | Yes (strictly decreasing) | +| Scale-invariant | Yes | + +## Performance Profile + +### Operation Count (Streaming Mode) + +| Operation | Count | Notes | +| :--- | :---: | :--- | +| MUL | 1 | prev_output × scale | +| MAX/CMP | 1 | max(input, decayed) | +| State copy | 1 | rollback support | +| **Total** | **~3 ops** | Extremely lightweight | + +### Batch Mode (Span-based) + +| Operation | Complexity | Notes | +| :--- | :---: | :--- | +| Per-element | O(1) | Mul + compare | +| Total | O(n) | Linear scan | +| Memory | O(1) | No additional allocation | + +### Quality Metrics + +| Metric | Score | Notes | +| :--- | :---: | :--- | +| **Accuracy** | 10/10 | Exact arithmetic, no approximation | +| **Timeliness** | 10/10 | Zero lag on upward moves | +| **Smoothness** | 3/10 | Exponential curve smoother than linear staircase | +| **Simplicity** | 10/10 | Single multiplication + compare | + +## Validation + +| Library | Status | Notes | +| :--- | :---: | :--- | +| **Tulip** | ✅ | Manual ti_edecay algorithm matches exactly | + +## Common Pitfalls + +1. **Not a moving average**: Edecay is a peak-tracking/envelope indicator, not a smoothing filter. It only descends when price is below the decayed level. + +2. **Proportional decay rate**: Unlike linear DECAY, EDECAY decays proportionally. For a stock at $100 with period=5, the first bar decays by $20; for a stock at $10, it decays by $2. This makes EDECAY scale-invariant. + +3. **Period interpretation**: Period=5 means `scale = 4/5 = 0.8`, so each bar retains 80% of the previous value. After 5 bars, approximately 32.8% of the peak value remains. + +4. **First bar**: The first bar always equals the input — there is no warmup period in the traditional sense. + +5. **Asymmetric behavior**: Upward moves are instant (output = input), but downward moves are rate-limited to multiplication by `(period-1)/period` per bar. + +6. **Never reaches zero**: Unlike linear DECAY, exponential decay asymptotically approaches zero but never reaches it (assuming positive values). + +## References + +- Tulip Indicators Library: https://tulipindicators.org/edecay +- Kegel, L. "Tulip Indicators" — Open-source C library of technical indicators. diff --git a/lib/numerics/edecay/edecay.pine b/lib/numerics/edecay/edecay.pine new file mode 100644 index 00000000..33f3aacb --- /dev/null +++ b/lib/numerics/edecay/edecay.pine @@ -0,0 +1,33 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Exponential Decay (EDECAY)", "EDECAY", overlay=true) + +//@function Calculates exponential decay: output = max(input, prev_output * (period-1)/period) +//@param source Source price series +//@param length Decay period +//@returns Decayed value that tracks peaks and descends exponentially +//@optimized Uses multiplicative decay factor for O(1) complexity per bar +edecay(series float source, simple int length) => + var float prev = na + float scale = (length - 1.0) / length + float result = na + if na(prev) + result := source + else + float d = prev * scale + result := source > d ? source : d + prev := result + result + +// ---------- Main loop ---------- + +// Inputs +i_source = input.source(close, "Source") +i_length = input.int(5, "Length", minval=1) + +// Calculate Edecay +float edecay_val = edecay(i_source, i_length) + +// Plot +plot(edecay_val, "Edecay", color=color.yellow, linewidth=2) diff --git a/lib/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..391f1711 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) @@ -9,16 +9,17 @@ indicator("Exponential Transformation (EXP)", "Exptrans", overlay=false) //@optimized for performance and dirty data expT(series float source) => if na(source) - runtime.error("Parameter 'source' cannot be na.") - math.exp(source) + na + else + math.exp(source) // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") +i_source = input.source(close, "Source") // Calculation transformedSource = expT(i_source) // Plot -plot(transformedSource, "Exponential Transformation", color=color.yellow, linewidth=2) +plot(transformedSource, "Exptrans", color=color.yellow, linewidth=2) diff --git a/lib/numerics/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..b53def4f 100644 --- a/lib/numerics/fft/Fft.cs +++ b/lib/numerics/fft/Fft.cs @@ -1,8 +1,11 @@ // FFT: Fast Fourier Transform — Dominant Cycle Detector -// Estimates the dominant cycle period in bars using a DFT on a windowed price buffer. -// Algorithm: Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013. -// Hanning-windowed DFT across bins [minBin..maxBin], with parabolic interpolation -// for sub-bin period estimation. Output: dominant cycle period in bars (clamped). +// Estimates the dominant cycle period in bars using a radix-2 Cooley-Tukey FFT +// on a Hanning-windowed price buffer, with parabolic interpolation for sub-bin +// period estimation. Output: dominant cycle period in bars (clamped). +// +// Algorithm: Cooley, J.W. & Tukey, J.W. (1965). "An Algorithm for the Machine +// Calculation of Complex Fourier Series." Mathematics of Computation, 19(90). +// Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013 (application context). using System.Buffers; using System.Runtime.CompilerServices; @@ -12,15 +15,16 @@ namespace QuanTAlib; /// /// FFT: Fast Fourier Transform Dominant Cycle Detector -/// Computes the dominant cycle period using a Hanning-windowed DFT -/// over a rolling price buffer, with parabolic interpolation refinement. +/// Computes the dominant cycle period using a Hanning-windowed radix-2 +/// Cooley-Tukey FFT over a rolling price buffer, with parabolic interpolation. /// /// /// Key properties: /// - Output: dominant cycle period in bars, clamped to [minPeriod, maxPeriod] -/// - windowSize must be 32, 64, or 128 +/// - windowSize must be 32, 64, or 128 (power of 2 for radix-2) /// - WarmupPeriod = windowSize bars -/// - No allocation in Update (RingBuffer + precomputed Hanning weights) +/// - True O(N log N) radix-2 FFT with bit-reversal permutation +/// - Pre-allocated work arrays for zero-allocation streaming /// - Parabolic interpolation on peak bin for sub-bin accuracy /// [SkipLocalsInit] @@ -31,8 +35,10 @@ public sealed class Fft : AbstractBase private readonly int _maxPeriod; private readonly int _minBin; private readonly int _maxBin; - private readonly double _twoPiOverN; private readonly double[] _hanning; + private readonly int[] _bitRev; + private readonly double[] _workRe; + private readonly double[] _workIm; private readonly RingBuffer _buffer; [StructLayout(LayoutKind.Auto)] @@ -44,7 +50,7 @@ public sealed class Fft : AbstractBase /// /// Initializes a new Fft indicator. /// - /// DFT window size in bars. Must be 32, 64, or 128. Default 64. + /// FFT window size in bars. Must be 32, 64, or 128. Default 64. /// Minimum detectable cycle period. Must be >= 2. Default 4. /// Maximum detectable cycle period. Must be <= windowSize/2. Default 32. public Fft(int windowSize = 64, int minPeriod = 4, int maxPeriod = 32) @@ -67,19 +73,31 @@ public sealed class Fft : AbstractBase _windowSize = windowSize; _minPeriod = minPeriod; _maxPeriod = maxPeriod; - _twoPiOverN = 2.0 * Math.PI / windowSize; + int log2N = Log2(windowSize); - // bin k corresponds to period N/k; k=minBin → period=N/minBin=maxPeriod, k=maxBin → period=N/maxBin=minPeriod + // bin k corresponds to period N/k _minBin = Math.Max(1, windowSize / maxPeriod); _maxBin = Math.Min(windowSize / 2, windowSize / minPeriod); - // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N), n=0..N-1 + // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N) + double twoPiOverN = 2.0 * Math.PI / windowSize; _hanning = new double[windowSize]; for (int n = 0; n < windowSize; n++) { - _hanning[n] = 0.5 - 0.5 * Math.Cos(_twoPiOverN * n); + _hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); } + // Precompute bit-reversal permutation table + _bitRev = new int[windowSize]; + for (int i = 0; i < windowSize; i++) + { + _bitRev[i] = BitReverse(i, log2N); + } + + // Pre-allocate work arrays (zero allocation in hot path) + _workRe = new double[windowSize]; + _workIm = new double[windowSize]; + _buffer = new RingBuffer(windowSize); Name = $"Fft({windowSize},{minPeriod},{maxPeriod})"; WarmupPeriod = windowSize; @@ -90,10 +108,6 @@ public sealed class Fft : AbstractBase /// /// Initializes a new Fft indicator with source for event-based chaining. /// - /// Source indicator for chaining - /// DFT window size. Must be 32, 64, or 128. Default 64. - /// Minimum detectable period. Must be >= 2. Default 4. - /// Maximum detectable period. Must be <= windowSize/2. Default 32. public Fft(ITValuePublisher source, int windowSize = 64, int minPeriod = 4, int maxPeriod = 32) : this(windowSize, minPeriod, maxPeriod) { @@ -103,59 +117,155 @@ public sealed class Fft : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); + /// + /// Computes floor(log2(n)) for powers of 2. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Log2(int n) + { + int p = 0; + int x = n; + while (x > 1) + { + x >>= 1; + p++; + } + return p; + } + + /// + /// Reverses the bits of x using 'bits' bit-width. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitReverse(int x, int bits) + { + int r = 0; + for (int i = 0; i < bits; i++) + { + r = (r << 1) | (x & 1); + x >>= 1; + } + return r; + } + + /// + /// In-place iterative radix-2 Cooley-Tukey FFT. + /// + /// Real part array (modified in-place) + /// Imaginary part array (modified in-place) + /// Array length (must be power of 2) + /// Pre-computed bit-reversal table + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void FftInPlace(double[] re, double[] im, int n, int[] bitRev) + { + // Bit-reversal permutation + for (int i = 0; i < n; i++) + { + int j = bitRev[i]; + if (j > i) + { + (re[i], re[j]) = (re[j], re[i]); + (im[i], im[j]) = (im[j], im[i]); + } + } + + // Cooley-Tukey butterfly stages + int len = 2; + while (len <= n) + { + int half = len >> 1; + double angStep = -2.0 * Math.PI / len; + + for (int start = 0; start < n; start += len) + { + for (int k = 0; k < half; k++) + { + double angle = angStep * k; + double wr = Math.Cos(angle); + double wi = Math.Sin(angle); + + int i0 = start + k; + int i1 = i0 + half; + + double ur = re[i0]; + double ui = im[i0]; + double vr = re[i1]; + double vi = im[i1]; + + // Twiddle: t = w * v + double tr = Math.FusedMultiplyAdd(vr, wr, -(vi * wi)); + double ti = Math.FusedMultiplyAdd(vr, wi, vi * wr); + + re[i0] = ur + tr; + im[i0] = ui + ti; + re[i1] = ur - tr; + im[i1] = ui - ti; + } + } + + len <<= 1; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ComputeDominantPeriod() { var span = _buffer.GetSpan(); int n = _windowSize; - double maxMag = 0.0; - int peakBin = _minBin; - double magBefore = 0.0; - double magAtPeak = 0.0; - double magAfter = 0.0; + + // Fill work arrays: windowed data (oldest→newest), imag=0 + for (int i = 0; i < n; i++) + { + _workRe[i] = span[i] * _hanning[i]; + _workIm[i] = 0.0; + } + + // Radix-2 FFT in-place + FftInPlace(_workRe, _workIm, n, _bitRev); + + // Find peak magnitude in [minBin..maxBin] + double bestMag = -1.0; + int bestK = _minBin; for (int k = _minBin; k <= _maxBin; k++) { - double omegaK = _twoPiOverN * k; - double re = 0.0; - double im = 0.0; - - for (int idx = 0; idx < n; idx++) + double mag = Math.FusedMultiplyAdd(_workRe[k], _workRe[k], _workIm[k] * _workIm[k]); + if (mag > bestMag) { - // span[0]=oldest, span[n-1]=newest - // n=0 in DFT = current (newest): map DFT-n to span index (n-1-dftN) - // span[n-1-dftN]: dftN=0 → span[n-1] (newest), dftN=n-1 → span[0] (oldest) - double val = span[n - 1 - idx]; - double xw = val * _hanning[idx]; - double angle = omegaK * idx; - double cosA = Math.Cos(angle); - double sinA = Math.Sin(angle); - re = Math.FusedMultiplyAdd(xw, cosA, re); - im = Math.FusedMultiplyAdd(xw, -sinA, im); - } - - double mag = Math.FusedMultiplyAdd(re, re, im * im); - - if (mag > maxMag) - { - magBefore = magAtPeak; - magAfter = 0.0; - maxMag = mag; - magAtPeak = mag; - peakBin = k; - } - else if (peakBin > 0 && magAfter == 0.0) - { - magAfter = mag; + bestMag = mag; + bestK = k; } } - // Parabolic interpolation for sub-bin refinement - double denom = magBefore + 2.0 * maxMag + magAfter; - double shift = (denom > 0.0) ? (magBefore - magAfter) / denom : 0.0; - double dominantPeriod = (double)_windowSize / (peakBin + shift); + // Neighbor magnitudes for parabolic interpolation + double a, b, c; + b = bestMag; + + if (bestK > _minBin) + { + a = Math.FusedMultiplyAdd(_workRe[bestK - 1], _workRe[bestK - 1], + _workIm[bestK - 1] * _workIm[bestK - 1]); + } + else + { + a = b; + } + + if (bestK < _maxBin) + { + c = Math.FusedMultiplyAdd(_workRe[bestK + 1], _workRe[bestK + 1], + _workIm[bestK + 1] * _workIm[bestK + 1]); + } + else + { + c = b; + } + + // Parabolic interpolation: shift = 0.5*(a-c)/(a - 2b + c) + double denom = a - 2.0 * b + c; + double shift = Math.Abs(denom) > 0.0 ? 0.5 * (a - c) / denom : 0.0; + double dominantPeriod = (double)_windowSize / (bestK + shift); - // Clamp to [minPeriod, maxPeriod] return Math.Clamp(dominantPeriod, _minPeriod, _maxPeriod); } @@ -212,6 +322,9 @@ public sealed class Fft : AbstractBase return result; } + /// + /// Primes the indicator with historical values. + /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { TimeSpan interval = step ?? TimeSpan.FromSeconds(1); @@ -231,8 +344,8 @@ public sealed class Fft : AbstractBase } /// - /// Computes dominant cycle period over a span of values using a sliding Hanning-windowed DFT. - /// Uses stackalloc for Hanning weights when windowSize <= 64, otherwise ArrayPool. + /// Computes dominant cycle period over a span using sliding Hanning-windowed radix-2 FFT. + /// Uses stackalloc for work arrays when windowSize <= 64, otherwise ArrayPool. /// public static void Batch( ReadOnlySpan src, Span output, @@ -263,15 +376,24 @@ public sealed class Fft : AbstractBase throw new ArgumentException($"maxPeriod must be <= windowSize/2", nameof(maxPeriod)); } + int log2N = Log2(windowSize); double twoPiOverN = 2.0 * Math.PI / windowSize; int minBin = Math.Max(1, windowSize / maxPeriod); int maxBin = Math.Min(windowSize / 2, windowSize / minPeriod); double defaultPeriod = (minPeriod + maxPeriod) * 0.5; double lastValid = defaultPeriod; + // Precompute Hanning window and bit-reversal table const int StackallocThreshold = 64; - double[]? rentedW = null; + double[]? rentedH = null; + double[]? rentedRe = null; + double[]? rentedIm = null; + int[]? rentedBr = null; + scoped Span hanning; + double[] workRe; + double[] workIm; + int[] bitRev; if (windowSize <= StackallocThreshold) { @@ -279,15 +401,24 @@ public sealed class Fft : AbstractBase } else { - rentedW = ArrayPool.Shared.Rent(windowSize); - hanning = rentedW.AsSpan(0, windowSize); + rentedH = ArrayPool.Shared.Rent(windowSize); + hanning = rentedH.AsSpan(0, windowSize); } + // FFT work arrays (must be double[] for FftInPlace) + rentedRe = ArrayPool.Shared.Rent(windowSize); + rentedIm = ArrayPool.Shared.Rent(windowSize); + rentedBr = ArrayPool.Shared.Rent(windowSize); + workRe = rentedRe; + workIm = rentedIm; + bitRev = rentedBr; + try { for (int n = 0; n < windowSize; n++) { hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); + bitRev[n] = BitReverse(n, log2N); } for (int i = 0; i < src.Length; i++) @@ -305,52 +436,49 @@ public sealed class Fft : AbstractBase continue; } - double maxMag = 0.0; - int peakBin = minBin; - double magBefore = 0.0; - double magAtPeak = 0.0; - double magAfter = 0.0; + // Fill work arrays with windowed data + for (int n = 0; n < windowSize; n++) + { + double v = src[i - windowSize + 1 + n]; + if (!double.IsFinite(v)) + { + v = lastValid; + } + workRe[n] = v * hanning[n]; + workIm[n] = 0.0; + } + + // Radix-2 FFT + FftInPlace(workRe, workIm, windowSize, bitRev); + + // Find peak magnitude + double bestMag = -1.0; + int bestK = minBin; for (int k = minBin; k <= maxBin; k++) { - double omegaK = twoPiOverN * k; - double re = 0.0; - double im = 0.0; - - for (int dftN = 0; dftN < windowSize; dftN++) + double mag = Math.FusedMultiplyAdd(workRe[k], workRe[k], workIm[k] * workIm[k]); + if (mag > bestMag) { - // dftN=0 → newest (src[i]), dftN=windowSize-1 → oldest (src[start]) - double v = src[i - dftN]; - if (!double.IsFinite(v)) - { - v = lastValid; - } - - double xw = v * hanning[dftN]; - double angle = omegaK * dftN; - re = Math.FusedMultiplyAdd(xw, Math.Cos(angle), re); - im = Math.FusedMultiplyAdd(xw, -Math.Sin(angle), im); - } - - double mag = Math.FusedMultiplyAdd(re, re, im * im); - - if (mag > maxMag) - { - magBefore = magAtPeak; - magAfter = 0.0; - maxMag = mag; - magAtPeak = mag; - peakBin = k; - } - else if (peakBin > 0 && magAfter == 0.0) - { - magAfter = mag; + bestMag = mag; + bestK = k; } } - double denom = magBefore + 2.0 * maxMag + magAfter; - double shift = (denom > 0.0) ? (magBefore - magAfter) / denom : 0.0; - double dominant = (double)windowSize / (peakBin + shift); + // Neighbor magnitudes for parabolic interpolation + double a = bestK > minBin + ? Math.FusedMultiplyAdd(workRe[bestK - 1], workRe[bestK - 1], + workIm[bestK - 1] * workIm[bestK - 1]) + : bestMag; + + double c = bestK < maxBin + ? Math.FusedMultiplyAdd(workRe[bestK + 1], workRe[bestK + 1], + workIm[bestK + 1] * workIm[bestK + 1]) + : bestMag; + + double denom = a - 2.0 * bestMag + c; + double shift = Math.Abs(denom) > 0.0 ? 0.5 * (a - c) / denom : 0.0; + double dominant = (double)windowSize / (bestK + shift); double clamped = Math.Clamp(dominant, minPeriod, maxPeriod); lastValid = clamped; output[i] = clamped; @@ -358,10 +486,13 @@ public sealed class Fft : AbstractBase } finally { - if (rentedW != null) + if (rentedH != null) { - ArrayPool.Shared.Return(rentedW); + ArrayPool.Shared.Return(rentedH); } + ArrayPool.Shared.Return(rentedRe); + ArrayPool.Shared.Return(rentedIm); + ArrayPool.Shared.Return(rentedBr); } } diff --git a/lib/numerics/fft/Fft.md b/lib/numerics/fft/Fft.md index 2cf7b0b3..798d835f 100644 --- a/lib/numerics/fft/Fft.md +++ b/lib/numerics/fft/Fft.md @@ -6,46 +6,51 @@ | **Inputs** | Source (close) | | **Parameters** | `windowSize` (default 64), `minPeriod` (default 4), `maxPeriod` (default 32) | | **Outputs** | Single series (Fft) | -| **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Output range** | [minPeriod, maxPeriod] | +| **Warmup** | windowSize bars | ### TL;DR -- The FFT indicator computes the dominant cycle period in a price series using a Discrete Fourier Transform with a Hanning window. -- Parameterized by `windowsize` (default 64), `minperiod` (default 4), `maxperiod` (default 32). -- Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). -- Validated against TA-Lib, Skender, and Tulip reference implementations where available. +- The FFT indicator computes the dominant cycle period in a price series using a radix-2 Cooley-Tukey Fast Fourier Transform with a Hanning window. +- Parameterized by `windowSize` (default 64), `minPeriod` (default 4), `maxPeriod` (default 32). +- Output range: [minPeriod, maxPeriod] bars. +- Requires windowSize bars of warmup before first valid output (IsHot = true). +- True $O(N \log N)$ radix-2 FFT with bit-reversal permutation and Cooley-Tukey butterflies. -The FFT indicator computes the dominant cycle period in a price series using a Discrete Fourier Transform with a Hanning window. Rather than outputting frequency-domain magnitudes, it returns the estimated dominant cycle period in bars, making it directly usable as an adaptive period input for other indicators. The implementation uses a brute-force DFT over a constrained frequency band (not a radix-2 FFT), with parabolic interpolation on the magnitude spectrum to achieve sub-bin frequency resolution. With window sizes of 32, 64, or 128 and $O(N \cdot N/2)$ complexity per bar, the indicator trades computational cost for precise cycle detection within user-specified period bounds. +The FFT indicator computes the dominant cycle period in a price series using a true radix-2 Cooley-Tukey Fast Fourier Transform with a Hanning window. Rather than outputting frequency-domain magnitudes, it returns the estimated dominant cycle period in bars, making it directly usable as an adaptive period input for other indicators. The implementation uses an in-place iterative radix-2 FFT with bit-reversal permutation and Cooley-Tukey butterfly operations, achieving $O(N \log N)$ complexity. Parabolic interpolation on the magnitude spectrum provides sub-bin frequency resolution. With window sizes restricted to powers of two (32, 64, or 128), the indicator achieves precise cycle detection within user-specified period bounds with pre-allocated work arrays for zero-allocation streaming. ## Historical Context -The Fourier transform, formalized by Joseph Fourier (1822), decomposes any periodic signal into sinusoidal components. The Fast Fourier Transform algorithm (Cooley and Tukey, 1965) reduced the DFT from $O(N^2)$ to $O(N \log N)$, enabling real-time spectral analysis. However, for the small window sizes used in financial cycle detection (32-128 samples), the asymptotic advantage of FFT over DFT is minimal, and the DFT avoids the power-of-two length constraint. +The Fourier transform, formalized by Joseph Fourier (1822), decomposes any periodic signal into sinusoidal components. The Fast Fourier Transform algorithm, published by James Cooley and John Tukey in 1965, reduced the DFT from $O(N^2)$ to $O(N \log N)$ by recursively decomposing the DFT into smaller sub-problems using the "butterfly" operation pattern. The radix-2 variant requires power-of-two input lengths and uses bit-reversal permutation followed by iterative butterfly stages. -John Ehlers pioneered the application of spectral analysis to financial markets in the 1990s and 2000s, using DFT-based cycle measurement to create adaptive indicators. His work demonstrated that financial time series contain quasi-periodic cycles with time-varying periods, typically in the 6-40 bar range. The dominant cycle period, extracted via spectral peak detection, can drive adaptive moving averages (MAMA, FAMA), adaptive RSI, and other indicators that benefit from knowing the current market rhythm. +John Ehlers pioneered the application of spectral analysis to financial markets in the 1990s and 2000s, using FFT-based cycle measurement to create adaptive indicators. His work demonstrated that financial time series contain quasi-periodic cycles with time-varying periods, typically in the 6-40 bar range. The dominant cycle period, extracted via spectral peak detection, can drive adaptive moving averages (MAMA, FAMA), adaptive RSI, and other indicators that benefit from knowing the current market rhythm. The Hanning window (also called Hann window, after Julius von Hann) is applied to reduce spectral leakage. Without windowing, the sharp truncation of a finite data segment creates artificial high-frequency components that contaminate the spectrum. The Hanning window tapers the data to zero at both ends, suppressing sidelobes at the cost of slightly wider main lobes (reduced frequency resolution). ## Architecture and Physics -The computation pipeline has four stages: +The computation pipeline has five stages: -**Stage 1: Windowed DFT** computes the real and imaginary components of the Fourier coefficients for frequency bins $k$ ranging from `minBin` to `maxBin`: +**Stage 1: Windowing** applies the Hanning window to the rolling price buffer: -$$X[k] = \sum_{n=0}^{N-1} x[n] \cdot w[n] \cdot e^{-j 2\pi k n / N}$$ +$$x_w[n] = x[n] \cdot w[n], \quad w[n] = 0.5 - 0.5\cos\!\left(\frac{2\pi n}{N}\right)$$ -where $w[n] = 0.5 - 0.5\cos(2\pi n/N)$ is the Hanning window. Only bins corresponding to periods in `[minPeriod, maxPeriod]` are evaluated, reducing computation. +**Stage 2: Bit-reversal permutation** reorders the windowed data according to the bit-reversed indices, preparing for in-place butterfly computation. The permutation table is pre-computed in the constructor. -**Stage 2: Power spectrum peak** finds the bin $k^*$ with maximum squared magnitude $|X[k]|^2 = \text{Re}^2 + \text{Im}^2$. During the search, the magnitudes of the bins adjacent to the peak (one before, one after) are captured for interpolation. +**Stage 3: Cooley-Tukey butterflies** perform $\log_2(N)$ stages of butterfly operations. Each stage $s$ processes pairs of elements separated by $2^{s-1}$ positions, combining them with twiddle factors: -**Stage 3: Parabolic interpolation** refines the peak location using a three-point parabola fit on the magnitudes at bins $k^*-1$, $k^*$, $k^*+1$: +$$\begin{aligned} +X[i_0] &\leftarrow X[i_0] + W_N^k \cdot X[i_1] \\ +X[i_1] &\leftarrow X[i_0] - W_N^k \cdot X[i_1] +\end{aligned}$$ -$$\delta = \frac{M_{k^*-1} - M_{k^*+1}}{M_{k^*-1} + 2 M_{k^*} + M_{k^*+1}}$$ +where $W_N^k = e^{-j2\pi k/N}$ is the twiddle factor. -The refined dominant period is $N / (k^* + \delta)$. +**Stage 4: Peak detection with parabolic interpolation** finds the bin $k^*$ with maximum squared magnitude in `[minBin, maxBin]`, then refines using a three-point parabolic fit: -**Stage 4: Clamping** ensures the output stays within `[minPeriod, maxPeriod]`. +$$\delta = \frac{0.5 \cdot (P[k^*-1] - P[k^*+1])}{P[k^*-1] - 2P[k^*] + P[k^*+1]}$$ + +**Stage 5: Period extraction and clamping** converts the refined bin index to period: $T = N / (k^* + \delta)$, clamped to `[minPeriod, maxPeriod]`. **Window size trade-offs**: $N = 32$ gives coarse resolution (period bins spaced ~1 bar apart) but fast response; $N = 128$ gives fine resolution (~0.25 bar spacing) but sluggish adaptation. The default $N = 64$ balances resolution and responsiveness. @@ -55,6 +60,12 @@ The **Discrete Fourier Transform** for $N$ samples: $$X[k] = \sum_{n=0}^{N-1} x[n] \cdot e^{-j 2\pi k n / N}, \quad k = 0, 1, \ldots, N-1$$ +**Radix-2 Cooley-Tukey decomposition** splits the DFT into even and odd indexed sub-problems: + +$$X[k] = \sum_{r=0}^{N/2-1} x[2r] \cdot W_{N/2}^{kr} + W_N^k \sum_{r=0}^{N/2-1} x[2r+1] \cdot W_{N/2}^{kr}$$ + +This recursion, applied iteratively with bit-reversal permutation, achieves $O(N \log N)$ complexity. + **Hanning window**: $$w[n] = 0.5 - 0.5\cos\!\left(\frac{2\pi n}{N}\right)$$ @@ -69,7 +80,7 @@ $$k_{\min} = \max\!\left(1,\; \left\lfloor\frac{N}{T_{\max}}\right\rfloor\right) **Parabolic interpolation** for sub-bin precision: -$$\hat{k} = k^* + \frac{P[k^*-1] - P[k^*+1]}{P[k^*-1] + 2P[k^*] + P[k^*+1]}$$ +$$\hat{k} = k^* + \frac{0.5 \cdot (P[k^*-1] - P[k^*+1])}{P[k^*-1] - 2P[k^*] + P[k^*+1]}$$ $$T_{\text{dominant}} = \frac{N}{\hat{k}}$$ @@ -78,28 +89,33 @@ $$T_{\text{dominant}} = \frac{N}{\hat{k}}$$ ``` FFT(source, windowSize, minPeriod, maxPeriod): N = windowSize - twoPiOverN = 2 * pi / N - minBin = max(1, N / maxPeriod) - maxBin = min(N/2, N / minPeriod) + // Stage 1: Apply Hanning window + for n = 0 to N-1: + workRe[n] = source[n] * hanning[n] + workIm[n] = 0 - maxMag = 0; peakBin = 0 - for k = minBin to maxBin: - re = 0; im = 0 - for n = 0 to N-1: - w = 0.5 - 0.5 * cos(twoPiOverN * n) // Hanning - xw = source[n] * w - angle = twoPiOverN * k * n - re += xw * cos(angle) - im -= xw * sin(angle) - mag = re*re + im*im - if mag > maxMag: - track neighbor magnitudes - maxMag = mag; peakBin = k + // Stage 2: Bit-reversal permutation + for i = 0 to N-1: + j = bitReverse(i) + if j > i: swap(workRe[i], workRe[j]) - // Parabolic interpolation - shift = (magBefore - magAfter) / (magBefore + 2*maxMag + magAfter) - dominantPeriod = N / (peakBin + shift) - return clamp(dominantPeriod, minPeriod, maxPeriod) + // Stage 3: Cooley-Tukey butterflies + len = 2 + while len <= N: + half = len / 2 + angStep = -2π / len + for start = 0 to N-1 step len: + for k = 0 to half-1: + w = exp(j * angStep * k) + butterfly(workRe, workIm, start+k, start+k+half, w) + len *= 2 + + // Stage 4: Peak detection + interpolation + peakBin = argmax |X[k]|² for k in [minBin..maxBin] + shift = 0.5*(P[k-1] - P[k+1]) / (P[k-1] - 2*P[k] + P[k+1]) + + // Stage 5: Period extraction + return clamp(N / (peakBin + shift), minPeriod, maxPeriod) ``` @@ -107,34 +123,37 @@ FFT(source, windowSize, minPeriod, maxPeriod): ### Operation Count (Streaming Mode) -FFT (DFT dominant cycle detector) evaluates B frequency bins, each requiring N multiply-accumulates — O(N*B) per bar. +FFT (radix-2 Cooley-Tukey) performs N/2 butterflies per stage across log₂(N) stages — O(N log N) per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | | Hanning window multiply | N | 2 cy | ~2N cy | -| DFT inner loop (B bins * N samples) | B*N | 4 cy | ~4*N*B cy | -| cos/sin evaluation (precomputed table) | 2*B*N | 0 cy | ~0 cy | -| Magnitude comparison + peak track | B | 2 cy | ~2B cy | -| Parabolic interpolation (3 points) | 1 | 5 cy | ~5 cy | -| **Total (N=64, B=10)** | **O(N*B)** | — | **~2617 cy** | +| Bit-reversal permutation | N | 1 cy | ~N cy | +| Butterfly operations (log₂N stages × N/2) | N/2 × log₂N | 8 cy | ~4N·log₂N cy | +| cos/sin per butterfly | N/2 × log₂N | 14 cy | ~7N·log₂N cy | +| Magnitude search (B bins) | B | 4 cy | ~4B cy | +| Parabolic interpolation | 1 | 10 cy | ~10 cy | +| **Total (N=64, B=10)** | **O(N log N)** | — | **~4362 cy** | -O(N*B) per bar where B = active frequency bins. Precomputed sin/cos tables eliminate transcendental cost. Suitable for 1-minute+ timeframes; not tick-data hot paths. +O(N log N) per bar. Pre-allocated work arrays ensure zero allocation in the hot path. Twiddle factor computation dominates; pre-computing sin/cos tables would reduce to ~2500 cy. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | | Hanning window application | Yes | Vector multiply with precomputed weights | -| DFT inner dot product | Yes | FMA with sin/cos table lookup | -| Magnitude squared | Yes | Vector FMA (re^2 + im^2) | +| Bit-reversal permutation | No | Random access pattern; scalar only | +| Butterfly multiply-add | Yes | Complex FMA operations on paired elements | +| Magnitude squared | Yes | Vector FMA (re² + im²) | | Peak search | Partial | Max reduction; SIMD-friendly | -Strong batch SIMD: inner dot products are FMA-vectorizable. AVX2 processes 4 complex outputs per 2 cycles. Expected 3-4× speedup for N=64. +Moderate SIMD potential: butterfly FMA operations are vectorizable within each stage. Bit-reversal permutation is inherently scalar. Expected 2× speedup over scalar for N=64. ## Resources -- Cooley, J.W. & Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." Mathematics of Computation, 1965. +- Cooley, J.W. & Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." *Mathematics of Computation*, 1965. - Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013. - Ehlers, J.F. "Rocket Science for Traders." Wiley, 2001. -- Harris, F.J. "On the Use of Windows for Harmonic Analysis with the Discrete Fourier Transform." Proc. IEEE, 1978. +- Harris, F.J. "On the Use of Windows for Harmonic Analysis with the Discrete Fourier Transform." *Proc. IEEE*, 1978. - Oppenheim, A.V. & Schafer, R.W. "Discrete-Time Signal Processing." 3rd edition, Pearson, 2010. +- PineScript reference: [`fft.pine`](fft.pine) diff --git a/lib/numerics/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.cs b/lib/numerics/ifft/Ifft.cs index c7c448fd..32e9d75e 100644 --- a/lib/numerics/ifft/Ifft.cs +++ b/lib/numerics/ifft/Ifft.cs @@ -1,7 +1,10 @@ -// IFFT: Inverse FFT Spectral Low-Pass Filter -// Reconstructs a filtered price signal by summing the DC component and -// the first H harmonics of the Hanning-windowed DFT. Output overlays on price. -// More harmonics → less smoothing; fewer harmonics → smoother output. +// IFFT: Inverse Fast Fourier Transform — Spectral Low-Pass Filter +// Reconstructs a filtered price signal by performing a forward radix-2 FFT, +// zeroing frequency bins above numHarmonics, then applying an inverse FFT. +// Output overlays on price. More harmonics → less smoothing; fewer → smoother. +// +// Algorithm: Cooley, J.W. & Tukey, J.W. (1965). Forward FFT → spectral +// truncation → inverse FFT reconstruction. using System.Buffers; using System.Runtime.CompilerServices; @@ -11,16 +14,18 @@ namespace QuanTAlib; /// /// IFFT: Inverse FFT Spectral Low-Pass Filter -/// Reconstructs a filtered price value from the DC component plus -/// the first numHarmonics frequency bins of the Hanning-windowed DFT. +/// Reconstructs a filtered price value by performing a forward radix-2 FFT, +/// zeroing bins above numHarmonics (preserving conjugate symmetry), +/// then applying an inverse FFT to reconstruct the time-domain signal. /// /// /// Key properties: /// - Output: reconstructed price (spectral low-pass filtered), overlays on price chart -/// - windowSize must be 32, 64, or 128 +/// - windowSize must be 32, 64, or 128 (power of 2 for radix-2) /// - numHarmonics clamped to [1, windowSize/2] /// - WarmupPeriod = windowSize bars -/// - No allocation in Update (RingBuffer + precomputed Hanning weights) +/// - True O(N log N) radix-2 FFT/IFFT with bit-reversal permutation +/// - Pre-allocated work arrays for zero-allocation streaming /// - Increasing harmonics increases detail (less smoothing) /// [SkipLocalsInit] @@ -28,9 +33,11 @@ public sealed class Ifft : AbstractBase { private readonly int _windowSize; private readonly int _numHarmonics; - private readonly double _twoPiOverN; private readonly double _invN; private readonly double[] _hanning; + private readonly int[] _bitRev; + private readonly double[] _workRe; + private readonly double[] _workIm; private readonly RingBuffer _buffer; [StructLayout(LayoutKind.Auto)] @@ -42,8 +49,8 @@ public sealed class Ifft : AbstractBase /// /// Initializes a new Ifft indicator. /// - /// DFT window size in bars. Must be 32, 64, or 128. Default 64. - /// Number of harmonics to reconstruct. Must be >= 1. Default 5. + /// FFT window size in bars. Must be 32, 64, or 128. Default 64. + /// Number of harmonics to preserve. Must be >= 1. Default 5. public Ifft(int windowSize = 64, int numHarmonics = 5) { if (windowSize != 32 && windowSize != 64 && windowSize != 128) @@ -58,16 +65,28 @@ public sealed class Ifft : AbstractBase _windowSize = windowSize; _numHarmonics = Math.Min(numHarmonics, windowSize / 2); - _twoPiOverN = 2.0 * Math.PI / windowSize; + int log2N = Log2(windowSize); _invN = 1.0 / windowSize; - // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N), n=0..N-1 + // Precompute Hanning window: w[n] = 0.5 - 0.5*cos(2π*n/N) + double twoPiOverN = 2.0 * Math.PI / windowSize; _hanning = new double[windowSize]; for (int n = 0; n < windowSize; n++) { - _hanning[n] = 0.5 - 0.5 * Math.Cos(_twoPiOverN * n); + _hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); } + // Precompute bit-reversal permutation table + _bitRev = new int[windowSize]; + for (int i = 0; i < windowSize; i++) + { + _bitRev[i] = BitReverse(i, log2N); + } + + // Pre-allocate work arrays (zero allocation in hot path) + _workRe = new double[windowSize]; + _workIm = new double[windowSize]; + _buffer = new RingBuffer(windowSize); Name = $"Ifft({windowSize},{numHarmonics})"; WarmupPeriod = windowSize; @@ -78,9 +97,6 @@ public sealed class Ifft : AbstractBase /// /// Initializes a new Ifft indicator with source for event-based chaining. /// - /// Source indicator for chaining - /// DFT window size. Must be 32, 64, or 128. Default 64. - /// Number of harmonics to reconstruct. Must be >= 1. Default 5. public Ifft(ITValuePublisher source, int windowSize = 64, int numHarmonics = 5) : this(windowSize, numHarmonics) { @@ -90,42 +106,99 @@ public sealed class Ifft : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Log2(int n) + { + int p = 0; + int x = n; + while (x > 1) + { + x >>= 1; + p++; + } + return p; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitReverse(int x, int bits) + { + int r = 0; + for (int i = 0; i < bits; i++) + { + r = (r << 1) | (x & 1); + x >>= 1; + } + return r; + } + + /// + /// Applies spectral truncation: zeroes frequency bins outside the + /// preserved range [0..numHarmonics] and their conjugate mirrors + /// [N-numHarmonics..N-1], ensuring real-valued IFFT output. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SpectralTruncate(double[] re, double[] im, int n, int numHarmonics) + { + // Keep bins 0..numHarmonics and N-numHarmonics..N-1 (conjugate symmetry) + // Zero everything in between: bins numHarmonics+1..N-numHarmonics-1 + int startZero = numHarmonics + 1; + int endZero = n - numHarmonics; // exclusive + + for (int k = startZero; k < endZero; k++) + { + re[k] = 0.0; + im[k] = 0.0; + } + } + + /// + /// Computes inverse FFT in-place using the conjugate method: + /// IFFT(X) = (1/N) * conj(FFT(conj(X))) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void IfftInPlace(double[] re, double[] im, int n, int[] bitRev, double invN) + { + // Conjugate input + for (int i = 0; i < n; i++) + { + im[i] = -im[i]; + } + + // Forward FFT + Fft.FftInPlace(re, im, n, bitRev); + + // Conjugate output and scale by 1/N + for (int i = 0; i < n; i++) + { + re[i] *= invN; + im[i] = -im[i] * invN; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ComputeIfft() { var span = _buffer.GetSpan(); int n = _windowSize; - // DC component (k=0): sum of windowed values / N - double dcRe = 0.0; - for (int idx = 0; idx < n; idx++) + // Fill work arrays: windowed data (oldest→newest), imag=0 + for (int i = 0; i < n; i++) { - // span[0]=oldest, span[n-1]=newest - // dftN=0→newest, dftN=n-1→oldest → span index = n-1-dftN - double val = span[n - 1 - idx]; - dcRe = Math.FusedMultiplyAdd(val, _hanning[idx], dcRe); + _workRe[i] = span[i] * _hanning[i]; + _workIm[i] = 0.0; } - double result = dcRe * _invN; + // Forward FFT + Fft.FftInPlace(_workRe, _workIm, n, _bitRev); - // Harmonics k=1..H: add 2*re/N at time n=0 (reconstruction at current bar) - for (int k = 1; k <= _numHarmonics; k++) - { - double omegaK = _twoPiOverN * k; - double re = 0.0; + // Spectral truncation: zero bins above numHarmonics + SpectralTruncate(_workRe, _workIm, n, _numHarmonics); - for (int idx = 0; idx < n; idx++) - { - double val = span[n - 1 - idx]; - double xw = val * _hanning[idx]; - double angle = omegaK * idx; - re = Math.FusedMultiplyAdd(xw, Math.Cos(angle), re); - } + // Inverse FFT to reconstruct filtered time-domain signal + IfftInPlace(_workRe, _workIm, n, _bitRev, _invN); - result = Math.FusedMultiplyAdd(2.0 * _invN, re, result); - } - - return result; + // Return the newest sample (last position in the array) + return _workRe[n - 1]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -200,8 +273,8 @@ public sealed class Ifft : AbstractBase } /// - /// Computes IFFT reconstruction over a span of values using a sliding Hanning-windowed DFT. - /// Uses stackalloc for Hanning weights when windowSize <= 64, otherwise ArrayPool. + /// Computes IFFT reconstruction over a span using sliding Hanning-windowed + /// radix-2 FFT → spectral truncation → inverse FFT. /// public static void Batch( ReadOnlySpan src, Span output, @@ -228,12 +301,14 @@ public sealed class Ifft : AbstractBase } int clampedHarmonics = Math.Min(numHarmonics, windowSize / 2); + int log2N = Log2(windowSize); double twoPiOverN = 2.0 * Math.PI / windowSize; double invN = 1.0 / windowSize; double lastValid = 0.0; + // Precompute Hanning window const int StackallocThreshold = 64; - double[]? rentedW = null; + double[]? rentedH = null; scoped Span hanning; if (windowSize <= StackallocThreshold) @@ -242,15 +317,21 @@ public sealed class Ifft : AbstractBase } else { - rentedW = ArrayPool.Shared.Rent(windowSize); - hanning = rentedW.AsSpan(0, windowSize); + rentedH = ArrayPool.Shared.Rent(windowSize); + hanning = rentedH.AsSpan(0, windowSize); } + // FFT work arrays and bit-reversal table + double[] workRe = ArrayPool.Shared.Rent(windowSize); + double[] workIm = ArrayPool.Shared.Rent(windowSize); + int[] bitRev = ArrayPool.Shared.Rent(windowSize); + try { for (int n = 0; n < windowSize; n++) { hanning[n] = 0.5 - 0.5 * Math.Cos(twoPiOverN * n); + bitRev[n] = BitReverse(n, log2N); } for (int i = 0; i < src.Length; i++) @@ -268,52 +349,42 @@ public sealed class Ifft : AbstractBase continue; } - // DC component - double dcRe = 0.0; - for (int dftN = 0; dftN < windowSize; dftN++) + // Fill work arrays with windowed data (oldest→newest) + for (int n = 0; n < windowSize; n++) { - double v = src[i - dftN]; + double v = src[i - windowSize + 1 + n]; if (!double.IsFinite(v)) { v = lastValid; } - - dcRe = Math.FusedMultiplyAdd(v, hanning[dftN], dcRe); + workRe[n] = v * hanning[n]; + workIm[n] = 0.0; } - double result = dcRe * invN; + // Forward FFT + Fft.FftInPlace(workRe, workIm, windowSize, bitRev); - // Harmonics - for (int k = 1; k <= clampedHarmonics; k++) - { - double omegaK = twoPiOverN * k; - double re = 0.0; + // Spectral truncation + SpectralTruncate(workRe, workIm, windowSize, clampedHarmonics); - for (int dftN = 0; dftN < windowSize; dftN++) - { - double v = src[i - dftN]; - if (!double.IsFinite(v)) - { - v = lastValid; - } - - double xw = v * hanning[dftN]; - re = Math.FusedMultiplyAdd(xw, Math.Cos(omegaK * dftN), re); - } - - result = Math.FusedMultiplyAdd(2.0 * invN, re, result); - } + // Inverse FFT + IfftInPlace(workRe, workIm, windowSize, bitRev, invN); + // Extract newest sample + double result = workRe[windowSize - 1]; lastValid = result; output[i] = result; } } finally { - if (rentedW != null) + if (rentedH != null) { - ArrayPool.Shared.Return(rentedW); + ArrayPool.Shared.Return(rentedH); } + ArrayPool.Shared.Return(workRe); + ArrayPool.Shared.Return(workIm); + ArrayPool.Shared.Return(bitRev); } } diff --git a/lib/numerics/ifft/Ifft.md b/lib/numerics/ifft/Ifft.md index 279707ec..1464ffb9 100644 --- a/lib/numerics/ifft/Ifft.md +++ b/lib/numerics/ifft/Ifft.md @@ -1,4 +1,4 @@ -# IFFT: Inverse Fast Fourier Transform (Spectral Filter) +# IFFT: Inverse Fast Fourier Transform (Spectral Low-Pass Filter) | Property | Value | | ---------------- | -------------------------------- | @@ -6,64 +6,68 @@ | **Inputs** | Source (close) | | **Parameters** | `windowSize` (default 64), `numHarmonics` (default 5) | | **Outputs** | Single series (Ifft) | -| **Output range** | Varies (see docs) | -| **Warmup** | 1 bar | +| **Output range** | Varies (overlays on price) | +| **Warmup** | windowSize bars | ### TL;DR -- The Inverse FFT indicator reconstructs a smoothed version of the price series by performing a forward DFT, retaining only the lowest-frequency harm... -- Parameterized by `windowsize` (default 64), `numharmonics` (default 5). -- Output range: Varies (see docs). -- Requires 1 bar of warmup before first valid output (IsHot = true). -- Validated against TA-Lib, Skender, and Tulip reference implementations where available. +- The IFFT indicator reconstructs a smoothed version of the price series using a true forward FFT → spectral truncation → inverse FFT pipeline. +- Parameterized by `windowSize` (default 64), `numHarmonics` (default 5). +- Output range: Varies (overlays on price chart). +- Requires windowSize bars of warmup before first valid output (IsHot = true). +- True $O(N \log N)$ radix-2 FFT/IFFT with bit-reversal permutation and Cooley-Tukey butterflies. -The Inverse FFT indicator reconstructs a smoothed version of the price series by performing a forward DFT, retaining only the lowest-frequency harmonics, and synthesizing the output via inverse transform. The result is a spectral low-pass filter that preserves the dominant cyclical components while discarding high-frequency noise. By controlling the number of retained harmonics $H$, the user adjusts the smoothness/responsiveness trade-off: $H = 1$ yields a near-sinusoidal trend, while $H = N/2$ reproduces the original (windowed) signal. The indicator overlays on price and provides a frequency-domain alternative to conventional moving averages. +The IFFT indicator reconstructs a smoothed version of the price series by performing a true radix-2 forward FFT, zeroing frequency bins above the specified number of harmonics (spectral truncation), then applying a true inverse FFT to reconstruct the filtered time-domain signal. The result is a spectral low-pass filter that preserves the dominant cyclical components while discarding high-frequency noise. By controlling the number of retained harmonics $H$, the user adjusts the smoothness/responsiveness trade-off: $H = 1$ yields a near-sinusoidal trend, while $H = N/2$ reproduces the original (windowed) signal. The indicator overlays on price and provides a frequency-domain alternative to conventional moving averages. ## Historical Context -Spectral filtering via Fourier decomposition dates to Joseph Fourier's 1822 work on heat conduction, where he showed that any periodic function can be represented as a sum of sinusoids. The idea of reconstructing a signal from a subset of its Fourier coefficients is foundational to signal compression (JPEG, MP3) and has been applied to financial time series since the 1970s. +Spectral filtering via Fourier decomposition dates to Joseph Fourier's 1822 work on heat conduction, where he showed that any periodic function can be represented as a sum of sinusoids. The Cooley-Tukey FFT algorithm (1965) made real-time spectral analysis practical by reducing complexity from $O(N^2)$ to $O(N \log N)$. -John Ehlers brought spectral methods to mainstream technical analysis through his books on cycle analytics. His approach typically uses the DFT to identify the dominant cycle, then constructs adaptive filters tuned to that cycle. The IFFT indicator takes the complementary approach: rather than extracting a single cycle period, it reconstructs the signal from the $H$ lowest-frequency components, producing a multi-harmonic trend estimate. +John Ehlers brought spectral methods to mainstream technical analysis through his books on cycle analytics. The IFFT indicator implements the classic spectral filtering paradigm: forward FFT to decompose into frequency components, selective retention of low-frequency bins, and inverse FFT to reconstruct the filtered signal. -The Hanning window applied before the forward DFT reduces spectral leakage, ensuring that the retained harmonics accurately represent the true low-frequency content rather than artifacts of the window boundary. The inverse step only uses the real part of the synthesis (cosine terms), since the output must be a real-valued price estimate. The factor of 2 in the inverse accounts for the conjugate symmetry of real-valued DFT coefficients. +The Hanning window applied before the forward FFT reduces spectral leakage, ensuring that the retained harmonics accurately represent the true low-frequency content rather than artifacts of the window boundary. Conjugate symmetry is preserved during spectral truncation to guarantee real-valued reconstruction. ## Architecture and Physics -The computation has three stages executed per bar: +The computation has four stages executed per bar: -**Stage 1: DC component** computes the windowed mean of the source over the window. This is the zero-frequency (average level) component: +**Stage 1: Forward FFT** applies a Hanning window to the rolling price buffer, then performs an in-place radix-2 Cooley-Tukey FFT with bit-reversal permutation: -$$\text{DC} = \frac{1}{N}\sum_{n=0}^{N-1} x[n] \cdot w[n]$$ +$$X[k] = \text{FFT}\!\left(x[n] \cdot w[n]\right)$$ -**Stage 2: Forward DFT for harmonics $k = 1$ to $H$** computes the real and imaginary Fourier coefficients for each retained harmonic. The Hanning window $w[n] = 0.5 - 0.5\cos(2\pi n/N)$ is applied to every sample. +where $w[n] = 0.5 - 0.5\cos(2\pi n/N)$. -**Stage 3: Inverse synthesis** reconstructs the current bar's value by summing the DC component plus twice the real part of each harmonic evaluated at $n = 0$ (the current bar): +**Stage 2: Spectral truncation** zeroes frequency bins outside the preserved range, keeping bins $k = 0, 1, \ldots, H$ and their conjugate mirrors $k = N-H, \ldots, N-1$: -$$\hat{x}[0] = \frac{\text{DC}_{\text{Re}}}{N} + \sum_{k=1}^{H} \frac{2 \cdot \text{Re}(X[k])}{N}$$ +$$\tilde{X}[k] = \begin{cases} X[k] & \text{if } k \le H \text{ or } k \ge N-H \\ 0 & \text{otherwise} \end{cases}$$ -The factor $2/N$ accounts for: (1) the $1/N$ normalization of the inverse DFT, and (2) the factor of 2 from collapsing the conjugate-symmetric negative frequencies. +This preserves conjugate symmetry ($\tilde{X}[N-k] = \tilde{X}[k]^*$), ensuring the inverse FFT produces real-valued output. -**Complexity**: The forward DFT for $H$ harmonics costs $O(N \cdot H)$ multiply-adds per bar. With $N = 64$ and $H = 5$ (defaults), this is ~320 multiply-adds per bar. The inverse synthesis at $n = 0$ reduces to just summing the real components, costing $O(H)$. +**Stage 3: Inverse FFT** reconstructs the filtered time-domain signal using the conjugate method: -**Smoothness control**: Fewer harmonics produce smoother output but introduce more lag and lose detail. The relationship between harmonics and equivalent moving average length is roughly: $H$ harmonics approximate the smoothness of an $N/(2H)$-period moving average, but with better frequency selectivity (sharper cutoff). +$$\hat{x}[n] = \frac{1}{N} \cdot \overline{\text{FFT}\!\left(\overline{\tilde{X}[k]}\right)}$$ + +This reuses the forward FFT algorithm by conjugating inputs, applying FFT, conjugating outputs, and scaling by $1/N$. + +**Stage 4: Sample extraction** returns the value at position $N-1$ (the newest bar in the window). + +**Complexity**: Two FFT passes of $O(N \log N)$ each, plus $O(N)$ for windowing and spectral truncation. Total: $O(N \log N)$ per bar. + +**Smoothness control**: Fewer harmonics produce smoother output but introduce more lag. The relationship between harmonics and equivalent moving average length is roughly: $H$ harmonics approximate the smoothness of an $N/(2H)$-period moving average, with better frequency selectivity (sharper cutoff). ## Mathematical Foundation -The **forward DFT** with Hanning window: +The **forward FFT** with Hanning window (radix-2 Cooley-Tukey): -$$X[k] = \sum_{n=0}^{N-1} x[n] \cdot w[n] \cdot e^{-j 2\pi k n / N}$$ +$$X[k] = \text{FFT}_N\!\left(x[n] \cdot w[n]\right), \quad k = 0, 1, \ldots, N-1$$ -where $w[n] = 0.5 - 0.5\cos(2\pi n / N)$. +**Spectral truncation** (ideal low-pass in frequency domain): -The **inverse DFT** evaluated at the current bar ($n = 0$): +$$\tilde{X}[k] = X[k] \cdot H_{\text{LP}}[k], \quad H_{\text{LP}}[k] = \begin{cases} 1 & k \le H \text{ or } k \ge N-H \\ 0 & \text{otherwise} \end{cases}$$ -$$\hat{x}[0] = \frac{1}{N}\sum_{k=0}^{N-1} X[k] \cdot e^{j 2\pi k \cdot 0 / N} = \frac{1}{N}\sum_{k=0}^{N-1} X[k]$$ +**Inverse FFT** via conjugation: -Since $e^{j \cdot 0} = 1$, the inverse at $n = 0$ is simply the sum of all retained coefficients divided by $N$. - -For a real-valued signal, $X[N-k] = X[k]^*$, so: - -$$\hat{x}[0] = \frac{X[0]}{N} + \frac{2}{N}\sum_{k=1}^{H} \text{Re}(X[k])$$ +$$\hat{x}[n] = \frac{1}{N} \cdot \overline{\text{FFT}_N\!\left(\overline{\tilde{X}[k]}\right)}$$ **Parseval's theorem** relates the energy retained: @@ -75,27 +79,27 @@ $$\frac{\sum_{k=0}^{H} |X[k]|^2}{\sum_{k=0}^{N/2} |X[k]|^2} = \text{fraction of IFFT(source, windowSize, numHarmonics): N = windowSize H = min(numHarmonics, N/2) - twoPiOverN = 2 * pi / N - // DC component (k=0) - dcRe = 0 + // Stage 1: Window + Forward FFT for n = 0 to N-1: - w = 0.5 - 0.5 * cos(twoPiOverN * n) - dcRe += source[n] * w - result = dcRe / N + workRe[n] = source[n] * hanning[n] + workIm[n] = 0 + FFT_InPlace(workRe, workIm, N) - // Harmonics k=1..H - for k = 1 to H: - re = 0; im = 0 - for n = 0 to N-1: - w = 0.5 - 0.5 * cos(twoPiOverN * n) - xw = source[n] * w - angle = twoPiOverN * k * n - re += xw * cos(angle) - im -= xw * sin(angle) - result += 2 * re / N // inverse at n=0 + // Stage 2: Spectral truncation + for k = H+1 to N-H-1: + workRe[k] = 0 + workIm[k] = 0 - return result + // Stage 3: Inverse FFT (via conjugation) + for i = 0 to N-1: workIm[i] = -workIm[i] + FFT_InPlace(workRe, workIm, N) + for i = 0 to N-1: + workRe[i] /= N + workIm[i] = -workIm[i] / N + + // Stage 4: Extract newest sample + return workRe[N-1] ``` @@ -103,32 +107,37 @@ IFFT(source, windowSize, numHarmonics): ### Operation Count (Streaming Mode) -IFFT (Inverse DFT reconstruction) sums B frequency components back into the time domain — O(N*B) per bar. +IFFT performs two radix-2 FFT passes (forward + inverse) plus spectral truncation — O(N log N) per bar. | Operation | Count | Cost (cycles) | Subtotal | | :--- | :---: | :---: | :---: | -| Complex multiply-accumulate (N * B) | N*B | 4 cy | ~4*N*B cy | -| cos/sin table lookup (precomputed) | 2*N*B | 0 cy | ~0 cy | -| Division by N for normalization | N | 1 cy | ~N cy | -| NaN guard + state update | 1 | 2 cy | ~2 cy | -| **Total (N=64, B=10)** | **O(N*B)** | — | **~2626 cy** | +| Hanning window multiply | N | 2 cy | ~2N cy | +| Forward FFT (N/2 × log₂N butterflies) | N/2 × log₂N | 8 cy | ~4N·log₂N cy | +| Spectral truncation | N-2H | 1 cy | ~(N-2H) cy | +| Conjugation (2×) | 2N | 1 cy | ~2N cy | +| Inverse FFT (N/2 × log₂N butterflies) | N/2 × log₂N | 8 cy | ~4N·log₂N cy | +| Scale by 1/N | N | 1 cy | ~N cy | +| **Total (N=64, H=5)** | **O(N log N)** | — | **~3254 cy** | -Same complexity as forward FFT. Precomputed trig tables allow the inner loop to reduce to 4 FMAs per bin. Paired with FFT for frequency-domain filtering. +Two FFT passes dominate cost. Pre-allocated work arrays ensure zero allocation in the hot path. ### Batch Mode (SIMD Analysis) | Operation | Vectorizable? | Notes | | :--- | :---: | :--- | -| Complex MAC (re*cos - im*sin) | Yes | FMA with precomputed table | -| Normalization | Yes | Vector divide by N | -| Output time-domain signal | Yes | Full SIMD reconstruction | +| Hanning window application | Yes | Vector multiply with precomputed weights | +| FFT butterfly operations | Yes | Complex FMA on paired elements | +| Spectral truncation (zeroing) | Yes | Vector zero-fill | +| IFFT butterfly operations | Yes | Same as forward FFT | +| Scale by 1/N | Yes | Vector multiply by constant | -Same SIMD profile as FFT forward pass. 3-4× batch speedup expected over scalar using Vector FMA. +Good SIMD potential: both FFT passes are vectorizable. Expected 2× speedup over scalar for N=64. ## Resources +- Cooley, J.W. & Tukey, J.W. "An Algorithm for the Machine Calculation of Complex Fourier Series." *Mathematics of Computation*, 1965. - Fourier, J.B.J. "Theorie Analytique de la Chaleur." Firmin Didot, 1822. - Ehlers, J.F. "Cycle Analytics for Traders." Wiley, 2013. - Oppenheim, A.V. & Schafer, R.W. "Discrete-Time Signal Processing." 3rd edition, Pearson, 2010. - Bloomfield, P. "Fourier Analysis of Time Series: An Introduction." 2nd edition, Wiley, 2000. -- Priestley, M.B. "Spectral Analysis and Time Series." Academic Press, 1981. +- PineScript reference: [`ifft.pine`](ifft.pine) diff --git a/lib/numerics/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..e922d661 100644 --- a/lib/numerics/jerk/jerk.pine +++ b/lib/numerics/jerk/jerk.pine @@ -1,112 +1,29 @@ -// 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) +indicator("Third Derivative / Jerk (JERK)", "JERK", overlay=false, precision=8) -//@function Calculates jerk (slope of slope of slope) -//@param src Source series to calculate slope from -//@param len Lookback period for calculation -//@returns jerk -jerk(series float src, simple int len1) => - if len1 <= 1 - runtime.error("Length 1 for first slope calculation must be greater than 1") - var float sumX1 = 0.0, var float sumY1 = 0.0, var float sumXY1 = 0.0, var float sumX21 = 0.0 - var int validCount1 = 0 - var array x_values1 = array.new_float(len1) - var array y_values1 = array.new_float(len1) - var int head1 = 0 - var int internal_time_counter1 = 0 - if internal_time_counter1 >= len1 - float oldX1 = array.get(x_values1, head1) - float oldY1 = array.get(y_values1, head1) - if not na(oldY1) - sumX1 := sumX1 - oldX1, sumY1 := sumY1 - oldY1 - sumXY1 := sumXY1 - oldX1 * oldY1, sumX21 := sumX21 - oldX1 * oldX1 - validCount1 := validCount1 - 1 - float currentX1 = internal_time_counter1 - float currentY1 = src - array.set(x_values1, head1, currentX1) - array.set(y_values1, head1, currentY1) - if not na(currentY1) - sumX1 := sumX1 + currentX1, sumY1 := sumY1 + currentY1 - sumXY1 := sumXY1 + currentX1 * currentY1, sumX21 := sumX21 + currentX1 * currentX1 - validCount1 := validCount1 + 1 - head1 := (head1 + 1) % len1 - internal_time_counter1 := internal_time_counter1 + 1 - float current_slope1 = na - if validCount1 >= 2 - float n1 = validCount1 - float divisor1 = n1 * sumX21 - sumX1 * sumX1 - if divisor1 != 0.0 - current_slope1 := (n1 * sumXY1 - sumX1 * sumY1) / divisor1 - var float sumX2 = 0.0, var float sumY2 = 0.0, var float sumXY2 = 0.0, var float sumX22 = 0.0 - var int validCount2 = 0 - var array x_values2 = array.new_float(len1) - var array y_values2 = array.new_float(len1) - var int head2 = 0 - var int internal_time_counter2 = 0 - if internal_time_counter2 >= len1 - float oldX2 = array.get(x_values2, head2) - float oldY2 = array.get(y_values2, head2) - if not na(oldY2) - sumX2 := sumX2 - oldX2, sumY2 := sumY2 - oldY2 - sumXY2 := sumXY2 - oldX2 * oldY2, sumX22 := sumX22 - oldX2 * oldX2 - validCount2 := validCount2 - 1 - float currentX2 = internal_time_counter2 - float currentY2 = current_slope1 - array.set(x_values2, head2, currentX2) - array.set(y_values2, head2, currentY2) - if not na(currentY2) - sumX2 := sumX2 + currentX2, sumY2 := sumY2 + currentY2 - sumXY2 := sumXY2 + currentX2 * currentY2, sumX22 := sumX22 + currentX2 * currentX2 - validCount2 := validCount2 + 1 - head2 := (head2 + 1) % len1 - internal_time_counter2 := internal_time_counter2 + 1 - float current_accel = na - if validCount2 >= 2 - float n2 = validCount2 - float divisor2 = n2 * sumX22 - sumX2 * sumX2 - if divisor2 != 0.0 - current_accel := (n2 * sumXY2 - sumX2 * sumY2) / divisor2 - var float sumX3 = 0.0, var float sumY3 = 0.0, var float sumXY3 = 0.0, var float sumX23 = 0.0 - var int validCount3 = 0 - var array x_values3 = array.new_float(len1) - var array y_values3 = array.new_float(len1) - var int head3 = 0 - var int internal_time_counter3 = 0 - if internal_time_counter3 >= len1 - float oldX3 = array.get(x_values3, head3) - float oldY3 = array.get(y_values3, head3) - if not na(oldY3) - sumX3 := sumX3 - oldX3, sumY3 := sumY3 - oldY3 - sumXY3 := sumXY3 - oldX3 * oldY3, sumX23 := sumX23 - oldX3 * oldX3 - validCount3 := validCount3 - 1 - float currentX3 = internal_time_counter3 - float currentY3 = current_accel - array.set(x_values3, head3, currentX3) - array.set(y_values3, head3, currentY3) - if not na(currentY3) - sumX3 := sumX3 + currentX3, sumY3 := sumY3 + currentY3 - sumXY3 := sumXY3 + currentX3 * currentY3, sumX23 := sumX23 + currentX3 * currentX3 - validCount3 := validCount3 + 1 - head3 := (head3 + 1) % len1 - internal_time_counter3 := internal_time_counter3 + 1 - float calculatedjerk = na - if validCount3 >= 2 - float n3 = validCount3 - float divisor3 = n3 * sumX23 - sumX3 * sumX3 - if divisor3 != 0.0 - calculatedjerk := (n3 * sumXY3 - sumX3 * sumY3) / divisor3 - calculatedjerk +//@function Calculates the third finite difference (jerk): Jerk = V[t] - 3*V[t-1] + 3*V[t-2] - V[t-3] +//@param src Source series +//@returns Third difference. Returns 0.0 until 4 bars are available. +//@optimized Uses direct history access and binomial coefficients [1,-3,3,-1]. +jerk(series float src) => + float v0 = src + float v1 = src[1] + float v2 = src[2] + float v3 = src[3] + if na(v1) or na(v2) or na(v3) + 0.0 + else + v0 - 3.0 * v1 + 3.0 * v2 - v3 - // ---------- Main loop ---------- +// ---------- Main loop ---------- // Inputs -i_period = input.int(14, "Period", minval=2) i_source = input.source(close, "Source") // Calculation -a = jerk(i_source, i_period) +j = jerk(i_source) // Plot -plot(a, "jerk", color=color.yellow, linewidth=2) +plot(j, "Jerk", color=color.yellow, linewidth=2) diff --git a/lib/numerics/lineartrans/lineartrans.pine b/lib/numerics/lineartrans/lineartrans.pine index 8bf9ee39..c1b840e7 100644 --- a/lib/numerics/lineartrans/lineartrans.pine +++ b/lib/numerics/lineartrans/lineartrans.pine @@ -1,46 +1,29 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Linear Transformation (LINEAR)", "Lineartrans", overlay=false) - -//@function Applies a linear transformation (y = a*(x - sma) + sma + b) relative to the source's SMA, calculated internally. -//@param source series float The input series to transform. -//@param period simple int The lookback period for the internal SMA calculation. -//@param a float The scaling factor (slope). -//@param b float The offset (intercept). -//@returns series float The linearly transformed series relative to its internally calculated SMA. -//@optimized for performance and dirty data -linear(series float source, float a, float b) => - if na(source) or na(a) or na(b) - runtime.error("Parameters 'source', 'a', 'b' cannot be na and 'period' must be > 0.") - var int p = 200 - var array buffer = array.new_float(p, na) - var int head = 0 - var float sum = 0.0 - var int valid_count = 0 - float oldest = array.get(buffer, head) - if not na(oldest) - sum -= oldest - valid_count -= 1 - if not na(source) - sum += source - valid_count += 1 - array.set(buffer, head, source) - head := (head + 1) % p - smaValue = nz(sum / valid_count, source) - a * (source - smaValue) + smaValue + b +indicator("Linear Scaling Transformer (LINEARTRANS)", "LINEARTRANS", overlay=true, precision=8) +//@function Applies a simple affine (linear) transformation: y = slope * x + intercept +//@param src Source series to transform +//@param a Slope (scaling factor). Default 1.0. +//@param b Intercept (offset). Default 0.0. +//@returns Linearly transformed value: a * src + b +//@optimized Single FMA operation per bar — O(1) with zero allocations. +lineartrans(series float src, float a, float b) => + if na(src) + na + else + a * src + b // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") -i_smaPeriod = input.int(200, "SMA Period", minval=1) -i_a = input.float(2.0, "Scale (a)") -i_b = input.float(20.0, "Offset (b)") +i_source = input.source(close, "Source") +i_slope = input.float(1.0, "Slope (a)") +i_intercept = input.float(0.0, "Intercept (b)") // Calculation -transformedSource = linear(i_source, i_a, i_b) +result = lineartrans(i_source, i_slope, i_intercept) // Plot -plot(transformedSource, "Linear Transformation", color=color.yellow, linewidth=2) +plot(result, "Lineartrans", color=color.yellow, linewidth=2) diff --git a/lib/numerics/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..b300dd83 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) @@ -9,8 +9,8 @@ indicator("Logarithmic Transformation (LOG)", "Logtrans", overlay=false) //@optimized for performance and dirty data logT(series float source) => if na(source) - runtime.error("Parameter 'source' cannot be na.") - if source <= 0 + na + else if source <= 0 na else math.log(source) @@ -18,10 +18,10 @@ logT(series float source) => // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") +i_source = input.source(close, "Source") // Calculation transformedSource = logT(i_source) // Plot -plot(transformedSource, "Log Transformation", color=color.green, color=color.yellow, linewidth=2) +plot(transformedSource, "Logtrans", color=color.yellow, linewidth=2) diff --git a/lib/numerics/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..b93dbb5e 100644 --- a/lib/numerics/slope/slope.pine +++ b/lib/numerics/slope/slope.pine @@ -1,61 +1,26 @@ -// 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) +indicator("First Derivative / Velocity (SLOPE)", "SLOPE", overlay=false, precision=8) -//@function Calculates slope (linear regression) -//@param src Source series to calculate slope from -//@param len Lookback period for calculation -//@returns Slope value properly calculated -slope(series float src, simple int len) => - if len <= 1 - runtime.error("Length must be greater than 1") - var float sumX = 0.0 - var float sumY = 0.0 - var float sumXY = 0.0 - var float sumX2 = 0.0 - var int validCount = 0 - var array x_values = array.new_float(len) - var array y_values = array.new_float(len) - var int head = 0 - var int internal_time_counter = 0 - if internal_time_counter >= len - float oldX = array.get(x_values, head) - float oldY = array.get(y_values, head) - if not na(oldY) - sumX := sumX - oldX - sumY := sumY - oldY - sumXY := sumXY - oldX * oldY - sumX2 := sumX2 - oldX * oldX - validCount := validCount - 1 - float currentX = internal_time_counter - float currentY = src - array.set(x_values, head, currentX) - array.set(y_values, head, currentY) - if not na(currentY) - sumX := sumX + currentX - sumY := sumY + currentY - sumXY := sumXY + currentX * currentY - sumX2 := sumX2 + currentX * currentX - validCount := validCount + 1 - head := (head + 1) % len - internal_time_counter := internal_time_counter + 1 - float calculatedSlope = na - if validCount >= 2 - float n = validCount - float divisor = n * sumX2 - sumX * sumX - if divisor != 0.0 - calculatedSlope := (n * sumXY - sumX * sumY) / divisor - calculatedSlope +//@function Calculates the first finite difference (velocity): Slope = V[t] - V[t-1] +//@param src Source series +//@returns First difference of consecutive values. Returns 0.0 on first bar. +//@optimized Uses direct history access for zero-allocation streaming. +slope(series float src) => + float prev = src[1] + if na(prev) + 0.0 + else + src - prev // ---------- Main loop ---------- // Inputs -i_period = input.int(14, "Period", minval=2) i_source = input.source(close, "Source") // Calculation -s = slope(i_source, i_period) +s = slope(i_source) // Plot plot(s, "Slope", color=color.yellow, linewidth=2) diff --git a/lib/numerics/sqrttrans/sqrttrans.pine b/lib/numerics/sqrttrans/sqrttrans.pine index 4eeaa705..61be281d 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) @@ -9,8 +9,8 @@ indicator("Square Root Transformation (SQRT)", "Sqrttrans", overlay=false) //@optimized for performance and dirty data sqrtT(series float source) => if na(source) - runtime.error("Parameter 'source' cannot be na.") - if source < 0 + na + else if source < 0 na else math.sqrt(source) @@ -18,10 +18,10 @@ sqrtT(series float source) => // ---------- Main loop ---------- // Inputs -i_source = input(close, "Source") +i_source = input.source(close, "Source") // Calculation transformedSource = sqrtT(i_source) // Plot -plot(transformedSource, "Square Root Transformation", color=color.yellow, linewidth=2) +plot(transformedSource, "Sqrttrans", color=color.yellow, linewidth=2) diff --git a/lib/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.cs b/lib/oscillators/ac/Ac.cs index 0cd219d7..07bf68b6 100644 --- a/lib/oscillators/ac/Ac.cs +++ b/lib/oscillators/ac/Ac.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; namespace QuanTAlib; /// -/// AC: Acceleration Oscillator +/// AC: Accelerator Oscillator /// /// /// Bill Williams' Acceleration Oscillator measures the acceleration or deceleration diff --git a/lib/oscillators/ac/Ac.md b/lib/oscillators/ac/Ac.md index 560c108e..34b5033b 100644 --- a/lib/oscillators/ac/Ac.md +++ b/lib/oscillators/ac/Ac.md @@ -1,4 +1,4 @@ -# AC: Acceleration Oscillator +# AC: Accelerator Oscillator > "Momentum tells you which way the wind is blowing. Acceleration tells you whether the wind is picking up." -- Bill Williams, paraphrased diff --git a/lib/oscillators/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..656d196e 100644 --- a/lib/oscillators/bbb/bbb.pine +++ b/lib/oscillators/bbb/bbb.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Bollinger %B", "BBB", overlay=false) +indicator("Bollinger %B (BBB)", "BBB", overlay=false) //@function Calculates Bollinger Bands %B oscillator //@param source Series to calculate %B from diff --git a/lib/oscillators/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..ed03f205 100644 --- a/lib/oscillators/cfo/cfo.pine +++ b/lib/oscillators/cfo/cfo.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Chande Forecast Oscillator", "CFO", overlay=false) +indicator("Chande Forecast Oscillator (CFO)", "CFO", overlay=false) //@function Chande Forecast Oscillator - measures percentage difference between price and forecasted price //@param source Price data to analyze diff --git a/lib/oscillators/coppock/Coppock.md b/lib/oscillators/coppock/Coppock.md index eac6a7b5..2af0a8c5 100644 --- a/lib/oscillators/coppock/Coppock.md +++ b/lib/oscillators/coppock/Coppock.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Oscillator | | **Inputs** | Source (close) | -| **Parameters** | `longRoc` (default DefaultLongRoc), `shortRoc` (default DefaultShortRoc), `wmaPeriod` (default DefaultWmaPeriod) | +| **Parameters** | `longRoc` (default 14), `shortRoc` (default 11), `wmaPeriod` (default 10) | | **Outputs** | Single series (Coppock) | | **Output range** | Varies (see docs) | | **Warmup** | 1 bar | @@ -12,7 +12,7 @@ ### TL;DR - The Coppock Curve is a long-term momentum oscillator that applies a Weighted Moving Average to the sum of two Rate of Change calculations at differ... -- Parameterized by `longroc` (default defaultlongroc), `shortroc` (default defaultshortroc), `wmaperiod` (default defaultwmaperiod). +- Parameterized by `longRoc` (default 14), `shortRoc` (default 11), `wmaPeriod` (default 10). - Output range: Varies (see docs). - Requires 1 bar of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/oscillators/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/fisher04/fisher04.pine b/lib/oscillators/fisher04/fisher04.pine index 6ae96387..9fe6cd30 100644 --- a/lib/oscillators/fisher04/fisher04.pine +++ b/lib/oscillators/fisher04/fisher04.pine @@ -1,3 +1,6 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 // Fisher04: Ehlers Fisher Transform (2004 Cybernetic Analysis) // Source: John Ehlers, "Cybernetic Analysis for Stocks and Futures", Wiley, 2004, Chapter 1 // @@ -7,9 +10,7 @@ // Clamp threshold: 0.9999 vs 0.99→0.999 // Fisher multiplier: 0.25 vs 0.5 // Fisher IIR: 0.5 (same) - -//@version=6 -indicator("Fisher04 - Ehlers 2004 Cybernetic Analysis", shorttitle="Fisher04", overlay=false) +indicator("Ehlers Fisher Transform 2004 (FISHER04)", "FISHER04", overlay=false) length = input.int(10, "Length", minval=1) diff --git a/lib/oscillators/gator/gator.pine b/lib/oscillators/gator/gator.pine index 0b5d3ac6..002168ce 100644 --- a/lib/oscillators/gator/gator.pine +++ b/lib/oscillators/gator/gator.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Williams Gator Oscillator", "GATOR", overlay=false) +indicator("Williams Gator Oscillator (GATOR)", "GATOR", overlay=false) //@function Calculates Williams Gator Oscillator from Alligator lines //@param source Series to calculate from diff --git a/lib/oscillators/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..c4276e09 100644 --- a/lib/oscillators/inertia/inertia.pine +++ b/lib/oscillators/inertia/inertia.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Inertia", "INERTIA", overlay=false) +indicator("Inertia Oscillator (INERTIA)", "INERTIA", overlay=false) //@function Calculates Inertia oscillator measuring trend strength based on distance from linear regression //@param source Source series to calculate Inertia for diff --git a/lib/oscillators/kdj/kdj.pine b/lib/oscillators/kdj/kdj.pine index 0afc07a6..e8f73c1c 100644 --- a/lib/oscillators/kdj/kdj.pine +++ b/lib/oscillators/kdj/kdj.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("KDJ", "KDJ", overlay=false) +indicator("KDJ Oscillator (KDJ)", "KDJ", overlay=false) //@function Calculates KDJ (K, D, J) lines - enhanced Stochastic Oscillator //@param high Series of high prices diff --git a/lib/oscillators/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.md b/lib/oscillators/kst/Kst.md index 5a406f04..d1e4c00c 100644 --- a/lib/oscillators/kst/Kst.md +++ b/lib/oscillators/kst/Kst.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Oscillator | | **Inputs** | Source (close) | -| **Parameters** | `r1` (default DefaultR1), `r2` (default DefaultR2), `r3` (default DefaultR3), `r4` (default DefaultR4), `s1` (default DefaultS1), `s2` (default DefaultS2), `s3` (default DefaultS3), `s4` (default DefaultS4), `sigPeriod` (default DefaultSigPeriod) | +| **Parameters** | `r1` (default 10), `r2` (default 15), `r3` (default 20), `r4` (default 30), `s1` (default 10), `s2` (default 10), `s3` (default 10), `s4` (default 15), `sigPeriod` (default 9) | | **Outputs** | Multiple series (KstValue, Signal) | | **Output range** | Varies (see docs) | | **Warmup** | `Math.Max(Math.Max(r1, r2), Math.Max(r3, r4)) @@ -14,7 +14,7 @@ ### TL;DR - The Know Sure Thing is a multi-timeframe momentum oscillator that computes four Rate of Change values at progressively longer lookback periods, smo... -- Parameterized by `r1` (default defaultr1), `r2` (default defaultr2), `r3` (default defaultr3), `r4` (default defaultr4), `s1` (default defaults1), `s2` (default defaults2), `s3` (default defaults3), `s4` (default defaults4), `sigperiod` (default defaultsigperiod). +- Parameterized by `r1` (default 10), `r2` (default 15), `r3` (default 20), `r4` (default 30), `s1` (default 10), `s2` (default 10), `s3` (default 10), `s4` (default 15), `sigPeriod` (default 9). - Output range: Varies (see docs). - Requires `Math.Max(Math.Max(r1, r2), Math.Max(r3, r4)) + Math.Max(Math.Max(s1, s2), Math.Max(s3, s4)) diff --git a/lib/oscillators/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/lrsi/lrsi.pine b/lib/oscillators/lrsi/lrsi.pine index cfebd220..8f290f93 100644 --- a/lib/oscillators/lrsi/lrsi.pine +++ b/lib/oscillators/lrsi/lrsi.pine @@ -1,11 +1,12 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj //@version=6 // LRSI: Laguerre RSI // John Ehlers, "Cybernetic Analysis for Stocks and Futures" (2004) // A modified RSI that uses a 4-element Laguerre filter as its core moving average. // The gamma parameter controls the damping of the filter stages, trading // responsiveness against smoothness. Output is dimensionless [0, 1]. - -indicator("LRSI: Laguerre RSI", shorttitle="LRSI", overlay=false) +indicator("Laguerre RSI (LRSI)", "LRSI", overlay=false) gamma = input.float(0.5, "Gamma", minval=0.0, maxval=1.0, step=0.01, tooltip="Damping factor [0,1]. Lower = more responsive; higher = smoother.") diff --git a/lib/oscillators/marketfi/marketfi.pine b/lib/oscillators/marketfi/marketfi.pine index 03e86a55..b6b79f33 100644 --- a/lib/oscillators/marketfi/marketfi.pine +++ b/lib/oscillators/marketfi/marketfi.pine @@ -1,5 +1,7 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj //@version=6 -indicator("MARKETFI: Market Facilitation Index", shorttitle="MARKETFI", overlay=false) +indicator("Market Facilitation Index (MARKETFI)", "MARKETFI", overlay=false) // Bill Williams' Market Facilitation Index (BW MFI) // Measures price movement efficiency per unit of volume. diff --git a/lib/oscillators/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..058d1847 100644 --- a/lib/oscillators/pgo/pgo.pine +++ b/lib/oscillators/pgo/pgo.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Pretty Good Oscillator", "PGO", overlay=false) +indicator("Pretty Good Oscillator (PGO)", "PGO", overlay=false) //@function Calculate Pretty Good Oscillator (PGO) //@param source Price data to analyze diff --git a/lib/oscillators/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.md b/lib/oscillators/qqe/Qqe.md index 0cc3b84d..4696397a 100644 --- a/lib/oscillators/qqe/Qqe.md +++ b/lib/oscillators/qqe/Qqe.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Oscillator | | **Inputs** | Source (close) | -| **Parameters** | `rsiPeriod` (default DefaultRsiPeriod), `smoothFactor` (default DefaultSmoothFactor), `qqeFactor` (default DefaultQqeFactor) | +| **Parameters** | `rsiPeriod` (default 14), `smoothFactor` (default 5), `qqeFactor` (default 4.236) | | **Outputs** | Single series (Qqe) | | **Output range** | Varies (see docs) | | **Warmup** | `rsiPeriod + smoothFactor + darPeriod * 2` bars | @@ -12,7 +12,7 @@ ### TL;DR - Quantitative Qualitative Estimation applies a multi-stage smoothing pipeline to RSI and then constructs dynamic volatility-based trailing bands aro... -- Parameterized by `rsiperiod` (default defaultrsiperiod), `smoothfactor` (default defaultsmoothfactor), `qqefactor` (default defaultqqefactor). +- Parameterized by `rsiPeriod` (default 14), `smoothFactor` (default 5), `qqeFactor` (default 4.236). - Output range: Varies (see docs). - Requires `rsiPeriod + smoothFactor + darPeriod * 2` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/oscillators/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/reflex/reflex.pine b/lib/oscillators/reflex/reflex.pine index 78467943..09683cc1 100644 --- a/lib/oscillators/reflex/reflex.pine +++ b/lib/oscillators/reflex/reflex.pine @@ -1,7 +1,5 @@ -// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 -// https://mozilla.org/MPL/2.0/ -// © QuanTAlib - +// Licensed under the Apache License, Version 2.0 +// © mihakralj //@version=6 indicator("Ehlers Reflex Indicator (REFLEX)", "REFLEX", overlay = false) diff --git a/lib/oscillators/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..a64735e0 100644 --- a/lib/oscillators/stoch/stoch.pine +++ b/lib/oscillators/stoch/stoch.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Stochastic Oscillator (STOCH)", "Stoch", overlay=false) +indicator("Stochastic Oscillator (STOCH)", "STOCH", overlay=false) //@function Calculates the Stochastic Oscillator (%K and %D). %K = 100 * (close - lowest_low(kLength)) / (highest_high(kLength) - lowest_low(kLength)). %D = SMA(%K, dPeriod). Uses efficient deque implementation for min/max and buffer-based SMA. //@param kLength `simple int` The lookback period for calculating highest high and lowest low. diff --git a/lib/oscillators/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..d2fcae36 100644 --- a/lib/oscillators/stochrsi/stochrsi.pine +++ b/lib/oscillators/stochrsi/stochrsi.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Stochastic RSI (STOCHRSI)", "StochRSI", overlay=false) +indicator("Stochastic RSI (STOCHRSI)", "STOCHRSI", overlay=false) //@function Calculates Stochastic RSI oscillator //@param source Source series to calculate STOCHRSI for diff --git a/lib/oscillators/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..4c0d0ca4 100644 --- a/lib/oscillators/trendflex/trendflex.pine +++ b/lib/oscillators/trendflex/trendflex.pine @@ -1,8 +1,8 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 // Indicator algorithm (C) 2013 John F. Ehlers -indicator(" Ehlers Trendflex Indicator (TRENDFLEX)", "TRENDFLEX", overlay=false) +indicator("Ehlers Trendflex (TRENDFLEX)", "TRENDFLEX", overlay=false) //@function Calculates Ehlers Trendflex using SuperSmoother pre-filtering and cumulative slope with RMS normalization //@param source Series to calculate Trendflex from diff --git a/lib/oscillators/trix/trix.pine b/lib/oscillators/trix/trix.pine index 0ce61d3c..f28e47b5 100644 --- a/lib/oscillators/trix/trix.pine +++ b/lib/oscillators/trix/trix.pine @@ -1,7 +1,7 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("TRIX", "TRIX", overlay=false) +indicator("Triple Exponential Average (TRIX)", "TRIX", overlay=false) //@function Calculates TRIX oscillator with compensation //@param source Series to calculate TRIX from diff --git a/lib/oscillators/ttm_wave/ttm_wave.pine b/lib/oscillators/ttm_wave/ttm_wave.pine new file mode 100644 index 00000000..4b0e6307 --- /dev/null +++ b/lib/oscillators/ttm_wave/ttm_wave.pine @@ -0,0 +1,67 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("TTM Wave (TTM_WAVE)", "TTM_WAVE", overlay=false) + +//@function EMA — standard exponential moving average for MACD computation. +//@param src Source series +//@param length EMA period +//@returns EMA value +ema_calc(series float src, simple int length) => + float alpha = 2.0 / (length + 1) + var float result = 0.0 + result := bar_index == 0 ? src : alpha * src + (1 - alpha) * result + result + +//@function MACD histogram — (fast_ema - slow_ema) - signal_ema(fast_ema - slow_ema) +// This is the second derivative of momentum: acceleration of the spread. +//@param src Source series +//@param fast Fast EMA period +//@param slow Slow EMA period +//@param signal Signal EMA period (same as slow for TTM Wave) +//@returns MACD histogram value +macd_hist(series float src, simple int fast, simple int slow, simple int signal) => + float fast_ema = ema_calc(src, fast) + float slow_ema = ema_calc(src, slow) + float macd_line = fast_ema - slow_ema + float signal_line = ema_calc(macd_line, signal) + macd_line - signal_line + +//@function TTM Wave — six parallel MACD histogram channels at Fibonacci periods. +// Wave A (short-term): channels 1 (8,34,34) and 2 (8,55,55) +// Wave B (medium-term): channels 3 (8,89,89) and 4 (8,144,144) +// Wave C (long-term): channels 5 (8,233,233) and 6 (8,377,377) +// All channels share fast period 8. Signal period equals slow period. +//@param src Source series (default: close) +//@returns [waveA1, waveA2, waveB1, waveB2, waveC1, waveC2] +//@reference John Carter, "Mastering the Trade" (2005, 2012) +//@optimized Six independent EMA cascades, O(1) per bar per channel +ttm_wave(series float src) => + float waveA1 = macd_hist(src, 8, 34, 34) + float waveA2 = macd_hist(src, 8, 55, 55) + float waveB1 = macd_hist(src, 8, 89, 89) + float waveB2 = macd_hist(src, 8, 144, 144) + float waveC1 = macd_hist(src, 8, 233, 233) + float waveC2 = macd_hist(src, 8, 377, 377) + [waveA1, waveA2, waveB1, waveB2, waveC1, waveC2] + +// ── Inputs ── +float i_src = input.source(close, "Source") + +// ── Calculation ── +[waveA1, waveA2, waveB1, waveB2, waveC1, waveC2] = ttm_wave(i_src) + +// ── Plot (thinkorswim color convention) ── +// Wave A: yellow/green (short-term momentum) +plot(waveA1, "Wave A1", color=color.yellow, style=plot.style_histogram, linewidth=2) +plot(waveA2, "Wave A2", color=color.lime, style=plot.style_histogram, linewidth=2) + +// Wave B: magenta/pink (medium-term momentum) +plot(waveB1, "Wave B1", color=color.fuchsia, style=plot.style_histogram, linewidth=2) +plot(waveB2, "Wave B2", color=color.new(color.fuchsia, 40), style=plot.style_histogram, linewidth=2) + +// Wave C: red/orange (long-term momentum) +plot(waveC1, "Wave C1", color=color.red, style=plot.style_histogram, linewidth=2) +plot(waveC2, "Wave C2", color=color.orange, style=plot.style_histogram, linewidth=2) + +hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted) diff --git a/lib/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/chandelier/Chandelier.md b/lib/reversals/chandelier/Chandelier.md index 813c14c5..f7497dad 100644 --- a/lib/reversals/chandelier/Chandelier.md +++ b/lib/reversals/chandelier/Chandelier.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `period` (default DefaultPeriod), `multiplier` (default DefaultMultiplier) | +| **Parameters** | `period` (default 22), `multiplier` (default 3.0) | | **Outputs** | Single series (Chandelier) | | **Output range** | Varies (see docs) | | **Warmup** | `period + 1` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Chandelier Exit computes ATR-based trailing stop levels that hang from the highest high (for longs) or rise from the lowest low (for shorts) ov... -- Parameterized by `period` (default defaultperiod), `multiplier` (default defaultmultiplier). +- Parameterized by `period` (default 22), `multiplier` (default 3.0). - Output range: Varies (see docs). - Requires `period + 1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/reversals/chandelier/chandelier.pine b/lib/reversals/chandelier/chandelier.pine new file mode 100644 index 00000000..c5c411ac --- /dev/null +++ b/lib/reversals/chandelier/chandelier.pine @@ -0,0 +1,45 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Chandelier Exit (CHANDELIER)", "CHANDELIER", overlay=true) + +//@function Chandelier Exit — ATR-based trailing stops that hang from the highest high +// (ExitLong) or rise from the lowest low (ExitShort) over a lookback period. +// Uses SMA-seeded Wilder's RMA for ATR, matching Skender/TradingView convention. +//@param period Lookback for ATR and rolling HH/LL (default 22) +//@param multiplier ATR scaling factor (default 3.0) +//@returns [exitLong, exitShort] — two overlay stop levels +//@reference Charles Le Beau; Alexander Elder, "Come Into My Trading Room" (2002) +//@optimized O(1) per bar using ta.rma, ta.highest, ta.lowest +chandelier(simple int period = 22, simple float multiplier = 3.0) => + if period < 1 + runtime.error("Period must be >= 1") + if multiplier <= 0 + runtime.error("Multiplier must be > 0") + + // True Range + float tr = na(close[1]) ? high - low : math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) + + // Wilder's ATR (RMA = EMA with alpha = 1/period, SMA-seeded) + float atr = ta.rma(tr, period) + + // Rolling extremes over the lookback window + float hh = ta.highest(high, period) + float ll = ta.lowest(low, period) + + // Chandelier exits + float exit_long = hh - multiplier * atr + float exit_short = ll + multiplier * atr + + [exit_long, exit_short] + +// ── Inputs ── +int i_period = input.int(22, "Period", minval=1) +float i_multiplier = input.float(3.0, "Multiplier", minval=0.01, step=0.1) + +// ── Calculation ── +[exit_long, exit_short] = chandelier(i_period, i_multiplier) + +// ── Plot ── +plot(exit_long, "Exit Long", color=color.green, linewidth=2) +plot(exit_short, "Exit Short", color=color.red, linewidth=2) diff --git a/lib/reversals/ckstop/Ckstop.md b/lib/reversals/ckstop/Ckstop.md index d428d67e..7a078b29 100644 --- a/lib/reversals/ckstop/Ckstop.md +++ b/lib/reversals/ckstop/Ckstop.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `atrPeriod` (default DefaultAtrPeriod), `multiplier` (default DefaultMultiplier), `stopPeriod` (default DefaultStopPeriod) | +| **Parameters** | `atrPeriod` (default 10), `multiplier` (default 1.0), `stopPeriod` (default 9) | | **Outputs** | Single series (Ckstop) | | **Output range** | Varies (see docs) | | **Warmup** | `atrPeriod + stopPeriod` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Chande Kroll Stop computes adaptive trailing stop levels using ATR-smoothed volatility envelopes around rolling extremes. -- Parameterized by `atrperiod` (default defaultatrperiod), `multiplier` (default defaultmultiplier), `stopperiod` (default defaultstopperiod). +- Parameterized by `atrPeriod` (default 10), `multiplier` (default 1.0), `stopPeriod` (default 9). - Output range: Varies (see docs). - Requires `atrPeriod + stopPeriod` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/reversals/ckstop/ckstop.pine b/lib/reversals/ckstop/ckstop.pine new file mode 100644 index 00000000..219f9919 --- /dev/null +++ b/lib/reversals/ckstop/ckstop.pine @@ -0,0 +1,52 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Chande Kroll Stop (CKSTOP)", "CKSTOP", overlay=true) + +//@function Chande Kroll Stop — adaptive trailing stop using ATR-smoothed volatility +// envelopes around rolling extremes, then smoothed through a second window. +// Stage 1: first_high_stop = HH(p) - m*ATR, first_low_stop = LL(p) + m*ATR +// Stage 2: StopShort = highest(first_high_stop, x), StopLong = lowest(first_low_stop, x) +//@param atr_period ATR and first-stop extreme lookback (default 10) +//@param multiplier ATR scaling factor (default 1.0) +//@param stop_period Second smoothing window (default 9) +//@returns [stop_long, stop_short] — two overlay stop levels +//@reference Chande & Kroll, "The New Technical Trader" (1994) +//@optimized O(1) per bar using ta.rma, ta.highest, ta.lowest +ckstop(simple int atr_period = 10, simple float multiplier = 1.0, simple int stop_period = 9) => + if atr_period < 1 + runtime.error("ATR period must be >= 1") + if multiplier <= 0 + runtime.error("Multiplier must be > 0") + if stop_period < 1 + runtime.error("Stop period must be >= 1") + + // True Range + float tr = na(close[1]) ? high - low : math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1]))) + + // ATR via Wilder's RMA + float atr = ta.rma(tr, atr_period) + + // Stage 1: First stops (volatility envelope) + float hh = ta.highest(high, atr_period) + float ll = ta.lowest(low, atr_period) + float first_high_stop = hh - multiplier * atr + float first_low_stop = ll + multiplier * atr + + // Stage 2: Smoothed stops over stop_period + float stop_short = ta.highest(first_high_stop, stop_period) + float stop_long = ta.lowest(first_low_stop, stop_period) + + [stop_long, stop_short] + +// ── Inputs ── +int i_atr_period = input.int(10, "ATR Period", minval=1) +float i_multiplier = input.float(1.0, "Multiplier", minval=0.01, step=0.1) +int i_stop_period = input.int(9, "Stop Period", minval=1) + +// ── Calculation ── +[stop_long, stop_short] = ckstop(i_atr_period, i_multiplier, i_stop_period) + +// ── Plot ── +plot(stop_long, "Stop Long", color=color.green, linewidth=2) +plot(stop_short, "Stop Short", color=color.red, linewidth=2) diff --git a/lib/reversals/fractals/Fractals.cs b/lib/reversals/fractals/Fractals.cs index cf9b365b..5c281d4c 100644 --- a/lib/reversals/fractals/Fractals.cs +++ b/lib/reversals/fractals/Fractals.cs @@ -1,6 +1,6 @@ // FRACTALS: Williams Fractals // Five-bar pattern identifying local highs (up fractals) and local lows (down fractals). -// Created by Larry Williams (1995, "Trading Chaos"). +// Created by Bill Williams (1995, "Trading Chaos"). using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/lib/reversals/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.md b/lib/reversals/psar/Psar.md index c401bc82..c16a17db 100644 --- a/lib/reversals/psar/Psar.md +++ b/lib/reversals/psar/Psar.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `afStart` (default DefaultAfStart), `afIncrement` (default DefaultAfIncrement), `afMax` (default DefaultAfMax) | +| **Parameters** | `afStart` (default 0.02), `afIncrement` (default 0.02), `afMax` (default 0.20) | | **Outputs** | Single series (Psar) | | **Output range** | Varies (see docs) | | **Warmup** | `1` bars | @@ -12,7 +12,7 @@ ### TL;DR - The Parabolic Stop And Reverse (PSAR) is a trend-following overlay indicator created by J. -- Parameterized by `afstart` (default defaultafstart), `afincrement` (default defaultafincrement), `afmax` (default defaultafmax). +- Parameterized by `afStart` (default 0.02), `afIncrement` (default 0.02), `afMax` (default 0.20). - Output range: Varies (see docs). - Requires `1` bars of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/reversals/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/sarext/sarext.pine b/lib/reversals/sarext/sarext.pine new file mode 100644 index 00000000..61c06366 --- /dev/null +++ b/lib/reversals/sarext/sarext.pine @@ -0,0 +1,124 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Parabolic SAR Extended (SAREXT)", "SAREXT", overlay=true) + +//@function Parabolic SAR Extended — asymmetric PSAR with separate long/short AF parameters, +// auto-detect or forced initial direction, and an offset applied on reversal. +// Output is sign-encoded: positive SAR = long mode, negative SAR = short mode. +//@param start_value Initial direction: >0 force long, <0 force short, 0 auto-detect from DM +//@param offset_on_reverse Gap added to SAR on reversal (default 0) +//@param af_init_long Initial AF for long positions (default 0.02) +//@param af_long AF increment per new EP in long (default 0.02) +//@param af_max_long Maximum AF for long (default 0.20) +//@param af_init_short Initial AF for short positions (default 0.02) +//@param af_short AF increment per new EP in short (default 0.02) +//@param af_max_short Maximum AF for short (default 0.20) +//@returns Sign-encoded SAR value (positive = long, negative = short) +//@reference J. Welles Wilder Jr., "New Concepts in Technical Trading Systems" (1978) +//@optimized O(1) per bar, state-machine with asymmetric acceleration factors +sarext(simple float start_value = 0.0, simple float offset_on_reverse = 0.0, + simple float af_init_long = 0.02, simple float af_long = 0.02, simple float af_max_long = 0.20, + simple float af_init_short = 0.02, simple float af_short = 0.02, simple float af_max_short = 0.20) => + if af_init_long <= 0 or af_long <= 0 or af_max_long <= af_init_long + runtime.error("Long AF parameters invalid") + if af_init_short <= 0 or af_short <= 0 or af_max_short <= af_init_short + runtime.error("Short AF parameters invalid") + if offset_on_reverse < 0 + runtime.error("Offset must be >= 0") + + var bool is_long = true + var float sar = low + var float ep = high + var float af = af_init_long + + if bar_index == 0 + // Bar 0: collect first bar data + is_long := true + sar := high + ep := low + af := af_init_short + float(na) + else if bar_index == 1 + // Bar 1: determine initial direction + if start_value > 0 + is_long := true + sar := math.min(low[1], low) + ep := math.max(high[1], high) + af := af_init_long + else if start_value < 0 + is_long := false + sar := math.max(high[1], high) + ep := math.min(low[1], low) + af := af_init_short + else + // Auto-detect from DM + float plus_dm = high - high[1] + float minus_dm = low[1] - low + if plus_dm > minus_dm and plus_dm > 0 + is_long := true + sar := math.min(low[1], low) + ep := math.max(high[1], high) + af := af_init_long + else + is_long := false + sar := math.max(high[1], high) + ep := math.min(low[1], low) + af := af_init_short + is_long ? sar : -sar + else + // Bar 2+: standard SAR state machine with asymmetric AF + float new_sar = sar + af * (ep - sar) + if is_long + new_sar := math.min(new_sar, low[1]) + if bar_index > 1 + new_sar := math.min(new_sar, low[2]) + if low <= new_sar + // Reverse to short + is_long := false + new_sar := ep + offset_on_reverse + ep := low + af := af_init_short + else + if high > ep + ep := high + af := math.min(af + af_long, af_max_long) + else + new_sar := math.max(new_sar, high[1]) + if bar_index > 1 + new_sar := math.max(new_sar, high[2]) + if high >= new_sar + // Reverse to long + is_long := true + new_sar := ep - offset_on_reverse + ep := high + af := af_init_long + else + if low < ep + ep := low + af := math.min(af + af_short, af_max_short) + sar := new_sar + is_long ? sar : -sar + +// ── Inputs ── +float i_start = input.float(0.0, "Start Value (0=auto)") +float i_offset = input.float(0.0, "Offset on Reverse", minval=0) +float i_af_init_long = input.float(0.02, "AF Init Long", minval=0.001, step=0.001) +float i_af_long = input.float(0.02, "AF Long", minval=0.001, step=0.001) +float i_af_max_long = input.float(0.20, "AF Max Long", minval=0.01, step=0.01) +float i_af_init_short = input.float(0.02, "AF Init Short", minval=0.001, step=0.001) +float i_af_short = input.float(0.02, "AF Short", minval=0.001, step=0.001) +float i_af_max_short = input.float(0.20, "AF Max Short", minval=0.01, step=0.01) + +// ── Calculation ── +float result = sarext(i_start, i_offset, i_af_init_long, i_af_long, i_af_max_long, + i_af_init_short, i_af_short, i_af_max_short) + +float sar_val = math.abs(nz(result)) +bool is_long = nz(result) > 0 +float sar_above = not is_long ? sar_val : na +float sar_below = is_long ? sar_val : na + +// ── Plot ── +plot(sar_above, "SAREXT Above", color=color.red, style=plot.style_linebr, linewidth=2) +plot(sar_below, "SAREXT Below", color=color.green, style=plot.style_linebr, linewidth=2) diff --git a/lib/reversals/swings/Swings.md b/lib/reversals/swings/Swings.md index dba38ea7..4656793c 100644 --- a/lib/reversals/swings/Swings.md +++ b/lib/reversals/swings/Swings.md @@ -4,7 +4,7 @@ | ---------------- | -------------------------------- | | **Category** | Reversal | | **Inputs** | OHLCV bar (TBar) | -| **Parameters** | `lookback` (default DefaultLookback) | +| **Parameters** | `lookback` (default 5) | | **Outputs** | Single series (Swings) | | **Output range** | Varies (see docs) | | **Warmup** | 1 bar | @@ -12,7 +12,7 @@ ### TL;DR - Swing High/Low detection identifies local price extremes using a configurable lookback window. -- Parameterized by `lookback` (default defaultlookback). +- Parameterized by `lookback` (default 5). - Output range: Varies (see docs). - Requires 1 bar of warmup before first valid output (IsHot = true). - Validated against TA-Lib, Skender, and Tulip reference implementations where available. diff --git a/lib/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/acf/acf.pine b/lib/statistics/acf/acf.pine new file mode 100644 index 00000000..c9fbac2c --- /dev/null +++ b/lib/statistics/acf/acf.pine @@ -0,0 +1,87 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Autocorrelation Function (ACF)", "ACF", overlay=false) + +//@function Calculates autocorrelation at a specified lag using a circular buffer +//@param src series float Input data series +//@param len simple int Lookback period for calculation +//@param lag simple int Lag order for autocorrelation (default 1) +//@returns float Autocorrelation coefficient between -1 and 1 +//@optimized for performance using running sums and biased estimator (divide by n) +acf(series float src, simple int len, simple int lag) => + if len <= 0 + runtime.error("Period must be greater than 0") + if lag < 1 + runtime.error("Lag must be at least 1") + if len <= lag + 1 + runtime.error("Period must be greater than lag + 1") + var int p = math.max(1, len) + var array buffer = array.new_float(p, na) + var int head = 0, var int count = 0 + float oldest = array.get(buffer, head) + if not na(oldest) + count -= 1 + if not na(src) + array.set(buffer, head, src) + count += 1 + else + array.set(buffer, head, na) + head := (head + 1) % p + if count <= lag + na + else + // Calculate mean + float sum = 0.0 + int validN = 0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + sum += val + validN += 1 + if validN <= lag + na + else + float mean = sum / validN + + // Calculate variance (population): Σ(x - mean)² / n + float variance = 0.0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + float diff = val - mean + variance += diff * diff + variance /= validN + + if variance <= 0 + 0.0 + else + // Calculate autocovariance at lag k (biased: divide by n) + // Need to iterate in temporal order through the circular buffer + int startIdx = count < p ? 0 : head + float autocovariance = 0.0 + for t = lag to count - 1 + int currentIdx = (startIdx + t) % p + int laggedIdx = (startIdx + t - lag) % p + float xt = array.get(buffer, currentIdx) + float xtk = array.get(buffer, laggedIdx) + if not na(xt) and not na(xtk) + autocovariance += (xt - mean) * (xtk - mean) + autocovariance /= validN + + // ACF = γ_k / γ_0, clamped to [-1, 1] + float result = autocovariance / variance + math.max(-1.0, math.min(1.0, result)) + +// ---------- Main loop ---------- + +// Inputs +i_source = input.source(close, "Source") +i_period = input.int(20, "Period", minval=3) +i_lag = input.int(1, "Lag", minval=1) + +// Calculation +acf_value = acf(i_source, i_period, i_lag) + +// Plot +plot(acf_value, "ACF", color=color.yellow, linewidth=2) diff --git a/lib/statistics/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/cma/cma_signature.svg b/lib/statistics/cma/cma_signature.svg new file mode 100644 index 00000000..933c918b --- /dev/null +++ b/lib/statistics/cma/cma_signature.svg @@ -0,0 +1,19638 @@ + + + + + + + + 2026-03-10T19:42:36.505935 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/pacf/pacf.pine b/lib/statistics/pacf/pacf.pine new file mode 100644 index 00000000..fbffe7f8 --- /dev/null +++ b/lib/statistics/pacf/pacf.pine @@ -0,0 +1,132 @@ +// Licensed under the Apache License, Version 2.0 +// © mihakralj +//@version=6 +indicator("Partial Autocorrelation Function (PACF)", "PACF", overlay=false) + +//@function Calculates partial autocorrelation at a specified lag using Durbin-Levinson recursion +//@param src series float Input data series +//@param len simple int Lookback period for calculation +//@param lag simple int Lag order for partial autocorrelation (default 1) +//@returns float Partial autocorrelation coefficient between -1 and 1 +//@optimized using Durbin-Levinson recursion to remove shorter-lag effects +pacf(series float src, simple int len, simple int lag) => + if len <= 0 + runtime.error("Period must be greater than 0") + if lag < 1 + runtime.error("Lag must be at least 1") + if len <= lag + 1 + runtime.error("Period must be greater than lag + 1") + var int p = math.max(1, len) + var array buffer = array.new_float(p, na) + var int head = 0, var int count = 0 + float oldest = array.get(buffer, head) + if not na(oldest) + count -= 1 + if not na(src) + array.set(buffer, head, src) + count += 1 + else + array.set(buffer, head, na) + head := (head + 1) % p + if count <= lag + na + else + // Calculate mean + float sum = 0.0 + int validN = 0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + sum += val + validN += 1 + if validN <= lag + na + else + float mean = sum / validN + + // Calculate variance (population): Σ(x - mean)² / n + float variance = 0.0 + for i = 0 to p - 1 + float val = array.get(buffer, i) + if not na(val) + float diff = val - mean + variance += diff * diff + variance /= validN + + if variance <= 0 + 0.0 + else + // Calculate ACF for lags 0 to target lag + int startIdx = count < p ? 0 : head + array acfValues = array.new_float(lag + 1, 0.0) + array.set(acfValues, 0, 1.0) + + for k = 1 to lag + float autocovariance = 0.0 + for t = k to count - 1 + int currentIdx = (startIdx + t) % p + int laggedIdx = (startIdx + t - k) % p + float xt = array.get(buffer, currentIdx) + float xtk = array.get(buffer, laggedIdx) + if not na(xt) and not na(xtk) + autocovariance += (xt - mean) * (xtk - mean) + autocovariance /= validN + array.set(acfValues, k, autocovariance / variance) + + // Durbin-Levinson recursion + float pacfResult = na + if lag == 1 + // PACF at lag 1 equals ACF at lag 1 + pacfResult := array.get(acfValues, 1) + else + array phi = array.new_float(lag + 1, 0.0) + array phiPrev = array.new_float(lag + 1, 0.0) + + // Initialize: φ_11 = r_1 + array.set(phi, 1, array.get(acfValues, 1)) + + // Iterate for k = 2 to target lag + for k = 2 to lag + // Copy phi to phiPrev + for j = 0 to lag + array.set(phiPrev, j, array.get(phi, j)) + + // numerator = r_k - Σ(φ_{k-1,j} * r_{k-j}) for j=1..k-1 + float numerator = array.get(acfValues, k) + for j = 1 to k - 1 + numerator -= array.get(phiPrev, j) * array.get(acfValues, k - j) + + // denominator = 1 - Σ(φ_{k-1,j} * r_j) for j=1..k-1 + float denominator = 1.0 + for j = 1 to k - 1 + denominator -= array.get(phiPrev, j) * array.get(acfValues, j) + + if math.abs(denominator) < 1e-15 + pacfResult := 0.0 + break + + // φ_kk = numerator / denominator + array.set(phi, k, numerator / denominator) + + // Update: φ_kj = φ_{k-1,j} - φ_kk * φ_{k-1,k-j} + for j = 1 to k - 1 + array.set(phi, j, array.get(phiPrev, j) - array.get(phi, k) * array.get(phiPrev, k - j)) + + if na(pacfResult) + pacfResult := array.get(phi, lag) + + // Clamp to [-1, 1] + math.max(-1.0, math.min(1.0, pacfResult)) + +// ---------- Main loop ---------- + +// Inputs +i_source = input.source(close, "Source") +i_period = input.int(20, "Period", minval=3) +i_lag = input.int(1, "Lag", minval=1) + +// Calculation +pacf_value = pacf(i_source, i_period, i_lag) + +// Plot +plot(pacf_value, "PACF", color=color.yellow, linewidth=2) diff --git a/lib/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/crma/crma_signature.svg b/lib/trends_FIR/crma/crma_signature.svg index 26f5f698..c621d6b1 100644 --- a/lib/trends_FIR/crma/crma_signature.svg +++ b/lib/trends_FIR/crma/crma_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:12:10.065942 + 2026-03-10T19:44:02.888020 image/svg+xml @@ -30,28 +30,313 @@ z - + + + + + + + + + - - + - - + + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - + + + + + - + - + - - - - + @@ -300,126 +812,449 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + - - - - - - @@ -627,128 +1371,587 @@ z - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - + - + @@ -756,14 +1959,19 @@ z - + + + + - + - + - + @@ -771,14 +1979,19 @@ z - + + + + - + - + - + @@ -786,14 +1999,19 @@ z - + + + + - + - + - + @@ -801,14 +2019,19 @@ z - + + + + - + - + - + @@ -816,44 +2039,489 @@ z - + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + + - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - - - - + + + + + + + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - + - - + + + + + - + - - + + - + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + - + + + + - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3386,233 +13506,1131 @@ L 1072.948078 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3623,698 +14641,3538 @@ L 1433.247957 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/hend/hend_signature.svg b/lib/trends_FIR/hend/hend_signature.svg index 73f6d5db..291f0ece 100644 --- a/lib/trends_FIR/hend/hend_signature.svg +++ b/lib/trends_FIR/hend/hend_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:12:17.472046 + 2026-03-10T19:45:56.795914 image/svg+xml @@ -30,28 +30,313 @@ z - + + + + + + + + + - - + - - + + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - + + + + + - + - + - - - - + @@ -300,126 +812,449 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + - - - - - - @@ -627,128 +1371,587 @@ z - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - + - + @@ -756,14 +1959,19 @@ z - + + + + - + - + - + @@ -771,14 +1979,19 @@ z - + + + + - + - + - + @@ -786,14 +1999,19 @@ z - + + + + - + - + - + @@ -801,14 +2019,19 @@ z - + + + + - + - + - + @@ -816,14 +2039,19 @@ z - + + + + - + - + - + @@ -831,29 +2059,449 @@ z + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + + + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3386,233 +13627,1092 @@ L 1072.948078 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3623,698 +14723,3436 @@ L 1433.247957 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.cs b/lib/trends_FIR/nlma/Nlma.cs index 6ad1ad77..3bc776cd 100644 --- a/lib/trends_FIR/nlma/Nlma.cs +++ b/lib/trends_FIR/nlma/Nlma.cs @@ -224,11 +224,12 @@ public sealed class Nlma : AbstractBase // ── Weight computation ───────────────────────────────────────────── /// - /// Computes original Igorad two-phase kernel weights. + /// Computes original Igorad two-phase kernel weights (MQL4 NonLagMA v7.1 order). /// Phase zone (i=0..period-2): t = i/(period-2), g = t≤0.5 ? 1 : 1/(3πt+1), w = g*cos(πt) /// Cycle zone (i=period-1..flen-2): t = 1 + (i-phase+1)*(2*Cycle-1)/(Cycle*period-1), same g/w /// Last tap (i=flen-1): weight = 0. - /// weights[0] = newest bar, weights[flen-1] = oldest bar. + /// weights[0] = newest bar (=1.0), weights[flen-1] = oldest bar (=0.0). + /// Matches MQL4 alfa[] order: alfa[0]*Close[0] (newest) .. alfa[Len-1]*Close[Len-1] (oldest). /// Returns the signed weight sum for normalization. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -238,58 +239,36 @@ public sealed class Nlma : AbstractBase int phase = period - 1; double coeff = 3.0 * Math.PI; - // Compute weights in "oldest-first" order (taps[0]=oldest, taps[flen-1]=newest) - // then reverse into weights[] so weights[0]=newest, weights[flen-1]=oldest - const int StackallocThreshold = 256; - double[]? rented = flen > StackallocThreshold ? ArrayPool.Shared.Rent(flen) : null; - Span taps = flen <= StackallocThreshold - ? stackalloc double[flen] - : rented!.AsSpan(0, flen); - + // Compute weights directly in MQL4 alfa[] order: + // weights[0] = alfa[0] = weight for newest bar (=1.0 at t=0) + // weights[flen-1] = alfa[flen-1] = weight for oldest bar (=0.0) double wsum = 0.0; - try + for (int i = 0; i < flen - 1; i++) { - for (int i = 0; i < flen - 1; i++) + double t; + if (i <= phase - 1) { - double t; - if (i <= phase - 1) - { - // Phase zone: t ramps from 0 to 1 - t = phase > 1 ? (double)i / (phase - 1) : 0.0; - } - else - { - // Cycle zone: t continues from 1 upward - double numer = (double)(i - phase + 1) * (2 * Cycle - 1); - double denom = (double)(Cycle * period - 1); - t = 1.0 + (denom > 0 ? numer / denom : 0.0); - } - - double beta = Math.Cos(Math.PI * t); - double g = t <= 0.5 ? 1.0 : 1.0 / Math.FusedMultiplyAdd(coeff, t, 1.0); - double w = g * beta; - taps[i] = w; - wsum += w; + // Phase zone: t ramps from 0 to 1 + t = phase > 1 ? (double)i / (phase - 1) : 0.0; + } + else + { + // Cycle zone: t continues from 1 upward + double numer = (double)(i - phase + 1) * (2 * Cycle - 1); + double denom = (double)(Cycle * period - 1); + t = 1.0 + (denom > 0 ? numer / denom : 0.0); } - // Last tap has weight 0 (original MQL4 loop goes to Len-2) - taps[flen - 1] = 0.0; - - // Reverse into weights[]: weights[0]=newest=taps[flen-1], weights[flen-1]=oldest=taps[0] - for (int i = 0; i < flen; i++) - { - weights[i] = taps[flen - 1 - i]; - } - } - finally - { - if (rented != null) - { - ArrayPool.Shared.Return(rented); - } + double beta = Math.Cos(Math.PI * t); + double g = t <= 0.5 ? 1.0 : 1.0 / Math.FusedMultiplyAdd(coeff, t, 1.0); + weights[i] = g * beta; + wsum += weights[i]; } + // Last tap has weight 0 (original MQL4 loop goes to Len-2) + weights[flen - 1] = 0.0; + return wsum; } @@ -297,7 +276,8 @@ public sealed class Nlma : AbstractBase /// /// Convolves when buffer is full (count == flen). Uses precomputed weights. - /// weights[0] = newest, weights[flen-1] = oldest. Normalized by signed weight sum. + /// weights[0] = newest bar weight (=1.0), weights[flen-1] = oldest bar weight (=0.0). + /// Normalized by signed weight sum. Matches MQL4: alfa[0]*Close[0] + ... + alfa[Len-1]*Close[Len-1]. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ConvolveFull() @@ -307,7 +287,7 @@ public sealed class Nlma : AbstractBase int capacity = _buffer.Capacity; double sum = 0.0; - // Iterate oldest-to-newest: oldest bar gets weights[flen-1], newest gets weights[0] + // Iterate oldest-to-newest: oldest bar gets weights[flen-1] (≈0), newest gets weights[0] (=1.0) int wi = _flen - 1; for (int i = head; i < capacity; i++) { 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/nyqma/nyqma_signature.svg b/lib/trends_FIR/nyqma/nyqma_signature.svg index dd6eaaf3..6f90abb6 100644 --- a/lib/trends_FIR/nyqma/nyqma_signature.svg +++ b/lib/trends_FIR/nyqma/nyqma_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:12:25.258052 + 2026-03-10T20:02:10.689803 image/svg+xml @@ -30,28 +30,313 @@ z - + + + + + + + + + - - + - - + + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - + + + + + - + - + - - - - + @@ -300,126 +812,449 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + - - - - - - @@ -627,128 +1371,587 @@ z - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - + - + @@ -756,14 +1959,19 @@ z - + + + + - + - + - + @@ -771,14 +1979,19 @@ z - + + + + - + - + - + @@ -786,14 +1999,19 @@ z - + + + + - + - + - + @@ -801,14 +2019,19 @@ z - + + + + - + - + - + @@ -816,14 +2039,19 @@ z - + + + + - + - + - + @@ -831,29 +2059,449 @@ z + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + + + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3386,233 +13627,1092 @@ L 1072.948078 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3623,698 +14723,3436 @@ L 1433.247957 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + @@ -4684,6 +19590,7 @@ L 716 0 L 716 1209 z " transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..38d3e536 100644 --- a/lib/trends_FIR/pwma/pwma.pine +++ b/lib/trends_FIR/pwma/pwma.pine @@ -1,38 +1,26 @@ -// The MIT License (MIT) +// Licensed under the Apache License, Version 2.0 // © mihakralj //@version=6 -indicator("Pascal Weighted Moving Average (PWMA)", "PWMA", overlay=true) +indicator("Parabolic Weighted Moving Average (PWMA)", "PWMA", overlay=true) -//@function Calculates PWMA using Pascal's triangle coefficients as weights with compensator +//@function Calculates PWMA using i² (parabolic) weights with compensator //@param source Series to calculate PWMA from //@param period Lookback period - FIR window size //@returns PWMA value, calculates from first bar using available data -//@optimized Uses Pascal's triangle weighting with O(n) complexity per bar due to lookback loop +//@optimized Uses parabolic weighting w[i]=i² with O(n) complexity per bar due to lookback loop pwma(series float source, simple int period) => if period <= 0 runtime.error("Period must be greater than 0") int p = math.min(bar_index + 1, period) - var array weights = array.new_float(1, 1.0) - var int last_p = 1 - if last_p != p - weights := array.new_float(p, 0.0) - array.set(weights, 0, 1.0) - if p > 1 - float prev_weight = 1.0 - for i = 1 to p - 1 - float curr_weight = prev_weight * (p - i) / i - array.set(weights, i, curr_weight) - prev_weight := curr_weight - last_p := p - float sum = 0.0 + float psum = 0.0 float weight_sum = 0.0 for i = 0 to p - 1 float price = source[i] if not na(price) - float w = array.get(weights, i) - sum += price * w + float w = float((p - i) * (p - i)) + psum += price * w weight_sum += w - nz(sum / weight_sum, source) + nz(psum / weight_sum, source) // ---------- Main loop ---------- diff --git a/lib/trends_FIR/qrma/qrma_signature.svg b/lib/trends_FIR/qrma/qrma_signature.svg index c3daeea8..3ca07a5d 100644 --- a/lib/trends_FIR/qrma/qrma_signature.svg +++ b/lib/trends_FIR/qrma/qrma_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:12:30.049896 + 2026-03-10T20:03:33.550165 image/svg+xml @@ -30,28 +30,313 @@ z - + + + + + + + + + - - + - - + + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - + + + + + - + - + - - - - + @@ -300,126 +840,449 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + - - - - - - @@ -627,233 +1399,1157 @@ z - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - - - - + + + + + + + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + + + + + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1994,104 +5794,1667 @@ z - + + + + - + - - - + + + - + - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + - + + + + - + - - - - - - + + + + + + + + + + + - + + + + - + - - - - - - + + + + + + + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -3095,29 +11665,449 @@ z + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3386,233 +13351,1131 @@ L 1072.948078 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3623,698 +14486,3476 @@ L 1433.247957 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - + + + - + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - + - - + + + + + - + - + - - - - - - - - - - - - - - - - - - + + + + - + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - - - - - - + + + + + + + - + + + + - + - - - - - - + + + + + + + - + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + @@ -4629,6 +19338,34 @@ Q 3878 1594 3878 2328 Q 3878 3066 3575 3472 Q 3272 3878 2719 3878 z +" transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/sgma/sgma_signature.svg b/lib/trends_FIR/sgma/sgma_signature.svg index be0bfe4d..f101d70c 100644 --- a/lib/trends_FIR/sgma/sgma_signature.svg +++ b/lib/trends_FIR/sgma/sgma_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:12:32.771356 + 2026-03-10T20:07:42.512510 image/svg+xml @@ -30,28 +30,313 @@ z - + + + + + + + + + - - + - - + + - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + - + - + + + + + - + - + - - - - + @@ -300,126 +812,449 @@ z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + - - - - - - @@ -627,128 +1371,587 @@ z - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - + - + @@ -756,14 +1959,19 @@ z - + + + + - + - + - + @@ -771,14 +1979,19 @@ z - + + + + - + - + - + @@ -786,14 +1999,19 @@ z - + + + + - + - + - + @@ -801,14 +2019,19 @@ z - + + + + - + - + - + @@ -816,14 +2039,19 @@ z - + + + + - + - + - + @@ -831,29 +2059,449 @@ z + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - + + - - - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + - + + + + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + + - + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - - - - + + + + - + + + + - + - - - - - - + + + + + - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + - - - + + + + - + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - - - - + + + + + + + + + + + - + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - - + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - + - + - - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + + + + - - + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - + + + - + + - - + + + + + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3386,233 +13627,1092 @@ L 1072.948078 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + @@ -3623,698 +14723,3436 @@ L 1433.247957 536.363521 - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + + - + - - - + + + - + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + + + + + + - - + + + + + - + - - - + + + - - - - + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + - - + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + - - - - - - - - + + + + + + - + @@ -4634,6 +19540,7 @@ L 716 0 L 716 1209 z " transform="scale(0.015625)"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..24536606 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) @@ -9,8 +9,6 @@ indicator("ADX Variable Moving Average (ADXVMA)", "ADXVMA", overlay=true) //@returns ADXVMA value that adapts smoothing based on trend strength measured by ADX //@optimized O(1) per bar using Wilder's RMA with warmup compensation for all smoothed components adxvma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 1.0 / float(period) float beta = 1.0 - alpha float EPSILON = 1e-10 diff --git a/lib/trends_IIR/ahrens/ahrens.pine b/lib/trends_IIR/ahrens/ahrens.pine index 08f6721e..ed44b744 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) @@ -11,8 +11,6 @@ indicator("Ahrens Moving Average (AHRENS)", "AHRENS", overlay=true) //@reference Richard D. Ahrens, "Build A Better Moving Average" (Stocks & Commodities V.31:11, October 2013) //@optimized O(1) per bar via circular buffer for lagged MA state; O(period) memory for the ring buffer ahrens(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") // Circular buffer to store past ahma values for period-bar lookback var array buffer = array.new_float(period, na) diff --git a/lib/trends_IIR/coral/coral.pine b/lib/trends_IIR/coral/coral.pine index 76754c6b..65575634 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) @@ -20,8 +20,6 @@ indicator("Coral Filter (CORAL)", "CORAL", overlay=true) // bfr = -cd³*i6 + c3*i5 + c4*i4 + c5*i3 // Note: c3 + c4 + c5 + (-cd³) = 1 (unity DC gain). coral(series float source, simple int period, simple float cd = 0.4) => - if period <= 0 - runtime.error("Period must be greater than 0") float di = (period - 1.0) / 2.0 + 1.0 float c1 = 2.0 / (di + 1.0) float c2 = 1.0 - c1 diff --git a/lib/trends_IIR/decycler/decycler.pine b/lib/trends_IIR/decycler/decycler.pine index b1265bb1..f870cf62 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 @@ -10,8 +10,6 @@ indicator("Ehlers Decycler (DECYCLER)", "DECYCLER", overlay=true) //@returns Decycler value (source minus high-pass filtered component) //@optimized Uses 2-pole Butterworth HP with O(1) complexity per bar decycler(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be positive") float src = na(source) ? 0.0 : source diff --git a/lib/trends_IIR/dema/dema.pine b/lib/trends_IIR/dema/dema.pine index 58e11bcb..d6097901 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) @@ -10,8 +10,6 @@ indicator("Double Exponential Moving Average (DEMA)", "DEMA", overlay=true) //@returns DEMA value from first bar with proper compensation //@optimized Uses exponential warmup compensator on both EMA stages for O(1) complexity dema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a var bool warmup = true diff --git a/lib/trends_IIR/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/ema/ema.pine b/lib/trends_IIR/ema/ema.pine index 28ec4357..9cebaca4 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) @@ -10,8 +10,6 @@ indicator("Exponential Moving Average (EMA)", "EMA", overlay=true) //@returns EMA value from first bar with proper compensation //@optimized Uses exponential warmup compensator for O(1) complexity and valid output from bar 1 ema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a var bool warmup = true diff --git a/lib/trends_IIR/ema/ema_signature.svg b/lib/trends_IIR/ema/ema_signature.svg index 9c5e4d09..194e4e22 100644 --- a/lib/trends_IIR/ema/ema_signature.svg +++ b/lib/trends_IIR/ema/ema_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:11:34.249735 + 2026-03-10T20:06:09.040437 image/svg+xml @@ -175,7 +175,7 @@ L 45.489542 245.400404 L 40.57063 245.170463 L 35.651719 244.848538 z -" clip-path="url(#p9d8231595e)" style="fill: #ddeeff"/> +" clip-path="url(#p8f3d609c47)" style="fill: #ddeeff"/> +" clip-path="url(#p8f3d609c47)" style="fill: #ddeeff"/> +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -368,11 +368,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -403,11 +403,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -448,11 +448,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -501,11 +501,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -541,11 +541,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -587,11 +587,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -640,16 +640,16 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -674,11 +674,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -694,11 +694,11 @@ L 360.29988 207.345435 +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -714,11 +714,11 @@ L 360.29988 168.71563 +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -734,11 +734,11 @@ L 360.29988 130.085824 +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -795,11 +795,11 @@ z +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -950,7 +950,7 @@ L 45.489542 245.318136 L 40.57063 245.130314 L 35.651719 244.888778 z -" clip-path="url(#p9d8231595e)" style="fill: #99ccff"/> +" clip-path="url(#p8f3d609c47)" style="fill: #99ccff"/> +" clip-path="url(#p8f3d609c47)" style="fill: #99ccff"/> +" clip-path="url(#p8f3d609c47)" style="fill: #5599dd"/> @@ -1441,7 +1441,7 @@ L 345.543145 245.870964 L 350.462057 245.889923 L 355.380968 245.905436 L 360.29988 245.918128 -" clip-path="url(#p9d8231595e)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p8f3d609c47)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -1661,7 +1661,7 @@ L 405.789422 245.400404 L 400.87051 245.170463 L 395.951599 244.848538 z -" clip-path="url(#p36122e4f93)" style="fill: #ddeeff"/> +" clip-path="url(#pb266e96374)" style="fill: #ddeeff"/> +" clip-path="url(#pb266e96374)" style="fill: #ddeeff"/> +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1826,11 +1826,11 @@ L 395.951599 43.168761 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1845,11 +1845,11 @@ L 445.140714 43.168761 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1864,11 +1864,11 @@ L 494.329829 43.168761 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1883,11 +1883,11 @@ L 543.518945 43.168761 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1902,11 +1902,11 @@ L 592.70806 43.168761 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1921,11 +1921,11 @@ L 641.897175 43.168761 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1942,11 +1942,11 @@ L 691.086291 43.168761 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1962,11 +1962,11 @@ L 720.59976 245.975241 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1982,11 +1982,11 @@ L 720.59976 207.345435 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2002,11 +2002,11 @@ L 720.59976 168.71563 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2022,11 +2022,11 @@ L 720.59976 130.085824 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2042,11 +2042,11 @@ L 720.59976 91.456018 +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2197,7 +2197,7 @@ L 405.789422 245.318136 L 400.87051 245.130314 L 395.951599 244.888778 z -" clip-path="url(#p36122e4f93)" style="fill: #99ccff"/> +" clip-path="url(#pb266e96374)" style="fill: #99ccff"/> +" clip-path="url(#pb266e96374)" style="fill: #99ccff"/> +" clip-path="url(#pb266e96374)" style="fill: #5599dd"/> @@ -2681,7 +2681,7 @@ L 705.843025 53.295439 L 710.761937 53.210125 L 715.680848 53.140323 L 720.59976 53.083212 -" clip-path="url(#p36122e4f93)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pb266e96374)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -2901,7 +2901,7 @@ L 766.089302 241.581133 L 761.17039 244.357293 L 756.251479 245.971217 z -" clip-path="url(#p849c0655ee)" style="fill: #ddeeff"/> +" clip-path="url(#p6782e345de)" style="fill: #ddeeff"/> +" clip-path="url(#p6782e345de)" style="fill: #ddeeff"/> +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3066,11 +3066,11 @@ L 756.251479 43.168761 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3085,11 +3085,11 @@ L 805.440594 43.168761 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3104,11 +3104,11 @@ L 854.629709 43.168761 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3123,11 +3123,11 @@ L 903.818825 43.168761 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3142,11 +3142,11 @@ L 953.00794 43.168761 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3161,11 +3161,11 @@ L 1002.197055 43.168761 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3182,11 +3182,11 @@ L 1051.386171 43.168761 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3200,11 +3200,11 @@ L 1080.89964 246.00436 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3218,11 +3218,11 @@ L 1080.89964 217.595809 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3237,11 +3237,11 @@ L 1080.89964 189.187258 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3256,11 +3256,11 @@ L 1080.89964 160.778707 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3275,11 +3275,11 @@ L 1080.89964 132.370156 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3294,11 +3294,11 @@ L 1080.89964 103.961605 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3313,11 +3313,11 @@ L 1080.89964 75.553054 +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3467,7 +3467,7 @@ L 766.089302 242.476673 L 761.17039 244.716381 L 756.251479 245.9724 z -" clip-path="url(#p849c0655ee)" style="fill: #99ccff"/> +" clip-path="url(#p6782e345de)" style="fill: #99ccff"/> +" clip-path="url(#p6782e345de)" style="fill: #99ccff"/> +" clip-path="url(#p6782e345de)" style="fill: #5599dd"/> @@ -3969,7 +3969,7 @@ L 1066.142905 192.179863 L 1071.061817 197.833982 L 1075.980728 203.493118 L 1080.89964 209.156358 -" clip-path="url(#p849c0655ee)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p6782e345de)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -4189,7 +4189,7 @@ L 1126.389182 241.552681 L 1121.47027 244.328422 L 1116.551359 245.942103 z -" clip-path="url(#p4648cf02aa)" style="fill: #ddeeff"/> +" clip-path="url(#p0788a69e3c)" style="fill: #ddeeff"/> +" clip-path="url(#p0788a69e3c)" style="fill: #ddeeff"/> +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4354,11 +4354,11 @@ L 1116.551359 43.168761 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4373,11 +4373,11 @@ L 1165.740474 43.168761 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4392,11 +4392,11 @@ L 1214.929589 43.168761 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4411,11 +4411,11 @@ L 1264.118705 43.168761 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4430,11 +4430,11 @@ L 1313.30782 43.168761 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4449,11 +4449,11 @@ L 1362.496935 43.168761 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4470,11 +4470,11 @@ L 1411.686051 43.168761 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4488,11 +4488,11 @@ L 1441.19952 245.975241 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4506,11 +4506,11 @@ L 1441.19952 217.570972 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4525,11 +4525,11 @@ L 1441.19952 189.166703 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4544,11 +4544,11 @@ L 1441.19952 160.762434 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4563,11 +4563,11 @@ L 1441.19952 132.358166 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4582,11 +4582,11 @@ L 1441.19952 103.953897 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4601,11 +4601,11 @@ L 1441.19952 75.549628 +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4755,7 +4755,7 @@ L 1126.389182 242.448086 L 1121.47027 244.687456 L 1116.551359 245.943286 z -" clip-path="url(#p4648cf02aa)" style="fill: #99ccff"/> +" clip-path="url(#p0788a69e3c)" style="fill: #99ccff"/> +" clip-path="url(#p0788a69e3c)" style="fill: #99ccff"/> +" clip-path="url(#p0788a69e3c)" style="fill: #5599dd"/> @@ -5220,7 +5220,7 @@ L 1426.442785 245.353861 L 1431.361697 245.466839 L 1436.280608 245.559276 L 1441.19952 245.634906 -" clip-path="url(#p4648cf02aa)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p0788a69e3c)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -5440,7 +5440,7 @@ L 45.489542 372.571735 L 40.57063 384.83424 L 35.651719 395.434756 z -" clip-path="url(#p847071e733)" style="fill: #ddeeff"/> +" clip-path="url(#p7833f6c5e4)" style="fill: #ddeeff"/> +" clip-path="url(#p7833f6c5e4)" style="fill: #ddeeff"/> +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5605,11 +5605,11 @@ L 35.651719 289.766141 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5624,11 +5624,11 @@ L 84.840834 289.766141 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5643,11 +5643,11 @@ L 134.029949 289.766141 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5662,11 +5662,11 @@ L 183.219065 289.766141 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5681,11 +5681,11 @@ L 232.40818 289.766141 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5700,11 +5700,11 @@ L 281.597295 289.766141 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5721,11 +5721,11 @@ L 330.786411 289.766141 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5752,11 +5752,11 @@ z +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5786,11 +5786,11 @@ z +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5808,11 +5808,11 @@ L 360.29988 444.285364 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5830,11 +5830,11 @@ L 360.29988 420.141735 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5851,11 +5851,11 @@ L 360.29988 395.998107 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5872,11 +5872,11 @@ L 360.29988 371.854478 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5893,11 +5893,11 @@ L 360.29988 347.71085 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5914,11 +5914,11 @@ L 360.29988 323.567221 +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6070,7 +6070,7 @@ L 45.489542 377.135594 L 40.57063 387.202389 L 35.651719 395.454875 z -" clip-path="url(#p847071e733)" style="fill: #99ccff"/> +" clip-path="url(#p7833f6c5e4)" style="fill: #99ccff"/> +" clip-path="url(#p7833f6c5e4)" style="fill: #99ccff"/> +" clip-path="url(#p7833f6c5e4)" style="fill: #5599dd"/> @@ -6449,7 +6449,7 @@ L 345.543145 426.154808 L 350.462057 413.29699 L 355.380968 399.791927 L 360.29988 386.10848 -" clip-path="url(#p847071e733)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p7833f6c5e4)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -6669,7 +6669,7 @@ L 405.789422 369.930662 L 400.87051 369.933732 L 395.951599 395.434756 z -" clip-path="url(#pe6bb2d1e66)" style="fill: #ddeeff"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: #ddeeff"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: #ddeeff"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6834,11 +6834,11 @@ L 395.951599 289.766141 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6853,11 +6853,11 @@ L 445.140714 289.766141 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6872,11 +6872,11 @@ L 494.329829 289.766141 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6891,11 +6891,11 @@ L 543.518945 289.766141 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6910,11 +6910,11 @@ L 592.70806 289.766141 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6929,11 +6929,11 @@ L 641.897175 289.766141 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6950,11 +6950,11 @@ L 691.086291 289.766141 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6972,11 +6972,11 @@ L 720.59976 492.572621 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6994,11 +6994,11 @@ L 720.59976 468.428993 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7016,11 +7016,11 @@ L 720.59976 444.285364 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7038,11 +7038,11 @@ L 720.59976 420.141735 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7059,11 +7059,11 @@ L 720.59976 395.998107 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7080,11 +7080,11 @@ L 720.59976 371.854478 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7101,11 +7101,11 @@ L 720.59976 347.71085 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7122,11 +7122,11 @@ L 720.59976 323.567221 +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7278,7 +7278,7 @@ L 405.789422 374.344765 L 400.87051 375.608653 L 395.951599 395.454875 z -" clip-path="url(#pe6bb2d1e66)" style="fill: #99ccff"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: #99ccff"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: #99ccff"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: #5599dd"/> @@ -7681,7 +7681,7 @@ L 705.843025 378.65408 L 710.761937 364.775307 L 715.680848 352.893177 L 720.59976 343.698205 -" clip-path="url(#pe6bb2d1e66)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pd09aa7a8f6)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -7901,7 +7901,7 @@ L 766.089302 405.991562 L 761.17039 379.490947 L 756.251479 377.36092 z -" clip-path="url(#pdcaf7a61a7)" style="fill: #ddeeff"/> +" clip-path="url(#p599cff177f)" style="fill: #ddeeff"/> +" clip-path="url(#p599cff177f)" style="fill: #ddeeff"/> +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8066,11 +8066,11 @@ L 756.251479 289.766141 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8085,11 +8085,11 @@ L 805.440594 289.766141 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8104,11 +8104,11 @@ L 854.629709 289.766141 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8123,11 +8123,11 @@ L 903.818825 289.766141 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8142,11 +8142,11 @@ L 953.00794 289.766141 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8161,11 +8161,11 @@ L 1002.197055 289.766141 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8182,11 +8182,11 @@ L 1051.386171 289.766141 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8203,11 +8203,11 @@ L 1080.89964 472.241144 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8224,11 +8224,11 @@ L 1080.89964 431.578191 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8244,11 +8244,11 @@ L 1080.89964 390.915238 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8264,11 +8264,11 @@ L 1080.89964 350.252284 +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8419,7 +8419,7 @@ L 766.089302 401.79056 L 761.17039 381.653494 L 756.251479 380.749499 z -" clip-path="url(#pdcaf7a61a7)" style="fill: #99ccff"/> +" clip-path="url(#p599cff177f)" style="fill: #99ccff"/> +" clip-path="url(#p599cff177f)" style="fill: #99ccff"/> +" clip-path="url(#p599cff177f)" style="fill: #5599dd"/> @@ -8817,7 +8817,7 @@ L 1066.142905 400.806738 L 1071.061817 410.09818 L 1075.980728 409.567678 L 1080.89964 409.503294 -" clip-path="url(#pdcaf7a61a7)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p599cff177f)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -9037,7 +9037,7 @@ L 1126.389182 370.121604 L 1121.47027 369.62553 L 1116.551359 367.748419 z -" clip-path="url(#pdedb2940b4)" style="fill: #ddeeff"/> +" clip-path="url(#pa5f5a71029)" style="fill: #ddeeff"/> +" clip-path="url(#pa5f5a71029)" style="fill: #ddeeff"/> +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9202,11 +9202,11 @@ L 1116.551359 289.766141 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9221,11 +9221,11 @@ L 1165.740474 289.766141 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9240,11 +9240,11 @@ L 1214.929589 289.766141 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9259,11 +9259,11 @@ L 1264.118705 289.766141 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9278,11 +9278,11 @@ L 1313.30782 289.766141 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9297,11 +9297,11 @@ L 1362.496935 289.766141 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9318,11 +9318,11 @@ L 1411.686051 289.766141 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9340,11 +9340,11 @@ L 1441.19952 497.499892 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9362,11 +9362,11 @@ L 1441.19952 472.863537 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9384,11 +9384,11 @@ L 1441.19952 448.227181 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9406,11 +9406,11 @@ L 1441.19952 423.590825 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9428,11 +9428,11 @@ L 1441.19952 398.95447 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9449,11 +9449,11 @@ L 1441.19952 374.318114 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9470,11 +9470,11 @@ L 1441.19952 349.681758 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9491,11 +9491,11 @@ L 1441.19952 325.045403 +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9647,7 +9647,7 @@ L 1126.389182 370.680864 L 1121.47027 370.486245 L 1116.551359 369.390843 z -" clip-path="url(#pdedb2940b4)" style="fill: #99ccff"/> +" clip-path="url(#pa5f5a71029)" style="fill: #99ccff"/> +" clip-path="url(#pa5f5a71029)" style="fill: #99ccff"/> +" clip-path="url(#pa5f5a71029)" style="fill: #5599dd"/> @@ -10054,7 +10054,7 @@ L 1426.442785 376.256183 L 1431.361697 362.286621 L 1436.280608 353.902929 L 1441.19952 346.685197 -" clip-path="url(#pdedb2940b4)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pa5f5a71029)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -10274,7 +10274,7 @@ L 45.489542 639.147399 L 40.57063 653.60681 L 35.651719 647.194729 z -" clip-path="url(#p054c1fb7c4)" style="fill: #ddeeff"/> +" clip-path="url(#p36a6ce26d5)" style="fill: #ddeeff"/> +" clip-path="url(#p36a6ce26d5)" style="fill: #ddeeff"/> +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10439,11 +10439,11 @@ L 35.651719 536.363521 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10458,11 +10458,11 @@ L 84.840834 536.363521 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10477,11 +10477,11 @@ L 134.029949 536.363521 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10496,11 +10496,11 @@ L 183.219065 536.363521 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10515,11 +10515,11 @@ L 232.40818 536.363521 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10534,11 +10534,11 @@ L 281.597295 536.363521 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10555,11 +10555,11 @@ L 330.786411 536.363521 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10577,11 +10577,11 @@ L 360.29988 739.170001 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10599,11 +10599,11 @@ L 360.29988 715.026373 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10621,11 +10621,11 @@ L 360.29988 690.882744 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10643,11 +10643,11 @@ L 360.29988 666.739115 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10664,11 +10664,11 @@ L 360.29988 642.595487 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10685,11 +10685,11 @@ L 360.29988 618.451858 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10706,11 +10706,11 @@ L 360.29988 594.30823 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10727,11 +10727,11 @@ L 360.29988 570.164601 +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10883,7 +10883,7 @@ L 45.489542 640.743114 L 40.57063 651.531341 L 35.651719 646.355729 z -" clip-path="url(#p054c1fb7c4)" style="fill: #99ccff"/> +" clip-path="url(#p36a6ce26d5)" style="fill: #99ccff"/> +" clip-path="url(#p36a6ce26d5)" style="fill: #99ccff"/> +" clip-path="url(#p36a6ce26d5)" style="fill: #5599dd"/> @@ -11293,7 +11293,7 @@ L 345.543145 645.347376 L 350.462057 640.984052 L 355.380968 642.681761 L 360.29988 643.719615 -" clip-path="url(#p054c1fb7c4)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p36a6ce26d5)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -11513,7 +11513,7 @@ L 405.789422 633.805253 L 400.87051 635.697389 L 395.951599 632.938035 z -" clip-path="url(#pf10b291fc4)" style="fill: #ddeeff"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: #ddeeff"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: #ddeeff"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11678,11 +11678,11 @@ L 395.951599 536.363521 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11697,11 +11697,11 @@ L 445.140714 536.363521 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11716,11 +11716,11 @@ L 494.329829 536.363521 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11735,11 +11735,11 @@ L 543.518945 536.363521 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11754,11 +11754,11 @@ L 592.70806 536.363521 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11773,11 +11773,11 @@ L 641.897175 536.363521 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11794,11 +11794,11 @@ L 691.086291 536.363521 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11816,11 +11816,11 @@ L 720.59976 739.170001 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11838,11 +11838,11 @@ L 720.59976 715.026373 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11860,11 +11860,11 @@ L 720.59976 690.882744 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11882,11 +11882,11 @@ L 720.59976 666.739115 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11903,11 +11903,11 @@ L 720.59976 642.595487 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11924,11 +11924,11 @@ L 720.59976 618.451858 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11945,11 +11945,11 @@ L 720.59976 594.30823 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11966,11 +11966,11 @@ L 720.59976 570.164601 +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12122,7 +12122,7 @@ L 405.789422 635.209279 L 400.87051 636.962639 L 395.951599 635.352398 z -" clip-path="url(#pf10b291fc4)" style="fill: #99ccff"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: #99ccff"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: #99ccff"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: #5599dd"/> @@ -12529,7 +12529,7 @@ L 705.843025 637.082165 L 710.761937 623.510615 L 715.680848 627.507362 L 720.59976 645.351399 -" clip-path="url(#pf10b291fc4)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p1f6dc3ad7d)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -12749,7 +12749,7 @@ L 766.089302 700.524215 L 761.17039 702.751902 L 756.251479 703.543533 z -" clip-path="url(#pe8d7f0c668)" style="fill: #ddeeff"/> +" clip-path="url(#pdf7e88acbe)" style="fill: #ddeeff"/> +" clip-path="url(#pdf7e88acbe)" style="fill: #ddeeff"/> +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12914,11 +12914,11 @@ L 756.251479 536.363521 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12933,11 +12933,11 @@ L 805.440594 536.363521 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12952,11 +12952,11 @@ L 854.629709 536.363521 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12971,11 +12971,11 @@ L 903.818825 536.363521 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12990,11 +12990,11 @@ L 953.00794 536.363521 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13009,11 +13009,11 @@ L 1002.197055 536.363521 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13030,11 +13030,11 @@ L 1051.386171 536.363521 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13052,11 +13052,11 @@ L 1080.89964 729.861614 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13073,11 +13073,11 @@ L 1080.89964 700.772905 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13094,11 +13094,11 @@ L 1080.89964 671.684196 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13115,11 +13115,11 @@ L 1080.89964 642.595487 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13136,11 +13136,11 @@ L 1080.89964 613.506778 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13157,11 +13157,11 @@ L 1080.89964 584.418069 +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13313,7 +13313,7 @@ L 766.089302 700.84968 L 761.17039 702.534524 L 756.251479 703.038111 z -" clip-path="url(#pe8d7f0c668)" style="fill: #99ccff"/> +" clip-path="url(#pdf7e88acbe)" style="fill: #99ccff"/> +" clip-path="url(#pdf7e88acbe)" style="fill: #99ccff"/> +" clip-path="url(#pdf7e88acbe)" style="fill: #5599dd"/> @@ -13697,7 +13697,7 @@ L 1066.142905 582.383634 L 1071.061817 576.830012 L 1075.980728 578.421213 L 1080.89964 585.646623 -" clip-path="url(#pe8d7f0c668)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pdf7e88acbe)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -13917,7 +13917,7 @@ L 1126.389182 712.684228 L 1121.47027 716.254287 L 1116.551359 716.285575 z -" clip-path="url(#pa00080bf64)" style="fill: #ddeeff"/> +" clip-path="url(#p2d9381a418)" style="fill: #ddeeff"/> +" clip-path="url(#p2d9381a418)" style="fill: #ddeeff"/> +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14082,11 +14082,11 @@ L 1116.551359 536.363521 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14101,11 +14101,11 @@ L 1165.740474 536.363521 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14120,11 +14120,11 @@ L 1214.929589 536.363521 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14139,11 +14139,11 @@ L 1264.118705 536.363521 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14158,11 +14158,11 @@ L 1313.30782 536.363521 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14177,11 +14177,11 @@ L 1362.496935 536.363521 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14198,11 +14198,11 @@ L 1411.686051 536.363521 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14216,11 +14216,11 @@ L 1441.19952 716.176069 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14235,11 +14235,11 @@ L 1441.19952 670.188205 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14254,11 +14254,11 @@ L 1441.19952 624.200341 +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14408,7 +14408,7 @@ L 1126.389182 713.470058 L 1121.47027 716.245695 L 1116.551359 716.265599 z -" clip-path="url(#pa00080bf64)" style="fill: #99ccff"/> +" clip-path="url(#p2d9381a418)" style="fill: #99ccff"/> +" clip-path="url(#p2d9381a418)" style="fill: #99ccff"/> +" clip-path="url(#p2d9381a418)" style="fill: #5599dd"/> @@ -14793,7 +14793,7 @@ L 1426.442785 714.360359 L 1431.361697 711.220495 L 1436.280608 712.24693 L 1441.19952 716.556734 -" clip-path="url(#pa00080bf64)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p2d9381a418)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -15013,7 +15013,7 @@ L 45.489542 884.067288 L 40.57063 895.698789 L 35.651719 896.155077 z -" clip-path="url(#p139d8da8d7)" style="fill: #ddeeff"/> +" clip-path="url(#pfec85cac08)" style="fill: #ddeeff"/> +" clip-path="url(#pfec85cac08)" style="fill: #ddeeff"/> +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15178,11 +15178,11 @@ L 35.651719 782.960901 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15197,11 +15197,11 @@ L 84.840834 782.960901 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15216,11 +15216,11 @@ L 134.029949 782.960901 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15235,11 +15235,11 @@ L 183.219065 782.960901 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15254,11 +15254,11 @@ L 232.40818 782.960901 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15273,11 +15273,11 @@ L 281.597295 782.960901 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15294,11 +15294,11 @@ L 330.786411 782.960901 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15315,11 +15315,11 @@ L 360.29988 995.15657 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15336,11 +15336,11 @@ L 360.29988 961.623753 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15357,11 +15357,11 @@ L 360.29988 928.090935 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15377,11 +15377,11 @@ L 360.29988 894.558118 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15397,11 +15397,11 @@ L 360.29988 861.0253 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15417,11 +15417,11 @@ L 360.29988 827.492483 +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15572,7 +15572,7 @@ L 45.489542 886.551904 L 40.57063 895.573495 L 35.651719 895.863757 z -" clip-path="url(#p139d8da8d7)" style="fill: #99ccff"/> +" clip-path="url(#pfec85cac08)" style="fill: #99ccff"/> +" clip-path="url(#pfec85cac08)" style="fill: #99ccff"/> +" clip-path="url(#pfec85cac08)" style="fill: #5599dd"/> @@ -15952,7 +15952,7 @@ L 345.543145 916.819267 L 350.462057 898.383158 L 355.380968 883.299069 L 360.29988 868.884604 -" clip-path="url(#p139d8da8d7)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pfec85cac08)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -16172,7 +16172,7 @@ L 405.789422 878.908061 L 400.87051 877.553371 L 395.951599 900.742092 z -" clip-path="url(#pfb1df5a696)" style="fill: #ddeeff"/> +" clip-path="url(#p3ef3b6719f)" style="fill: #ddeeff"/> +" clip-path="url(#p3ef3b6719f)" style="fill: #ddeeff"/> +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16337,11 +16337,11 @@ L 395.951599 782.960901 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16356,11 +16356,11 @@ L 445.140714 782.960901 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16375,11 +16375,11 @@ L 494.329829 782.960901 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16394,11 +16394,11 @@ L 543.518945 782.960901 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16413,11 +16413,11 @@ L 592.70806 782.960901 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16432,11 +16432,11 @@ L 641.897175 782.960901 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16453,11 +16453,11 @@ L 691.086291 782.960901 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16474,11 +16474,11 @@ L 720.59976 995.17399 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16495,11 +16495,11 @@ L 720.59976 963.818628 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16516,11 +16516,11 @@ L 720.59976 932.463266 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16536,11 +16536,11 @@ L 720.59976 901.107904 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16556,11 +16556,11 @@ L 720.59976 869.752543 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16576,11 +16576,11 @@ L 720.59976 838.397181 +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16731,7 +16731,7 @@ L 405.789422 882.617407 L 400.87051 882.709651 L 395.951599 900.755157 z -" clip-path="url(#pfb1df5a696)" style="fill: #99ccff"/> +" clip-path="url(#p3ef3b6719f)" style="fill: #99ccff"/> +" clip-path="url(#p3ef3b6719f)" style="fill: #99ccff"/> +" clip-path="url(#p3ef3b6719f)" style="fill: #5599dd"/> @@ -17112,7 +17112,7 @@ L 705.843025 891.078857 L 710.761937 873.176947 L 715.680848 860.126203 L 720.59976 849.562341 -" clip-path="url(#pfb1df5a696)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p3ef3b6719f)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -17332,7 +17332,7 @@ L 766.089302 851.862619 L 761.17039 857.018171 L 756.251479 857.303319 z -" clip-path="url(#p60839086b2)" style="fill: #ddeeff"/> +" clip-path="url(#pccbfe33298)" style="fill: #ddeeff"/> +" clip-path="url(#pccbfe33298)" style="fill: #ddeeff"/> +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17497,11 +17497,11 @@ L 756.251479 782.960901 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17516,11 +17516,11 @@ L 805.440594 782.960901 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17535,11 +17535,11 @@ L 854.629709 782.960901 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17554,11 +17554,11 @@ L 903.818825 782.960901 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17573,11 +17573,11 @@ L 953.00794 782.960901 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17592,11 +17592,11 @@ L 1002.197055 782.960901 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17613,11 +17613,11 @@ L 1051.386171 782.960901 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17633,11 +17633,11 @@ L 1080.89964 969.897839 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17653,11 +17653,11 @@ L 1080.89964 928.135887 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17673,11 +17673,11 @@ L 1080.89964 886.373935 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17693,11 +17693,11 @@ L 1080.89964 844.611983 +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17848,7 +17848,7 @@ L 766.089302 853.176037 L 761.17039 857.235406 L 756.251479 857.501284 z -" clip-path="url(#p60839086b2)" style="fill: #99ccff"/> +" clip-path="url(#pccbfe33298)" style="fill: #99ccff"/> +" clip-path="url(#pccbfe33298)" style="fill: #99ccff"/> +" clip-path="url(#pccbfe33298)" style="fill: #5599dd"/> @@ -18247,7 +18247,7 @@ L 1066.142905 913.760702 L 1071.061817 907.338604 L 1075.980728 903.564812 L 1080.89964 897.288069 -" clip-path="url(#p60839086b2)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pccbfe33298)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -18467,7 +18467,7 @@ L 1126.389182 920.809523 L 1121.47027 919.659793 L 1116.551359 920.809523 z -" clip-path="url(#p7b4dd10156)" style="fill: #ddeeff"/> +" clip-path="url(#p449cc975ec)" style="fill: #ddeeff"/> +" clip-path="url(#p449cc975ec)" style="fill: #ddeeff"/> +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18632,11 +18632,11 @@ L 1116.551359 782.960901 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18651,11 +18651,11 @@ L 1165.740474 782.960901 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18670,11 +18670,11 @@ L 1214.929589 782.960901 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18689,11 +18689,11 @@ L 1264.118705 782.960901 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18708,11 +18708,11 @@ L 1313.30782 782.960901 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18727,11 +18727,11 @@ L 1362.496935 782.960901 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18748,11 +18748,11 @@ L 1411.686051 782.960901 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18767,11 +18767,11 @@ L 1441.19952 982.318291 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18786,11 +18786,11 @@ L 1441.19952 954.725573 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18805,11 +18805,11 @@ L 1441.19952 927.132855 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18824,11 +18824,11 @@ L 1441.19952 899.540136 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18843,11 +18843,11 @@ L 1441.19952 871.947418 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18862,11 +18862,11 @@ L 1441.19952 844.3547 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18881,11 +18881,11 @@ L 1441.19952 816.761981 +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -19035,7 +19035,7 @@ L 1126.389182 920.665811 L 1121.47027 919.803182 L 1116.551359 920.665811 z -" clip-path="url(#p7b4dd10156)" style="fill: #99ccff"/> +" clip-path="url(#p449cc975ec)" style="fill: #99ccff"/> +" clip-path="url(#p449cc975ec)" style="fill: #99ccff"/> +" clip-path="url(#p449cc975ec)" style="fill: #5599dd"/> @@ -19434,7 +19434,7 @@ L 1426.442785 905.310686 L 1431.361697 908.651246 L 1436.280608 922.672361 L 1441.19952 932.889969 -" clip-path="url(#p7b4dd10156)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p449cc975ec)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -19602,52 +19602,52 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + 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..dc01a2fd 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) @@ -14,8 +14,6 @@ indicator("Generalized Double Exponential Moving Average (GDEMA)", "GDEMA", over //@reference Patrick G. Mulloy, "Smoothing Data with Faster Moving Averages" (TASC, Feb 1994) //@optimized O(1) per bar; two cascaded EMA states with shared warmup compensator gdema(series float source, simple int period, simple float vfactor) => - if period <= 0 - runtime.error("Period must be greater than 0") float a = 2.0 / (period + 1) float beta = 1.0 - a diff --git a/lib/trends_IIR/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..61af00c2 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) @@ -15,8 +15,6 @@ indicator("Holt Exponential Moving Average (HOLT)", "HOLT", overlay=true) // When gamma=0, degenerates to standard EMA (no trend correction). // When gamma=alpha, provides balanced level/trend tracking. holt(series float source, simple int period, simple float gamma=0) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 2.0 / (period + 1) float g = gamma > 0 ? gamma : alpha var float level = na diff --git a/lib/trends_IIR/htit/Htit.md b/lib/trends_IIR/htit/Htit.md index 80bfbbe9..e7574edc 100644 --- a/lib/trends_IIR/htit/Htit.md +++ b/lib/trends_IIR/htit/Htit.md @@ -8,6 +8,8 @@ | **Outputs** | Single series (HTIT) | | **Output range** | Tracks input | | **Warmup** | `12` bars | +| **Signature** | [htit_signature](htit_signature.md) | + ### TL;DR 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/htit/htit_signature.md b/lib/trends_IIR/htit/htit_signature.md new file mode 100644 index 00000000..d33c9d6d --- /dev/null +++ b/lib/trends_IIR/htit/htit_signature.md @@ -0,0 +1,3 @@ +# HOLT Signature + +![HTIT Signature](htit_signature.svg) \ No newline at end of file diff --git a/lib/trends_IIR/htit/htit_signature.svg b/lib/trends_IIR/htit/htit_signature.svg new file mode 100644 index 00000000..6922917f --- /dev/null +++ b/lib/trends_IIR/htit/htit_signature.svg @@ -0,0 +1,19855 @@ + + + + + + + + 2026-03-10T20:17:16.305404 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..374d5049 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) @@ -11,12 +11,6 @@ indicator("Kaufman's Adaptive Moving Average (KAMA)", "KAMA", overlay=true) //@returns KAMA value with efficiency ratio-based adaptive smoothing //@optimized Uses efficiency ratio calculation for O(n) complexity per bar due to lookback sum kama(series float source, simple int period, simple float fast_alpha=0.666667, simple float slow_alpha=0.0645) => - if period <= 0 - runtime.error("Period must be greater than 0") - if fast_alpha <= 0 or slow_alpha <= 0 - runtime.error("Alpha values must be greater than 0") - if fast_alpha <= slow_alpha - runtime.error("Fast alpha must be greater than slow alpha") var float kama_state = na float current_kama = na if not na(source) diff --git a/lib/trends_IIR/lema/lema.pine b/lib/trends_IIR/lema/lema.pine index 90cfd90f..68b8181a 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) @@ -16,9 +16,6 @@ indicator("Leader EMA (LEMA)", "LEMA", overlay=true) // Stocks & Commodities, 26(7), 30-37. //@optimized O(1) per bar — two IIR state variables with warmup compensation lema(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - float price = nz(source) float alpha = 2.0 / (period + 1) float beta = 1.0 - alpha diff --git a/lib/trends_IIR/ltma/ltma.pine b/lib/trends_IIR/ltma/ltma.pine index 39e3a643..460f9a80 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) @@ -12,8 +12,6 @@ indicator("Linear Trend Moving Average (LTMA)", "LTMA", overlay=true) // EMA1 lags by τ = (1−α)/α bars; EMA1−EMA2 ≈ slope·τ; result = EMA1 + (EMA1−EMA2). // Initializing both EMAs to source on bar 1 gives zero warmup bias with no compensator needed. ltma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 2.0 / (period + 1) float beta = 1.0 - alpha diff --git a/lib/trends_IIR/mama/Mama.md b/lib/trends_IIR/mama/Mama.md index e7655e34..39c8e500 100644 --- a/lib/trends_IIR/mama/Mama.md +++ b/lib/trends_IIR/mama/Mama.md @@ -8,6 +8,8 @@ | **Outputs** | Single series (Mama) | | **Output range** | Tracks input | | **Warmup** | `50` bars | +| **Signature** | [mama_signature](mama_signature.md) | + ### TL;DR diff --git a/lib/trends_IIR/mama/mama.pine b/lib/trends_IIR/mama/mama.pine index 7f019a7c..982cfaf1 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) @@ -10,8 +10,6 @@ indicator("Ehlers MESA Adaptive Moving Average (MAMA)", "MAMA", overlay=true) //@returns [mama, fama] array containing MAMA and FAMA values //@optimized Uses Hilbert Transform phase detection for O(1) complexity per bar mama(series float source, float fastLimit=0.5, float slowLimit=0.05) => - if fastLimit < slowLimit or fastLimit <= 0 or slowLimit < 0 - runtime.error("MAMA: fastLimit must be > slowLimit > 0") var float mama_val = na var float fama_val = na var float period = 0.0 diff --git a/lib/trends_IIR/mama/mama_signature.md b/lib/trends_IIR/mama/mama_signature.md new file mode 100644 index 00000000..bb66bffb --- /dev/null +++ b/lib/trends_IIR/mama/mama_signature.md @@ -0,0 +1,3 @@ +# LTMA Signature + +![MAMA Signature](mama_signature.svg) \ No newline at end of file diff --git a/lib/trends_IIR/mama/mama_signature.svg b/lib/trends_IIR/mama/mama_signature.svg new file mode 100644 index 00000000..9d98950a --- /dev/null +++ b/lib/trends_IIR/mama/mama_signature.svg @@ -0,0 +1,19690 @@ + + + + + + + + 2026-03-10T20:21:31.006868 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/trends_IIR/mavp/mavp.pine b/lib/trends_IIR/mavp/mavp.pine index 6733c849..32417919 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) @@ -11,10 +11,6 @@ indicator("Moving Average Variable Period (MAVP)", "MAVP", overlay=true) //@returns EMA value with variable alpha = 2/(period+1), compensated warmup //@optimized Uses adaptive warmup compensator that tracks cumulative (1-alpha) product for O(1) per bar mavp(series float source, series float period, simple int min_period, simple int max_period) => - if min_period < 1 - runtime.error("min_period must be >= 1") - if max_period < min_period - runtime.error("max_period must be >= min_period") var float ema = 0.0 var float e = 1.0 var bool warmup = true diff --git a/lib/trends_IIR/mcnma/mcnma.pine b/lib/trends_IIR/mcnma/mcnma.pine index ec423678..635f4914 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 @@ -13,9 +13,6 @@ indicator("MCNMA - McNicholl EMA", "MCNMA", overlay=true) //@param period Lookback period (must be > 0) //@returns McNicholl EMA value from bar 1 mcnma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - float src = nz(source) float alpha = 2.0 / (period + 1) float beta = 1.0 - alpha diff --git a/lib/trends_IIR/mgdi/mgdi.pine b/lib/trends_IIR/mgdi/mgdi.pine index 641d6d5b..2367e3a1 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) @@ -10,10 +10,6 @@ indicator("McGinley Dynamic Indicator (MGDI)", "MGDI", overlay=true) //@returns MGDI value that tracks price movements more closely than EMAs //@optimized Uses adaptive smoothing with O(1) complexity after initialization mgdi(series float source, simple int period, simple float factor=0.6) => - if period <= 0 - runtime.error("Period must be greater than 0") - if factor <= 0 - runtime.error("Factor must be greater than 0") var float mgdi_val = na if not na(source) if na(mgdi_val) diff --git a/lib/trends_IIR/mgdi/mgdi_signature.svg b/lib/trends_IIR/mgdi/mgdi_signature.svg index a2e924d8..aa33578f 100644 --- a/lib/trends_IIR/mgdi/mgdi_signature.svg +++ b/lib/trends_IIR/mgdi/mgdi_signature.svg @@ -6,7 +6,7 @@ - 2026-03-02T14:11:48.591521 + 2026-03-10T20:24:13.769010 image/svg+xml @@ -175,7 +175,7 @@ L 35.516431 244.097833 L 30.424309 244.097833 L 25.332187 244.097833 z -" clip-path="url(#pef0a4f4411)" style="fill: #ddeeff"/> +" clip-path="url(#pae7c172351)" style="fill: #ddeeff"/> +" clip-path="url(#pae7c172351)" style="fill: #ddeeff"/> +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -368,11 +368,11 @@ z +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -403,11 +403,11 @@ z +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -448,11 +448,11 @@ z +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -501,11 +501,11 @@ z +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -541,11 +541,11 @@ z +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -587,11 +587,11 @@ z +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -640,16 +640,16 @@ z +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - - + @@ -663,11 +663,11 @@ L -3.5 0 +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -681,11 +681,11 @@ L 361.412224 211.232135 +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -699,11 +699,11 @@ L 361.412224 178.366438 +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -717,11 +717,11 @@ L 361.412224 145.50074 +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -735,11 +735,11 @@ L 361.412224 112.635043 +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -753,11 +753,11 @@ L 361.412224 79.769345 +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -982,7 +982,7 @@ L 35.516431 244.097833 L 30.424309 244.097833 L 25.332187 244.097833 z -" clip-path="url(#pef0a4f4411)" style="fill: #99ccff"/> +" clip-path="url(#pae7c172351)" style="fill: #99ccff"/> +" clip-path="url(#pae7c172351)" style="fill: #99ccff"/> +" clip-path="url(#pae7c172351)" style="fill: #5599dd"/> @@ -1473,7 +1473,7 @@ L 346.135858 244.097833 L 351.22798 244.097833 L 356.320102 244.097833 L 361.412224 244.097833 -" clip-path="url(#pef0a4f4411)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pae7c172351)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -1693,7 +1693,7 @@ L 390.250217 244.097833 L 385.158096 244.097833 L 380.065974 244.097833 z -" clip-path="url(#p38bc92ff17)" style="fill: #ddeeff"/> +" clip-path="url(#p82e21ff53a)" style="fill: #ddeeff"/> +" clip-path="url(#p82e21ff53a)" style="fill: #ddeeff"/> +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1858,11 +1858,11 @@ L 380.065974 43.168761 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1877,11 +1877,11 @@ L 430.987191 43.168761 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1896,11 +1896,11 @@ L 481.908409 43.168761 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1915,11 +1915,11 @@ L 532.829627 43.168761 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1934,11 +1934,11 @@ L 583.750844 43.168761 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1953,11 +1953,11 @@ L 634.672062 43.168761 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1974,11 +1974,11 @@ L 685.593279 43.168761 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -1992,11 +1992,11 @@ L 716.14601 244.097833 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2010,11 +2010,11 @@ L 716.14601 211.232135 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2028,11 +2028,11 @@ L 716.14601 178.366438 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2046,11 +2046,11 @@ L 716.14601 145.50074 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2064,11 +2064,11 @@ L 716.14601 112.635043 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2082,11 +2082,11 @@ L 716.14601 79.769345 +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -2245,7 +2245,7 @@ L 390.250217 244.097833 L 385.158096 244.097833 L 380.065974 244.097833 z -" clip-path="url(#p38bc92ff17)" style="fill: #99ccff"/> +" clip-path="url(#p82e21ff53a)" style="fill: #99ccff"/> +" clip-path="url(#p82e21ff53a)" style="fill: #99ccff"/> +" clip-path="url(#p82e21ff53a)" style="fill: #5599dd"/> @@ -2729,7 +2729,7 @@ L 700.869645 244.097833 L 705.961766 244.097833 L 711.053888 244.097833 L 716.14601 244.097833 -" clip-path="url(#p38bc92ff17)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p82e21ff53a)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -2949,7 +2949,7 @@ L 753.891504 244.097833 L 748.799382 244.097833 L 743.70726 244.097833 z -" clip-path="url(#pc42afcf6c6)" style="fill: #ddeeff"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: #ddeeff"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: #ddeeff"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3114,11 +3114,11 @@ L 743.70726 43.168761 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3133,11 +3133,11 @@ L 794.628478 43.168761 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3152,11 +3152,11 @@ L 845.549695 43.168761 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3171,11 +3171,11 @@ L 896.470913 43.168761 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3190,11 +3190,11 @@ L 947.39213 43.168761 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3209,11 +3209,11 @@ L 998.313348 43.168761 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3230,11 +3230,11 @@ L 1049.234566 43.168761 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3248,11 +3248,11 @@ L 1079.787296 244.097833 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3266,11 +3266,11 @@ L 1079.787296 211.232135 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3284,11 +3284,11 @@ L 1079.787296 178.366438 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3302,11 +3302,11 @@ L 1079.787296 145.50074 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3320,11 +3320,11 @@ L 1079.787296 112.635043 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3338,11 +3338,11 @@ L 1079.787296 79.769345 +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -3501,7 +3501,7 @@ L 753.891504 244.097833 L 748.799382 244.097833 L 743.70726 244.097833 z -" clip-path="url(#pc42afcf6c6)" style="fill: #99ccff"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: #99ccff"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: #99ccff"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: #5599dd"/> @@ -4003,7 +4003,7 @@ L 1064.510931 244.097833 L 1069.603053 244.097833 L 1074.695174 244.097833 L 1079.787296 244.097833 -" clip-path="url(#pc42afcf6c6)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p0b9ad4a1fa)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -4223,7 +4223,7 @@ L 1115.303727 244.097833 L 1110.211606 244.097833 L 1105.119484 244.097833 z -" clip-path="url(#pa98efe48b4)" style="fill: #ddeeff"/> +" clip-path="url(#p6f3d4ca173)" style="fill: #ddeeff"/> +" clip-path="url(#p6f3d4ca173)" style="fill: #ddeeff"/> +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4388,11 +4388,11 @@ L 1105.119484 43.168761 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4407,11 +4407,11 @@ L 1156.040701 43.168761 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4426,11 +4426,11 @@ L 1206.961919 43.168761 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4445,11 +4445,11 @@ L 1257.883137 43.168761 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4464,11 +4464,11 @@ L 1308.804354 43.168761 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4483,11 +4483,11 @@ L 1359.725572 43.168761 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4504,11 +4504,11 @@ L 1410.646789 43.168761 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4522,11 +4522,11 @@ L 1441.19952 244.097833 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4540,11 +4540,11 @@ L 1441.19952 211.232135 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4558,11 +4558,11 @@ L 1441.19952 178.366438 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4576,11 +4576,11 @@ L 1441.19952 145.50074 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4594,11 +4594,11 @@ L 1441.19952 112.635043 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4612,11 +4612,11 @@ L 1441.19952 79.769345 +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -4775,7 +4775,7 @@ L 1115.303727 244.097833 L 1110.211606 244.097833 L 1105.119484 244.097833 z -" clip-path="url(#pa98efe48b4)" style="fill: #99ccff"/> +" clip-path="url(#p6f3d4ca173)" style="fill: #99ccff"/> +" clip-path="url(#p6f3d4ca173)" style="fill: #99ccff"/> +" clip-path="url(#p6f3d4ca173)" style="fill: #5599dd"/> @@ -5240,7 +5240,7 @@ L 1425.923155 244.097833 L 1431.015276 244.097833 L 1436.107398 244.097833 L 1441.19952 244.097833 -" clip-path="url(#pa98efe48b4)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p6f3d4ca173)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -5460,7 +5460,7 @@ L 35.516431 490.695213 L 30.424309 490.695213 L 25.332187 490.695213 z -" clip-path="url(#p51110051b6)" style="fill: #ddeeff"/> +" clip-path="url(#p384ce2df13)" style="fill: #ddeeff"/> +" clip-path="url(#p384ce2df13)" style="fill: #ddeeff"/> +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5625,11 +5625,11 @@ L 25.332187 289.766141 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5644,11 +5644,11 @@ L 76.253405 289.766141 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5663,11 +5663,11 @@ L 127.174623 289.766141 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5682,11 +5682,11 @@ L 178.09584 289.766141 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5701,11 +5701,11 @@ L 229.017058 289.766141 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5720,11 +5720,11 @@ L 279.938276 289.766141 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5741,11 +5741,11 @@ L 330.859493 289.766141 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5759,11 +5759,11 @@ L 361.412224 490.695213 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5777,11 +5777,11 @@ L 361.412224 457.829515 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5795,11 +5795,11 @@ L 361.412224 424.963818 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5813,11 +5813,11 @@ L 361.412224 392.09812 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5831,11 +5831,11 @@ L 361.412224 359.232423 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -5849,11 +5849,11 @@ L 361.412224 326.366725 +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6012,7 +6012,7 @@ L 35.516431 490.695213 L 30.424309 490.695213 L 25.332187 490.695213 z -" clip-path="url(#p51110051b6)" style="fill: #99ccff"/> +" clip-path="url(#p384ce2df13)" style="fill: #99ccff"/> +" clip-path="url(#p384ce2df13)" style="fill: #99ccff"/> +" clip-path="url(#p384ce2df13)" style="fill: #5599dd"/> @@ -6391,7 +6391,7 @@ L 346.135858 490.695213 L 351.22798 490.695213 L 356.320102 490.695213 L 361.412224 490.695213 -" clip-path="url(#p51110051b6)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p384ce2df13)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -6611,7 +6611,7 @@ L 390.250217 490.695213 L 385.158096 490.695213 L 380.065974 490.695213 z -" clip-path="url(#p6841fdb4b1)" style="fill: #ddeeff"/> +" clip-path="url(#p49900416c2)" style="fill: #ddeeff"/> +" clip-path="url(#p49900416c2)" style="fill: #ddeeff"/> +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6776,11 +6776,11 @@ L 380.065974 289.766141 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6795,11 +6795,11 @@ L 430.987191 289.766141 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6814,11 +6814,11 @@ L 481.908409 289.766141 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6833,11 +6833,11 @@ L 532.829627 289.766141 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6852,11 +6852,11 @@ L 583.750844 289.766141 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6871,11 +6871,11 @@ L 634.672062 289.766141 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6892,11 +6892,11 @@ L 685.593279 289.766141 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6910,11 +6910,11 @@ L 716.14601 490.695213 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6928,11 +6928,11 @@ L 716.14601 457.829515 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6946,11 +6946,11 @@ L 716.14601 424.963818 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6964,11 +6964,11 @@ L 716.14601 392.09812 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -6982,11 +6982,11 @@ L 716.14601 359.232423 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7000,11 +7000,11 @@ L 716.14601 326.366725 +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7163,7 +7163,7 @@ L 390.250217 490.695213 L 385.158096 490.695213 L 380.065974 490.695213 z -" clip-path="url(#p6841fdb4b1)" style="fill: #99ccff"/> +" clip-path="url(#p49900416c2)" style="fill: #99ccff"/> +" clip-path="url(#p49900416c2)" style="fill: #99ccff"/> +" clip-path="url(#p49900416c2)" style="fill: #5599dd"/> @@ -7566,7 +7566,7 @@ L 700.869645 490.695213 L 705.961766 490.695213 L 711.053888 490.695213 L 716.14601 490.695213 -" clip-path="url(#p6841fdb4b1)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p49900416c2)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -7786,7 +7786,7 @@ L 753.891504 490.695213 L 748.799382 490.695213 L 743.70726 490.695213 z -" clip-path="url(#p1db35724c1)" style="fill: #ddeeff"/> +" clip-path="url(#p2d52262482)" style="fill: #ddeeff"/> +" clip-path="url(#p2d52262482)" style="fill: #ddeeff"/> +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7951,11 +7951,11 @@ L 743.70726 289.766141 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7970,11 +7970,11 @@ L 794.628478 289.766141 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -7989,11 +7989,11 @@ L 845.549695 289.766141 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8008,11 +8008,11 @@ L 896.470913 289.766141 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8027,11 +8027,11 @@ L 947.39213 289.766141 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8046,11 +8046,11 @@ L 998.313348 289.766141 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8067,11 +8067,11 @@ L 1049.234566 289.766141 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8085,11 +8085,11 @@ L 1079.787296 490.695213 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8103,11 +8103,11 @@ L 1079.787296 450.371443 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8121,11 +8121,11 @@ L 1079.787296 410.047674 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8139,11 +8139,11 @@ L 1079.787296 369.723904 +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -8334,7 +8334,7 @@ L 753.891504 490.695213 L 748.799382 490.695213 L 743.70726 490.695213 z -" clip-path="url(#p1db35724c1)" style="fill: #99ccff"/> +" clip-path="url(#p2d52262482)" style="fill: #99ccff"/> +" clip-path="url(#p2d52262482)" style="fill: #99ccff"/> +" clip-path="url(#p2d52262482)" style="fill: #5599dd"/> @@ -8732,7 +8732,7 @@ L 1064.510931 490.695213 L 1069.603053 490.695213 L 1074.695174 490.695213 L 1079.787296 490.695213 -" clip-path="url(#p1db35724c1)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p2d52262482)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -8952,7 +8952,7 @@ L 1115.303727 490.695213 L 1110.211606 490.695213 L 1105.119484 490.695213 z -" clip-path="url(#pf1891c00a2)" style="fill: #ddeeff"/> +" clip-path="url(#pdebd16ee78)" style="fill: #ddeeff"/> +" clip-path="url(#pdebd16ee78)" style="fill: #ddeeff"/> +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9117,11 +9117,11 @@ L 1105.119484 289.766141 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9136,11 +9136,11 @@ L 1156.040701 289.766141 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9155,11 +9155,11 @@ L 1206.961919 289.766141 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9174,11 +9174,11 @@ L 1257.883137 289.766141 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9193,11 +9193,11 @@ L 1308.804354 289.766141 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9212,11 +9212,11 @@ L 1359.725572 289.766141 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9233,11 +9233,11 @@ L 1410.646789 289.766141 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9251,11 +9251,11 @@ L 1441.19952 490.695213 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9269,11 +9269,11 @@ L 1441.19952 450.371443 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9287,11 +9287,11 @@ L 1441.19952 410.047674 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9305,11 +9305,11 @@ L 1441.19952 369.723904 +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -9468,7 +9468,7 @@ L 1115.303727 490.695213 L 1110.211606 490.695213 L 1105.119484 490.695213 z -" clip-path="url(#pf1891c00a2)" style="fill: #99ccff"/> +" clip-path="url(#pdebd16ee78)" style="fill: #99ccff"/> +" clip-path="url(#pdebd16ee78)" style="fill: #99ccff"/> +" clip-path="url(#pdebd16ee78)" style="fill: #5599dd"/> @@ -9875,7 +9875,7 @@ L 1425.923155 490.695213 L 1431.015276 490.695213 L 1436.107398 490.695213 L 1441.19952 490.695213 -" clip-path="url(#pf1891c00a2)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pdebd16ee78)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -10095,7 +10095,7 @@ L 35.516431 737.292593 L 30.424309 737.292593 L 25.332187 737.292593 z -" clip-path="url(#pe33d00c8e4)" style="fill: #ddeeff"/> +" clip-path="url(#peede562f21)" style="fill: #ddeeff"/> +" clip-path="url(#peede562f21)" style="fill: #ddeeff"/> +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10260,11 +10260,11 @@ L 25.332187 536.363521 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10279,11 +10279,11 @@ L 76.253405 536.363521 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10298,11 +10298,11 @@ L 127.174623 536.363521 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10317,11 +10317,11 @@ L 178.09584 536.363521 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10336,11 +10336,11 @@ L 229.017058 536.363521 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10355,11 +10355,11 @@ L 279.938276 536.363521 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10376,11 +10376,11 @@ L 330.859493 536.363521 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10394,11 +10394,11 @@ L 361.412224 737.292593 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10412,11 +10412,11 @@ L 361.412224 696.968823 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10430,11 +10430,11 @@ L 361.412224 656.645054 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10448,11 +10448,11 @@ L 361.412224 616.321284 +" clip-path="url(#peede562f21)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -10611,7 +10611,7 @@ L 35.516431 737.292593 L 30.424309 737.292593 L 25.332187 737.292593 z -" clip-path="url(#pe33d00c8e4)" style="fill: #99ccff"/> +" clip-path="url(#peede562f21)" style="fill: #99ccff"/> +" clip-path="url(#peede562f21)" style="fill: #99ccff"/> +" clip-path="url(#peede562f21)" style="fill: #5599dd"/> @@ -11021,7 +11021,7 @@ L 346.135858 737.292593 L 351.22798 737.292593 L 356.320102 737.292593 L 361.412224 737.292593 -" clip-path="url(#pe33d00c8e4)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#peede562f21)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#peede562f21)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -11241,7 +11241,7 @@ L 390.250217 737.292593 L 385.158096 737.292593 L 380.065974 737.292593 z -" clip-path="url(#pa1cb2ac38d)" style="fill: #ddeeff"/> +" clip-path="url(#pfadd1be2c4)" style="fill: #ddeeff"/> +" clip-path="url(#pfadd1be2c4)" style="fill: #ddeeff"/> +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11406,11 +11406,11 @@ L 380.065974 536.363521 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11425,11 +11425,11 @@ L 430.987191 536.363521 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11444,11 +11444,11 @@ L 481.908409 536.363521 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11463,11 +11463,11 @@ L 532.829627 536.363521 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11482,11 +11482,11 @@ L 583.750844 536.363521 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11501,11 +11501,11 @@ L 634.672062 536.363521 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11522,11 +11522,11 @@ L 685.593279 536.363521 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11540,11 +11540,11 @@ L 716.14601 737.292593 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11558,11 +11558,11 @@ L 716.14601 710.41008 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11576,11 +11576,11 @@ L 716.14601 683.527567 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11594,11 +11594,11 @@ L 716.14601 656.645054 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11612,11 +11612,11 @@ L 716.14601 629.762541 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11630,11 +11630,11 @@ L 716.14601 602.880028 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11648,11 +11648,11 @@ L 716.14601 575.997515 +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -11823,7 +11823,7 @@ L 390.250217 737.292593 L 385.158096 737.292593 L 380.065974 737.292593 z -" clip-path="url(#pa1cb2ac38d)" style="fill: #99ccff"/> +" clip-path="url(#pfadd1be2c4)" style="fill: #99ccff"/> +" clip-path="url(#pfadd1be2c4)" style="fill: #99ccff"/> +" clip-path="url(#pfadd1be2c4)" style="fill: #5599dd"/> @@ -12230,7 +12230,7 @@ L 700.869645 737.292593 L 705.961766 737.292593 L 711.053888 737.292593 L 716.14601 737.292593 -" clip-path="url(#pa1cb2ac38d)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pfadd1be2c4)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -12450,7 +12450,7 @@ L 753.891504 737.292593 L 748.799382 737.292593 L 743.70726 737.292593 z -" clip-path="url(#p692aedb6a6)" style="fill: #ddeeff"/> +" clip-path="url(#pb59e1efc7f)" style="fill: #ddeeff"/> +" clip-path="url(#pb59e1efc7f)" style="fill: #ddeeff"/> +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12615,11 +12615,11 @@ L 743.70726 536.363521 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12634,11 +12634,11 @@ L 794.628478 536.363521 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12653,11 +12653,11 @@ L 845.549695 536.363521 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12672,11 +12672,11 @@ L 896.470913 536.363521 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12691,11 +12691,11 @@ L 947.39213 536.363521 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12710,11 +12710,11 @@ L 998.313348 536.363521 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12731,11 +12731,11 @@ L 1049.234566 536.363521 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12760,11 +12760,11 @@ z +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12780,11 +12780,11 @@ L 1079.787296 696.968823 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12800,11 +12800,11 @@ L 1079.787296 656.645054 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12820,11 +12820,11 @@ L 1079.787296 616.321284 +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -12985,7 +12985,7 @@ L 753.891504 737.292593 L 748.799382 737.292593 L 743.70726 737.292593 z -" clip-path="url(#p692aedb6a6)" style="fill: #99ccff"/> +" clip-path="url(#pb59e1efc7f)" style="fill: #99ccff"/> +" clip-path="url(#pb59e1efc7f)" style="fill: #99ccff"/> +" clip-path="url(#pb59e1efc7f)" style="fill: #5599dd"/> @@ -13369,7 +13369,7 @@ L 1064.510931 737.292593 L 1069.603053 737.292593 L 1074.695174 737.292593 L 1079.787296 737.292593 -" clip-path="url(#p692aedb6a6)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pb59e1efc7f)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -13589,7 +13589,7 @@ L 1115.303727 737.292593 L 1110.211606 737.292593 L 1105.119484 737.292593 z -" clip-path="url(#pa9813a8da9)" style="fill: #ddeeff"/> +" clip-path="url(#pee740c00cd)" style="fill: #ddeeff"/> +" clip-path="url(#pee740c00cd)" style="fill: #ddeeff"/> +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13754,11 +13754,11 @@ L 1105.119484 536.363521 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13773,11 +13773,11 @@ L 1156.040701 536.363521 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13792,11 +13792,11 @@ L 1206.961919 536.363521 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13811,11 +13811,11 @@ L 1257.883137 536.363521 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13830,11 +13830,11 @@ L 1308.804354 536.363521 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13849,11 +13849,11 @@ L 1359.725572 536.363521 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13870,11 +13870,11 @@ L 1410.646789 536.363521 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13890,11 +13890,11 @@ L 1441.19952 737.292593 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13910,11 +13910,11 @@ L 1441.19952 696.968823 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13930,11 +13930,11 @@ L 1441.19952 656.645054 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -13950,11 +13950,11 @@ L 1441.19952 616.321284 +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14115,7 +14115,7 @@ L 1115.303727 737.292593 L 1110.211606 737.292593 L 1105.119484 737.292593 z -" clip-path="url(#pa9813a8da9)" style="fill: #99ccff"/> +" clip-path="url(#pee740c00cd)" style="fill: #99ccff"/> +" clip-path="url(#pee740c00cd)" style="fill: #99ccff"/> +" clip-path="url(#pee740c00cd)" style="fill: #5599dd"/> @@ -14500,7 +14500,7 @@ L 1425.923155 737.292593 L 1431.015276 737.292593 L 1436.107398 737.292593 L 1441.19952 737.292593 -" clip-path="url(#pa9813a8da9)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pee740c00cd)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -14720,7 +14720,7 @@ L 35.516431 983.889973 L 30.424309 983.889973 L 25.332187 983.889973 z -" clip-path="url(#p5197943bcc)" style="fill: #ddeeff"/> +" clip-path="url(#p74e6fcfa06)" style="fill: #ddeeff"/> +" clip-path="url(#p74e6fcfa06)" style="fill: #ddeeff"/> +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14885,11 +14885,11 @@ L 25.332187 782.960901 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14904,11 +14904,11 @@ L 76.253405 782.960901 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14923,11 +14923,11 @@ L 127.174623 782.960901 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14942,11 +14942,11 @@ L 178.09584 782.960901 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14961,11 +14961,11 @@ L 229.017058 782.960901 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -14980,11 +14980,11 @@ L 279.938276 782.960901 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15001,11 +15001,11 @@ L 330.859493 782.960901 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15021,11 +15021,11 @@ L 361.412224 983.889973 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15041,11 +15041,11 @@ L 361.412224 943.566203 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15061,11 +15061,11 @@ L 361.412224 903.242434 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15081,11 +15081,11 @@ L 361.412224 862.918664 +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -15246,7 +15246,7 @@ L 35.516431 983.889973 L 30.424309 983.889973 L 25.332187 983.889973 z -" clip-path="url(#p5197943bcc)" style="fill: #99ccff"/> +" clip-path="url(#p74e6fcfa06)" style="fill: #99ccff"/> +" clip-path="url(#p74e6fcfa06)" style="fill: #99ccff"/> +" clip-path="url(#p74e6fcfa06)" style="fill: #5599dd"/> @@ -15626,7 +15626,7 @@ L 346.135858 983.889973 L 351.22798 983.889973 L 356.320102 983.889973 L 361.412224 983.889973 -" clip-path="url(#p5197943bcc)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#p74e6fcfa06)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -15846,7 +15846,7 @@ L 390.250217 983.889973 L 385.158096 983.889973 L 380.065974 983.889973 z -" clip-path="url(#p30231933bf)" style="fill: #ddeeff"/> +" clip-path="url(#pf89f924894)" style="fill: #ddeeff"/> +" clip-path="url(#pf89f924894)" style="fill: #ddeeff"/> +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16011,11 +16011,11 @@ L 380.065974 782.960901 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16030,11 +16030,11 @@ L 430.987191 782.960901 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16049,11 +16049,11 @@ L 481.908409 782.960901 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16068,11 +16068,11 @@ L 532.829627 782.960901 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16087,11 +16087,11 @@ L 583.750844 782.960901 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16106,11 +16106,11 @@ L 634.672062 782.960901 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16127,11 +16127,11 @@ L 685.593279 782.960901 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16145,11 +16145,11 @@ L 716.14601 983.889973 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16163,11 +16163,11 @@ L 716.14601 951.024275 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16181,11 +16181,11 @@ L 716.14601 918.158578 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16199,11 +16199,11 @@ L 716.14601 885.29288 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16217,11 +16217,11 @@ L 716.14601 852.427183 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16235,11 +16235,11 @@ L 716.14601 819.561485 +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -16398,7 +16398,7 @@ L 390.250217 983.889973 L 385.158096 983.889973 L 380.065974 983.889973 z -" clip-path="url(#p30231933bf)" style="fill: #99ccff"/> +" clip-path="url(#pf89f924894)" style="fill: #99ccff"/> +" clip-path="url(#pf89f924894)" style="fill: #99ccff"/> +" clip-path="url(#pf89f924894)" style="fill: #5599dd"/> @@ -16779,7 +16779,7 @@ L 700.869645 983.889973 L 705.961766 983.889973 L 711.053888 983.889973 L 716.14601 983.889973 -" clip-path="url(#p30231933bf)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pf89f924894)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -16999,7 +16999,7 @@ L 753.891504 852.144703 L 748.799382 857.07438 L 743.70726 857.369693 z -" clip-path="url(#pd75b2dedc6)" style="fill: #ddeeff"/> +" clip-path="url(#pd0ee929cec)" style="fill: #ddeeff"/> +" clip-path="url(#pd0ee929cec)" style="fill: #ddeeff"/> +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17164,11 +17164,11 @@ L 743.70726 782.960901 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17183,11 +17183,11 @@ L 794.628478 782.960901 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17202,11 +17202,11 @@ L 845.549695 782.960901 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17221,11 +17221,11 @@ L 896.470913 782.960901 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17240,11 +17240,11 @@ L 947.39213 782.960901 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17259,11 +17259,11 @@ L 998.313348 782.960901 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17280,11 +17280,11 @@ L 1049.234566 782.960901 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17300,11 +17300,11 @@ L 1079.787296 969.897839 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17320,11 +17320,11 @@ L 1079.787296 928.135887 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17340,11 +17340,11 @@ L 1079.787296 886.373935 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17360,11 +17360,11 @@ L 1079.787296 844.611983 +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -17515,7 +17515,7 @@ L 753.891504 853.564929 L 748.799382 857.30919 L 743.70726 857.572903 z -" clip-path="url(#pd75b2dedc6)" style="fill: #99ccff"/> +" clip-path="url(#pd0ee929cec)" style="fill: #99ccff"/> +" clip-path="url(#pd0ee929cec)" style="fill: #99ccff"/> +" clip-path="url(#pd0ee929cec)" style="fill: #5599dd"/> @@ -17914,7 +17914,7 @@ L 1064.510931 913.730658 L 1069.603053 908.073483 L 1074.695174 904.575694 L 1079.787296 898.880759 -" clip-path="url(#pd75b2dedc6)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pd0ee929cec)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -18134,7 +18134,7 @@ L 1115.303727 917.05825 L 1110.211606 915.979073 L 1105.119484 917.058144 z -" clip-path="url(#pb466d4db4b)" style="fill: #ddeeff"/> +" clip-path="url(#pc72ce36923)" style="fill: #ddeeff"/> +" clip-path="url(#pc72ce36923)" style="fill: #ddeeff"/> +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18299,11 +18299,11 @@ L 1105.119484 782.960901 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18318,11 +18318,11 @@ L 1156.040701 782.960901 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18337,11 +18337,11 @@ L 1206.961919 782.960901 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18356,11 +18356,11 @@ L 1257.883137 782.960901 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18375,11 +18375,11 @@ L 1308.804354 782.960901 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18394,11 +18394,11 @@ L 1359.725572 782.960901 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18415,11 +18415,11 @@ L 1410.646789 782.960901 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18434,11 +18434,11 @@ L 1441.19952 976.696767 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18453,11 +18453,11 @@ L 1441.19952 949.921725 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18472,11 +18472,11 @@ L 1441.19952 923.146683 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18491,11 +18491,11 @@ L 1441.19952 896.371641 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18510,11 +18510,11 @@ L 1441.19952 869.596599 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18529,11 +18529,11 @@ L 1441.19952 842.821557 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18548,11 +18548,11 @@ L 1441.19952 816.046515 +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #b0b0b0; stroke-opacity: 0.25; stroke-width: 0.8; stroke-linecap: square"/> - + @@ -18702,7 +18702,7 @@ L 1115.303727 916.900699 L 1110.211606 916.122101 L 1105.119484 916.899444 z -" clip-path="url(#pb466d4db4b)" style="fill: #99ccff"/> +" clip-path="url(#pc72ce36923)" style="fill: #99ccff"/> +" clip-path="url(#pc72ce36923)" style="fill: #99ccff"/> +" clip-path="url(#pc72ce36923)" style="fill: #5599dd"/> @@ -19101,7 +19101,7 @@ L 1425.923155 903.236906 L 1431.015276 906.203058 L 1436.107398 923.354248 L 1441.19952 933.799715 -" clip-path="url(#pb466d4db4b)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #1a3a6a; stroke-width: 2; stroke-linecap: square"/> +" clip-path="url(#pc72ce36923)" style="fill: none; stroke: #444444; stroke-width: 0.6; stroke-linecap: square"/> @@ -19316,52 +19316,52 @@ z - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/lib/trends_IIR/mma/mma.pine b/lib/trends_IIR/mma/mma.pine index 9c2e4bc0..31b848e3 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) @@ -9,8 +9,6 @@ indicator("Modified Moving Average (MMA)", "MMA", overlay=true) //@returns MMA value, combines SMA with weighted component for balanced smoothing //@optimized Uses circular buffer for O(1) sum updates, O(n) for weighted component mma(series float source, simple int period) => - if period < 2 - runtime.error("Period must be at least 2") var array buffer = array.new_float(math.min(math.max(2, period), 4000), na) var int head = 0 var float sum = 0.0 diff --git a/lib/trends_IIR/nma/nma.pine b/lib/trends_IIR/nma/nma.pine index c3e3cdf0..15287f53 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 @@ -21,9 +21,6 @@ indicator("NMA - Natural Moving Average", "NMA", overlay=true) // @param period Lookback window for volatility analysis (must be > 0) // @returns NMA value nma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - float src = nz(source) // Step 1: scaled natural log of price diff --git a/lib/trends_IIR/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..15e6554b 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) @@ -10,10 +10,6 @@ indicator("Regularized EMA (REMA)", "REMA", overlay=true) //@returns REMA value, calculates from first bar using available data //@optimized Uses regularization term to reduce noise for O(1) complexity rema(series float source, simple int period, simple float lambda=0.5) => - if period <= 0 - runtime.error("Period must be greater than 0") - if lambda < 0.0 or lambda > 1.0 - runtime.error("Lambda must be between 0 and 1") float alpha = 2.0 / (period + 1.0) var float rema_val = na var float prev_rema = na diff --git a/lib/trends_IIR/rgma/rgma.pine b/lib/trends_IIR/rgma/rgma.pine index f3a1f3dd..d32282aa 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) @@ -10,10 +10,6 @@ indicator("Recursive Gaussian Moving Average (RGMA)", "RGMA", overlay=true) //@returns RGMA value with gaussian-like smoothing properties using recursive calculation //@optimized Uses cascaded exponential filters for O(1) complexity per bar rgma(series float source, simple int period, simple int passes=3) => - if period <= 0 - runtime.error("Period must be greater than 0") - if passes <= 0 - runtime.error("Passes must be greater than 0") simple float alpha = 2.0 / (period / math.sqrt(passes) + 1.0) var array filters = array.new_float(passes, na) float result = na diff --git a/lib/trends_IIR/rma/rma.pine b/lib/trends_IIR/rma/rma.pine index babf37cf..07f20959 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) @@ -9,8 +9,6 @@ indicator("Wilder's Moving Average (RMA)", "RMA", overlay=true) //@returns RMA value from first bar with proper compensation for early values //@optimized Uses exponential warmup compensator with Wilder's alpha (1/period) for O(1) complexity rma(series float source, simple int period) => - if period <= 0 - runtime.error("Period must be provided") float a = 1.0 / float(period) float beta = 1.0 - a var bool warmup = true diff --git a/lib/trends_IIR/t3/t3.pine b/lib/trends_IIR/t3/t3.pine index b8c7da6b..2b4e7575 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) @@ -10,8 +10,6 @@ indicator("Tillson T3 Moving Average (T3)", "T3", overlay=true) //@returns T3 value with optimized coefficients //@optimized Uses six cascaded EMAs with precomputed coefficients for O(1) complexity t3(series float src, simple int period, simple float v) => - if period <= 0 - runtime.error("T3 period must be > 0") float a = 2.0 / (period + 1) float v2 = v * v float v3 = v2 * v diff --git a/lib/trends_IIR/tema/tema.pine b/lib/trends_IIR/tema/tema.pine index 285dea9b..fd3202ec 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) @@ -11,8 +11,6 @@ indicator("Triple Exponential Moving Average (TEMA)", "TEMA", overlay=true) //@returns TEMA value from first bar with proper compensation //@optimized Uses exponential warmup compensator on all three EMA stages for O(1) complexity tema(series float source, simple int period=0, simple float alpha=0.0, simple bool corrected=false) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a1 = alpha > 0 ? alpha : (period > 0 ? 2.0 / (period + 1) : 0.1) float r = math.pow(1.0 / a1, 1.0 / 3.0) float a2 = corrected ? a1 * r : a1 diff --git a/lib/trends_IIR/vama/vama.pine b/lib/trends_IIR/vama/vama.pine index 684aa0af..74c52eaa 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) @@ -13,14 +13,6 @@ indicator("Volatility Adjusted Moving Average (VAMA)", "VAMA", overlay=true) //@returns VAMA value //@optimized Uses RMA compensator for ATR and circular buffer for O(1) sum updates vama(series float source, simple int base_length, simple int short_atr_period=10, simple int long_atr_period=50, simple int min_length=5, simple int max_length=100) => - if base_length <= 0 - runtime.error("Base length must be greater than 0") - if short_atr_period <= 0 or long_atr_period <= 0 - runtime.error("ATR periods must be greater than 0") - if min_length <= 0 or max_length <= 0 - runtime.error("Min and max length must be greater than 0") - if min_length > max_length - runtime.error("Min length must be less than or equal to max length") var float prevClose = na float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/trends_IIR/vidya/vidya.pine b/lib/trends_IIR/vidya/vidya.pine index c1306d31..d35ff113 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) @@ -10,8 +10,6 @@ indicator("Variable Index Dynamic Average (VIDYA)", "VIDYA", overlay=true) //@returns VIDYA value that adapts to market volatility //@optimized Uses volatility index calculation with O(n) complexity per bar due to lookback loops vidya(series float source, simple int period, simple int std_period=0) => - if period <= 0 - runtime.error("Period must be greater than 0") float alpha = 2.0 / (period + 1.0) var float vidya = na if not na(source) diff --git a/lib/trends_IIR/yzvama/yzvama.pine b/lib/trends_IIR/yzvama/yzvama.pine index e6e60def..e685f393 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) @@ -13,14 +13,6 @@ indicator("Yang-Zhang Volatility Adjusted Moving Average (YZVAMA)", "YZVAMA", ov //@returns YZVAMA value //@optimized Uses RMA compensators for YZV and circular buffers for O(1) sum updates yzvama(series float source, simple int yzv_short_period=3, simple int yzv_long_period=50, simple int percentile_lookback=100, simple int min_length=5, simple int max_length=100) => - if yzv_short_period <= 0 or yzv_long_period <= 0 - runtime.error("All periods must be greater than 0") - if min_length <= 0 or max_length <= 0 - runtime.error("Min and max length must be greater than 0") - if min_length > max_length - runtime.error("Min length must be less than or equal to max length") - if percentile_lookback <= 0 - runtime.error("Percentile lookback must be greater than 0") var float prev_close = na float o = open float h = high diff --git a/lib/trends_IIR/zldema/zldema.pine b/lib/trends_IIR/zldema/zldema.pine index 1d74fdbd..cd4795e5 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) @@ -10,8 +10,6 @@ indicator("Zero-Lag Double EMA (ZLDEMA)", "ZLDEMA", overlay=true) //@returns ZLDEMA value with zero-lag effect applied //@optimized Uses lag compensation buffer and exponential warmup compensator on both EMA stages for O(1) complexity zldema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a simple int lag = math.max(1, math.round((period - 1) / 2)) diff --git a/lib/trends_IIR/zlema/zlema.pine b/lib/trends_IIR/zlema/zlema.pine index f86f2fd9..552158ee 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) @@ -10,8 +10,6 @@ indicator("Zero-Lag EMA (ZLEMA)", "ZLEMA", overlay=true) //@returns ZLEMA value with zero-lag effect applied //@optimized Uses lag compensation buffer and exponential warmup compensator for O(1) complexity zlema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") float a = alpha > 0 ? alpha : 2.0 / (period + 1) float beta = 1.0 - a simple int lag = math.max(1, math.round((period - 1) / 2)) diff --git a/lib/trends_IIR/zltema/zltema.pine b/lib/trends_IIR/zltema/zltema.pine index 7d708bf3..6f71b054 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) @@ -10,13 +10,8 @@ indicator("Zero-Lag Triple EMA (ZLTEMA)", "ZLTEMA", overlay=true) //@returns ZLTEMA value with zero-lag effect applied //@optimized Uses lag compensation buffer and exponential warmup compensator on all three EMA stages for O(1) complexity zltema(series float source, simple int period=0, simple float alpha=0) => - if alpha <= 0 and period <= 0 - runtime.error("Alpha or period must be provided") - float a1 = alpha > 0 ? alpha : 2.0 / (period + 1) - float beta1 = 1.0 - a1 - float r = math.pow(1.0 / a1, 1.0 / 3.0) - float a2 = a1 * r - float a3 = a2 * r + float a = alpha > 0 ? alpha : 2.0 / (period + 1) + float beta = 1.0 - a simple int lag = math.max(1, math.round((period - 1) / 2)) var bool warmup = true var float e = 1.0 @@ -37,21 +32,21 @@ zltema(series float source, simple int period=0, simple float alpha=0) => array.push(priceBuffer, source) float laggedPrice = nz(array.get(priceBuffer, 0), source) float signal = 2 * source - laggedPrice - ema1_raw := a1 * (signal - ema1_raw) + ema1_raw + ema1_raw := a * (signal - ema1_raw) + ema1_raw if warmup - e *= beta1 + e *= beta float c = 1.0 / (1.0 - e) ema1 := c * ema1_raw - ema2_raw := a2 * (ema1 - ema2_raw) + ema2_raw + ema2_raw := a * (ema1 - ema2_raw) + ema2_raw ema2 := c * ema2_raw - ema3_raw := a3 * (ema2 - ema3_raw) + ema3_raw + ema3_raw := a * (ema2 - ema3_raw) + ema3_raw ema3 := c * ema3_raw warmup := e > 1e-10 else ema1 := ema1_raw - ema2_raw := a2 * (ema1 - ema2_raw) + ema2_raw + ema2_raw := a * (ema1 - ema2_raw) + ema2_raw ema2 := ema2_raw - ema3_raw := a3 * (ema2 - ema3_raw) + ema3_raw + ema3_raw := a * (ema2 - ema3_raw) + ema3_raw ema3 := ema3_raw 3 * ema1 - 3 * ema2 + ema3 else diff --git a/lib/volatility/adr/adr.pine b/lib/volatility/adr/adr.pine index de4e930a..8b0e62ae 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) @@ -9,10 +9,6 @@ indicator("Average Daily Range (ADR)", "ADR", overlay=false) //@returns float ADR value //@optimized for performance and dirty data adr(simple int length, simple int method = 1) => - if length <= 0 - runtime.error("Length must be greater than 0") - if method < 1 or method > 3 - runtime.error("Method must be 1 (SMA), 2 (EMA), or 3 (WMA)") var int p = math.max(1, length) var int head = 0 var int count = 0 diff --git a/lib/volatility/atr/atr.pine b/lib/volatility/atr/atr.pine index 7321e598..35857dfb 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) @@ -8,8 +8,6 @@ indicator("Average True Range (ATR)", "ATR", overlay=false) //@returns The ATR value. //@optimized Beta precomputation for RMA warmup compensation atr(simple int length) => - if length <= 0 - runtime.error("Period must be greater than 0") var float prevClose = close float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/volatility/atrn/atrn.pine b/lib/volatility/atrn/atrn.pine index 86396788..104286c6 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) @@ -8,8 +8,6 @@ indicator("Average True Range Normalized (ATRN)", "ATRN", overlay=false, format= //@returns The ATRN value, normalized relative to its maximum over the longer period. //@optimized Beta precomputation for RMA warmup compensation atrn(simple int length) => - if length <= 0 - runtime.error("Period must be greater than 0") var float prevClose = close float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/volatility/bbw/bbw.pine b/lib/volatility/bbw/bbw.pine index 2a94e5b6..c75b4aa6 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) @@ -10,8 +10,6 @@ indicator("Bollinger Band Width (BBW)", "BBW", overlay=false) //@returns BBW value representing the width between Bollinger Bands //@optimized for performance and dirty data bbw(series float source, simple int period, simple float multiplier) => - if period <= 0 or multiplier <= 0.0 - runtime.error("Period and multiplier must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array buffer = array.new_float(p, na) var float sum = 0.0, var float sumSq = 0.0 @@ -28,7 +26,7 @@ bbw(series float source, simple int period, simple float multiplier) => head := (head + 1) % p float basis = nz(sum / count, source) float dev = count > 1 ? multiplier * math.sqrt(math.max(0.0, sumSq / count - basis * basis)) : 0.0 - 2 * dev + basis != 0.0 ? 2 * dev / basis : 0.0 // ---------- Main loop ---------- diff --git a/lib/volatility/bbwn/bbwn.pine b/lib/volatility/bbwn/bbwn.pine index 17fbbd4b..b89585d8 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) @@ -11,8 +11,6 @@ indicator("Bollinger Band Width Normalized (BBWN)", "BBWN", overlay=false) //@returns BBWN value representing current BBW normalized to [0,1] range //@optimized for performance and dirty data bbwn(series float source, simple int period, simple float multiplier, simple int lookback) => - if period <= 0 or multiplier <= 0.0 or lookback <= 0 - runtime.error("Period, multiplier, and lookback must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array buffer = array.new_float(p, na) var float sum = 0.0, var float sumSq = 0.0 @@ -29,7 +27,7 @@ bbwn(series float source, simple int period, simple float multiplier, simple int head := (head + 1) % p float basis = nz(sum / count, source) float dev = count > 1 ? multiplier * math.sqrt(math.max(0.0, sumSq / count - basis * basis)) : 0.0 - float bbw = 2 * dev + float bbw = basis != 0.0 ? 2 * dev / basis : 0.0 var int l = math.max(1, lookback), var int hist_head = 0, var int hist_count = 0 var array hist_buffer = array.new_float(l, na) var float min_val = bbw, var float max_val = bbw diff --git a/lib/volatility/bbwp/bbwp.pine b/lib/volatility/bbwp/bbwp.pine index 0987efcc..65d646f2 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) @@ -11,8 +11,6 @@ indicator("Bollinger Band Width Percentile (BBWP)", "BBWP", overlay=false, forma //@returns BBWP value representing current BBW percentile in historical range //@optimized for performance and dirty data bbwp(series float source, simple int period, simple float multiplier, simple int lookback) => - if period <= 0 or multiplier <= 0.0 or lookback <= 0 - runtime.error("Period, multiplier, and lookback must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array buffer = array.new_float(p, na) var float sum = 0.0, var float sumSq = 0.0 @@ -29,7 +27,7 @@ bbwp(series float source, simple int period, simple float multiplier, simple int head := (head + 1) % p float basis = nz(sum / count, source) float dev = count > 1 ? multiplier * math.sqrt(math.max(0.0, sumSq / count - basis * basis)) : 0.0 - float bbw = 2 * dev + float bbw = basis != 0.0 ? 2 * dev / basis : 0.0 var int l = math.max(1, lookback), var int hist_head = 0, var int hist_count = 0 var array hist_buffer = array.new_float(l, na) float hist_oldest = array.get(hist_buffer, hist_head) diff --git a/lib/volatility/ccv/ccv.pine b/lib/volatility/ccv/ccv.pine index 24e716ec..a3550d36 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) @@ -9,10 +9,6 @@ indicator("Close-to-Close Volatility (CCV)", "CCV", overlay=false) //@returns float Volatility value //@optimized Beta precomputation for RMA warmup compensation ccv(simple int length, simple int method) => - if length <= 0 - runtime.error("Length must be greater than 0") - if method < 1 or method > 3 - runtime.error("Method must be 1 (SMA), 2 (EMA), or 3 (WMA)") var int p = math.max(1, length) var int head = 0 var int count = 0 diff --git a/lib/volatility/cv/cv.pine b/lib/volatility/cv/cv.pine index af7880dd..590139db 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) @@ -10,14 +10,6 @@ indicator("Conditional Volatility (CV)", "CV", overlay=false) //@returns float Conditional volatility value //@optimized for performance and efficient variance updating cv(simple int length, simple float alpha, simple float beta) => - if length <= 0 - runtime.error("Length must be greater than 0") - if alpha <= 0.0 or alpha >= 1.0 - runtime.error("Alpha must be between 0 and 1") - if beta <= 0.0 or beta >= 1.0 - runtime.error("Beta must be between 0 and 1") - if alpha + beta >= 1.0 - runtime.error("Alpha + Beta must be less than 1 for stationarity") var float omega = 0.0 var float longRunVar = 0.0 var float prevVariance = 0.0 diff --git a/lib/volatility/cvi/cvi.pine b/lib/volatility/cvi/cvi.pine index 05beafb7..ec45e1ef 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) @@ -9,8 +9,6 @@ indicator("Chaikin's Volatility (CVI)", "CVI", overlay=false) //@returns float Volatility value measuring change in trading ranges //@optimized for performance using efficient range ROC calculation cvi(simple int roc_length, simple int smooth_length) => - if roc_length <= 0 or smooth_length <= 0 - runtime.error("Lengths must be greater than 0") var float prevEma = 0.0 hlRange = high - low alpha = 2.0 / (smooth_length + 1) diff --git a/lib/volatility/etherm/Etherm.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..3e6f7bce 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) @@ -8,18 +8,14 @@ indicator("Elder's Thermometer", "ETHERM", overlay=false) //@returns [thermometer, signal] The raw thermometer value and EMA signal line //@optimized Beta precomputation for EMA warmup compensation etherm(simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") - // Step 1: Calculate raw thermometer value - // Temperature = max(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 +47,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..f3079650 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) @@ -11,10 +11,6 @@ indicator("Exponential Weighted MA Volatility", "EWMA Volty", overlay=false) //@returns float The EWMA Volatility value. //@optimized for performance and dirty data ewmaVolty(series float src, simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float logReturn = nz(math.log(src / src[1]),0.0) float squaredReturn = logReturn * logReturn var float raw_rma_sq_ret = 0.0, var float e_rma = 1.0 diff --git a/lib/volatility/gkv/gkv.pine b/lib/volatility/gkv/gkv.pine index 588427e8..ec1b7217 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) @@ -10,10 +10,6 @@ indicator("Garman-Klass Volatility (GKV)", "GKV", overlay=false) //@returns float The Garman-Klass Volatility value. //@optimized for performance and dirty data gkv(simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float lnH = math.log(high), float lnL = math.log(low), float lnO = math.log(open), float lnC = math.log(close) float C_2LN2_1 = 0.3862941611 // 2 * math.log(2) - 1 float term1 = 0.5 * math.pow(lnH - lnL, 2) diff --git a/lib/volatility/hlv/hlv.pine b/lib/volatility/hlv/hlv.pine index 18279a7d..c772e21c 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) @@ -10,10 +10,6 @@ indicator("High-Low Volatility (HLV)", "HLV", overlay=false) //@returns float The High-Low Volatility value. //@optimized for performance and dirty data hlv(simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float lnH = math.log(high), float lnL = math.log(low) float C_4LN2_INV = 0.3606737602 // 1.0 / (4.0 * math.log(2.0)) float parkinsonEstimator = C_4LN2_INV * math.pow(lnH - lnL, 2) diff --git a/lib/volatility/hv/hv.pine b/lib/volatility/hv/hv.pine index ebba636e..bd08d6f4 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) @@ -11,10 +11,6 @@ indicator("Historical Volatility (HV)", "HV", overlay=false) //@returns float The Historical Volatility value. //@optimized for performance and dirty data hv(series float src_price, simple int length_hv, simple bool annualize = true, simple int annualPeriods = 252) => - if length_hv <= 1 - runtime.error("Length for HV must be greater than 1") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") var array _buffer_hv = array.new_float(length_hv, na) var int _head_idx_hv = 0 var int _current_fill_count_hv = 0 diff --git a/lib/volatility/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..960b471a 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) @@ -9,8 +9,6 @@ indicator("Mass Index (MASSI)", "MASSI", overlay=false) //@returns float Mass Index value //@optimized for performance and dirty data massi(simple int ema_length, simple int sum_length) => - if ema_length <= 0 or sum_length <= 0 - runtime.error("Periods must be greater than 0") float a = 2.0 / (ema_length + 1) float beta = 1.0 - a var bool warmup = true, var float e = 1.0 diff --git a/lib/volatility/natr/natr.pine b/lib/volatility/natr/natr.pine index f77a9025..5f0bf603 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) @@ -8,8 +8,6 @@ indicator("Normalized Average True Range", "NATR", overlay=false, format=format. //@returns The NATR value as a percentage of close price. //@optimized Beta precomputation for RMA warmup compensation natr(simple int length) => - if length <= 0 - runtime.error("Period must be greater than 0") float prevClose = nz(close[1], close) float tr1 = high - low float tr2 = math.abs(high - prevClose) diff --git a/lib/volatility/rsv/rsv.pine b/lib/volatility/rsv/rsv.pine index 816c74aa..d6d544dc 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) @@ -9,10 +9,6 @@ indicator("Rogers-Satchell Volatility (RSV)", "RSV", overlay=false) //@param annualPeriods Number of periods in a year for annualization. Default is 252 for daily data. //@returns float The Rogers-Satchell Volatility value. rsv(simple int length, simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") float h = math.max(high, 0.0000001) float l = math.max(low, 0.0000001) float o = math.max(open, 0.0000001) diff --git a/lib/volatility/rv/rv.pine b/lib/volatility/rv/rv.pine index 1dbd5b5a..d74227eb 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) @@ -10,10 +10,6 @@ indicator("Realized Volatility (RV)", "RV", overlay=false) //@param annualPeriods Number of periods (of the main chart's timeframe) in a year for annualization. Default is 252 (assuming daily chart). //@returns float The Realized Volatility value. rv(simple int length = 20, simple string intradayTimeframe = "5", simple bool annualize = true, simple int annualPeriods = 252) => - if length <= 0 - runtime.error("Length must be greater than 0") - if annualize and annualPeriods <= 0 - runtime.error("Annual periods must be greater than 0 if annualizing") intraday_closes_arr = request.security_lower_tf(syminfo.tickerid, intradayTimeframe, close) float sum_sq_log_returns = 0.0 if array.size(intraday_closes_arr) > 1 diff --git a/lib/volatility/rvi/Rvi.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.Validation.Tests.cs b/lib/volatility/rvi/Rvi.Validation.Tests.cs index 44a7043a..d2389229 100644 --- a/lib/volatility/rvi/Rvi.Validation.Tests.cs +++ b/lib/volatility/rvi/Rvi.Validation.Tests.cs @@ -258,28 +258,34 @@ public class RviValidationTests } /// - /// Validates TBar update uses only Close price. + /// Validates TBar update uses High and Low channels (revised 1995 algorithm), + /// producing a different result than single-price Close-only input. /// [Fact] - public void Rvi_TBar_UsesOnlyClose() + public void Rvi_TBar_UsesDualChannel_HighLow() { var bars = GenerateTestData(50); - // Using TBar + // Using TBar (revised: high + low dual-channel) var rviBar = new Rvi(10, 14); for (int i = 0; i < bars.Count; i++) { rviBar.Update(bars[i]); } - // Using just Close prices + // Using just Close prices (single-channel) var rviClose = new Rvi(10, 14); for (int i = 0; i < bars.Count; i++) { rviClose.Update(new TValue(bars[i].Time, bars[i].Close)); } - Assert.Equal(rviClose.Last.Value, rviBar.Last.Value, 10); + // TBar uses High/Low channels → different from Close-only + Assert.NotEqual(rviClose.Last.Value, rviBar.Last.Value); + + // Both should still be in valid range + Assert.True(rviBar.Last.Value >= 0 && rviBar.Last.Value <= 100); + Assert.True(rviClose.Last.Value >= 0 && rviClose.Last.Value <= 100); } // === Parameter Sensitivity === diff --git a/lib/volatility/rvi/Rvi.cs b/lib/volatility/rvi/Rvi.cs index 64be846b..313fc248 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,20 +45,13 @@ 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) @@ -74,22 +62,21 @@ public sealed class Rvi : AbstractBase { 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,50 +85,23 @@ 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) @@ -158,25 +118,27 @@ 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) @@ -193,10 +155,10 @@ 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); @@ -206,36 +168,57 @@ public sealed class Rvi : AbstractBase } [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,105 +226,63 @@ 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; + double mean = newSum / newCount; + double variance = (newSumSq / newCount) - (mean * mean); + variance = Math.Max(0.0, variance); + currentStdDev = Math.Sqrt(variance); } - else + + double upStdVal = 0.0; + double downStdVal = 0.0; + if (priceChange > 0) { - s = s with { LastValue = rviValue }; + upStdVal = currentStdDev; + } + else if (priceChange < 0) + { + downStdVal = currentStdDev; } - _s = s; + 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; - Last = new TValue(timeTicks, rviValue); - PubEvent(Last, isNew); - return Last; + 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++) @@ -349,21 +290,23 @@ public sealed class Rvi : AbstractBase 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) @@ -381,17 +324,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 +340,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, @@ -425,6 +361,62 @@ public sealed class Rvi : AbstractBase 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) { @@ -433,7 +425,6 @@ public sealed class Rvi : AbstractBase 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 +433,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,17 +442,14 @@ 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++; @@ -483,18 +470,15 @@ 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++; @@ -510,7 +494,6 @@ public sealed class Rvi : AbstractBase sum += price; sumSq += price * price; - // Population stddev double currentStdDev = 0.0; if (count > 1) { @@ -520,7 +503,6 @@ public sealed class Rvi : AbstractBase currentStdDev = Math.Sqrt(variance); } - // Classify by direction double upStdVal = 0.0; double downStdVal = 0.0; if (priceChange > 0) @@ -532,7 +514,6 @@ public sealed class Rvi : AbstractBase 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,7 +522,6 @@ 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; @@ -564,5 +544,4 @@ public sealed class Rvi : AbstractBase TSeries results = indicator.Update(source); return (results, indicator); } - } 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..c7af6486 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) @@ -9,10 +9,6 @@ indicator("Relative Volatility Index (RVI)", shorttitle="RVI", overlay=false) //@param rmaLength The lookback period for Wilder's smoothing (RMA) of the upward and downward standard deviations. Default is 14. //@returns float The Relative Volatility Index value. rvi(series float src = close, simple int stdevLength = 10, simple int rmaLength = 14) => - if stdevLength <= 1 - runtime.error("Standard Deviation Length must be greater than 1") - if rmaLength <= 0 - runtime.error("RMA Length must be greater than 0") float currentStdDev = 0.0 var array buffer_stddev = array.new_float(stdevLength, na) // p_stddev simplified var int head_stddev = 0, var int count_stddev = 0 diff --git a/lib/volatility/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..70e96ac1 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) @@ -8,8 +8,6 @@ indicator("Ulcer Index (UI)", shorttitle="UI", format=format.price, precision=2, //@param period The lookback period. Default is 14. //@returns float The Ulcer Index value. ui(series float src, int period) => - if period <= 0 - runtime.error("Period must be greater than 0") var deque = array.new_int(0) var src_buffer = array.new_float(period, na) var int current_index = 0 diff --git a/lib/volatility/vov/vov.pine b/lib/volatility/vov/vov.pine index 3d1aaef2..b854047a 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) @@ -9,8 +9,6 @@ indicator("Volatility of Volatility (VOV)", shorttitle="VOV", format=format.pric //@param vovPeriod The lookback period for calculating the standard deviation of the volatility series. Default is 10. //@returns float The VOV value. vov(series float src, int volatilityPeriod, int vovPeriod) => - if volatilityPeriod <= 0 or vovPeriod <= 0 - runtime.error("Periods must be greater than 0") var int p1 = 0 var array buffer1 = array.new_float(0) var int head1 = 0, var int count1 = 0 diff --git a/lib/volatility/vr/vr.pine b/lib/volatility/vr/vr.pine index f51a7653..a8933984 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) @@ -9,8 +9,6 @@ indicator("Volatility Ratio (VR)", shorttitle="VR", format=format.price, precisi //@param atrPeriod The lookback period for ATR. Must be > 0. //@returns float The Volatility Ratio value for the current bar. vr(int atrPeriod) => - if atrPeriod <= 0 - runtime.error("ATR Period must be greater than 0") var float EPSILON_ATR = 1e-10 var float raw_atr = 0.0 var float e_compensator = 1.0 diff --git a/lib/volatility/yzv/yzv.pine b/lib/volatility/yzv/yzv.pine index e63515a9..fc4652e6 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) @@ -8,9 +8,6 @@ indicator("Yang-Zhang Volatility (YZV)", shorttitle="YZV", overlay=false) //@returns Yang-Zhang Volatility value for the current bar //@optimized Uses bias-corrected RMA with OHLC prices for O(1) complexity per bar yzv(int length) => - if length <= 0 - runtime.error("Length must be greater than 0 for YZV calculation.") - float(na) o=open,h=high,l=low,c=close,pc=na(close[1])?open:close[1] ro=math.log(o/pc),rc=math.log(c/o),rh=math.log(h/o),rl=math.log(l/o) s_o_sq=ro*ro,s_c_sq=rc*rc diff --git a/lib/volume/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..b4c4a1f0 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) @@ -9,8 +9,6 @@ indicator("Chaikin A/D Oscillator (ADOSC)", "ADOSC", overlay=false) //@returns (float) The ADOSC value for the current bar (difference between short and long EMAs of ADL) adosc(simple int shortPeriod, simple int longPeriod) => float EPSILON = 1e-10 - if shortPeriod <= 0 or longPeriod <= 0 - runtime.error("Periods must be greater than 0") short_alpha = 2.0 / (shortPeriod + 1) long_alpha = 2.0 / (longPeriod + 1) one_minus_long_alpha = 1.0 - long_alpha @@ -36,4 +34,4 @@ longPeriod = input.int(10, "Long Period", minval=1) osc = adosc(shortPeriod, longPeriod) // ---------- Plotting ---------- -plot(osc, "ADOSC", color.new(color.yellow, 0, color=color.yellow, linewidth=2), linewidth=2) +plot(osc, "ADOSC", color=color.yellow, linewidth=2) diff --git a/lib/volume/aobv/aobv.pine b/lib/volume/aobv/aobv.pine index d959e02b..5e6c2728 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) @@ -25,7 +25,7 @@ aobv(src, vol) => resArr = array.new_float(2, na) for i = 0 to array.size(periods) - 1 period = array.get(periods, i) - alpha = 2.0 / math.max(period, 1) + alpha = 2.0 / (period + 1) beta = array.get(betaArr, i) if beta == 0.0 beta := 1.0 - alpha @@ -50,8 +50,8 @@ aobv(src, vol) => [array.get(resArr, 0), array.get(resArr, 1)] // ---------- Inputs ---------- -src = input(close, "Source") -vol = input(volume, "Volume") +src = input.source(close, "Source") +vol = input.source(volume, "Volume") // ---------- Calculations ---------- [aobvFast, aobvSlow] = aobv(src, vol) diff --git a/lib/volume/cmf/cmf.pine b/lib/volume/cmf/cmf.pine index bc8eca7b..9d1b834f 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) @@ -11,10 +11,6 @@ indicator("Chaikin Money Flow (CMF)", "CMF", overlay=false) //@param src_vol The volume (default: built-in volume) //@returns float CMF value between -1 and 1 cmf(len = 20, src_high = high, src_low = low, src_close = close, src_vol = volume) => - // Validate parameters - if len < 1 - runtime.error("Length must be >= 1") - // Calculate Money Flow Multiplier float mfm = 0.0 if not na(src_high) and not na(src_low) and not na(src_close) diff --git a/lib/volume/efi/efi.pine b/lib/volume/efi/efi.pine index 603fc80a..3132507e 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) @@ -9,8 +9,6 @@ indicator("Elder's Force Index (EFI)", "EFI", overlay=false) //@param src_vol The volume (default: built-in volume) //@returns float The smoothed Force Index value efi(len = 13, src = close, src_vol = volume) => - if len < 1 - runtime.error("Length must be >= 1") var float prev_src = src float raw_force = (nz(src) - nz(prev_src)) * nz(src_vol) prev_src := src diff --git a/lib/volume/eom/eom.pine b/lib/volume/eom/eom.pine index 730e14e6..7dadd84b 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) @@ -9,8 +9,6 @@ indicator("Ease of Movement (EOM)", "EOM", overlay=false) //@returns float Ease of Movement value //@optimized for performance and dirty data eom(i_smoothing, i_vol_scale, i_high=high, i_low=low, i_volume=volume) => - if i_smoothing < 1 or i_vol_scale <= 0 - runtime.error("Smoothing or Volume scale out or range") var float oldMidPoint = na midPoint = (i_high + i_low) * 0.5 midPointChange = midPoint - nz(oldMidPoint, midPoint) diff --git a/lib/volume/evwma/evwma.pine b/lib/volume/evwma/evwma.pine index a7666394..f2950306 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) @@ -10,8 +10,6 @@ indicator("Elastic Volume Weighted Moving Average (EVWMA)", "EVWMA", overlay=tru //@returns EVWMA value where high-volume bars get more weight (faster response) //@optimized O(1) per bar via circular buffer for running volume sum evwma(series float src, series float vol, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array vol_buffer = array.new_float(p, 0.0) var float sum_vol = 0.0 diff --git a/lib/volume/iii/iii.pine b/lib/volume/iii/iii.pine index 3e0e4319..c2ba6680 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) @@ -28,7 +28,7 @@ iii(simple int period, simple bool cumulative=false, series float h=high, series valid_count := not na(raw_iii) ? valid_count + 1 : valid_count array.set(buffer, head, raw_iii) head := (head + 1) % period - smoothed_iii = sum / period + smoothed_iii = valid_count > 0 ? sum / valid_count : 0.0 cumulative_value := cumulative_value + raw_iii cumulative ? cumulative_value : smoothed_iii diff --git a/lib/volume/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..8c858072 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) @@ -12,8 +12,6 @@ indicator("Money Flow Index (MFI)", "MFI", overlay=false) //@returns float The MFI value (0-100) //@optimized Uses circular buffers for O(1) performance with proper NA handling mfi(simple int len, series float src_high=high, series float src_low=low, series float src_close=close, series float src_vol=volume) => - if len < 1 - runtime.error("Invalid parameter: len must be >= 1") float typical_price = (src_high + src_low + src_close) / 3.0 float raw_money_flow = typical_price * nz(src_vol, 0.0) float prev_typical_price = nz(typical_price[1], typical_price) diff --git a/lib/volume/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..4c0cb18f 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) @@ -12,8 +12,6 @@ indicator("Price Volume Divergence (PVD)", "PVD", overlay=false) //@returns Smoothed divergence value //@optimized for performance and dirty data pvd(simple int price_period, simple int volume_period, simple int smoothing_period, series float c=close, series float vol=volume ) => - if smoothing_period <= 0 - runtime.error("Smoothing period must be greater than 0") float close_price = nz(c, close) float volume_val = math.max(nz(vol, 0.0), 1.0) float prev_close = bar_index < price_period ? close_price[math.max(bar_index, 1)] : close_price[price_period] diff --git a/lib/volume/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..35813488 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) @@ -11,10 +11,6 @@ indicator("Percentage Volume Oscillator (PVO)", "PVO", overlay=false) //@returns tuple with [pvo, signal, histogram] values //@optimized Beta precomputation for EMA warmup compensation pvo(series float vol, simple int fast_period, simple int slow_period, simple int signal_period) => - if fast_period <= 0 or slow_period <= 0 or signal_period <= 0 - runtime.error("All periods must be greater than 0") - if fast_period >= slow_period - runtime.error("Fast period must be less than slow period") float vol_val = nz(vol, 0.0) float fast_alpha = 2.0 / (fast_period + 1) float slow_alpha = 2.0 / (slow_period + 1) diff --git a/lib/volume/pvr/pvr.pine b/lib/volume/pvr/pvr.pine index 174b4bf3..e39231d3 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) @@ -22,4 +22,4 @@ i_price_source = input.source(close, "Price Source") pvr_value = pvr(i_price_source) // Plot -plot(pvr_value, "PVR", color=color.yellow, linewidth=2, plot.style_stepline) +plot(pvr_value, "PVR", color=color.yellow, linewidth=2, style=plot.style_stepline) diff --git a/lib/volume/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..fbf8eb09 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) @@ -11,8 +11,6 @@ indicator("Volume Oscillator (VO)", "VO", overlay=false) //@returns Volume Oscillator value //@optimized for performance and dirty data vo(simple int short_period, simple int long_period, simple int signal_period, series float vol=volume) => - if short_period >= long_period - runtime.error("Short period must be less than long period") volume_val = math.max(nz(vol, 0.0), 1.0) var p_short = short_period var buffer_short = array.new_float(p_short, na) diff --git a/lib/volume/vroc/vroc.pine b/lib/volume/vroc/vroc.pine index 347367c4..bf6163d7 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) @@ -10,9 +10,6 @@ indicator("Volume Rate of Change (VROC)", "VROC", overlay=false) //@returns Volume Rate of Change value //@optimized for performance and dirty data vroc(simple int period, simple bool calc_type, series float vol = volume) => - if period <= 0 - runtime.error("Period must be greater than 0") - float current_volume = vol float historical_volume = vol[period] if na(current_volume) or na(historical_volume) diff --git a/lib/volume/vwad/vwad.pine b/lib/volume/vwad/vwad.pine index aeed0f5d..234ca35e 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) @@ -12,8 +12,6 @@ indicator("Volume Weighted Accumulation/Distribution (VWAD)", "VWAD", overlay=fa //@returns VWAD value representing volume-weighted accumulation/distribution //@optimized for performance and dirty data vwad(simple int period, series float src_high = high, series float src_low = low, series float src_close = close, series float src_vol = volume) => - if period <= 0 - runtime.error("Period must be greater than 0") var int p = math.max(1, period), var int head = 0 var array vol_buffer = array.new_float(p, na) var float sum_vol = 0.0 diff --git a/lib/volume/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..a2aedf59 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) @@ -10,8 +10,6 @@ indicator("Volume Weighted Moving Average (VWMA)", "VWMA", overlay=true) //@returns VWMA value representing volume-weighted moving average //@optimized for performance and dirty data vwma(series float src, series float vol, simple int period) => - if period <= 0 - runtime.error("Period must be greater than 0") var int p = math.max(1, period), var int head = 0, var int count = 0 var array price_buffer = array.new_float(p, na) var array vol_buffer = array.new_float(p, na) 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