mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 09:08:04 +00:00
filters update
This commit is contained in:
@@ -78,3 +78,4 @@ plans/
|
||||
|
||||
# SonarLint IDE plugin
|
||||
.sonarlint/
|
||||
.obsidian/
|
||||
@@ -7,7 +7,7 @@
|
||||
[](https://www.nuget.org/packages/QuanTAlib/)
|
||||
[](https://dotnet.microsoft.com/en-us/download/dotnet)
|
||||
|
||||
[](lib/_index.md)
|
||||
[](lib/_index.md)
|
||||
[](ndepend/ndependout/ndependreport.html)
|
||||
[](ndepend/ndependout/ndependreport.html)
|
||||
[](ndepend/ndependout/ndependreport.html)
|
||||
@@ -35,23 +35,24 @@ TA libraries face a fundamental choice: accept approximations for simplicity OR
|
||||
|
||||
| Category | Count | What It Measures | Representative Indicators |
|
||||
| -------- | :---: | ---------------- | ------------------------- |
|
||||
| [**Trends (FIR)**](lib/trends_FIR/_index.md) | 18 | Finite Impulse Response moving averages | SMA, WMA, HMA, ALMA, TRIMA, LSMA, TSF |
|
||||
| [**Trends (IIR)**](lib/trends_IIR/_index.md) | 27 | Infinite Impulse Response moving averages | EMA, DEMA, TEMA, T3, JMA, KAMA, VIDYA |
|
||||
| [**Filters**](lib/filters/_index.md) | 31 | Signal processing and noise reduction filters | Bessel, Butterworth, Gaussian, Savitzky-Golay, Ehlers Super Smoother |
|
||||
| [**Oscillators**](lib/oscillators/_index.md) | 20 | Indicators that fluctuate around a center line | RSI, MACD, Stochastic, AO, APO, CCI, Ultimate Oscillator |
|
||||
| [**Dynamics**](lib/dynamics/_index.md) | 18 | Trend strength and direction indicators | ADX, Aroon, SuperTrend, Vortex, Chop, Ichimoku |
|
||||
| [**Momentum**](lib/momentum/_index.md) | 16 | Speed and magnitude of price changes | Momentum, ROC, Velocity, RSX, Qstick, KDJ |
|
||||
| [**Volatility**](lib/volatility/_index.md) | 26 | Size and variability of price movements | ATR, Bollinger Band Width, Historical Volatility, True Range |
|
||||
| [**Volume**](lib/volume/_index.md) | 27 | Trading activity and price-volume relationships | OBV, VWAP, MFI, ADL, CMF, TVI, Force Index |
|
||||
| [**Statistics**](lib/statistics/_index.md) | 30 | Statistical measures and tests | Correlation, Variance, StdDev, Skewness, Kurtosis, Z-Score |
|
||||
| [**Channels**](lib/channels/_index.md) | 23 | Price boundaries and range definitions | Bollinger Bands, Keltner Channels, Donchian Channels |
|
||||
| [**Cycles**](lib/cycles/_index.md) | 14 | Cycle analysis and signal processing | Hilbert Transform, Homodyne, Phasor, Ehlers Sine Wave |
|
||||
| [**Reversals**](lib/reversals/_index.md) | 12 | Pattern recognition and reversal detection | Pivot Points, Fractals, Swings, Pivot Components |
|
||||
| [**Forecasts**](lib/forecasts/_index.md) | 1 | Predictive indicators and projections | Time Series Forecast, AFIRMA, Chande Forecast Oscillator |
|
||||
| [**Errors**](lib/errors/_index.md) | 26 | Error metrics and loss functions | RMSE, MAE, MAPE, SMAPE, MASE, R-Squared |
|
||||
| [**Numerics**](lib/numerics/_index.md) | 15 | Mathematical transformations | Log, Exp, Sqrt, Tanh, ReLU, Sigmoid |
|
||||
| [**Trends (FIR)**](lib/trends_FIR/_index.md) | 24 | Finite Impulse Response moving averages | SMA, WMA, HMA, ALMA, TRIMA, LSMA, TSF |
|
||||
| [**Trends (IIR)**](lib/trends_IIR/_index.md) | 32 | Infinite Impulse Response moving averages | EMA, DEMA, TEMA, T3, JMA, KAMA, VIDYA |
|
||||
| [**Filters**](lib/filters/_index.md) | 16 | Signal processing and noise reduction filters | Bessel, Butterworth, Gaussian, Savitzky-Golay, Ehlers Super Smoother |
|
||||
| [**Oscillators**](lib/oscillators/_index.md) | 16 | Indicators that fluctuate around a center line | RSI, MACD, Stochastic, AO, APO, CCI, Ultimate Oscillator |
|
||||
| [**Dynamics**](lib/dynamics/_index.md) | 14 | Trend strength and direction indicators | ADX, Aroon, SuperTrend, Vortex, Chop, Ichimoku |
|
||||
| [**Momentum**](lib/momentum/_index.md) | 19 | Speed and magnitude of price changes | Momentum, ROC, Velocity, RSX, Qstick, KDJ |
|
||||
| [**Volatility**](lib/volatility/_index.md) | 20 | Size and variability of price movements | ATR, Bollinger Band Width, Historical Volatility, True Range |
|
||||
| [**Volume**](lib/volume/_index.md) | 15 | Trading activity and price-volume relationships | OBV, VWAP, MFI, ADL, CMF, TVI, Force Index |
|
||||
| [**Statistics**](lib/statistics/_index.md) | 13 | Statistical measures and tests | Correlation, Variance, StdDev, Skewness, Kurtosis, Z-Score |
|
||||
| [**Channels**](lib/channels/_index.md) | 18 | Price boundaries and range definitions | Bollinger Bands, Keltner Channels, Donchian Channels |
|
||||
| [**Cycles**](lib/cycles/_index.md) | 5 | Cycle analysis and signal processing | Hilbert Transform, Homodyne, Phasor, Ehlers Sine Wave |
|
||||
| [**Reversals**](lib/reversals/_index.md) | 7 | Pattern recognition and reversal detection | Pivot Points, Fractals, Swings, Pivot Components |
|
||||
| [**Forecasts**](lib/forecasts/_index.md) | 4 | Predictive indicators and projections | Time Series Forecast, AFIRMA, Chande Forecast Oscillator |
|
||||
| [**Errors**](lib/errors/_index.md) | 13 | Error metrics and loss functions | RMSE, MAE, MAPE, SMAPE, MASE, R-Squared |
|
||||
| [**Numerics**](lib/numerics/_index.md) | 5 | Mathematical transformations | Log, Exp, Sqrt, Tanh, ReLU, Sigmoid |
|
||||
| [**Core**](lib/core/_index.md) | 8 | Price transforms and fundamental building blocks | AVGPRICE, MEDPRICE, MIDPRICE, TYPPRICE, WCLPRICE |
|
||||
|
||||
**[Browse all 305 indicators →](lib/_index.md)**
|
||||
**[Browse all 229 indicators →](lib/_index.md)**
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
+22
-15
@@ -43,7 +43,7 @@
|
||||
* [FRAMA - Ehlers Fractal Adaptive MA](/lib/trends_IIR/frama/Frama.md)
|
||||
* [HEMA - Hull Exponential MA](/lib/trends_IIR/hema/Hema.md)
|
||||
* [HOLT - Holt Exponential Smoothing](/lib/trends_IIR/holt/Holt.md)
|
||||
* [HTIT - Ehlers Hilbert Transform Instant Trendline](/lib/trends_IIR/htit/Htit.md)
|
||||
* [HTIT - Ehlers Hilbert Transform Instant Trendline (also known as HT_TRENDLINE)](/lib/trends_IIR/htit/Htit.md)
|
||||
* [HWMA - Holt-Winters MA](/lib/trends_IIR/hwma/Hwma.md)
|
||||
* [JMA - Jurik MA](/lib/trends_IIR/jma/Jma.md)
|
||||
* [KAMA - Kaufman Adaptive MA](/lib/trends_IIR/kama/Kama.md)
|
||||
@@ -55,7 +55,6 @@
|
||||
* [MMA - Modified MA](/lib/trends_IIR/mma/Mma.md)
|
||||
* [QEMA - Quadruple Exponential MA](/lib/trends_IIR/qema/Qema.md)
|
||||
* [REMA - Regularized Exponential MA](/lib/trends_IIR/rema/Rema.md)
|
||||
* [REVERSEEMA - Reverse EMA](/lib/trends_IIR/reverseema/ReverseEma.md)
|
||||
* [RGMA - Recursive Gaussian MA](/lib/trends_IIR/rgma/Rgma.md)
|
||||
* [RMA - Rolling MA](/lib/trends_IIR/rma/Rma.md)
|
||||
* [T3 - Tillson T3 MA](/lib/trends_IIR/t3/T3.md)
|
||||
@@ -69,6 +68,7 @@
|
||||
|
||||
* **Filters**
|
||||
* [Overview](/lib/filters/_index.md)
|
||||
* [AGC - Ehlers Automatic Gain Control](/lib/filters/agc/Agc.md)
|
||||
* [ALAGUERRE - Ehlers Adaptive Laguerre Filter](/lib/filters/alaguerre/ALaguerre.md)
|
||||
* [BAXTERKING - Baxter-King Band-Pass Filter](/lib/filters/baxterking/BaxterKing.md)
|
||||
* [CFITZ - Christiano-Fitzgerald Filter](/lib/filters/cfitz/Cfitz.md)
|
||||
@@ -90,7 +90,10 @@
|
||||
* [LMS - Least Mean Squares](/lib/filters/lms/Lms.md)
|
||||
* [RLS - Recursive Least Squares](/lib/filters/rls/Rls.md)
|
||||
* [LOESS - LOESS Smoothing](/lib/filters/loess/Loess.md)
|
||||
* [MODF - Modular Filter](/lib/filters/modf/Modf.md)
|
||||
* [NOTCH - Notch Filter](/lib/filters/notch/Notch.md)
|
||||
* [NW - Nadaraya-Watson Estimator](/lib/filters/nw/Nw.md)
|
||||
* [RMED - Ehlers Recursive Median Filter](/lib/filters/rmed/Rmed.md)
|
||||
* [ONEEURO - One Euro Filter](/lib/filters/oneeuro/OneEuro.md)
|
||||
* [ROOFING - Ehlers Roofing Filter](/lib/filters/roofing/Roofing.md)
|
||||
* [SGF - Savitzky-Golay Filter](/lib/filters/sgf/Sgf.md)
|
||||
@@ -115,7 +118,6 @@
|
||||
* [DX - Directional Movement Index](/lib/dynamics/dx/Dx.md)
|
||||
* [HT_TRENDMODE - Ehlers Hilbert Transform Trend vs Cycle Mode](/lib/dynamics/ht_trendmode/HtTrendmode.md)
|
||||
* [ICHIMOKU - Ichimoku Cloud](/lib/dynamics/ichimoku/Ichimoku.md)
|
||||
* [IMI - Intraday Momentum Index](/lib/dynamics/imi/Imi.md)
|
||||
* [IMPULSE - Elder Impulse System](/lib/dynamics/impulse/Impulse.md)
|
||||
* [QSTICK - Qstick Indicator](/lib/dynamics/qstick/Qstick.md)
|
||||
* [SUPER - SuperTrend](/lib/dynamics/super/Super.md)
|
||||
@@ -134,11 +136,14 @@
|
||||
* [DECO - Ehlers Decycler Oscillator](/lib/oscillators/deco/Deco.md)
|
||||
* [DPO - Detrended Price Oscillator](/lib/oscillators/dpo/Dpo.md)
|
||||
* [FISHER - Ehlers Fisher Transform](/lib/oscillators/fisher/Fisher.md)
|
||||
* [IMI - Intraday Momentum Index](/lib/oscillators/imi/Imi.md)
|
||||
* [INERTIA - Inertia](/lib/oscillators/inertia/Inertia.md)
|
||||
* [KDJ - KDJ Indicator](/lib/oscillators/kdj/Kdj.md)
|
||||
* [PGO - Pretty Good Oscillator](/lib/oscillators/pgo/Pgo.md)
|
||||
* [REFLEX - Ehlers Reflex](/lib/oscillators/reflex/Reflex.md)
|
||||
* [REVERSEEMA - Ehlers Reverse EMA](/lib/oscillators/reverseema/ReverseEma.md)
|
||||
* [SMI - Stochastic Momentum Index](/lib/oscillators/smi/Smi.md)
|
||||
* [STC - Schaff Trend Cycle](/lib/oscillators/stc/Stc.md)
|
||||
* [STOCH - Stochastic Oscillator](/lib/oscillators/stoch/Stoch.md)
|
||||
* [STOCHF - Stochastic Fast](/lib/oscillators/stochf/Stochf.md)
|
||||
* [STOCHRSI - Stochastic RSI](/lib/oscillators/stochrsi/Stochrsi.md)
|
||||
@@ -150,7 +155,7 @@
|
||||
|
||||
* **Momentum**
|
||||
* [Overview](/lib/momentum/_index.md)
|
||||
* [APO - Absolute Price Oscillator](/lib/momentum/apo/Apo.md)
|
||||
* [BIAS - Bias / Disparity Index](/lib/momentum/bias/Bias.md)
|
||||
* [BOP - Balance of Power](/lib/momentum/bop/Bop.md)
|
||||
* [CCI - Commodity Channel Index](/lib/momentum/cci/Cci.md)
|
||||
* [CFB - Jurik Composite Fractal Behavior](/lib/momentum/cfb/Cfb.md)
|
||||
@@ -174,7 +179,6 @@
|
||||
* [ADR - Average Daily Range](/lib/volatility/adr/Adr.md)
|
||||
* [ATR - Average True Range](/lib/volatility/atr/Atr.md)
|
||||
* [ATRN - ATR Normalized](/lib/volatility/atrn/Atrn.md)
|
||||
* [ATRP - ATR Percent](/lib/volatility/atrp/Atrp.md)
|
||||
* [BBW - Bollinger Band Width](/lib/volatility/bbw/Bbw.md)
|
||||
* [BBWN - Bollinger Band Width Normalized](/lib/volatility/bbwn/Bbwn.md)
|
||||
* [BBWP - Bollinger Band Width Percentile](/lib/volatility/bbwp/Bbwp.md)
|
||||
@@ -258,7 +262,6 @@
|
||||
* [Overview](/lib/statistics/_index.md)
|
||||
* [ACF - Autocorrelation Function](/lib/statistics/acf/Acf.md)
|
||||
* [BETA - Beta Coefficient](/lib/statistics/beta/Beta.md)
|
||||
* [BIAS - Bias](/lib/statistics/bias/Bias.md)
|
||||
* [CMA - Cumulative MA](/lib/statistics/cma/Cma.md)
|
||||
* [COINTEGRATION - Cointegration](/lib/statistics/cointegration/Cointegration.md)
|
||||
* [CORRELATION - Correlation](/lib/statistics/correlation/Correlation.md)
|
||||
@@ -287,10 +290,18 @@
|
||||
* [ZSCORE - Z-score](/lib/statistics/zscore/Zscore.md)
|
||||
* [ZTEST - Z-Test](/lib/statistics/ztest/Ztest.md)
|
||||
|
||||
* **Core**
|
||||
* [Overview](/lib/core/_index.md)
|
||||
* [AVGPRICE - Average Price](/lib/core/avgprice/Avgprice.md)
|
||||
* [MEDPRICE - Median Price](/lib/core/medprice/Medprice.md)
|
||||
* [MIDPOINT - Rolling Range Midpoint](/lib/core/midpoint/Midpoint.md)
|
||||
* [MIDPRICE - Midpoint Price](/lib/core/midprice/Midprice.md)
|
||||
* [TYPPRICE - Typical Price](/lib/core/typprice/Typprice.md)
|
||||
* [WCLPRICE - Weighted Close Price](/lib/core/wclprice/Wclprice.md)
|
||||
|
||||
* **Numerics**
|
||||
* [Overview](/lib/numerics/_index.md)
|
||||
* [ACCEL - Acceleration](/lib/numerics/accel/Accel.md)
|
||||
* [AGC - Ehlers Automatic Gain Control](/lib/numerics/agc/Agc.md)
|
||||
* [CHANGE - Percentage Change](/lib/numerics/change/Change.md)
|
||||
* [EXPTRANS - Exponential Transform](/lib/numerics/exptrans/Exptrans.md)
|
||||
* [HIGHEST - Rolling Maximum](/lib/numerics/highest/Highest.md)
|
||||
@@ -298,13 +309,11 @@
|
||||
* [LINEARTRANS - Linear Transform](/lib/numerics/lineartrans/Lineartrans.md)
|
||||
* [LOGTRANS - Logarithmic Transform](/lib/numerics/logtrans/Logtrans.md)
|
||||
* [LOWEST - Rolling Minimum](/lib/numerics/lowest/Lowest.md)
|
||||
* [MIDPOINT - Midrange](/lib/numerics/midpoint/Midpoint.md)
|
||||
* [NORMALIZE - Min-Max Normalization](/lib/numerics/normalize/Normalize.md)
|
||||
* [RELU - Rectified Linear Unit](/lib/numerics/relu/Relu.md)
|
||||
* [SIGMOID - Logistic Function](/lib/numerics/sigmoid/Sigmoid.md)
|
||||
* [SLOPE - Rate of Change](/lib/numerics/slope/Slope.md)
|
||||
* [SLOPE - First Derivative](/lib/numerics/slope/Slope.md)
|
||||
* [SQRTTRANS - Square Root Transform](/lib/numerics/sqrttrans/Sqrttrans.md)
|
||||
* [STANDARDIZE - Z-Score Normalization](/lib/numerics/standardize/Standardize.md)
|
||||
|
||||
* **Errors**
|
||||
* [Overview](/lib/errors/_index.md)
|
||||
@@ -323,7 +332,7 @@
|
||||
* [MSE - Mean Squared Error](/lib/errors/mse/Mse.md)
|
||||
* [MSLE - Mean Squared Logarithmic Error](/lib/errors/msle/Msle.md)
|
||||
* [PSEUDOHUBER - Pseudo-Huber Loss](/lib/errors/pseudohuber/PseudoHuber.md)
|
||||
* [QUANTILE - Quantile Loss](/lib/errors/quantile/QuantileLoss.md)
|
||||
* [QUANTILE - Quantile Loss](/lib/errors/quantileloss/QuantileLoss.md)
|
||||
* [RAE - Relative Absolute Error](/lib/errors/rae/Rae.md)
|
||||
* [RMSE - Root Mean Squared Error](/lib/errors/rmse/Rmse.md)
|
||||
* [RMSLE - Root Mean Squared Logarithmic Error](/lib/errors/rmsle/Rmsle.md)
|
||||
@@ -331,7 +340,7 @@
|
||||
* [RSQUARED - Coefficient of Determination](/lib/errors/rsquared/Rsquared.md)
|
||||
* [SMAPE - Symmetric Mean Absolute Percentage Error](/lib/errors/smape/Smape.md)
|
||||
* [THEILU - Theil's U Statistic](/lib/errors/theilu/TheilU.md)
|
||||
* [TUKEY - Tukey Biweight Loss](/lib/errors/tukey/TukeyBiweight.md)
|
||||
* [TUKEY - Tukey Biweight Loss](/lib/errors/tukeybiweight/TukeyBiweight.md)
|
||||
* [WMAPE - Weighted Mean Absolute Percentage Error](/lib/errors/wmape/Wmape.md)
|
||||
* [WRMSE - Weighted RMSE](/lib/errors/wrmse/Wrmse.md)
|
||||
|
||||
@@ -351,12 +360,10 @@
|
||||
* [HT_DCPERIOD - Ehlers Hilbert Transform Dominant Cycle Period](/lib/cycles/ht_dcperiod/HtDcperiod.md)
|
||||
* [HT_DCPHASE - Ehlers Hilbert Transform Dominant Cycle Phase](/lib/cycles/ht_dcphase/HtDcphase.md)
|
||||
* [HT_PHASOR - Ehlers Hilbert Transform Phasor Components](/lib/cycles/ht_phasor/HtPhasor.md)
|
||||
* [HT_SINE - Ehlers Hilbert Transform SineWave](/lib/cycles/ht_sine/HtSine.md)
|
||||
* [HT_SINE - Ehlers Hilbert Transform SineWave (also known as SINE)](/lib/cycles/ht_sine/HtSine.md)
|
||||
* [LUNAR - Lunar Phase](/lib/cycles/lunar/Lunar.md)
|
||||
* [SINE - Ehlers Sine Wave](/lib/cycles/sine/Sine.md)
|
||||
* [SOLAR - Solar Activity Cycle](/lib/cycles/solar/Solar.md)
|
||||
* [SSFDSP - Ehlers SSF Detrended Synthetic Price](/lib/cycles/ssfdsp/Ssfdsp.md)
|
||||
* [STC - Schaff Trend Cycle](/lib/cycles/stc/Stc.md)
|
||||
|
||||
* **Reversals**
|
||||
* [Overview](/lib/reversals/_index.md)
|
||||
|
||||
+27
-14
@@ -6,6 +6,7 @@ QuanTAlib provides technical indicators organized into mathematical families. Un
|
||||
|
||||
| Category | What It Measures | Representative Indicators | When to Reach for It |
|
||||
| :------- | :--------------- | :------------------------ | :------------------- |
|
||||
| [**Core**](../lib/core/_index.md) | Price transforms and fundamental building blocks | AVGPRICE, MEDPRICE, MIDPRICE, TYPPRICE, WCLPRICE, MIDPOINT | Derived prices from OHLCV bars. Inputs to higher-order indicators. |
|
||||
| [**Trends (FIR)**](../lib/trends_FIR/_index.md) | Trend direction via finite impulse response filters | SMA, WMA, ALMA, HMA, LSMA | Trend identification with predictable lag and finite memory. Output depends only on a fixed window of past prices. |
|
||||
| [**Trends (IIR)**](../lib/trends_IIR/_index.md) | Trend direction via infinite impulse response filters | EMA, DEMA, TEMA, JMA, KAMA, MAMA | Trend identification with recursive calculation and theoretically infinite memory. More responsive per unit of smoothness. |
|
||||
| [**Filters**](../lib/filters/_index.md) | Signal processing filters for noise reduction | Bessel, Butterworth, Super Smoother | Removing noise while preserving trend structure. Designed by engineers, borrowed by traders. |
|
||||
@@ -19,7 +20,7 @@ QuanTAlib provides technical indicators organized into mathematical families. Un
|
||||
| [**Numerics**](../lib/numerics/_index.md) | Mathematical transformations and signal processing | Slope, Accel, Normalize, Sigmoid | Custom indicator development and advanced signal processing. Building blocks for novel indicators. |
|
||||
| [**Errors**](../lib/errors/_index.md) | Measurement accuracy and model fit quality | MAE, RMSE, R², Huber | Model validation and forecast assessment. Quantifying wrongness before production quantifies losses. |
|
||||
| [**Forecasts**](../lib/forecasts/_index.md) | Future price prediction and projection | AFIRMA | Projecting price based on historical patterns. Predictions that invite humility. |
|
||||
| [**Cycles**](../lib/cycles/_index.md) | Periodic patterns and dominant frequencies | Hilbert Transform, EBSW, STC | Identifying cyclical market behavior. Markets exhibit cycles; detecting them reliably remains hard. |
|
||||
| [**Cycles**](../lib/cycles/_index.md) | Periodic patterns and dominant frequencies | Hilbert Transform, EBSW | Identifying cyclical market behavior. Markets exhibit cycles; detecting them reliably remains hard. |
|
||||
| [**Reversals**](../lib/reversals/_index.md) | Turning points and stop levels | Pivot Points, PSAR, Chandelier, Swings | Identifying potential trend reversals, computing adaptive stops, and defining support/resistance. |
|
||||
|
||||
## Selection by Experience Level
|
||||
@@ -74,7 +75,7 @@ Infinite Impulse Response filters. Output depends on current input and past outp
|
||||
| [**FRAMA**](../lib/trends_IIR/frama/Frama.md) | Ehlers Fractal Adaptive MA | Dimension-based adaptation |
|
||||
| [**HEMA**](../lib/trends_IIR/hema/Hema.md) | Hull Exponential MA | Hull concept with EMA |
|
||||
| [**HOLT**](../lib/trends_IIR/holt/Holt.md) | Holt Exponential Smoothing | Double exponential smoothing (level + trend) |
|
||||
| [**HTIT**](../lib/trends_IIR/htit/Htit.md) | Ehlers Hilbert Instantaneous Trend | Dominant cycle extraction |
|
||||
| [**HTIT**](../lib/trends_IIR/htit/Htit.md) | Ehlers Hilbert Instantaneous Trend (also known as HT_TRENDLINE) | Dominant cycle extraction |
|
||||
| [**JMA**](../lib/trends_IIR/jma/Jma.md) | Jurik MA | Adaptive, low-lag, proprietary algorithm |
|
||||
| [**KAMA**](../lib/trends_IIR/kama/Kama.md) | Kaufman Adaptive MA | Efficiency ratio adaptation |
|
||||
| [**MAMA**](../lib/trends_IIR/mama/Mama.md) | Ehlers MESA Adaptive MA | Homodyne discriminator based |
|
||||
@@ -85,7 +86,6 @@ Infinite Impulse Response filters. Output depends on current input and past outp
|
||||
| [**HWMA**](../lib/trends_IIR/hwma/Hwma.md) | Holt-Winters MA | Triple exponential smoothing (IIR) |
|
||||
| [**QEMA**](../lib/trends_IIR/qema/Qema.md) | Quad Exponential MA | Four-stage exponential |
|
||||
| [**REMA**](../lib/trends_IIR/rema/Rema.md) | Regularized Exponential MA | Regularization for stability |
|
||||
| [**REVERSEEMA**](../lib/trends_IIR/reverseema/ReverseEma.md) | Reverse EMA | Inverse EMA deconvolution |
|
||||
| [**RGMA**](../lib/trends_IIR/rgma/Rgma.md) | Recursive Gaussian MA | Gaussian approximation |
|
||||
| [**RMA**](../lib/trends_IIR/rma/Rma.md) | WildeR MA | Wilder's smoothing (1/n decay) |
|
||||
| [**T3**](../lib/trends_IIR/t3/T3.md) | Tillson T3 MA | Six-stage DEMA variant |
|
||||
@@ -104,6 +104,7 @@ Signal processing filters adapted for financial time series. Designed to separat
|
||||
|
||||
| Indicator | Full Name | Notes |
|
||||
| :-------- | :-------- | :---- |
|
||||
| [**AGC**](../lib/filters/agc/Agc.md) | Ehlers Automatic Gain Control | Amplitude normalization via peak tracking |
|
||||
| [**ALAGUERRE**](../lib/filters/alaguerre/ALaguerre.md) | Ehlers Adaptive Laguerre Filter | Ehlers variable-alpha from tracking error |
|
||||
| [**BAXTERKING**](../lib/filters/baxterking/BaxterKing.md) | Baxter-King Band-Pass Filter | Symmetric FIR band-pass for cycle extraction |
|
||||
| [**CFITZ**](../lib/filters/cfitz/Cfitz.md) | Christiano-Fitzgerald Filter | Asymmetric full-sample band-pass, random-walk optimal |
|
||||
@@ -125,7 +126,10 @@ Signal processing filters adapted for financial time series. Designed to separat
|
||||
| [**LMS**](../lib/filters/lms/Lms.md) | Least Mean Squares | Widrow-Hoff adaptive FIR filter |
|
||||
| [**RLS**](../lib/filters/rls/Rls.md) | Recursive Least Squares | Faster convergence than LMS |
|
||||
| [**LOESS**](../lib/filters/loess/Loess.md) | LOESS Smoothing | Local polynomial regression |
|
||||
| [**MODF**](../lib/filters/modf/Modf.md) | Modular Filter | Dual-path adaptive filter with state selection |
|
||||
| [**NOTCH**](../lib/filters/notch/Notch.md) | Notch Filter | Single frequency rejection |
|
||||
| [**NW**](../lib/filters/nw/Nw.md) | Nadaraya-Watson Estimator | Non-parametric Gaussian kernel regression smoothing |
|
||||
| [**RMED**](../lib/filters/rmed/Rmed.md) | Ehlers Recursive Median Filter | Recursive median + IIR smoothing, outlier-resistant |
|
||||
| [**ONEEURO**](../lib/filters/oneeuro/OneEuro.md) | One Euro Filter | Speed-adaptive low-pass, adaptive cutoff |
|
||||
| [**ROOFING**](../lib/filters/roofing/Roofing.md) | Ehlers Roofing Filter | Ehlers HP + SS bandpass cascade |
|
||||
| [**SGF**](../lib/filters/sgf/Sgf.md) | Savitzky-Golay Filter | Polynomial least-squares fitting |
|
||||
@@ -152,11 +156,14 @@ Bounded indicators that oscillate around a centerline or between fixed extremes.
|
||||
| [**DECO**](../lib/oscillators/deco/Deco.md) | Ehlers Decycler Oscillator | Dual HP bandpass cycle isolation |
|
||||
| [**DPO**](../lib/oscillators/dpo/Dpo.md) | Detrended Price Oscillator | Displaced SMA trend removal |
|
||||
| [**FISHER**](../lib/oscillators/fisher/Fisher.md) | Ehlers Fisher Transform | Gaussian-normalized price reversal |
|
||||
| [**IMI**](../lib/oscillators/imi/Imi.md) | Intraday Momentum Index | Candlestick RSI (0-100 oscillator) |
|
||||
| [**INERTIA**](../lib/oscillators/inertia/Inertia.md) | Inertia | Linear regression residual |
|
||||
| [**KDJ**](../lib/oscillators/kdj/Kdj.md) | KDJ Indicator | Enhanced Stochastic (J = 3K − 2D) |
|
||||
| [**PGO**](../lib/oscillators/pgo/Pgo.md) | Pretty Good Oscillator | ATR-normalized SMA displacement |
|
||||
| [**REFLEX**](../lib/oscillators/reflex/Reflex.md) | Ehlers Reflex | Zero-centered reversal oscillator |
|
||||
| [**REVERSEEMA**](../lib/oscillators/reverseema/ReverseEma.md) | Ehlers Reverse EMA | 8-stage cascaded Z-transform inversion oscillator |
|
||||
| [**SMI**](../lib/oscillators/smi/Smi.md) | Stochastic Momentum Index | Distance from range midpoint (K/D lines) |
|
||||
| [**STC**](../lib/oscillators/stc/Stc.md) | Schaff Trend Cycle | MACD + double Stochastic (0-100 momentum oscillator) |
|
||||
| [**STOCH**](../lib/oscillators/stoch/Stoch.md) | Stochastic Oscillator | Close within N-period H/L range (%K/%D) |
|
||||
| [**STOCHF**](../lib/oscillators/stochf/Stochf.md) | Stochastic Fast | Unsmoothed Stochastic (%K/%D, SMA smoothing only) |
|
||||
| [**STOCHRSI**](../lib/oscillators/stochrsi/Stochrsi.md) | Stochastic RSI | Stochastic applied to RSI (%K/%D) |
|
||||
@@ -183,7 +190,6 @@ Indicators measuring trend strength, regime, and directional movement quality.
|
||||
| [**DX**](../lib/dynamics/dx/Dx.md) | Directional Movement Index | Raw directional strength |
|
||||
| [**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 |
|
||||
| [**IMI**](../lib/dynamics/imi/Imi.md) | Intraday Momentum Index | Candlestick-based momentum |
|
||||
| [**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 |
|
||||
| [**SUPER**](../lib/dynamics/super/Super.md) | SuperTrend | ATR-based trend bands |
|
||||
@@ -197,6 +203,7 @@ Rate of change and velocity measurements. First derivatives of price.
|
||||
|
||||
| Indicator | Full Name | Notes |
|
||||
| :-------- | :-------- | :---- |
|
||||
| [**BIAS**](../lib/momentum/bias/Bias.md) | Bias / Disparity Index | Percentage deviation from SMA |
|
||||
| [**BOP**](../lib/momentum/bop/Bop.md) | Balance of Power | Close position in range |
|
||||
| [**CCI**](../lib/momentum/cci/Cci.md) | Commodity Channel Index | Mean deviation normalized |
|
||||
| [**CFB**](../lib/momentum/cfb/Cfb.md) | Composite Fractal Behavior | Jurik fractal momentum |
|
||||
@@ -224,7 +231,6 @@ Measures of price variability and range. Essential for position sizing and stop
|
||||
| [**ADR**](../lib/volatility/adr/Adr.md) | Average Daily Range | Simple range averaging |
|
||||
| [**ATR**](../lib/volatility/atr/Atr.md) | Average True Range | Gap-adjusted range |
|
||||
| [**ATRN**](../lib/volatility/atrn/Atrn.md) | ATR Normalized | ATR scaled to [0,1] |
|
||||
| [**ATRP**](../lib/volatility/atrp/Atrp.md) | ATR Percent | Percentage-based ATR |
|
||||
| [**BBW**](../lib/volatility/bbw/Bbw.md) | Bollinger Band Width | Band width as percentage of middle band |
|
||||
| [**BBWN**](../lib/volatility/bbwn/Bbwn.md) | BB Width Normalized | Band width normalized to [0,1] |
|
||||
| [**BBWP**](../lib/volatility/bbwp/Bbwp.md) | BB Width Percentile | Band width historical percentile |
|
||||
@@ -320,7 +326,6 @@ Mathematical and statistical computations on price series.
|
||||
| :-------- | :-------- | :---- |
|
||||
| [**ACF**](../lib/statistics/acf/Acf.md) | Autocorrelation Function | Lagged self-correlation |
|
||||
| [**BETA**](../lib/statistics/beta/Beta.md) | Beta Coefficient | Systematic risk measure |
|
||||
| [**BIAS**](../lib/statistics/bias/Bias.md) | Bias | Percentage deviation from SMA |
|
||||
| [**CMA**](../lib/statistics/cma/Cma.md) | Cumulative Moving Average | Expanding window average |
|
||||
| [**COINTEGRATION**](../lib/statistics/cointegration/Cointegration.md) | Cointegration | Engle-Granger two-step with ADF test |
|
||||
| [**CORRELATION**](../lib/statistics/correlation/Correlation.md) | Pearson Correlation | Linear relationship [-1, +1] |
|
||||
@@ -373,12 +378,23 @@ Periodic pattern detection and dominant frequency extraction. Markets exhibit cy
|
||||
| [**HT_DCPERIOD**](../lib/cycles/ht_dcperiod/HtDcperiod.md) | Ehlers HT Dominant Cycle Period | Hilbert Transform period estimation |
|
||||
| [**HT_DCPHASE**](../lib/cycles/ht_dcphase/HtDcphase.md) | Ehlers HT Dominant Cycle Phase | Hilbert Transform phase angle |
|
||||
| [**HT_PHASOR**](../lib/cycles/ht_phasor/HtPhasor.md) | Ehlers HT Phasor Components | In-phase and quadrature components |
|
||||
| [**HT_SINE**](../lib/cycles/ht_sine/HtSine.md) | Ehlers HT SineWave | Dominant cycle phase with lead signal |
|
||||
| [**HT_SINE**](../lib/cycles/ht_sine/HtSine.md) | Ehlers HT SineWave (also known as SINE) | Dominant cycle phase with lead signal |
|
||||
| [**LUNAR**](../lib/cycles/lunar/Lunar.md) | Lunar Phase | Moon phase cycle |
|
||||
| [**SINE**](../lib/cycles/sine/Sine.md) | Ehlers Sine Wave | Periodic sine oscillation |
|
||||
| [**SOLAR**](../lib/cycles/solar/Solar.md) | Solar Activity Cycle | Solar activity periodicity |
|
||||
| [**SSFDSP**](../lib/cycles/ssfdsp/Ssfdsp.md) | Ehlers SSF Detrended Synthetic Price | Dual Super Smoother oscillator |
|
||||
| [**STC**](../lib/cycles/stc/Stc.md) | Schaff Trend Cycle | MACD-based cycle oscillator |
|
||||
|
||||
### Core
|
||||
|
||||
Price transforms and fundamental building blocks. These indicators compute derived prices from OHLCV bars and serve as inputs to higher-order indicators.
|
||||
|
||||
| Indicator | Full Name | Notes |
|
||||
| :-------- | :-------- | :---- |
|
||||
| [**AVGPRICE**](../lib/core/avgprice/Avgprice.md) | Average Price | (O+H+L+C) * 0.25 via FMA |
|
||||
| [**MEDPRICE**](../lib/core/medprice/Medprice.md) | Median Price | (H+L) * 0.5 |
|
||||
| [**MIDPOINT**](../lib/core/midpoint/Midpoint.md) | Rolling Midpoint | (Max+Min) * 0.5 over lookback window |
|
||||
| [**MIDPRICE**](../lib/core/midprice/Midprice.md) | Mid Price | (Highest High + Lowest Low) * 0.5 |
|
||||
| [**TYPPRICE**](../lib/core/typprice/Typprice.md) | Typical Price | (H+L+C) * OneThird via FMA |
|
||||
| [**WCLPRICE**](../lib/core/wclprice/Wclprice.md) | Weighted Close Price | (H+L+2C) * 0.25 via FMA |
|
||||
|
||||
### Numerics
|
||||
|
||||
@@ -387,7 +403,6 @@ Mathematical transformations and derivative indicators. Building blocks for anal
|
||||
| Indicator | Full Name | Notes |
|
||||
| :-------- | :-------- | :---- |
|
||||
| [**ACCEL**](../lib/numerics/accel/Accel.md) | Acceleration (2nd Derivative) | Change in slope |
|
||||
| [**AGC**](../lib/numerics/agc/Agc.md) | Ehlers Automatic Gain Control | Amplitude normalization via peak tracking |
|
||||
| [**CHANGE**](../lib/numerics/change/Change.md) | Percentage Change | Relative price movement |
|
||||
| [**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 |
|
||||
@@ -395,13 +410,11 @@ Mathematical transformations and derivative indicators. Building blocks for anal
|
||||
| [**LINEARTRANS**](../lib/numerics/lineartrans/Lineartrans.md) | Linear Transform | y = ax + b scaling |
|
||||
| [**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 |
|
||||
| [**MIDPOINT**](../lib/numerics/midpoint/Midpoint.md) | Rolling Midpoint | (Highest + Lowest) / 2 |
|
||||
| [**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] |
|
||||
| [**SLOPE**](../lib/numerics/slope/Slope.md) | Slope (1st Derivative) | Rate of change; velocity |
|
||||
| [**SQRTTRANS**](../lib/numerics/sqrttrans/Sqrttrans.md) | Square Root Transform | √x variance-to-StdDev conversion |
|
||||
| [**STANDARDIZE**](../lib/numerics/standardize/Standardize.md) | Z-Score Normalization | (x - mean) / StdDev scaling |
|
||||
|
||||
### Errors
|
||||
|
||||
@@ -424,7 +437,7 @@ Error metrics and loss functions for model evaluation, forecast assessment, and
|
||||
| [**MSE**](../lib/errors/mse/Mse.md) | Mean Squared Error | Variance of residuals |
|
||||
| [**MSLE**](../lib/errors/msle/Msle.md) | Mean Squared Log Error | Ratio-sensitive error |
|
||||
| [**PSEUDOHUBER**](../lib/errors/pseudohuber/PseudoHuber.md) | Pseudo-Huber Loss | Differentiable Huber approximation |
|
||||
| [**QUANTILELOSS**](../lib/errors/quantile/QuantileLoss.md) | Quantile Loss | Asymmetric pinball loss |
|
||||
| [**QUANTILELOSS**](../lib/errors/quantileloss/QuantileLoss.md) | Quantile Loss | Asymmetric pinball loss |
|
||||
| [**RAE**](../lib/errors/rae/Rae.md) | Relative Absolute Error | MAE relative to baseline |
|
||||
| [**RMSE**](../lib/errors/rmse/Rmse.md) | Root Mean Squared Error | Standard error magnitude |
|
||||
| [**RMSLE**](../lib/errors/rmsle/Rmsle.md) | Root Mean Squared Log Error | Ratio-sensitive RMSE |
|
||||
@@ -432,7 +445,7 @@ Error metrics and loss functions for model evaluation, forecast assessment, and
|
||||
| [**RSQUARED**](../lib/errors/rsquared/Rsquared.md) | R² (Coefficient of Determination) | Explained variance fraction |
|
||||
| [**SMAPE**](../lib/errors/smape/Smape.md) | Symmetric MAPE | Symmetric percentage error |
|
||||
| [**THEILU**](../lib/errors/theilu/TheilU.md) | Theil's U Statistic | Forecast accuracy relative to naive |
|
||||
| [**TUKEY**](../lib/errors/tukey/TukeyBiweight.md) | Tukey Biweight Loss | Robust regression loss |
|
||||
| [**TUKEY**](../lib/errors/tukeybiweight/TukeyBiweight.md) | Tukey Biweight Loss | Robust regression loss |
|
||||
| [**WMAPE**](../lib/errors/wmape/Wmape.md) | Weighted MAPE | Volume-weighted percentage error |
|
||||
| [**WRMSE**](../lib/errors/wrmse/Wrmse.md) | Weighted RMSE | Observation-weighted RMSE |
|
||||
|
||||
|
||||
+51
-44
@@ -40,32 +40,32 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Accumulation/Distribution Oscillator** | [Adosc](../lib/volume/adosc/adosc.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Adaptive Price Zone** | [Apz](../lib/channels/apz/apz.md) | - | - | - | ❔ |
|
||||
| **Andrews' Pitchfork** | Apchannel | - | - | ✔️ | - |
|
||||
| **Archer Moving Averages Trends** | [Amat](../lib/momentum/amat/Amat.md) | - | - | ✔️ | ✔️ |
|
||||
| **Archer Moving Averages Trends** | [Amat](../lib/dynamics/amat/Amat.md) | - | - | ✔️ | ✔️ |
|
||||
| **Archer On-Balance Volume** | [Aobv](../lib/volume/aobv/Aobv.md) | - | - | - | - |
|
||||
| **Arnaud Legoux Moving Average** | [Alma](../lib/trends/alma/alma.md) | - | - | ✔️ | ✔️ |
|
||||
| **Aroon** | [Aroon](../lib/momentum/aroon/aroon.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **Aroon Oscillator** | [AroonOsc](../lib/momentum/aroonosc/AroonOsc.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **Arnaud Legoux Moving Average** | [Alma](../lib/trends_FIR/alma/Alma.md) | - | - | ✔️ | ✔️ |
|
||||
| **Aroon** | [Aroon](../lib/dynamics/aroon/Aroon.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **Aroon Oscillator** | [AroonOsc](../lib/dynamics/aroonosc/AroonOsc.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **ATR Bands** | Atrbands | ✔️ | - | ✔️ | ❔ |
|
||||
| **Adaptive FIR Moving Average** | [Afirma](../lib/forecasts/afirma/Afirma.md) | - | - | - | - |
|
||||
| **Ehlers Adaptive Laguerre Filter** | [ALaguerre](../lib/filters/alaguerre/ALaguerre.md) | - | - | - | - |
|
||||
| **Ehlers Automatic Gain Control** | [Agc](../lib/numerics/agc/Agc.md) | - | - | - | - |
|
||||
| **Ehlers Automatic Gain Control** | [Agc](../lib/filters/agc/Agc.md) | - | - | - | - |
|
||||
| **Average Daily Range** | [Adr](../lib/volatility/adr/Adr.md) | - | - | - | - |
|
||||
| **Average Directional Index** | [Adx](../lib/momentum/adx/adx.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Average Directional Movement Rating** | [Adxr](../lib/momentum/adxr/Adxr.md) | ✔️ | ✔️ | - | - |
|
||||
| **Average Directional Index** | [Adx](../lib/dynamics/adx/Adx.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Average Directional Movement Rating** | [Adxr](../lib/dynamics/adxr/Adxr.md) | ✔️ | ✔️ | - | - |
|
||||
| **Average True Range** | [Atr](../lib/volatility/atr/atr.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Average True Range Normalized [0,1]** | [Atrn](../lib/volatility/atrn/Atrn.md) | - | - | ✔️ | - |
|
||||
| **Average True Range Percent** | [Atrp](../lib/volatility/atrp/Atrp.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Awesome Oscillator** | [Ao](../lib/momentum/ao/ao.md) | - | ✔️ | ✔️ | ✔️ |
|
||||
| **Average Price** | [Avgprice](../lib/core/avgprice/Avgprice.md) | ✔️ | - | - | - |
|
||||
| **Awesome Oscillator** | [Ao](../lib/oscillators/ao/Ao.md) | - | ✔️ | ✔️ | ✔️ |
|
||||
| **Balance of Power** | [Bop](../lib/momentum/bop/Bop.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Baxter-King Band-Pass Filter** | [BaxterKing](../lib/filters/baxterking/BaxterKing.md) | - | - | - | - |
|
||||
| **Christiano-Fitzgerald Filter** | [Cfitz](../lib/filters/cfitz/Cfitz.md) | - | - | - | - |
|
||||
| **Bollinger Bands** | [Bbands](../lib/channels/bbands/Bbands.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Bessel Filter** | [Bessel](../lib/trends/bessel/Bessel.md) | - | - | - | - |
|
||||
| **Bessel Filter** | [Bessel](../lib/filters/bessel/Bessel.md) | - | - | - | - |
|
||||
| **Bessel-Weighted MA** | [Bwma](../lib/trends_FIR/bwma/Bwma.md) | - | - | - | - |
|
||||
| **Beta Coefficient** | [Beta](../lib/statistics/beta/Beta.md) | ❔ | - | ✔️ | - |
|
||||
| **Bias** | [Bias](../lib/statistics/bias/Bias.md) | - | - | - | - |
|
||||
| **Bilateral Filter** | [Bilateral](../lib/trends/bilateral/Bilateral.md) | - | - | - | - |
|
||||
| **Blackman Window MA** | [Blma](../lib/trends/blma/Blma.md) | - | - | - | - |
|
||||
| **Bias** | [Bias](../lib/momentum/bias/Bias.md) | - | - | - | - |
|
||||
| **Bilateral Filter** | [Bilateral](../lib/filters/bilateral/Bilateral.md) | - | - | - | - |
|
||||
| **Blackman Window MA** | [Blma](../lib/trends_FIR/blma/Blma.md) | - | - | - | - |
|
||||
| **Bollinger %B** | [Bbb](../lib/oscillators/bbb/Bbb.md) | - | - | ✔️ | ❔ |
|
||||
| **Bollinger Band Squeeze** | [Bbs](../lib/oscillators/bbs/Bbs.md) | - | - | ✔️ | ❔ |
|
||||
| **Bollinger Band Width** | Bbw | - | - | ✔️ | ❔ |
|
||||
@@ -87,9 +87,9 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Close-to-Close Volatility** | Ccv | - | - | - | - |
|
||||
| **Cointegration** | Cointegration | - | - | - | - |
|
||||
| **Commodity Channel Index** | Cci | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
| **Composite Fractal Behavior** | [Cfb](../lib/momentum/cfb/cfb.md) | - | - | - | - |
|
||||
| **Composite Fractal Behavior** | [Cfb](../lib/momentum/cfb/Cfb.md) | - | - | - | - |
|
||||
| **Conditional Volatility** | [Cv](../lib/volatility/cv/Cv.md) | - | - | - | - |
|
||||
| **Convolution Moving Average** | [Conv](../lib/trends/conv/conv.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Convolution Moving Average** | [Conv](../lib/trends_FIR/conv/Conv.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Coral Trend Filter** | [Coral](../lib/trends_IIR/coral/Coral.md) | - | - | - | - |
|
||||
| **Correlation** | Correlation | - | - | ✔️ | - |
|
||||
| **Cumulative Moving Average** | [Cma](../lib/statistics/cma/Cma.md) | - | - | - | - |
|
||||
@@ -100,11 +100,11 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Ehlers Detrended Synthetic Price** | Dsp | - | - | - | ❔ |
|
||||
| **Deviation-Scaled MA** | Dsma | - | - | - | ❔ |
|
||||
| **Directional Movement Index** | Dx | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Directional Movement Index (Jurik)** | [Dmx](../lib/momentum/dmx/dmx.md) | - | - | - | - |
|
||||
| **Directional Movement Index (Jurik)** | [Dmx](../lib/dynamics/dmx/Dmx.md) | - | - | - | - |
|
||||
| **Dirty Data Detection** | Dirty | - | - | - | - |
|
||||
| **Donchian Channels** | [Dchannel](../lib/channels/dchannel/Dchannel.md) | - | - | ✔️ | ❔ |
|
||||
| **Double Exponential Moving Average** | [Dema](../lib/trends/dema/dema.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Double Weighted Moving Average** | [Dwma](../lib/trends/dwma/dwma.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **Double Exponential Moving Average** | [Dema](../lib/trends_IIR/dema/Dema.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Double Weighted Moving Average** | [Dwma](../lib/trends_FIR/dwma/Dwma.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **Ease of Movement** | [Eom](../lib/volume/eom/Eom.md) | - | ✔️ | - | - |
|
||||
| **Ehlers Autocorrelation Periodogram** | [Eacp](../lib/cycles/eacp/eacp.md) | - | - | - | - |
|
||||
| **BandPass Filter** | [Bpf](../lib/filters/bpf/Bpf.md) | - | - | - | - |
|
||||
@@ -116,13 +116,13 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Ehlers Fractal Adaptive MA** | [Frama](../lib/trends_IIR/frama/Frama.md) | - | - | - | ❔ |
|
||||
| **Ehlers Highpass Filter** | [Hpf](../lib/filters/hpf/Hpf.md) | - | - | - | ❔ |
|
||||
| **Ehlers Phasor Analysis** | Phasor | - | - | - | - |
|
||||
| **Ehlers Sine Wave** | Sine | - | - | - | ❔ |
|
||||
| **Ehlers Sine Wave** | [HtSine](../lib/cycles/ht_sine/HtSine.md) | ✔️ | - | - | - |
|
||||
| **Ehlers SSF-Based Detrended Synthetic Price** | Ssfdsp | - | - | - | - |
|
||||
| **Ehlers 2-Pole Super Smooth Filter** | [Ssf2](../lib/filters/ssf2/Ssf2.md) | - | - | - | ✔️ |
|
||||
| **Ehlers 3-Pole Super Smooth Filter** | [Ssf3](../lib/filters/ssf3/Ssf3.md) | - | - | - | - |
|
||||
| **Ehlers Ultrasmooth Filter** | Usf | - | - | - | - |
|
||||
| **Elliptic (Cauer) Filter** | [Elliptic](../lib/filters/elliptic/Elliptic.md) | - | - | - | ❔ |
|
||||
| **Exponential Moving Average** | [Ema](../lib/trends/ema/ema.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Exponential Moving Average** | [Ema](../lib/trends_IIR/ema/Ema.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Exponential Transformation** | Exptrans | - | - | - | - |
|
||||
| **Exponential Weighted MA Volatility** | [Ewma](../lib/volatility/ewma/Ewma.md) | - | - | - | ❔ |
|
||||
| **Extended Traditional Pivots** | [Pivotext](../lib/reversals/pivotext/Pivotext.md) | - | - | - | - |
|
||||
@@ -143,7 +143,7 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Highest value** | [Highest](../lib/numerics/highest/Highest.md) | ✔️ | ✔️ | - | - |
|
||||
| **Ehlers Hilbert Transform Dominant Cycle Period** | [HtDcPeriod](../lib/cycles/ht_dcperiod/ht_dcperiod.md) | ✔️ | - | - | - |
|
||||
| **Ehlers Hilbert Transform Dominant Cycle Phase** | [HtDcPhase](../lib/cycles/ht_dcphase/ht_dcphase.md) | ✔️ | - | - | - |
|
||||
| **Ehlers Hilbert Transform Instantaneous Trend** | [Htit](../lib/trends/htit/htit.md) | ✔️ | - | ✔️ | ✔️ |
|
||||
| **Ehlers Hilbert Transform Instantaneous Trend** | [Htit](../lib/trends_IIR/htit/Htit.md) | ✔️ | - | ✔️ | ✔️ |
|
||||
| **Ehlers Hilbert Transform Phasor Components** | [HtPhasor](../lib/cycles/ht_phasor/ht_phasor.md) | ✔️ | - | - | - |
|
||||
| **Ehlers Hilbert Transform SineWave** | [HtSine](../lib/cycles/ht_sine/ht_sine.md) | ✔️ | - | - | - |
|
||||
| **Ehlers Hilbert Transform Trend vs Cycle Mode** | Ht_trendmode | ✔️ | - | - | - |
|
||||
@@ -154,7 +154,7 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Ehlers Homodyne Discriminator** | [Homod](../lib/cycles/homod/homod.md) | - | - | - | ❔ |
|
||||
| **Huber Loss** | Huber | - | - | - | - |
|
||||
| **Hull Exponential MA** | [Hema](../lib/trends_IIR/hema/Hema.md) | - | - | - | - |
|
||||
| **Hull Moving Average** | [Hma](../lib/trends/hma/hma.md) | - | ✔️ | ✔️ | [⚠️](../lib/trends/hma/hma.md#external-library-discrepancies) |
|
||||
| **Hull Moving Average** | [Hma](../lib/trends_FIR/hma/Hma.md) | - | ✔️ | ✔️ | [⚠️](../lib/trends_FIR/hma/Hma.md#external-library-discrepancies) |
|
||||
| **Hurst Exponent** | Hurst | - | - | - | ❔ |
|
||||
| **Ichimoku Cloud** | Ichimoku | - | - | ✔️ | ❔ |
|
||||
| **Impulse (Elder)** | [Impulse](../lib/dynamics/impulse/Impulse.md) | - | - | - | - |
|
||||
@@ -163,12 +163,12 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Intraday Intensity Index** | [Iii](../lib/volume/iii/Iii.md) | - | - | - | - |
|
||||
| **Intraday Momentum Index** | Imi | - | - | - | ❔ |
|
||||
| **Jarque-Bera Test** | [Jb](../lib/statistics/jb/Jb.md) | - | - | - | - |
|
||||
| **Jurik Moving Average** | [Jma](../lib/trends/jma/jma.md) | - | - | - | ❔ |
|
||||
| **Jurik Moving Average** | [Jma](../lib/trends_IIR/jma/Jma.md) | - | - | - | ❔ |
|
||||
| **Jurik Volatility** | [Jvolty](../lib/volatility/jvolty/Jvolty.md) | - | - | - | - |
|
||||
| **Jurik Adaptive Envelope Bands** | [Jbands](../lib/channels/jbands/Jbands.md) | - | - | - | - |
|
||||
| **Jurik Volatility Normalized [0,100]** | [Jvoltyn](../lib/volatility/jvoltyn/Jvoltyn.md) | - | - | - | - |
|
||||
| **Kalman Filter** | [Kalman](../lib/filters/kalman/Kalman.md) | - | - | - | - |
|
||||
| **Kaufman Adaptive Moving Average** | [Kama](../lib/trends/kama/kama.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Kaufman Adaptive Moving Average** | [Kama](../lib/trends_IIR/kama/Kama.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **KDJ Indicator** | [Kdj](../lib/oscillators/kdj/Kdj.md) | - | - | - | - |
|
||||
| **Keltner Channel** | [Kchannel](../lib/channels/kchannel/kchannel.md) | - | - | ✔️ | ❔ |
|
||||
| **Kendall Rank Correlation** | [Kendall](../lib/statistics/kendall/Kendall.md) | - | - | - | - |
|
||||
@@ -177,7 +177,7 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Ehlers Laguerre Filter** | [Laguerre](../lib/filters/laguerre/Laguerre.md) | - | - | - | - |
|
||||
| **Least Mean Squares** | [Lms](../lib/filters/lms/Lms.md) | - | - | - | - |
|
||||
| **Recursive Least Squares** | [Rls](../lib/filters/rls/Rls.md) | - | - | - | - |
|
||||
| **Least Squares Moving Average** | [Lsma](../lib/trends/lsma/lsma.md) | - | - | ✔️ | ❔ |
|
||||
| **Least Squares Moving Average** | [Lsma](../lib/trends_FIR/lsma/Lsma.md) | - | - | ✔️ | ❔ |
|
||||
| **Linear Regression** | [LinReg](../lib/statistics/linreg/LinReg.md) | - | - | ✔️ | [⚠️](../lib/statistics/linreg/LinReg.md#validation) |
|
||||
| **Linear Transformation** | Lineartrans | - | - | - | - |
|
||||
| **Linear Trend MA** | Ltma | - | - | - | - |
|
||||
@@ -189,7 +189,7 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Lowest value** | [Lowest](../lib/numerics/lowest/Lowest.md) | ✔️ | ✔️ | - | - |
|
||||
| **Lunar Phase** | Lunar | - | - | - | - |
|
||||
| **Mass Index** | [Massi](../lib/volatility/massi/Massi.md) | - | - | - | ❔ |
|
||||
| **McGinley Dynamic** | [Mgdi](../lib/trends/mgdi/mgdi.md) | - | - | ✔️ | ✔️ |
|
||||
| **McGinley Dynamic** | [Mgdi](../lib/trends_IIR/mgdi/Mgdi.md) | - | - | ✔️ | ✔️ |
|
||||
| **Mean Absolute Error** | Mae | - | - | - | - |
|
||||
| **Mean Absolute Percentage Difference** | Mapd | - | - | - | - |
|
||||
| **Mean Absolute Percentage Error** | Mape | - | - | - | - |
|
||||
@@ -198,11 +198,14 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Mean Percentage Error** | Mpe | - | - | - | - |
|
||||
| **Mean Squared Error** | Mse | - | - | - | - |
|
||||
| **Mean Squared Logarithmic Error** | Msle | - | - | - | - |
|
||||
| **Ehlers MESA Adaptive Moving Average** | [Mama](../lib/trends/mama/mama.md) | - | - | ✔️ | ✔️ |
|
||||
| **Midpoint** | [Midpoint](../lib/numerics/midpoint/Midpoint.md) | ✔️ | - | - | - |
|
||||
| **Ehlers MESA Adaptive Moving Average** | [Mama](../lib/trends_IIR/mama/Mama.md) | - | - | ✔️ | ✔️ |
|
||||
| **Median Price** | [Medprice](../lib/core/medprice/Medprice.md) | ✔️ | - | - | - |
|
||||
| **Mid Price** | [Midprice](../lib/core/midprice/Midprice.md) | ✔️ | - | - | - |
|
||||
| **Midpoint** | [Midpoint](../lib/core/midpoint/Midpoint.md) | ✔️ | - | - | - |
|
||||
| **Min-Max Channel** | [Mmchannel](../lib/channels/mmchannel/mmchannel.md) | - | - | ✔️ | - |
|
||||
| **Min-Max Scaling (Normalization)** | [Normalize](../lib/numerics/normalize/Normalize.md) | - | - | - | - |
|
||||
| **Mode (Most Frequent)** | Mode | - | - | - | - |
|
||||
| **Modular Filter** | [Modf](../lib/filters/modf/Modf.md) | - | - | - | - |
|
||||
| **Modified MA** | [Mma](../lib/trends_IIR/mma/Mma.md) | - | - | - | - |
|
||||
| **Natural Moving Average** | [Nma](../lib/trends_IIR/nma/Nma.md) | - | - | - | - |
|
||||
| **Momentum** | Mom | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
@@ -215,10 +218,11 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Normalized Average True Range** | Natr | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Normalized Shannon Entropy** | Entropy | - | - | - | - |
|
||||
| **Notch Filter** | [Notch](../lib/filters/notch/Notch.md) | - | - | - | - |
|
||||
| **Nadaraya-Watson Estimator** | [Nw](../lib/filters/nw/Nw.md) | - | - | - | - |
|
||||
| **One Euro Filter** | [OneEuro](../lib/filters/oneeuro/OneEuro.md) | - | - | - | - |
|
||||
| **On Balance Volume** | [Obv](../lib/volume/obv/Obv.md) | [⚠️](../lib/volume/obv/Obv.md#validation) | ✔️ | ✔️ | [⚠️](../lib/volume/obv/Obv.md#validation) |
|
||||
| **Parabolic SAR** | [Psar](../lib/reversals/psar/Psar.md) | - | - | ✔️ | ❔ |
|
||||
| **Pascal Weighted Moving Average** | [Pwma](../lib/trends/pwma/pwma.md) | - | - | - | ✔️ |
|
||||
| **Pascal Weighted Moving Average** | [Pwma](../lib/trends_FIR/pwma/Pwma.md) | - | - | - | ✔️ |
|
||||
| **Percentage Change** | [Change](../lib/numerics/change/Change.md) | - | ✔️ | - | - |
|
||||
| **Percentage Price Oscillator** | Ppo | ✔️ | ✔️ | - | ✔️ |
|
||||
| **Percentage Volume Oscillator** | [Pvo](../lib/volume/pvo/Pvo.md) | - | - | - | ❔ |
|
||||
@@ -247,12 +251,13 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Realized Volatility** | [Rv](../lib/volatility/rv/Rv.md) | - | - | - | - |
|
||||
| **Rectified Linear Unit** | [Relu](../lib/numerics/relu/Relu.md) | - | - | - | - |
|
||||
| **Recursive Gaussian MA** | [Rgma](../lib/trends_IIR/rgma/Rgma.md) | - | - | - | - |
|
||||
| **Ehlers Recursive Median Filter** | [Rmed](../lib/filters/rmed/Rmed.md) | - | - | - | - |
|
||||
| **Regression Channels** | [Regchannel](../lib/channels/regchannel/regchannel.md) | - | - | - | - |
|
||||
| **Regularized Exponential MA** | [Rema](../lib/trends_IIR/rema/Rema.md) | - | - | - | ❔ |
|
||||
| **Relative Absolute Error** | Rae | - | - | - | - |
|
||||
| **Relative Squared Error** | Rse | - | - | - | - |
|
||||
| **Relative Strength Index** | [Rsi](../lib/momentum/rsi/Rsi.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Relative Strength Quality Index** | [Rsx](../lib/momentum/rsx/rsx.md) | - | - | - | ❔ |
|
||||
| **Relative Strength Quality Index** | [Rsx](../lib/momentum/rsx/Rsx.md) | - | - | - | ❔ |
|
||||
| **Relative Volatility Index** | [Rvi](../lib/volatility/rvi/Rvi.md) | - | - | - | ❔ |
|
||||
| **Renko** | - | - | - | ✔️ | - |
|
||||
| **Rogers-Satchell Volatility** | Rsv | - | - | - | - |
|
||||
@@ -263,16 +268,16 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Savitzky-Golay Filter** | [Sgf](../lib/filters/sgf/Sgf.md) | - | - | - | - |
|
||||
| **Savitzky-Golay MA** | [Sgma](../lib/trends_FIR/sgma/Sgma.md) | - | - | - | - |
|
||||
| **Smoothed Adaptive Momentum** | [Sam](../lib/momentum/sam/Sam.md) | - | - | - | - |
|
||||
| **Schaff Trend Cycle** | [Stc](../lib/cycles/stc/Stc.md) | - | - | ✔️ | ❔ |
|
||||
| **Simple Moving Average** | [Sma](../lib/trends/sma/sma.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Schaff Trend Cycle** | [Stc](../lib/oscillators/stc/stc.md) | - | - | ✔️ | ❔ |
|
||||
| **Simple Moving Average** | [Sma](../lib/trends_FIR/sma/Sma.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Sine-weighted MA** | [Sinema](../lib/trends_FIR/sinema/Sinema.md) | - | - | - | - |
|
||||
| **Smoothed Moving Average** | [Rma](../lib/trends/rma/rma.md) | - | - | ✔️ | ✔️ |
|
||||
| **Smoothed Moving Average** | [Rma](../lib/trends_IIR/rma/Rma.md) | - | - | ✔️ | ✔️ |
|
||||
| **Solar Activity Cycle** | Solar | - | - | - | - |
|
||||
| **Spearman Rank Correlation** | Spearman | - | - | - | ❔ |
|
||||
| **Ehlers Super Passband Filter** | [Spbf](../lib/filters/spbf/Spbf.md) | - | - | - | - |
|
||||
| **Square Root Transformation** | [Sqrttrans](../lib/numerics/sqrttrans/Sqrttrans.md) | - | - | - | - |
|
||||
| **Standard Deviation Channel** | [Sdchannel](../lib/channels/sdchannel/sdchannel.md) | - | - | - | ❔ |
|
||||
| **Standardization (Z-score)** | Standardize | - | - | - | ❔ |
|
||||
| **Standardization (Z-score)** | [Zscore](../lib/statistics/zscore/Zscore.md) | - | - | - | ❔ |
|
||||
| **Starc Bands** | Starc | - | - | - | - |
|
||||
| **Stochastic Fast** | [Stochf](../lib/oscillators/stochf/Stochf.md) | ✔️ | - | ✔️ | ❔ |
|
||||
| **Stochastic Momentum Index** | [Smi](../lib/oscillators/smi/Smi.md) | - | - | - | ❔ |
|
||||
@@ -280,20 +285,21 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Stochastic RSI** | [Stochrsi](../lib/oscillators/stochrsi/Stochrsi.md) | ✔️ | - | ✔️ | ✔️ |
|
||||
| **Stoller Average Range Channel** | [Starchannel](../lib/channels/starchannel/starchannel.md) | - | - | ✔️ | ❔ |
|
||||
| **Super Trend Bands** | [Stbands](../lib/channels/stbands/Stbands.md) | - | - | - | - |
|
||||
| **SuperTrend** | [Super](../lib/trends/super/super.md) | - | - | ✔️ | ❔ |
|
||||
| **SuperTrend** | [Super](../lib/dynamics/super/Super.md) | - | - | ✔️ | ❔ |
|
||||
| **Swing High/Low Detection** | [Swings](../lib/reversals/swings/Swings.md) | - | - | - | ❔ |
|
||||
| **Symmetric Mean Absolute Percentage Error** | Smape | - | - | - | - |
|
||||
| **T3 Moving Average** | [T3](../lib/trends/t3/t3.md) | ✔️ | - | ✔️ | ✔️ |
|
||||
| **T3 Moving Average** | [T3](../lib/trends_IIR/t3/T3.md) | ✔️ | - | ✔️ | ✔️ |
|
||||
| **Theil Index** | Theil | - | - | - | - |
|
||||
| **Time Series Forecast** | [Tsf](../lib/trends_FIR/tsf/Tsf.md) | ✔️ | ✔️ | - | ❔ |
|
||||
| **Time Weighted Average Price** | Twap | - | - | - | - |
|
||||
| **Trade Volume Index** | Tvi | - | - | - | ❔ |
|
||||
| **Triangular Moving Average** | [Trima](../lib/trends/trima/trima.md) | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
| **Triangular Moving Average** | [Trima](../lib/trends_FIR/trima/Trima.md) | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
| **Triple Exponential Average** | [Trix](../lib/oscillators/trix/Trix.md) | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
| **Triple Exponential Moving Average** | [Tema](../lib/trends/tema/tema.md) | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
| **Triple Exponential Moving Average** | [Tema](../lib/trends_IIR/tema/Tema.md) | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
| **Trend Regularity Adaptive MA** | [Trama](../lib/trends_IIR/trama/Trama.md) | - | - | - | - |
|
||||
| **True Range** | Tr | ✔️ | ✔️ | - | - |
|
||||
| **True Strength Index** | Tsi | - | - | ✔️ | ✔️ |
|
||||
| **Typical Price** | [Typprice](../lib/core/typprice/Typprice.md) | ✔️ | - | - | - |
|
||||
| **TTM Trend** | Ttm | - | - | - | - |
|
||||
| **TTM Scalper Alert** | [TtmScalper](../lib/reversals/ttm_scalper/TtmScalper.md) | - | - | - | - |
|
||||
| **TTM Wave** | [TtmWave](../lib/oscillators/ttm_wave/TtmWave.md) | - | - | - | - |
|
||||
@@ -301,9 +307,9 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Ulcer Index** | Ui | - | - | - | ❔ |
|
||||
| **Ehlers Ultimate Bands** | [Ubands](../lib/channels/ubands/Ubands.md) | - | - | - | - |
|
||||
| **Ehlers Ultimate Channel** | [Uchannel](../lib/channels/uchannel/Uchannel.md) | - | - | - | - |
|
||||
| **Ultimate Oscillator** | [Ultosc](../lib/momentum/ultosc/Ultosc.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Variable Index Dynamic Average** | [Vidya](../lib/trends/vidya/vidya.md) | - | - | - | ❔ |
|
||||
| **Velocity (Jurik)** | [Vel](../lib/momentum/vel/vel.md) | - | - | - | - |
|
||||
| **Ultimate Oscillator** | [Ultosc](../lib/oscillators/ultosc/Ultosc.md) | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| **Variable Index Dynamic Average** | [Vidya](../lib/trends_IIR/vidya/Vidya.md) | - | - | - | ❔ |
|
||||
| **Velocity (Jurik)** | [Vel](../lib/momentum/vel/Vel.md) | - | - | - | - |
|
||||
| **Volatility Adjusted Moving Average** | [Vama](../lib/trends_IIR/vama/Vama.md) | - | - | - | ❔ |
|
||||
| **Volatility of Volatility** | [Vov](../lib/volatility/vov/Vov.md) | - | - | - | - |
|
||||
| **Volatility Ratio** | [Vr](../lib/volatility/vr/Vr.md) | - | - | - | - |
|
||||
@@ -319,9 +325,10 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **VWAP Bands** | [Vwapbands](../lib/channels/vwapbands/Vwapbands.md) | - | - | - | - |
|
||||
| **VWAP with Standard Deviation Bands** | [Vwapsd](../lib/channels/vwapsd/Vwapsd.md) | - | - | - | - |
|
||||
| **Wavelet Denoising Filter** | [Wavelet](../lib/filters/wavelet/Wavelet.md) | - | - | - | - |
|
||||
| **Weighted Moving Average** | [Wma](../lib/trends/wma/wma.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **Weighted Moving Average** | [Wma](../lib/trends_FIR/wma/Wma.md) | ✔️ | ✔️ | ✔️ | - |
|
||||
| **Wiener Filter** | Wiener | - | - | - | - |
|
||||
| **Williams %R** | [Willr](../lib/oscillators/willr/Willr.md) | ✔️ | ✔️ | ✔️ | ❔ |
|
||||
| **Weighted Close Price** | [Wclprice](../lib/core/wclprice/Wclprice.md) | ✔️ | - | - | - |
|
||||
| **Williams Accumulation/Distribution** | [Wad](../lib/volume/wad/Wad.md) | - | ✔️ | - | [⚠️](../lib/volume/wad/Wad.md#validation) |
|
||||
| **Williams Alligator** | Alligator | - | - | ✔️ | ❔ |
|
||||
| **Williams Fractal** | [Fractals](../lib/reversals/fractals/Fractals.md) | - | - | ✔️ | ❔ |
|
||||
@@ -374,8 +381,8 @@ No external reference exists. Implementation verified through unit tests, edge c
|
||||
| **Huber Loss** | [Huber](../lib/errors/huber/Huber.md) | - | No external validation available |
|
||||
| **Pseudo-Huber Loss** | [PseudoHuber](../lib/errors/pseudohuber/PseudoHuber.md) | - | No external validation available |
|
||||
| **Log-Cosh Loss** | [LogCosh](../lib/errors/logcosh/LogCosh.md) | - | No external validation available |
|
||||
| **Tukey Loss** | [Tukey](../lib/errors/tukey/Tukey.md) | - | No external validation available |
|
||||
| **Quantile Loss** | [Quantile](../lib/errors/quantile/Quantile.md) | - | No external validation available |
|
||||
| **Tukey Loss** | [Tukey](../lib/errors/tukeybiweight/TukeyBiweight.md) | - | No external validation available |
|
||||
| **Quantile Loss** | [Quantile](../lib/errors/quantileloss/QuantileLoss.md) | - | No external validation available |
|
||||
| **MAPE** | [Mape](../lib/errors/mape/Mape.md) | - | No external validation available |
|
||||
| **SMAPE** | [Smape](../lib/errors/smape/Smape.md) | - | No external validation available |
|
||||
| **MAAPE** | [Maape](../lib/errors/maape/Maape.md) | - | No external validation available |
|
||||
|
||||
+409
-395
@@ -4,401 +4,415 @@
|
||||
|
||||
| Category | Count | Description |
|
||||
| :--- | :---: | :--- |
|
||||
| [Trends (FIR)](trends_FIR/_index.md) | 32 | Finite Impulse Response moving averages |
|
||||
| [Trends (IIR)](trends_IIR/_index.md) | 37 | Infinite Impulse Response moving averages |
|
||||
| [Filters](filters/_index.md) | 37 | Signal processing filters |
|
||||
| [Oscillators](oscillators/_index.md) | 40 | Indicators that fluctuate around a center line |
|
||||
| [Dynamics](dynamics/_index.md) | 18 | Trend strength and direction indicators |
|
||||
| [Momentum](momentum/_index.md) | 17 | Momentum-based indicators |
|
||||
| [Volatility](volatility/_index.md) | 26 | Volatility estimators and indicators |
|
||||
| [Volume](volume/_index.md) | 27 | Volume-based indicators |
|
||||
| [Statistics](statistics/_index.md) | 34 | Statistical measures and tests |
|
||||
| [Channels](channels/_index.md) | 23 | Price channels and bands |
|
||||
| [Cycles](cycles/_index.md) | 16 | Cycle analysis and signal processing |
|
||||
| [Reversals](reversals/_index.md) | 12 | Pattern recognition and reversal detection |
|
||||
| [Forecasts](forecasts/_index.md) | 1 | Predictive indicators |
|
||||
| [Errors](errors/_index.md) | 26 | Error metrics and loss functions |
|
||||
| [Numerics](numerics/_index.md) | 30 | Mathematical transformations |
|
||||
| **Total** | **376** | |
|
||||
| [Core](core/_index.md) | 8 | Price transforms and fundamental building blocks |
|
||||
| [Trends (FIR)](trends_FIR/_index.md) | 24 | Finite Impulse Response moving averages |
|
||||
| [Trends (IIR)](trends_IIR/_index.md) | 32 | Infinite Impulse Response moving averages |
|
||||
| [Filters](filters/_index.md) | 16 | Signal processing filters |
|
||||
| [Oscillators](oscillators/_index.md) | 16 | Indicators that fluctuate around a center line |
|
||||
| [Dynamics](dynamics/_index.md) | 14 | Trend strength and direction indicators |
|
||||
| [Momentum](momentum/_index.md) | 19 | Momentum-based indicators |
|
||||
| [Volatility](volatility/_index.md) | 20 | Volatility estimators and indicators |
|
||||
| [Volume](volume/_index.md) | 15 | Volume-based indicators |
|
||||
| [Statistics](statistics/_index.md) | 13 | Statistical measures and tests |
|
||||
| [Channels](channels/_index.md) | 18 | Price channels and bands |
|
||||
| [Cycles](cycles/_index.md) | 5 | Cycle analysis and signal processing |
|
||||
| [Reversals](reversals/_index.md) | 7 | Pattern recognition and reversal detection |
|
||||
| [Forecasts](forecasts/_index.md) | 4 | Predictive indicators |
|
||||
| [Errors](errors/_index.md) | 13 | Error metrics and loss functions |
|
||||
| [Numerics](numerics/_index.md) | 5 | Mathematical transformations |
|
||||
| **Total** | **229** | |
|
||||
|
||||
## All Indicators
|
||||
|
||||
| Indicator | Full Name | Category |
|
||||
| :--- | :--- | :--- |
|
||||
| [ABBER](channels/abber/Abber.md) | Aberration Bands | Channels |
|
||||
| [AC](oscillators/ac/Ac.md) | Acceleration Oscillator | Oscillators |
|
||||
| [ACCBANDS](channels/accbands/Accbands.md) | Acceleration Bands | Channels |
|
||||
| [ACCEL](numerics/accel/Accel.md) | Acceleration | Numerics |
|
||||
| [ACF](statistics/acf/Acf.md) | Autocorrelation Function | Statistics |
|
||||
| [ADL](volume/adl/Adl.md) | Accumulation/Distribution Line | Volume |
|
||||
| [ADOSC](volume/adosc/Adosc.md) | Chaikin A/D Oscillator | Volume |
|
||||
| [ADR](volatility/adr/Adr.md) | Average Daily Range | Volatility |
|
||||
| [ADX](dynamics/adx/Adx.md) | Average Directional Index | Dynamics |
|
||||
| ADXVMA | ADX Variable MA | Trends (IIR) |
|
||||
| [ADXR](dynamics/adxr/Adxr.md) | Average Directional Movement Rating | Dynamics |
|
||||
| [AFIRMA](forecasts/afirma/Afirma.md) | Adaptive FIR Moving Average | Forecasts |
|
||||
| [AGC](numerics/agc/Agc.md) | Ehlers Automatic Gain Control | Numerics |
|
||||
| AHRENS | Ahrens MA | Trends (IIR) |
|
||||
| [ALAGUERRE](filters/alaguerre/ALaguerre.md) | Ehlers Adaptive Laguerre Filter | Filters |
|
||||
| [ALLIGATOR](dynamics/alligator/Alligator.md) | Williams Alligator | Dynamics |
|
||||
| [ALMA](trends_FIR/alma/Alma.md) | Arnaud Legoux MA | Trends (FIR) |
|
||||
| [AMAT](dynamics/amat/Amat.md) | Archer Moving Averages Trends | Dynamics |
|
||||
| [AO](oscillators/ao/Ao.md) | Awesome Oscillator | Oscillators |
|
||||
| [AOBV](volume/aobv/Aobv.md) | Archer On-Balance Volume | Volume |
|
||||
| [APCHANNEL](channels/apchannel/Apchannel.md) | Andrews' Pitchfork | Channels |
|
||||
| [APO](oscillators/apo/Apo.md) | Absolute Price Oscillator | Oscillators |
|
||||
| [APZ](channels/apz/Apz.md) | Adaptive Price Zone | Channels |
|
||||
| [AROON](dynamics/aroon/Aroon.md) | Aroon | Dynamics |
|
||||
| [AROONOSC](dynamics/aroonosc/Aroonosc.md) | Aroon Oscillator | Dynamics |
|
||||
| [ATR](volatility/atr/Atr.md) | Average True Range | Volatility |
|
||||
| [ATRBANDS](channels/atrbands/Atrbands.md) | ATR Bands | Channels |
|
||||
| [BAXTERKING](filters/baxterking/BaxterKing.md) | Baxter-King Band-Pass Filter | Filters |
|
||||
| [CFITZ](filters/cfitz/Cfitz.md) | Christiano-Fitzgerald Filter | Filters |
|
||||
| [BBANDS](channels/bbands/Bbands.md) | Bollinger Bands | Channels |
|
||||
| [ATRN](volatility/atrn/Atrn.md) | ATR Normalized | Volatility |
|
||||
| [ATRP](volatility/atrp/Atrp.md) | ATR Percent | Volatility |
|
||||
| [BBI](oscillators/bbi/Bbi.md) | Bulls Bears Index | Oscillators |
|
||||
| [BBB](oscillators/bbb/Bbb.md) | Bollinger %B | 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 |
|
||||
| [BBWP](volatility/bbwp/Bbwp.md) | BB Width Percentile | Volatility |
|
||||
| [BESSEL](filters/bessel/Bessel.md) | Bessel Filter | Filters |
|
||||
| [BETA](statistics/beta/Beta.md) | Beta Coefficient | Statistics |
|
||||
| BETADIST | Beta Distribution | Numerics |
|
||||
| [BIAS](statistics/bias/Bias.md) | Bias | Statistics |
|
||||
| [BILATERAL](filters/bilateral/Bilateral.md) | Bilateral Filter | Filters |
|
||||
| BINOMDIST | 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 |
|
||||
| BRAR | BRAR | Oscillators |
|
||||
| [BUTTER2](filters/butter2/Butter2.md) | Ehlers 2-Pole Butterworth Filter | Filters |
|
||||
| [BUTTER3](filters/butter3/Butter3.md) | Ehlers 3-Pole Butterworth Filter | Filters |
|
||||
| [BWMA](trends_FIR/bwma/Bwma.md) | Bessel-Weighted MA | Trends (FIR) |
|
||||
| [CCOR](cycles/ccor/Ccor.md) | Ehlers Correlation Cycle | Cycles |
|
||||
| [CCI](momentum/cci/Cci.md) | Commodity Channel Index | Momentum |
|
||||
| [CCV](volatility/ccv/Ccv.md) | Close-to-Close Volatility | Volatility |
|
||||
| CCYC | Ehlers Cyber Cycle | Cycles |
|
||||
| [CFB](momentum/cfb/Cfb.md) | Composite Fractal Behavior | Momentum |
|
||||
| [CFO](oscillators/cfo/Cfo.md) | Chande Forecast Oscillator | Oscillators |
|
||||
| [CCYC](cycles/ccyc/Ccyc.md) | Ehlers Cyber Cycle | Cycles |
|
||||
| [CG](cycles/cg/Cg.md) | Ehlers Center of Gravity | Cycles |
|
||||
| [CHANDELIER](reversals/chandelier/Chandelier.md) | Chandelier Exit | Reversals |
|
||||
| [CHANGE](numerics/change/Change.md) | Percentage Change | Numerics |
|
||||
| [CHEBY1](filters/cheby1/Cheby1.md) | Chebyshev Type I | Filters |
|
||||
| [CHEBY2](filters/cheby2/Cheby2.md) | Chebyshev Type II | Filters |
|
||||
| [CHOP](dynamics/chop/Chop.md) | Choppiness Index | Dynamics |
|
||||
| [CKSTOP](reversals/ckstop/Ckstop.md) | Chande Kroll Stop | Reversals |
|
||||
| [CMA](statistics/cma/Cma.md) | Cumulative Moving Average | Statistics |
|
||||
| [CMF](volume/cmf/Cmf.md) | Chaikin Money Flow | Volume |
|
||||
| [CMO](momentum/cmo/Cmo.md) | Chande Momentum Oscillator | Momentum |
|
||||
| [COINTEGRATION](statistics/cointegration/Cointegration.md) | Cointegration | Statistics |
|
||||
| [CONV](trends_FIR/conv/Conv.md) | Convolution MA | Trends (FIR) |
|
||||
| [CORAL](trends_IIR/coral/Coral.md) | Coral Trend Filter | Trends (IIR) |
|
||||
| COPPOCK | Coppock Curve | Oscillators |
|
||||
| [CORRELATION](statistics/correlation/Correlation.md) | Correlation | Statistics |
|
||||
| [COVARIANCE](statistics/covariance/Covariance.md) | Covariance | Statistics |
|
||||
| CRMA | Cubic Regression MA | Trends (FIR) |
|
||||
| CRSI | Connors RSI | Oscillators |
|
||||
| CTI | Correlation Trend Indicator | Oscillators |
|
||||
| [CV](volatility/cv/Cv.md) | Coefficient of Variation | Volatility |
|
||||
| [CVI](volatility/cvi/Cvi.md) | Chaikin Volatility | Volatility |
|
||||
| CWT | Continuous Wavelet Transform | Numerics |
|
||||
| [DCHANNEL](channels/dchannel/Dchannel.md) | Donchian Channels | Channels |
|
||||
| [DECAYCHANNEL](channels/decaychannel/decaychannel.md) | Decay Min-Max Channel | Channels |
|
||||
| [DECO](oscillators/deco/Deco.md) | Ehlers Decycler Oscillator | Oscillators |
|
||||
| [DECYCLER](trends_IIR/decycler/Decycler.md) | Ehlers Decycler | Trends (IIR) |
|
||||
| [DEMA](trends_IIR/dema/Dema.md) | Double Exponential MA | Trends (IIR) |
|
||||
| [DMX](dynamics/dmx/Dmx.md) | Jurik Directional Movement Index | Dynamics |
|
||||
| DOSC | Derivative Oscillator | Oscillators |
|
||||
| [DPO](oscillators/dpo/Dpo.md) | Detrended Price Oscillator | 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 | Discrete Wavelet Transform | Numerics |
|
||||
| [DX](dynamics/dx/Dx.md) | Directional Movement Index | Dynamics |
|
||||
| [EACP](cycles/eacp/Eacp.md) | Ehlers Autocorrelation Periodogram | Cycles |
|
||||
| [EBSW](cycles/ebsw/Ebsw.md) | Ehlers Even Better Sinewave | Cycles |
|
||||
| [EDCF](filters/edcf/Edcf.md) | Ehlers Distance Coefficient Filter | Filters |
|
||||
| [EFI](volume/efi/Efi.md) | Elder's Force Index | Volume |
|
||||
| [ELLIPTIC](filters/elliptic/Elliptic.md) | Elliptic Filter | Filters |
|
||||
| [EMA](trends_IIR/ema/Ema.md) | Exponential MA | Trends (IIR) |
|
||||
| [ENTROPY](statistics/entropy/Entropy.md) | Shannon Entropy | Statistics |
|
||||
| [EOM](volume/eom/Eom.md) | Ease of Movement | Volume |
|
||||
| [EVWMA](volume/evwma/Evwma.md) | Elastic Volume Weighted MA | Volume |
|
||||
| [ER](oscillators/er/Er.md) | Efficiency Ratio | Oscillators |
|
||||
| [ERI](oscillators/eri/Eri.md) | Elder Ray Index | Oscillators |
|
||||
| [EWMA](volatility/ewma/Ewma.md) | EWMA Volatility | Volatility |
|
||||
| EXPDIST | Exponential Distribution | Numerics |
|
||||
| [EXPTRANS](numerics/exptrans/Exptrans.md) | Exponential Transform | Numerics |
|
||||
| FDIST | F-Distribution | Numerics |
|
||||
| FFT | Fast Fourier Transform | Numerics |
|
||||
| [FISHER](oscillators/fisher/Fisher.md) | Ehlers Fisher Transform | Oscillators |
|
||||
| FOSC | Forecast Oscillator | Oscillators |
|
||||
| [FRACTALS](reversals/fractals/Fractals.md) | Williams Fractals | Reversals |
|
||||
| [FCB](channels/fcb/fcb.md) | Fractal Chaos Bands | Channels |
|
||||
| GDEMA | Generalized DEMA | Trends (IIR) |
|
||||
| [FRAMA](trends_IIR/frama/Frama.md) | Ehlers Fractal Adaptive MA | Trends (IIR) |
|
||||
| GAMMADIST | Gamma Distribution | Numerics |
|
||||
| [GAUSS](filters/gauss/Gauss.md) | Gaussian Filter | Filters |
|
||||
| [GEOMEAN](statistics/geomean/Geomean.md) | Geometric Mean | Statistics |
|
||||
| [GKV](volatility/gkv/Gkv.md) | Garman-Klass Volatility | Volatility |
|
||||
| [GRANGER](statistics/granger/Granger.md) | Granger Causality | Statistics |
|
||||
| [GWMA](trends_FIR/gwma/Gwma.md) | Gaussian Weighted MA | Trends (FIR) |
|
||||
| [HAMMA](trends_FIR/hamma/Hamma.md) | Hamming MA | Trends (FIR) |
|
||||
| HEND | Henderson Moving Average | Trends (FIR) |
|
||||
| [HANN](filters/hann/Hann.md) | Hann Filter | Filters |
|
||||
| [HANMA](trends_FIR/hanma/Hanma.md) | Hanning MA | Trends (FIR) |
|
||||
| [HARMEAN](statistics/harmean/Harmean.md) | Harmonic Mean | Statistics |
|
||||
| [HEMA](trends_IIR/hema/Hema.md) | Hull Exponential MA | Trends (IIR) |
|
||||
| [HIGHEST](numerics/highest/Highest.md) | Rolling Maximum | Numerics |
|
||||
| [HLV](volatility/hlv/Hlv.md) | High-Low Volatility | Volatility |
|
||||
| [HOLT](trends_IIR/holt/Holt.md) | Holt Exponential Smoothing | Trends (IIR) |
|
||||
| [HMA](trends_FIR/hma/Hma.md) | Hull MA | Trends (FIR) |
|
||||
| [HOMOD](cycles/homod/Homod.md) | Ehlers Homodyne Discriminator | Cycles |
|
||||
| [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 | Trends (IIR) |
|
||||
| [HT_DCPERIOD](cycles/ht_dcperiod/Ht_dcperiod.md) | Ehlers HT Dominant Cycle Period | Cycles |
|
||||
| [HT_DCPHASE](cycles/ht_dcphase/Ht_dcphase.md) | Ehlers HT Dominant Cycle Phase | Cycles |
|
||||
| [HT_PHASOR](cycles/ht_phasor/Ht_phasor.md) | Ehlers HT Phasor Components | Cycles |
|
||||
| [HT_SINE](cycles/ht_sine/Ht_sine.md) | Ehlers HT SineWave | Cycles |
|
||||
| [HT_TRENDMODE](dynamics/ht_trendmode/Ht_trendmode.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 |
|
||||
| [HWMA](trends_IIR/hwma/Hwma.md) | Holt-Winters MA | Trends (IIR) |
|
||||
| [ICHIMOKU](dynamics/ichimoku/Ichimoku.md) | Ichimoku Cloud | Dynamics |
|
||||
| IFFT | Inverse Fast Fourier Transform | Numerics |
|
||||
| ILRS | Integral of LinReg Slope | Trends (FIR) |
|
||||
| [III](volume/iii/Iii.md) | Intraday Intensity Index | Volume |
|
||||
| [IMI](dynamics/imi/Imi.md) | Intraday Momentum Index | Dynamics |
|
||||
| [IMPULSE](dynamics/impulse/Impulse.md) | Elder Impulse System | Dynamics |
|
||||
| [INERTIA](oscillators/inertia/Inertia.md) | Inertia | Oscillators |
|
||||
| [IQR](statistics/iqr/Iqr.md) | Interquartile Range | Statistics |
|
||||
| [JB](statistics/jb/Jb.md) | Jarque-Bera Test | Statistics |
|
||||
| [JBANDS](channels/jbands/Jbands.md) | Jurik Adaptive Envelope Bands | Channels |
|
||||
| [JERK](numerics/jerk/Jerk.md) | Jerk | Numerics |
|
||||
| [JMA](trends_IIR/jma/Jma.md) | Jurik MA | Trends (IIR) |
|
||||
| [JVOLTY](volatility/jvolty/Jvolty.md) | Jurik Volatility | Volatility |
|
||||
| KAISER | Kaiser Window MA | Trends (FIR) |
|
||||
| [JVOLTYN](volatility/jvoltyn/Jvoltyn.md) | Jurik Volatility Normalized | Volatility |
|
||||
| [KALMAN](filters/kalman/Kalman.md) | Kalman Filter | Filters |
|
||||
| [LAGUERRE](filters/laguerre/Laguerre.md) | Ehlers Laguerre Filter | Filters |
|
||||
| [LMS](filters/lms/Lms.md) | Least Mean Squares Adaptive Filter | Filters |
|
||||
| [RLS](filters/rls/Rls.md) | Recursive Least Squares Adaptive Filter | Filters |
|
||||
| [KAMA](trends_IIR/kama/Kama.md) | Kaufman Adaptive MA | Trends (IIR) |
|
||||
| [KCHANNEL](channels/kchannel/kchannel.md) | Keltner Channel | Channels |
|
||||
| [KDJ](oscillators/kdj/Kdj.md) | KDJ Indicator | Oscillators |
|
||||
| [KENDALL](statistics/kendall/Kendall.md) | Kendall Rank Correlation | Statistics |
|
||||
| [KRI](oscillators/kri/Kri.md) | Kairi Relative Index | Oscillators |
|
||||
| LANCZOS | Lanczos (sinc) Window MA | Trends (FIR) |
|
||||
| KST | KST Oscillator | Oscillators |
|
||||
| [KURTOSIS](statistics/kurtosis/Kurtosis.md) | Kurtosis | Statistics |
|
||||
| [KVO](volume/kvo/Kvo.md) | Klinger Volume Oscillator | Volume |
|
||||
| [LEMA](trends_IIR/lema/Lema.md) | Leader EMA | Trends (IIR) |
|
||||
| [LINEARTRANS](numerics/lineartrans/Lineartrans.md) | Linear Transform | Numerics |
|
||||
| [LINREG](statistics/linreg/LinReg.md) | Linear Regression | Statistics |
|
||||
| [LOESS](filters/loess/Loess.md) | LOESS Smoothing | Filters |
|
||||
| [LOGCOSH](errors/logcosh/Logcosh.md) | Log-Cosh Loss | Errors |
|
||||
| LOGNORMDIST | Log-normal Distribution | Numerics |
|
||||
| [LOGTRANS](numerics/logtrans/Logtrans.md) | Logarithmic Transform | Numerics |
|
||||
| [LOWEST](numerics/lowest/Lowest.md) | Rolling Minimum | Numerics |
|
||||
| [LSMA](trends_FIR/lsma/Lsma.md) | Least Squares MA | Trends (FIR) |
|
||||
| LTMA | Linear Trend MA | Trends (IIR) |
|
||||
| [LUNAR](cycles/lunar/Lunar.md) | Lunar Phase | Cycles |
|
||||
| [MAAPE](errors/maape/Maape.md) | Mean Arctangent APE | Errors |
|
||||
| [MACD](momentum/macd/Macd.md) | Moving Average Convergence Divergence | Momentum |
|
||||
| [MAE](errors/mae/Mae.md) | Mean Absolute Error | Errors |
|
||||
| [MCNMA](trends_IIR/mcnma/Mcnma.md) | McNicholl EMA | Trends (IIR) |
|
||||
| [MAENV](channels/maenv/maenv.md) | Moving Average Envelope | Channels |
|
||||
| [MAMA](trends_IIR/mama/Mama.md) | Ehlers MESA Adaptive MA | Trends (IIR) |
|
||||
| [MAVP](trends_IIR/mavp/Mavp.md) | Moving Average Variable Period | Trends (IIR) |
|
||||
| [MAPD](errors/mapd/Mapd.md) | Mean Absolute % Deviation | Errors |
|
||||
| [MAPE](errors/mape/Mape.md) | Mean Absolute % Error | Errors |
|
||||
| [MASE](errors/mase/Mase.md) | Mean Absolute Scaled Error | Errors |
|
||||
| [MASSI](volatility/massi/Massi.md) | Mass Index | Volatility |
|
||||
| [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 |
|
||||
| [MEDIAN](statistics/median/Median.md) | Median | Statistics |
|
||||
| MEDF | Moving Median Filter | Filters |
|
||||
| [MFI](volume/mfi/Mfi.md) | Money Flow Index | Volume |
|
||||
| [MGDI](trends_IIR/mgdi/Mgdi.md) | McGinley Dynamic Indicator | Trends (IIR) |
|
||||
| [MIDPOINT](numerics/midpoint/Midpoint.md) | Midrange | Numerics |
|
||||
| [MMA](trends_IIR/mma/Mma.md) | Modified MA | Trends (IIR) |
|
||||
| MODF | Modular Filter | Filters |
|
||||
| [MMCHANNEL](channels/mmchannel/Mmchannel.md) | Min-Max Channel | Channels |
|
||||
| [MODE](statistics/mode/Mode.md) | Mode | Statistics |
|
||||
| [MOM](momentum/mom/Mom.md) | Momentum | Momentum |
|
||||
| [MPE](errors/mpe/Mpe.md) | Mean Percentage Error | Errors |
|
||||
| [MRAE](errors/mrae/Mrae.md) | Mean Relative Absolute Error | Errors |
|
||||
| [MSE](errors/mse/Mse.md) | Mean Squared Error | Errors |
|
||||
| [MSLE](errors/msle/Msle.md) | Mean Squared Log Error | Errors |
|
||||
| MSTOCH | Ehlers MESA Stochastic | Oscillators |
|
||||
| [NATR](volatility/natr/Natr.md) | Normalized ATR | Volatility |
|
||||
| NORMDIST | Normal Distribution | Numerics |
|
||||
| [NORMALIZE](numerics/normalize/Normalize.md) | Min-Max Normalization | Numerics |
|
||||
| NLMA | Non-Lag Moving Average | Trends (FIR) |
|
||||
| [NMA](trends_IIR/nma/Nma.md) | Natural Moving Average | Trends (IIR) |
|
||||
| [NOTCH](filters/notch/Notch.md) | Notch Filter | Filters |
|
||||
| NW | Nadaraya-Watson Kernel Regression | Filters |
|
||||
| [ONEEURO](filters/oneeuro/OneEuro.md) | One Euro Filter | Filters |
|
||||
| [NVI](volume/nvi/Nvi.md) | Negative Volume Index | Volume |
|
||||
| NYQMA | Nyquist MA | Trends (FIR) |
|
||||
| [OBV](volume/obv/Obv.md) | On Balance Volume | Volume |
|
||||
| [PACF](statistics/pacf/Pacf.md) | Partial Autocorrelation Function | Statistics |
|
||||
| PARZEN | Parzen Window MA | Trends (FIR) |
|
||||
| [PCHANNEL](channels/pchannel/Pchannel.md) | Price Channel | Channels |
|
||||
| [PERCENTILE](statistics/percentile/Percentile.md) | Percentile | Statistics |
|
||||
| [PGO](oscillators/pgo/Pgo.md) | Pretty Good Oscillator | Oscillators |
|
||||
| [PIVOT](reversals/pivot/Pivot.md) | Pivot Points (Classic) | Reversals |
|
||||
| [PIVOTCAM](reversals/pivotcam/Pivotcam.md) | Camarilla Pivot Points | Reversals |
|
||||
| [PIVOTDEM](reversals/pivotdem/Pivotdem.md) | DeMark Pivot Points | Reversals |
|
||||
| [PIVOTEXT](reversals/pivotext/Pivotext.md) | Extended Traditional Pivots | Reversals |
|
||||
| [PIVOTFIB](reversals/pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Reversals |
|
||||
| [PIVOTWOOD](reversals/pivotwood/Pivotwood.md) | Woodie's Pivot Points | Reversals |
|
||||
| [PMA](trends_FIR/pma/Pma.md) | Predictive Moving Average | Trends (FIR) |
|
||||
| [PMO](momentum/pmo/Pmo.md) | Price Momentum Oscillator | Momentum |
|
||||
| POISSONDIST | Poisson Distribution | Numerics |
|
||||
| POLYFIT | 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 |
|
||||
| [PSL](oscillators/psl/Psl.md) | Psychological Line | Oscillators |
|
||||
| [PSEUDOHUBER](errors/pseudohuber/Pseudohuber.md) | Pseudo-Huber Loss | Errors |
|
||||
| [PVD](volume/pvd/Pvd.md) | Price Volume Divergence | Volume |
|
||||
| [PVI](volume/pvi/Pvi.md) | Positive Volume Index | Volume |
|
||||
| [PVO](volume/pvo/Pvo.md) | Percentage Volume Oscillator | Volume |
|
||||
| [PVR](volume/pvr/Pvr.md) | Price Volume Rank | Volume |
|
||||
| [PVT](volume/pvt/Pvt.md) | Price Volume Trend | Volume |
|
||||
| [PWMA](trends_FIR/pwma/Pwma.md) | Pascal Weighted MA | Trends (FIR) |
|
||||
| [QEMA](trends_IIR/qema/Qema.md) | Quad Exponential MA | Trends (IIR) |
|
||||
| QQE | Quantitative Qualitative Estimation | Oscillators |
|
||||
| QRMA | Quadratic Regression MA | Trends (FIR) |
|
||||
| [QSTICK](dynamics/qstick/Qstick.md) | Qstick | Dynamics |
|
||||
| [QUANTILE](statistics/quantile/Quantile.md) | Quantile | Statistics |
|
||||
| [QUANTILELOSS](errors/quantileloss/QuantileLoss.md) | Quantile Loss | Errors |
|
||||
| [RAE](errors/rae/Rae.md) | Relative Absolute Error | Errors |
|
||||
| RAIN | Rainbow MA | Trends (FIR) |
|
||||
| [REGCHANNEL](channels/regchannel/Regchannel.md) | Regression Channels | Channels |
|
||||
| REFLEX | Ehlers Reflex Indicator | Oscillators |
|
||||
| [RELU](numerics/relu/Relu.md) | Rectified Linear Unit | Numerics |
|
||||
| [REMA](trends_IIR/rema/Rema.md) | Regularized Exponential MA | Trends (IIR) |
|
||||
| [REVERSEEMA](trends_IIR/reverseema/ReverseEma.md) | Reverse EMA | Trends (IIR) |
|
||||
| [RGMA](trends_IIR/rgma/Rgma.md) | Recursive Gaussian MA | Trends (IIR) |
|
||||
| RMED | Ehlers Recursive Median Filter | Filters |
|
||||
| [RMA](trends_IIR/rma/Rma.md) | wildeR MA | Trends (IIR) |
|
||||
| [RMSE](errors/rmse/Rmse.md) | Root Mean Squared Error | Errors |
|
||||
| [RMSLE](errors/rmsle/Rmsle.md) | Root Mean Squared Log Error | Errors |
|
||||
| [ROC](momentum/roc/Roc.md) | Rate of Change | Momentum |
|
||||
| [ROCP](momentum/rocp/Rocp.md) | Rate of Change Percentage | Momentum |
|
||||
| [ROCR](momentum/rocr/Rocr.md) | Rate of Change Ratio | Momentum |
|
||||
| [ROOFING](filters/roofing/Roofing.md) | Ehlers Roofing Filter | Filters |
|
||||
| [RSE](errors/rse/Rse.md) | Relative Squared Error | Errors |
|
||||
| [RSI](momentum/rsi/Rsi.md) | Relative Strength Index | Momentum |
|
||||
| [RSQUARED](errors/rsquared/Rsquared.md) | R² (Coefficient of Determination) | Errors |
|
||||
| [RSV](volatility/rsv/Rsv.md) | Rogers-Satchell Volatility | Volatility |
|
||||
| [RSX](momentum/rsx/Rsx.md) | Relative Strength Quality Index | Momentum |
|
||||
| [RV](volatility/rv/Rv.md) | Realized Volatility | Volatility |
|
||||
| [RVI](volatility/rvi/Rvi.md) | Relative Volatility Index | Volatility |
|
||||
| RVGI | Relative Vigor Index | Oscillators |
|
||||
| RWMA | Range Weighted MA | Trends (FIR) |
|
||||
| [SDCHANNEL](channels/sdchannel/Sdchannel.md) | Standard Deviation Channel | Channels |
|
||||
| SAK | Ehlers Swiss Army Knife | Filters |
|
||||
| [SAM](momentum/sam/Sam.md) | Smoothed Adaptive Momentum | Momentum |
|
||||
| SAM | Smoothed Adaptive Momentum | Momentum |
|
||||
| [SGF](filters/sgf/Sgf.md) | Savitzky-Golay Filter | Filters |
|
||||
| [SGMA](trends_FIR/sgma/Sgma.md) | Savitzky-Golay MA | Trends (FIR) |
|
||||
| [SIGMOID](numerics/sigmoid/Sigmoid.md) | Logistic Function | Numerics |
|
||||
| [SINE](cycles/sine/Sine.md) | Ehlers Sine Wave | Cycles |
|
||||
| [SINEMA](trends_FIR/sinema/Sinema.md) | Sine-Weighted MA | Trends (FIR) |
|
||||
| [SKEW](statistics/skew/Skew.md) | Skewness | Statistics |
|
||||
| [SLOPE](numerics/slope/Slope.md) | Rate of Change | Numerics |
|
||||
| [SMA](trends_FIR/sma/Sma.md) | Simple MA | Trends (FIR) |
|
||||
| [SMAPE](errors/smape/Smape.md) | Symmetric MAPE | Errors |
|
||||
| [SMI](oscillators/smi/Smi.md) | Stochastic Momentum Index | Oscillators |
|
||||
| [SOLAR](cycles/solar/Solar.md) | Solar Activity Cycle | Cycles |
|
||||
| SP15 | Spencer's 15-point MA | Trends (FIR) |
|
||||
| [SPBF](filters/spbf/Spbf.md) | Ehlers Super Passband Filter | Filters |
|
||||
| [SPEARMAN](statistics/spearman/Spearman.md) | Spearman Rank Correlation | Statistics |
|
||||
| SQUEEZE | Squeeze | Oscillators |
|
||||
| [SQRTTRANS](numerics/sqrttrans/Sqrttrans.md) | Square Root Transform | Numerics |
|
||||
| [SSF2](filters/ssf2/Ssf2.md) | Ehlers 2-Pole Super Smoother | Filters |
|
||||
| [SSF3](filters/ssf3/Ssf3.md) | Ehlers 3-Pole Super Smoother | Filters |
|
||||
| [SSFDSP](cycles/ssfdsp/Ssfdsp.md) | Ehlers SSF Detrended Synthetic Price | Cycles |
|
||||
| [STANDARDIZE](numerics/standardize/Standardize.md) | Z-Score Normalization | Numerics |
|
||||
| [STARCHANNEL](channels/starchannel/Starchannel.md) | Stoller Average Range Channel | Channels |
|
||||
| [STBANDS](channels/stbands/Stbands.md) | Super Trend Bands | Channels |
|
||||
| [STC](cycles/stc/Stc.md) | Schaff Trend Cycle | Cycles |
|
||||
| [STDDEV](statistics/stddev/StdDev.md) | Standard Deviation | Statistics |
|
||||
| [STOCH](oscillators/stoch/Stoch.md) | Stochastic Oscillator | Oscillators |
|
||||
| [STOCHF](oscillators/stochf/Stochf.md) | Stochastic Fast | Oscillators |
|
||||
| [STOCHRSI](oscillators/stochrsi/Stochrsi.md) | Stochastic RSI | Oscillators |
|
||||
| [SUM](statistics/sum/Sum.md) | Rolling Sum | Statistics |
|
||||
| [SUPER](dynamics/super/Super.md) | SuperTrend | Dynamics |
|
||||
| SWMA | Symmetric Weighted MA | Trends (FIR) |
|
||||
| [SWINGS](reversals/swings/Swings.md) | Swing High/Low Detection | Reversals |
|
||||
| [T3](trends_IIR/t3/T3.md) | Tillson T3 MA | Trends (IIR) |
|
||||
| TDIST | Student's t-Distribution | Numerics |
|
||||
| TD_SEQ | TD Sequential | Oscillators |
|
||||
| [TEMA](trends_IIR/tema/Tema.md) | Triple Exponential MA | Trends (IIR) |
|
||||
| [THEIL](statistics/theil/Theil.md) | Theil Index | Statistics |
|
||||
| [THEILU](errors/theilu/Theilu.md) | Theil's U Statistic | Errors |
|
||||
| [TR](volatility/tr/Tr.md) | True Range | Volatility |
|
||||
| [TRAMA](trends_IIR/trama/Trama.md) | Trend Regularity Adaptive MA | Trends (IIR) |
|
||||
| [TRENDFLEX](oscillators/trendflex/Trendflex.md) | Ehlers Trendflex | Oscillators |
|
||||
| TRIM | Trimmed Mean MA | Statistics |
|
||||
| [TRIMA](trends_FIR/trima/Trima.md) | Triangular MA | Trends (FIR) |
|
||||
| [TSF](trends_FIR/tsf/Tsf.md) | Time Series Forecast | Trends (FIR) |
|
||||
| [TRIX](oscillators/trix/Trix.md) | Triple Exponential Average | Oscillators |
|
||||
| [TSI](momentum/tsi/Tsi.md) | True Strength Index | Momentum |
|
||||
| [TTM_LRC](channels/ttm_lrc/TtmLrc.md) | TTM Linear Regression Channel | Channels |
|
||||
| [TTM_SCALPER](reversals/ttm_scalper/TtmScalper.md) | TTM Scalper Alert | Reversals |
|
||||
| [TTM_SQUEEZE](dynamics/ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | Dynamics |
|
||||
| [TTM_TREND](dynamics/ttm_trend/TtmTrend.md) | TTM Trend | Dynamics |
|
||||
| [TTM_WAVE](oscillators/ttm_wave/TtmWave.md) | TTM Wave | Oscillators |
|
||||
| [TUKEY](errors/tukeybiweight/TukeyBiweight.md) | Tukey Biweight Loss | Errors |
|
||||
| TUKEY_W | Tukey (Tapered Cosine) Window MA | Trends (FIR) |
|
||||
| [TVI](volume/tvi/Tvi.md) | Trade Volume Index | Volume |
|
||||
| [TWAP](volume/twap/Twap.md) | Time Weighted Average Price | Volume |
|
||||
| [UBANDS](channels/ubands/Ubands.md) | Ehlers Ultimate Bands | Channels |
|
||||
| [UCHANNEL](channels/uchannel/Uchannel.md) | Ehlers Ultimate Channel | Channels |
|
||||
| [UI](volatility/ui/Ui.md) | Ulcer Index | Volatility |
|
||||
| [ULTOSC](oscillators/ultosc/Ultosc.md) | Ultimate Oscillator | Oscillators |
|
||||
| [USF](filters/usf/Usf.md) | Ehlers Ultimate Smoother | Filters |
|
||||
| [VA](volume/va/Va.md) | Volume Accumulation | Volume |
|
||||
| [VAMA](trends_IIR/vama/Vama.md) | Volatility Adjusted MA | Trends (IIR) |
|
||||
| [VARIANCE](statistics/variance/Variance.md) | Variance | Statistics |
|
||||
| [VEL](momentum/vel/Vel.md) | Jurik Velocity | Momentum |
|
||||
| [VF](volume/vf/Vf.md) | Volume Force | Volume |
|
||||
| [VIDYA](trends_IIR/vidya/Vidya.md) | Variable Index Dynamic Average | Trends (IIR) |
|
||||
| [VO](volume/vo/Vo.md) | Volume Oscillator | Volume |
|
||||
| [VORTEX](dynamics/vortex/Vortex.md) | Vortex Indicator | Dynamics |
|
||||
| [VOSS](filters/voss/Voss.md) | Ehlers Voss Predictive Filter | Filters |
|
||||
| [VOV](volatility/vov/Vov.md) | Volatility of Volatility | Volatility |
|
||||
| [VR](volatility/vr/Vr.md) | Volatility Ratio | Volatility |
|
||||
| [VROC](volume/vroc/Vroc.md) | Volume Rate of Change | Volume |
|
||||
| [VWAD](volume/vwad/Vwad.md) | Volume Weighted A/D | Volume |
|
||||
| [VWAP](volume/vwap/Vwap.md) | Volume Weighted Average Price | Volume |
|
||||
| [VWAPBANDS](channels/vwapbands/Vwapbands.md) | VWAP Bands | Channels |
|
||||
| [VWAPSD](channels/vwapsd/Vwapsd.md) | VWAP Standard Deviation Bands | Channels |
|
||||
| [VWMA](volume/vwma/Vwma.md) | Volume Weighted MA | Volume |
|
||||
| [WAD](volume/wad/Wad.md) | Williams A/D | Volume |
|
||||
| [WAVELET](filters/wavelet/Wavelet.md) | Wavelet Denoising Filter | Filters |
|
||||
| WAVG | Weighted Average | Statistics |
|
||||
| WEIBULLDIST | Weibull Distribution | Numerics |
|
||||
| [WIENER](filters/wiener/Wiener.md) | Wiener Filter | Filters |
|
||||
| [WILLR](oscillators/willr/Willr.md) | Williams %R | Oscillators |
|
||||
| WINS | Winsorized Mean MA | Statistics |
|
||||
| [WMA](trends_FIR/wma/Wma.md) | Weighted MA | Trends (FIR) |
|
||||
| [WMAPE](errors/wmape/Wmape.md) | Weighted MAPE | Errors |
|
||||
| [WRMSE](errors/wrmse/Wrmse.md) | Weighted RMSE | Errors |
|
||||
| [YZV](volatility/yzv/Yzv.md) | Yang-Zhang Volatility | Volatility |
|
||||
| [YZVAMA](trends_IIR/yzvama/Yzvama.md) | Yang-Zhang Volatility Adjusted MA | Trends (IIR) |
|
||||
| [ZLDEMA](trends_IIR/zldema/Zldema.md) | Zero-Lag Double Exponential MA | Trends (IIR) |
|
||||
| [ZLEMA](trends_IIR/zlema/Zlema.md) | Zero-Lag Exponential MA | Trends (IIR) |
|
||||
| [ZLTEMA](trends_IIR/zltema/Zltema.md) | Zero-Lag Triple Exponential MA | Trends (IIR) |
|
||||
| [ZSCORE](statistics/zscore/Zscore.md) | Z-Score | Statistics |
|
||||
| [ZTEST](statistics/ztest/Ztest.md) | Z-Test | Statistics |
|
||||
| Indicator | Full Name | Category |
|
||||
| :--------------------------------------------------------- | :------------------------------------------------------------------------ | :----------- |
|
||||
| [ABBER](channels/abber/Abber.md) | Aberration Bands | Channels |
|
||||
| [AC](oscillators/ac/Ac.md) | Acceleration Oscillator | Oscillators |
|
||||
| [ACCBANDS](channels/accbands/Accbands.md) | Acceleration Bands | Channels |
|
||||
| [ACCEL](numerics/accel/Accel.md) | Acceleration | Numerics |
|
||||
| [ACF](statistics/acf/Acf.md) | Autocorrelation Function | Statistics |
|
||||
| [ADL](volume/adl/Adl.md) | Accumulation/Distribution Line | Volume |
|
||||
| [ADOSC](volume/adosc/Adosc.md) | Chaikin A/D Oscillator | Volume |
|
||||
| [ADR](volatility/adr/Adr.md) | Average Daily Range | Volatility |
|
||||
| [ADX](dynamics/adx/Adx.md) | Average Directional Index | Dynamics |
|
||||
| [ADXVMA](trends_IIR/adxvma/Adxvma.md) | ADX Variable MA | Trends (IIR) |
|
||||
| [ADXR](dynamics/adxr/Adxr.md) | Average Directional Movement Rating | Dynamics |
|
||||
| [AFIRMA](forecasts/afirma/Afirma.md) | Adaptive FIR Moving Average | Forecasts |
|
||||
| [AGC](filters/agc/Agc.md) | Ehlers Automatic Gain Control | Filters |
|
||||
| [AHRENS](trends_IIR/ahrens/Ahrens.md) | Ahrens MA | Trends (IIR) |
|
||||
| [ALAGUERRE](filters/alaguerre/ALaguerre.md) | Ehlers Adaptive Laguerre Filter | Filters |
|
||||
| [ALLIGATOR](dynamics/alligator/Alligator.md) | Williams Alligator | Dynamics |
|
||||
| [ALMA](trends_FIR/alma/Alma.md) | Arnaud Legoux MA | Trends (FIR) |
|
||||
| [AMAT](dynamics/amat/Amat.md) | Archer Moving Averages Trends | Dynamics |
|
||||
| [AO](oscillators/ao/Ao.md) | Awesome Oscillator | Oscillators |
|
||||
| [AOBV](volume/aobv/Aobv.md) | Archer On-Balance Volume | Volume |
|
||||
| [APCHANNEL](channels/apchannel/Apchannel.md) | Andrews' Pitchfork | Channels |
|
||||
| [APO](oscillators/apo/Apo.md) | Absolute Price Oscillator | Oscillators |
|
||||
| [APZ](channels/apz/Apz.md) | Adaptive Price Zone | Channels |
|
||||
| [AROON](dynamics/aroon/Aroon.md) | Aroon | Dynamics |
|
||||
| [AROONOSC](dynamics/aroonosc/Aroonosc.md) | Aroon Oscillator | Dynamics |
|
||||
| ASI | Accumulation Swing Index | Momentum |
|
||||
| [ATR](volatility/atr/Atr.md) | Average True Range | Volatility |
|
||||
| [ATRBANDS](channels/atrbands/Atrbands.md) | ATR Bands | Channels |
|
||||
| [BAXTERKING](filters/baxterking/BaxterKing.md) | Baxter-King Band-Pass Filter | Filters |
|
||||
| [CFITZ](filters/cfitz/Cfitz.md) | Christiano-Fitzgerald Filter | Filters |
|
||||
| [BBANDS](channels/bbands/Bbands.md) | Bollinger Bands | Channels |
|
||||
| [ATRN](volatility/atrn/Atrn.md) | ATR Normalized | Volatility |
|
||||
| [AVGPRICE](core/avgprice/Avgprice.cs) | Average Price | Core |
|
||||
| [BBI](oscillators/bbi/Bbi.md) | Bulls Bears Index | Oscillators |
|
||||
| [BBB](oscillators/bbb/Bbb.md) | Bollinger %B | 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 |
|
||||
| [BBWP](volatility/bbwp/Bbwp.md) | BB Width Percentile | Volatility |
|
||||
| [BESSEL](filters/bessel/Bessel.md) | Bessel Filter | Filters |
|
||||
| [BETA](statistics/beta/Beta.md) | Beta Coefficient | Statistics |
|
||||
| BETADIST | Beta Distribution | Numerics |
|
||||
| [BIAS](momentum/bias/Bias.md) | Bias (also known as Disparity Index) | Momentum |
|
||||
| [BILATERAL](filters/bilateral/Bilateral.md) | Bilateral Filter | Filters |
|
||||
| BINOMDIST | 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 |
|
||||
| BRAR | BRAR | Oscillators |
|
||||
| [BUTTER2](filters/butter2/Butter2.md) | Ehlers 2-Pole Butterworth Filter | Filters |
|
||||
| [BUTTER3](filters/butter3/Butter3.md) | Ehlers 3-Pole Butterworth Filter | Filters |
|
||||
| [BWMA](trends_FIR/bwma/Bwma.md) | Bessel-Weighted MA | Trends (FIR) |
|
||||
| [CCOR](cycles/ccor/Ccor.md) | Ehlers Correlation Cycle | Cycles |
|
||||
| [CCI](momentum/cci/Cci.md) | Commodity Channel Index | Momentum |
|
||||
| [CCV](volatility/ccv/Ccv.md) | Close-to-Close Volatility | Volatility |
|
||||
| [CCYC](cycles/ccyc/Ccyc.md) | Ehlers Cyber Cycle | Cycles |
|
||||
| [CFB](momentum/cfb/Cfb.md) | Composite Fractal Behavior | Momentum |
|
||||
| [CFO](oscillators/cfo/Cfo.md) | Chande Forecast Oscillator | Oscillators |
|
||||
| [CG](cycles/cg/Cg.md) | Ehlers Center of Gravity | Cycles |
|
||||
| [CHANDELIER](reversals/chandelier/Chandelier.md) | Chandelier Exit | Reversals |
|
||||
| [CHANGE](numerics/change/Change.md) | Percentage Change | Numerics |
|
||||
| [CHEBY1](filters/cheby1/Cheby1.md) | Chebyshev Type I | Filters |
|
||||
| [CHEBY2](filters/cheby2/Cheby2.md) | Chebyshev Type II | Filters |
|
||||
| [CHOP](dynamics/chop/Chop.md) | Choppiness Index | Dynamics |
|
||||
| [CKSTOP](reversals/ckstop/Ckstop.md) | Chande Kroll Stop | Reversals |
|
||||
| [CMA](statistics/cma/Cma.md) | Cumulative Moving Average | Statistics |
|
||||
| [CMF](volume/cmf/Cmf.md) | Chaikin Money Flow | Volume |
|
||||
| [CMO](momentum/cmo/Cmo.md) | Chande Momentum Oscillator | Momentum |
|
||||
| [COINTEGRATION](statistics/cointegration/Cointegration.md) | Cointegration | Statistics |
|
||||
| [CONV](trends_FIR/conv/Conv.md) | Convolution MA | Trends (FIR) |
|
||||
| [CORAL](trends_IIR/coral/Coral.md) | Coral Trend Filter | Trends (IIR) |
|
||||
| COPPOCK | Coppock Curve | Oscillators |
|
||||
| [CORRELATION](statistics/correlation/Correlation.md) | Correlation | Statistics |
|
||||
| [COVARIANCE](statistics/covariance/Covariance.md) | Covariance | Statistics |
|
||||
| [CRMA](trends_FIR/crma/Crma.md) | Cubic Regression MA | Trends (FIR) |
|
||||
| CRSI | Connors RSI | Oscillators |
|
||||
| CTI | Correlation Trend Indicator | Oscillators |
|
||||
| [CV](volatility/cv/Cv.md) | Coefficient of Variation | Volatility |
|
||||
| [CVI](volatility/cvi/Cvi.md) | Chaikin Volatility | Volatility |
|
||||
| CWT | Continuous Wavelet Transform | Numerics |
|
||||
| [DCHANNEL](channels/dchannel/Dchannel.md) | Donchian Channels | Channels |
|
||||
| [DECAYCHANNEL](channels/decaychannel/decaychannel.md) | Decay Min-Max Channel | Channels |
|
||||
| [DECO](oscillators/deco/Deco.md) | Ehlers Decycler Oscillator | Oscillators |
|
||||
| [DECYCLER](trends_IIR/decycler/Decycler.md) | Ehlers Decycler | Trends (IIR) |
|
||||
| DEM | DeMarker Oscillator | Oscillators |
|
||||
| [DEMA](trends_IIR/dema/Dema.md) | Double Exponential MA | Trends (IIR) |
|
||||
| [DMX](dynamics/dmx/Dmx.md) | Jurik Directional Movement Index | Dynamics |
|
||||
| DOSC | Derivative Oscillator | Oscillators |
|
||||
| [DPO](oscillators/dpo/Dpo.md) | Detrended Price Oscillator | 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 | Discrete Wavelet Transform | Numerics |
|
||||
| DYMOI | Dynamic Momentum Index | Oscillators |
|
||||
| [DX](dynamics/dx/Dx.md) | Directional Movement Index | Dynamics |
|
||||
| [EACP](cycles/eacp/Eacp.md) | Ehlers Autocorrelation Periodogram | Cycles |
|
||||
| [EBSW](cycles/ebsw/Ebsw.md) | Ehlers Even Better Sinewave | Cycles |
|
||||
| [EDCF](filters/edcf/Edcf.md) | Ehlers Distance Coefficient Filter | Filters |
|
||||
| [EFI](volume/efi/Efi.md) | Elder's Force Index | Volume |
|
||||
| [ELLIPTIC](filters/elliptic/Elliptic.md) | Elliptic Filter | Filters |
|
||||
| [EMA](trends_IIR/ema/Ema.md) | Exponential MA | Trends (IIR) |
|
||||
| [ENTROPY](statistics/entropy/Entropy.md) | Shannon Entropy | Statistics |
|
||||
| [EOM](volume/eom/Eom.md) | Ease of Movement | Volume |
|
||||
| [EVWMA](volume/evwma/Evwma.md) | Elastic Volume Weighted MA | Volume |
|
||||
| [ER](oscillators/er/Er.md) | Efficiency Ratio | Oscillators |
|
||||
| [ERI](oscillators/eri/Eri.md) | Elder Ray Index | Oscillators |
|
||||
| ETHERM | Elder's Thermometer | Volatility |
|
||||
| [EWMA](volatility/ewma/Ewma.md) | EWMA Volatility | Volatility |
|
||||
| EXPDIST | Exponential Distribution | Numerics |
|
||||
| [EXPTRANS](numerics/exptrans/Exptrans.md) | Exponential Transform | Numerics |
|
||||
| FDIST | F-Distribution | Numerics |
|
||||
| FFT | Fast Fourier Transform | Numerics |
|
||||
| [FISHER](oscillators/fisher/Fisher.md) | Ehlers Fisher Transform | Oscillators |
|
||||
| [FRACTALS](reversals/fractals/Fractals.md) | Williams Fractals | Reversals |
|
||||
| [FCB](channels/fcb/fcb.md) | Fractal Chaos Bands | Channels |
|
||||
| [GDEMA](trends_IIR/gdema/Gdema.md) | Generalized DEMA | Trends (IIR) |
|
||||
| [FRAMA](trends_IIR/frama/Frama.md) | Ehlers Fractal Adaptive MA | Trends (IIR) |
|
||||
| [FWMA](trends_FIR/fwma/Fwma.md) | Fibonacci Weighted MA | Trends (FIR) |
|
||||
| GAMMADIST | Gamma Distribution | Numerics |
|
||||
| GATOR | Williams Gator Oscillator | Dynamics |
|
||||
| [GAUSS](filters/gauss/Gauss.md) | Gaussian Filter | Filters |
|
||||
| [GEOMEAN](statistics/geomean/Geomean.md) | Geometric Mean | Statistics |
|
||||
| [GKV](volatility/gkv/Gkv.md) | Garman-Klass Volatility | Volatility |
|
||||
| GHLA | Gann High-Low Activator | Dynamics |
|
||||
| [GRANGER](statistics/granger/Granger.md) | Granger Causality | Statistics |
|
||||
| [GWMA](trends_FIR/gwma/Gwma.md) | Gaussian Weighted MA | Trends (FIR) |
|
||||
| [HA](core/ha/Ha.md) | Heikin-Ashi | Core |
|
||||
| [HAMMA](trends_FIR/hamma/Hamma.md) | Hamming MA | Trends (FIR) |
|
||||
| [HEND](trends_FIR/hend/Hend.md) | Henderson Moving Average | Trends (FIR) |
|
||||
| [HANN](filters/hann/Hann.md) | Hann Filter | Filters |
|
||||
| [HANMA](trends_FIR/hanma/Hanma.md) | Hanning MA | Trends (FIR) |
|
||||
| [HARMEAN](statistics/harmean/Harmean.md) | Harmonic Mean | Statistics |
|
||||
| [HEMA](trends_IIR/hema/Hema.md) | Hull Exponential MA | Trends (IIR) |
|
||||
| [HIGHEST](numerics/highest/Highest.md) | Rolling Maximum | Numerics |
|
||||
| [HLV](volatility/hlv/Hlv.md) | High-Low Volatility | Volatility |
|
||||
| [HOLT](trends_IIR/holt/Holt.md) | Holt Exponential Smoothing | Trends (IIR) |
|
||||
| [HMA](trends_FIR/hma/Hma.md) | Hull MA | Trends (FIR) |
|
||||
| [HOMOD](cycles/homod/Homod.md) | Ehlers Homodyne Discriminator | Cycles |
|
||||
| [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 (also known as 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 |
|
||||
| [HUBER](errors/huber/Huber.md) | Huber Loss | Errors |
|
||||
| [HURST](statistics/hurst/Hurst.md) | Hurst Exponent | Statistics |
|
||||
| [HV](volatility/hv/Hv.md) | Historical Volatility | Volatility |
|
||||
| [HWMA](trends_IIR/hwma/Hwma.md) | Holt-Winters MA | Trends (IIR) |
|
||||
| [ICHIMOKU](dynamics/ichimoku/Ichimoku.md) | Ichimoku Cloud | Dynamics |
|
||||
| IFFT | Inverse Fast Fourier Transform | Numerics |
|
||||
| [ILRS](trends_FIR/ilrs/Ilrs.md) | Integral of LinReg Slope | Trends (FIR) |
|
||||
| [III](volume/iii/Iii.md) | Intraday Intensity Index | Volume |
|
||||
| [IMI](oscillators/imi/Imi.md) | Intraday Momentum Index | Oscillators |
|
||||
| [IMPULSE](dynamics/impulse/Impulse.md) | Elder Impulse System | Dynamics |
|
||||
| [INERTIA](oscillators/inertia/Inertia.md) | Inertia | Oscillators |
|
||||
| [IQR](statistics/iqr/Iqr.md) | Interquartile Range | Statistics |
|
||||
| [JB](statistics/jb/Jb.md) | Jarque-Bera Test | Statistics |
|
||||
| [JBANDS](channels/jbands/Jbands.md) | Jurik Adaptive Envelope Bands | Channels |
|
||||
| [JERK](numerics/jerk/Jerk.md) | Jerk | Numerics |
|
||||
| [JMA](trends_IIR/jma/Jma.md) | Jurik MA | Trends (IIR) |
|
||||
| [JVOLTY](volatility/jvolty/Jvolty.md) | Jurik Volatility | Volatility |
|
||||
| [KAISER](trends_FIR/kaiser/Kaiser.md) | Kaiser Window MA | Trends (FIR) |
|
||||
| [JVOLTYN](volatility/jvoltyn/Jvoltyn.md) | Jurik Volatility Normalized | Volatility |
|
||||
| [KALMAN](filters/kalman/Kalman.md) | Kalman Filter | Filters |
|
||||
| [LAGUERRE](filters/laguerre/Laguerre.md) | Ehlers Laguerre Filter | Filters |
|
||||
| [LMS](filters/lms/Lms.md) | Least Mean Squares Adaptive Filter | Filters |
|
||||
| [RLS](filters/rls/Rls.md) | Recursive Least Squares Adaptive Filter | Filters |
|
||||
| [KAMA](trends_IIR/kama/Kama.md) | Kaufman Adaptive MA | Trends (IIR) |
|
||||
| [KCHANNEL](channels/kchannel/kchannel.md) | Keltner Channel | Channels |
|
||||
| [KDJ](oscillators/kdj/Kdj.md) | KDJ Indicator | Oscillators |
|
||||
| [KENDALL](statistics/kendall/Kendall.md) | Kendall Rank Correlation | Statistics |
|
||||
| [KRI](oscillators/kri/Kri.md) | Kairi Relative Index | Oscillators |
|
||||
| [LANCZOS](trends_FIR/lanczos/Lanczos.md) | Lanczos (sinc) Window MA | Trends (FIR) |
|
||||
| KST | KST Oscillator | Oscillators |
|
||||
| [KURTOSIS](statistics/kurtosis/Kurtosis.md) | Kurtosis | Statistics |
|
||||
| [KVO](volume/kvo/Kvo.md) | Klinger Volume Oscillator | Volume |
|
||||
| [LEMA](trends_IIR/lema/Lema.md) | Leader EMA | Trends (IIR) |
|
||||
| [LINEARTRANS](numerics/lineartrans/Lineartrans.md) | Linear Transform | Numerics |
|
||||
| [LINREG](statistics/linreg/LinReg.md) | Linear Regression | Statistics |
|
||||
| [LOESS](filters/loess/Loess.md) | LOESS Smoothing | Filters |
|
||||
| [LOGCOSH](errors/logcosh/Logcosh.md) | Log-Cosh Loss | Errors |
|
||||
| LOGNORMDIST | Log-normal Distribution | Numerics |
|
||||
| [LOGTRANS](numerics/logtrans/Logtrans.md) | Logarithmic Transform | Numerics |
|
||||
| [LOWEST](numerics/lowest/Lowest.md) | Rolling Minimum | Numerics |
|
||||
| LRSI | Ehlers Laguerre RSI | Oscillators |
|
||||
| [LSMA](trends_FIR/lsma/Lsma.md) | Least Squares MA | Trends (FIR) |
|
||||
| LTMA | Linear Trend MA | Trends (IIR) |
|
||||
| [LUNAR](cycles/lunar/Lunar.md) | Lunar Phase | Cycles |
|
||||
| [MAAPE](errors/maape/Maape.md) | Mean Arctangent APE | Errors |
|
||||
| [MACD](momentum/macd/Macd.md) | Moving Average Convergence Divergence | Momentum |
|
||||
| [MAE](errors/mae/Mae.md) | Mean Absolute Error | Errors |
|
||||
| [MCNMA](trends_IIR/mcnma/Mcnma.md) | McNicholl EMA | Trends (IIR) |
|
||||
| [MAENV](channels/maenv/maenv.md) | Moving Average Envelope | Channels |
|
||||
| [MAMA](trends_IIR/mama/Mama.md) | Ehlers MESA Adaptive MA | Trends (IIR) |
|
||||
| [MAVP](trends_IIR/mavp/Mavp.md) | Moving Average Variable Period | Trends (IIR) |
|
||||
| [MAPD](errors/mapd/Mapd.md) | Mean Absolute % Deviation | Errors |
|
||||
| [MAPE](errors/mape/Mape.md) | Mean Absolute % Error | Errors |
|
||||
| [MASE](errors/mase/Mase.md) | Mean Absolute Scaled Error | Errors |
|
||||
| [MASSI](volatility/massi/Massi.md) | Mass Index | Volatility |
|
||||
| MARKETFI | Market Facilitation Index | Volume |
|
||||
| [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 |
|
||||
| MEANDEV | Mean Absolute Deviation | Statistics |
|
||||
| [MEDIAN](statistics/median/Median.md) | Median | Statistics |
|
||||
| [MEDPRICE](core/medprice/Medprice.cs) | Median Price | Core |
|
||||
| [MFI](volume/mfi/Mfi.md) | Money Flow Index | Volume |
|
||||
| [MGDI](trends_IIR/mgdi/Mgdi.md) | McGinley Dynamic Indicator | Trends (IIR) |
|
||||
| [MIDPOINT](core/midpoint/Midpoint.md) | Rolling Range Midpoint | Core |
|
||||
| [MIDPRICE](core/midprice/Midprice.cs) | Midpoint Price | Core |
|
||||
| [MMA](trends_IIR/mma/Mma.md) | Modified MA | Trends (IIR) |
|
||||
| [MODF](filters/modf/Modf.md) | Modular Filter | Filters |
|
||||
| [MMCHANNEL](channels/mmchannel/Mmchannel.md) | Min-Max Channel | Channels |
|
||||
| [MODE](statistics/mode/Mode.md) | Mode | Statistics |
|
||||
| [MOM](momentum/mom/Mom.md) | Momentum | Momentum |
|
||||
| [MPE](errors/mpe/Mpe.md) | Mean Percentage Error | Errors |
|
||||
| [MRAE](errors/mrae/Mrae.md) | Mean Relative Absolute Error | Errors |
|
||||
| [MSE](errors/mse/Mse.md) | Mean Squared Error | Errors |
|
||||
| [MSLE](errors/msle/Msle.md) | Mean Squared Log Error | Errors |
|
||||
| MSTOCH | Ehlers MESA Stochastic | Oscillators |
|
||||
| [NATR](volatility/natr/Natr.md) | Normalized ATR | Volatility |
|
||||
| NORMDIST | Normal Distribution | Numerics |
|
||||
| [NORMALIZE](numerics/normalize/Normalize.md) | Min-Max Normalization | Numerics |
|
||||
| [NLMA](trends_FIR/nlma/Nlma.md) | Non-Lag Moving Average | Trends (FIR) |
|
||||
| [NMA](trends_IIR/nma/Nma.md) | Natural Moving Average | Trends (IIR) |
|
||||
| [NOTCH](filters/notch/Notch.md) | Notch Filter | Filters |
|
||||
| [NW](filters/nw/Nw.md) | Nadaraya-Watson Kernel Regression | Filters |
|
||||
| [ONEEURO](filters/oneeuro/OneEuro.md) | One Euro Filter | Filters |
|
||||
| [NVI](volume/nvi/Nvi.md) | Negative Volume Index | Volume |
|
||||
| NYQMA | Nyquist MA | Trends (FIR) |
|
||||
| [OBV](volume/obv/Obv.md) | On Balance Volume | Volume |
|
||||
| [PACF](statistics/pacf/Pacf.md) | Partial Autocorrelation Function | Statistics |
|
||||
| [PARZEN](trends_FIR/parzen/Parzen.md) | Parzen Window MA | Trends (FIR) |
|
||||
| [PCHANNEL](channels/pchannel/Pchannel.md) | Price Channel | Channels |
|
||||
| [PERCENTILE](statistics/percentile/Percentile.md) | Percentile | Statistics |
|
||||
| PFE | Polarized Fractal Efficiency | Dynamics |
|
||||
| [PGO](oscillators/pgo/Pgo.md) | Pretty Good Oscillator | Oscillators |
|
||||
| [PIVOT](reversals/pivot/Pivot.md) | Pivot Points (Classic) | Reversals |
|
||||
| [PIVOTCAM](reversals/pivotcam/Pivotcam.md) | Camarilla Pivot Points | Reversals |
|
||||
| [PIVOTDEM](reversals/pivotdem/Pivotdem.md) | DeMark Pivot Points | Reversals |
|
||||
| [PIVOTEXT](reversals/pivotext/Pivotext.md) | Extended Traditional Pivots | Reversals |
|
||||
| [PIVOTFIB](reversals/pivotfib/Pivotfib.md) | Fibonacci Pivot Points | Reversals |
|
||||
| [PIVOTWOOD](reversals/pivotwood/Pivotwood.md) | Woodie's Pivot Points | Reversals |
|
||||
| [PMA](trends_FIR/pma/Pma.md) | Predictive Moving Average | Trends (FIR) |
|
||||
| [PMO](momentum/pmo/Pmo.md) | Price Momentum Oscillator | Momentum |
|
||||
| POISSONDIST | Poisson Distribution | Numerics |
|
||||
| POLYFIT | 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 |
|
||||
| [PSL](oscillators/psl/Psl.md) | Psychological Line | Oscillators |
|
||||
| [PSEUDOHUBER](errors/pseudohuber/Pseudohuber.md) | Pseudo-Huber Loss | Errors |
|
||||
| [PVD](volume/pvd/Pvd.md) | Price Volume Divergence | Volume |
|
||||
| [PVI](volume/pvi/Pvi.md) | Positive Volume Index | Volume |
|
||||
| [PVO](volume/pvo/Pvo.md) | Percentage Volume Oscillator | Volume |
|
||||
| [PVR](volume/pvr/Pvr.md) | Price Volume Rank | Volume |
|
||||
| [PVT](volume/pvt/Pvt.md) | Price Volume Trend | Volume |
|
||||
| [PWMA](trends_FIR/pwma/Pwma.md) | Pascal Weighted MA | Trends (FIR) |
|
||||
| [QEMA](trends_IIR/qema/Qema.md) | Quad Exponential MA | Trends (IIR) |
|
||||
| QQE | Quantitative Qualitative Estimation | Oscillators |
|
||||
| [QRMA](trends_FIR/qrma/Qrma.md) | Quadratic Regression MA | Trends (FIR) |
|
||||
| [QSTICK](dynamics/qstick/Qstick.md) | Qstick | Dynamics |
|
||||
| [QUANTILE](statistics/quantile/Quantile.md) | Quantile | Statistics |
|
||||
| [QUANTILELOSS](errors/quantileloss/QuantileLoss.md) | Quantile Loss | Errors |
|
||||
| [RAE](errors/rae/Rae.md) | Relative Absolute Error | Errors |
|
||||
| RAIN | Rainbow MA | Trends (FIR) |
|
||||
| RAVI | Chande Range Action Verification Index | Dynamics |
|
||||
| [REGCHANNEL](channels/regchannel/Regchannel.md) | Regression Channels | Channels |
|
||||
| REFLEX | Ehlers Reflex Indicator | Oscillators |
|
||||
| [RELU](numerics/relu/Relu.md) | Rectified Linear Unit | Numerics |
|
||||
| [REMA](trends_IIR/rema/Rema.md) | Regularized Exponential MA | Trends (IIR) |
|
||||
| [REVERSEEMA](oscillators/reverseema/ReverseEma.md) | Reverse EMA | Oscillators |
|
||||
| [RGMA](trends_IIR/rgma/Rgma.md) | Recursive Gaussian MA | Trends (IIR) |
|
||||
| [RMED](filters/rmed/Rmed.md) | Ehlers Recursive Median Filter | Filters |
|
||||
| [RMA](trends_IIR/rma/Rma.md) | wildeR MA | Trends (IIR) |
|
||||
| [RMSE](errors/rmse/Rmse.md) | Root Mean Squared Error | Errors |
|
||||
| [RMSLE](errors/rmsle/Rmsle.md) | Root Mean Squared Log Error | Errors |
|
||||
| [ROC](momentum/roc/Roc.md) | Rate of Change | Momentum |
|
||||
| [ROCP](momentum/rocp/Rocp.md) | Rate of Change Percentage | Momentum |
|
||||
| [ROCR](momentum/rocr/Rocr.md) | Rate of Change Ratio | Momentum |
|
||||
| [ROOFING](filters/roofing/Roofing.md) | Ehlers Roofing Filter | Filters |
|
||||
| [RSE](errors/rse/Rse.md) | Relative Squared Error | Errors |
|
||||
| [RSI](momentum/rsi/Rsi.md) | Relative Strength Index | Momentum |
|
||||
| [RSQUARED](errors/rsquared/Rsquared.md) | R² (Coefficient of Determination) | Errors |
|
||||
| [RSV](volatility/rsv/Rsv.md) | Rogers-Satchell Volatility | Volatility |
|
||||
| [RSX](momentum/rsx/Rsx.md) | Relative Strength Quality Index | Momentum |
|
||||
| [RV](volatility/rv/Rv.md) | Realized Volatility | Volatility |
|
||||
| [RVI](volatility/rvi/Rvi.md) | Relative Volatility Index | Volatility |
|
||||
| RVGI | Relative Vigor Index | Oscillators |
|
||||
| [RWMA](trends_FIR/rwma/Rwma.md) | Range Weighted MA | Trends (FIR) |
|
||||
| [SDCHANNEL](channels/sdchannel/Sdchannel.md) | Standard Deviation Channel | Channels |
|
||||
| SAK | Ehlers Swiss Army Knife | Filters |
|
||||
| [SAM](momentum/sam/Sam.md) | Smoothed Adaptive Momentum | Momentum |
|
||||
| [SGF](filters/sgf/Sgf.md) | Savitzky-Golay Filter | Filters |
|
||||
| [SGMA](trends_FIR/sgma/Sgma.md) | Savitzky-Golay MA | Trends (FIR) |
|
||||
| [SIGMOID](numerics/sigmoid/Sigmoid.md) | Logistic Function | Numerics |
|
||||
| [SINEMA](trends_FIR/sinema/Sinema.md) | Sine-Weighted MA | Trends (FIR) |
|
||||
| [SKEW](statistics/skew/Skew.md) | Skewness | Statistics |
|
||||
| [SLOPE](numerics/slope/Slope.md) | First Derivative | Numerics |
|
||||
| [SMA](trends_FIR/sma/Sma.md) | Simple MA | Trends (FIR) |
|
||||
| [SMAPE](errors/smape/Smape.md) | Symmetric MAPE | Errors |
|
||||
| [SMI](oscillators/smi/Smi.md) | Stochastic Momentum Index | Oscillators |
|
||||
| [SOLAR](cycles/solar/Solar.md) | Solar Activity Cycle | Cycles |
|
||||
| [SP15](trends_FIR/sp15/Sp15.md) | Spencer's 15-point MA | Trends (FIR) |
|
||||
| [SPBF](filters/spbf/Spbf.md) | Ehlers Super Passband Filter | Filters |
|
||||
| [SPEARMAN](statistics/spearman/Spearman.md) | Spearman Rank Correlation | Statistics |
|
||||
| SQUEEZE | Squeeze | Oscillators |
|
||||
| [SQRTTRANS](numerics/sqrttrans/Sqrttrans.md) | Square Root Transform | Numerics |
|
||||
| [SSF2](filters/ssf2/Ssf2.md) | Ehlers 2-Pole Super Smoother | Filters |
|
||||
| [SSF3](filters/ssf3/Ssf3.md) | Ehlers 3-Pole Super Smoother | Filters |
|
||||
| [SSFDSP](cycles/ssfdsp/Ssfdsp.md) | Ehlers SSF Detrended Synthetic Price | Cycles |
|
||||
| [STARCHANNEL](channels/starchannel/Starchannel.md) | Stoller Average Range Channel | Channels |
|
||||
| [STBANDS](channels/stbands/Stbands.md) | Super Trend Bands | Channels |
|
||||
| [STC](oscillators/stc/Stc.md) | Schaff Trend Cycle | Oscillators |
|
||||
| [STDDEV](statistics/stddev/StdDev.md) | Standard Deviation | Statistics |
|
||||
| STDERR | Standard Error | Errors |
|
||||
| [STOCH](oscillators/stoch/Stoch.md) | Stochastic Oscillator | Oscillators |
|
||||
| [STOCHF](oscillators/stochf/Stochf.md) | Stochastic Fast | Oscillators |
|
||||
| [STOCHRSI](oscillators/stochrsi/Stochrsi.md) | Stochastic RSI | Oscillators |
|
||||
| [SUM](statistics/sum/Sum.md) | Rolling Sum | Statistics |
|
||||
| [SUPER](dynamics/super/Super.md) | SuperTrend | Dynamics |
|
||||
| [SWMA](trends_FIR/swma/Swma.md) | Symmetric Weighted MA | Trends (FIR) |
|
||||
| [SWINGS](reversals/swings/Swings.md) | Swing High/Low Detection | Reversals |
|
||||
| [T3](trends_IIR/t3/T3.md) | Tillson T3 MA | Trends (IIR) |
|
||||
| TDIST | Student's t-Distribution | Numerics |
|
||||
| TD_SEQ | TD Sequential | Oscillators |
|
||||
| [TEMA](trends_IIR/tema/Tema.md) | Triple Exponential MA | Trends (IIR) |
|
||||
| [THEIL](statistics/theil/Theil.md) | Theil Index | Statistics |
|
||||
| [THEILU](errors/theilu/Theilu.md) | Theil's U Statistic | Errors |
|
||||
| [TR](volatility/tr/Tr.md) | True Range | Volatility |
|
||||
| [TRAMA](trends_IIR/trama/Trama.md) | Trend Regularity Adaptive MA | Trends (IIR) |
|
||||
| [TRENDFLEX](oscillators/trendflex/Trendflex.md) | Ehlers Trendflex | Oscillators |
|
||||
| TRIM | Trimmed Mean MA | Statistics |
|
||||
| [TRIMA](trends_FIR/trima/Trima.md) | Triangular MA | Trends (FIR) |
|
||||
| [TSF](trends_FIR/tsf/Tsf.md) | Time Series Forecast | Trends (FIR) |
|
||||
| [TRIX](oscillators/trix/Trix.md) | Triple Exponential Average | Oscillators |
|
||||
| [TSI](momentum/tsi/Tsi.md) | True Strength Index | Momentum |
|
||||
| [TTM_LRC](channels/ttm_lrc/TtmLrc.md) | TTM Linear Regression Channel | Channels |
|
||||
| [TTM_SCALPER](reversals/ttm_scalper/TtmScalper.md) | TTM Scalper Alert | Reversals |
|
||||
| [TTM_SQUEEZE](dynamics/ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | Dynamics |
|
||||
| [TTM_TREND](dynamics/ttm_trend/TtmTrend.md) | TTM Trend | Dynamics |
|
||||
| [TTM_WAVE](oscillators/ttm_wave/TtmWave.md) | TTM Wave | Oscillators |
|
||||
| [TUKEY](errors/tukeybiweight/TukeyBiweight.md) | Tukey Biweight Loss | Errors |
|
||||
| [TUKEY_W](trends_FIR/tukey_w/Tukey_w.md) | Tukey (Tapered Cosine) Window MA | Trends (FIR) |
|
||||
| [TVI](volume/tvi/Tvi.md) | Trade Volume Index | Volume |
|
||||
| [TWAP](volume/twap/Twap.md) | Time Weighted Average Price | Volume |
|
||||
| [TYPPRICE](core/typprice/Typprice.cs) | Typical Price | Core |
|
||||
| [UBANDS](channels/ubands/Ubands.md) | Ehlers Ultimate Bands | Channels |
|
||||
| [UCHANNEL](channels/uchannel/Uchannel.md) | Ehlers Ultimate Channel | Channels |
|
||||
| [UI](volatility/ui/Ui.md) | Ulcer Index | Volatility |
|
||||
| [ULTOSC](oscillators/ultosc/Ultosc.md) | Ultimate Oscillator | Oscillators |
|
||||
| [USF](filters/usf/Usf.md) | Ehlers Ultimate Smoother | Filters |
|
||||
| [VA](volume/va/Va.md) | Volume Accumulation | Volume |
|
||||
| [VAMA](trends_IIR/vama/Vama.md) | Volatility Adjusted MA | Trends (IIR) |
|
||||
| [VARIANCE](statistics/variance/Variance.md) | Variance | Statistics |
|
||||
| [VEL](momentum/vel/Vel.md) | Jurik Velocity | Momentum |
|
||||
| [VF](volume/vf/Vf.md) | Volume Force | Volume |
|
||||
| VHF | Vertical Horizontal Filter | Dynamics |
|
||||
| [VIDYA](trends_IIR/vidya/Vidya.md) | Variable Index Dynamic Average | Trends (IIR) |
|
||||
| [VO](volume/vo/Vo.md) | Volume Oscillator | Volume |
|
||||
| [VORTEX](dynamics/vortex/Vortex.md) | Vortex Indicator | Dynamics |
|
||||
| [VOSS](filters/voss/Voss.md) | Ehlers Voss Predictive Filter | Filters |
|
||||
| [VOV](volatility/vov/Vov.md) | Volatility of Volatility | Volatility |
|
||||
| [VR](volatility/vr/Vr.md) | Volatility Ratio | Volatility |
|
||||
| [VROC](volume/vroc/Vroc.md) | Volume Rate of Change | Volume |
|
||||
| [VWAD](volume/vwad/Vwad.md) | Volume Weighted A/D | Volume |
|
||||
| [VWAP](volume/vwap/Vwap.md) | Volume Weighted Average Price | Volume |
|
||||
| [VWAPBANDS](channels/vwapbands/Vwapbands.md) | VWAP Bands | Channels |
|
||||
| [VWAPSD](channels/vwapsd/Vwapsd.md) | VWAP Standard Deviation Bands | Channels |
|
||||
| [VWMA](volume/vwma/Vwma.md) | Volume Weighted MA | Volume |
|
||||
| [WAD](volume/wad/Wad.md) | Williams A/D | Volume |
|
||||
| [WAVELET](filters/wavelet/Wavelet.md) | Wavelet Denoising Filter | Filters |
|
||||
| WAVG | Weighted Average | Statistics |
|
||||
| [WCLPRICE](core/wclprice/Wclprice.cs) | Weighted Close Price | Core |
|
||||
| WEIBULLDIST | Weibull Distribution | Numerics |
|
||||
| [WIENER](filters/wiener/Wiener.md) | Wiener Filter | Filters |
|
||||
| [WILLR](oscillators/willr/Willr.md) | Williams %R | Oscillators |
|
||||
| WINS | Winsorized Mean MA | Statistics |
|
||||
| [WMA](trends_FIR/wma/Wma.md) | Weighted MA | Trends (FIR) |
|
||||
| [WMAPE](errors/wmape/Wmape.md) | Weighted MAPE | Errors |
|
||||
| [WRMSE](errors/wrmse/Wrmse.md) | Weighted RMSE | Errors |
|
||||
| [YZV](volatility/yzv/Yzv.md) | Yang-Zhang Volatility | Volatility |
|
||||
| [YZVAMA](trends_IIR/yzvama/Yzvama.md) | Yang-Zhang Volatility Adjusted MA | Trends (IIR) |
|
||||
| [ZLDEMA](trends_IIR/zldema/Zldema.md) | Zero-Lag Double Exponential MA | Trends (IIR) |
|
||||
| [ZLEMA](trends_IIR/zlema/Zlema.md) | Zero-Lag Exponential MA | Trends (IIR) |
|
||||
| [ZLTEMA](trends_IIR/zltema/Zltema.md) | Zero-Lag Triple Exponential MA | Trends (IIR) |
|
||||
| [ZSCORE](statistics/zscore/Zscore.md) | Z-Score | Statistics |
|
||||
| [ZTEST](statistics/ztest/Ztest.md) | Z-Test | Statistics |
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Core
|
||||
|
||||
Price transforms and fundamental building blocks. These indicators compute derived prices from OHLCV bars and serve as inputs to higher-order indicators.
|
||||
|
||||
## Indicators
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :-------- | :-------- | :---------- |
|
||||
| [AVGPRICE](avgprice/Avgprice.md) | Average Price | (O+H+L+C) * 0.25 via FMA |
|
||||
| [MEDPRICE](medprice/Medprice.md) | Median Price | (H+L) * 0.5 |
|
||||
| [MIDPOINT](midpoint/Midpoint.md) | Rolling Midpoint | (Max+Min) * 0.5 over lookback window |
|
||||
| [MIDPRICE](midprice/Midprice.md) | Mid Price | (Highest High + Lowest Low) * 0.5 |
|
||||
| [TYPPRICE](typprice/Typprice.md) | Typical Price | (H+L+C) * OneThird via FMA |
|
||||
| [HA](ha/Ha.md) | Heikin-Ashi | Modified OHLC candles. Smoothed trend visualization. Output is TBar. |
|
||||
| [WCLPRICE](wclprice/Wclprice.md) | Weighted Close Price | (H+L+2C) * 0.25 via FMA |
|
||||
|
||||
## Architecture
|
||||
|
||||
All Core indicators share common traits:
|
||||
|
||||
- **Zero allocation** in `Update` hot path
|
||||
- **FMA optimization** where applicable (Avgprice, Typprice, Wclprice)
|
||||
- **Multiplication over division** (0.25 instead of /4, OneThird instead of /3)
|
||||
- **NaN/Infinity guard** via last-valid-value substitution
|
||||
- **Bar correction** via `isNew` rollback pattern
|
||||
- **Dual API** with stateful `Update` + stateless static `Calculate`
|
||||
- **SIMD batch** via `ReadOnlySpan<double>` / `Span<double>` overloads
|
||||
|
||||
### TBar-Based vs TValue-Based
|
||||
|
||||
| Type | Indicators | Input |
|
||||
| :--- | :--------- | :---- |
|
||||
| TBar | AVGPRICE, MEDPRICE, MIDPRICE, TYPPRICE, WCLPRICE | OHLCV bars |
|
||||
| TValue | MIDPOINT | Single value series |
|
||||
@@ -0,0 +1,131 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AvgpriceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("AVGPRICE - Average Price", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_ShortName_IsAvgprice()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
Assert.Equal("AVGPRICE", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
|
||||
Assert.Equal(1, AvgpriceIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 115, 105, 112, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Avgprice.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_ComputesCorrectAverage()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// O=100, H=110, L=90, C=105 → (100+110+90+105)/4 = 101.25
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(101.25, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AvgpriceIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new AvgpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class AvgpriceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Avgprice _avgprice = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 1;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "AVGPRICE";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/avgprice/Avgprice.Quantower.cs";
|
||||
|
||||
public AvgpriceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "AVGPRICE - Average Price";
|
||||
Description = "Average of Open, High, Low, and Close prices: (O+H+L+C)/4.";
|
||||
|
||||
_series = new LineSeries(name: "AVGPRICE", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_avgprice = new Avgprice();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _avgprice.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _avgprice.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// Avgprice Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AvgpriceTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public AvgpriceTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
Assert.Equal("Avgprice", indicator.Name);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Avgprice(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_ReturnsOHLC4()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.Update(bar);
|
||||
// (100 + 110 + 90 + 105) / 4 = 101.25
|
||||
Assert.Equal(101.25, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_MatchesTBarOHLC4()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 50, 60, 40, 55, 500);
|
||||
var result = indicator.Update(bar);
|
||||
Assert.Equal(bar.OHLC4, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ReturnsIdentity()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
Assert.Equal(42.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterFirstBar_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
Assert.False(indicator.IsHot);
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RestoresPreviousState()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = indicator.Last.Value;
|
||||
|
||||
// Second bar (new)
|
||||
indicator.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000), isNew: true);
|
||||
|
||||
// Correction on second bar — should produce same result as a fresh update
|
||||
var corrected = indicator.Update(new TBar(time.AddMinutes(1), 106, 116, 96, 111, 1000), isNew: false);
|
||||
double expected = (106 + 116 + 96 + 111) * 0.25;
|
||||
Assert.Equal(expected, corrected.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
|
||||
var bar = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000);
|
||||
var result1 = indicator.Update(bar, isNew: false);
|
||||
var result2 = indicator.Update(bar, isNew: false);
|
||||
var result3 = indicator.Update(bar, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, Tolerance);
|
||||
Assert.Equal(result2.Value, result3.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Valid bar first
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
double validResult = indicator.Last.Value;
|
||||
|
||||
// NaN bar — should substitute last valid values
|
||||
var nanBar = new TBar(time.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, 1000);
|
||||
var result = indicator.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(validResult, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = indicator.Last.Value;
|
||||
|
||||
var infBar = new TBar(time.AddMinutes(1), double.PositiveInfinity, double.NegativeInfinity, double.NaN, double.PositiveInfinity, 1000);
|
||||
var result = indicator.Update(infBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests (All Modes)
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceConsistentResults()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Avgprice();
|
||||
double[] streamingResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streaming.Update(bars[i], isNew: true).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TBarSeries)
|
||||
var batchResult = Avgprice.Batch(bars);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[bars.Count];
|
||||
Avgprice.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues, spanOutput);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllBars_MatchTBarOHLC4()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var indicator = new Avgprice();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i], isNew: true);
|
||||
Assert.Equal(bars[i].OHLC4, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] open = new double[10];
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[5]; // mismatched
|
||||
double[] close = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Avgprice.Batch(open, high, low, close, output));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
double[] open = new double[10];
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] close = new double[10];
|
||||
double[] output = new double[5]; // too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Avgprice.Batch(open, high, low, close, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOutput()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var result = Avgprice.Batch(bars);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBars(10_000);
|
||||
double[] output = new double[bars.Count];
|
||||
Avgprice.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues, output);
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
bool fired = false;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, ind) = Avgprice.Calculate(bars);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// AVGPRICE: Average Price
|
||||
/// Calculates the average of Open, High, Low, and Close prices.
|
||||
/// Equivalent to TBar.OHLC4 but as a proper streaming indicator with bar correction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation:</b>
|
||||
/// <list type="number">
|
||||
/// <item>AvgPrice = (Open + High + Low + Close) / 4</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Stateless bar-by-bar calculation (no lookback period)</item>
|
||||
/// <item>TA-Lib compatible (AVGPRICE function)</item>
|
||||
/// <item>Always hot after first bar</item>
|
||||
/// <item>Useful as a smoothed input for other indicators</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Avgprice : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double LastValidOpen,
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastValidClose,
|
||||
double LastResult,
|
||||
int Count
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Avgprice class.
|
||||
/// </summary>
|
||||
public Avgprice()
|
||||
{
|
||||
WarmupPeriod = 1;
|
||||
Name = "Avgprice";
|
||||
_s = new State(0, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Avgprice class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
public Avgprice(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the average price from OHLC values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeAvgPrice(double open, double high, double low, double close)
|
||||
{
|
||||
return Math.FusedMultiplyAdd(open + high, 0.25, (low + close) * 0.25);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TValue input, treats the value as all four OHLC prices (result = value).
|
||||
/// Prefer Update(TBar) for standard OHLC data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(input.Time, input.Value, input.Value, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (preferred method).
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated Average Price value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.Open, bar.High, bar.Low, bar.Close, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the Average Price values.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.OpenValues, source.HighValues, source.LowValues, source.CloseValues, vSpan);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
var values = source.Values;
|
||||
|
||||
// TValue-only: result = value (identity)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source.Times[i];
|
||||
vSpan[i] = values[i];
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue UpdateCore(long timeTicks, double open, double high, double low, double close, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values — use last valid values
|
||||
if (!double.IsFinite(open)) { open = s.LastValidOpen; } else { s.LastValidOpen = open; }
|
||||
if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; }
|
||||
if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; }
|
||||
if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; }
|
||||
|
||||
double result = ComputeAvgPrice(open, high, low, close);
|
||||
|
||||
if (!double.IsFinite(result))
|
||||
{
|
||||
result = s.LastResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastResult = result;
|
||||
}
|
||||
|
||||
if (isNew) { s.Count++; }
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(timeTicks, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(0, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Average Price for a bar series (static).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source)
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans for OHLC data.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> open,
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> output)
|
||||
{
|
||||
int len = open.Length;
|
||||
if (high.Length != len || low.Length != len || close.Length != len)
|
||||
{
|
||||
throw new ArgumentException("All input spans must have the same length", nameof(high));
|
||||
}
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
output[i] = ComputeAvgPrice(open[i], high[i], low[i], close[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using a TBarSeries (convenience overload).
|
||||
/// </summary>
|
||||
public static void Batch(TBarSeries source, Span<double> output)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as source", nameof(output));
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Batch(source.OpenValues, source.HighValues, source.LowValues, source.CloseValues, output);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Avgprice Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Avgprice();
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# AVGPRICE: Average Price
|
||||
|
||||
AVGPRICE computes the arithmetic mean of a bar's four canonical prices: Open, High, Low, and Close. The formula $\frac{O + H + L + C}{4}$ produces a single representative price that weights all four price components equally, unlike Typical Price (which excludes Open) or Weighted Close (which double-weights Close). This equal weighting makes AVGPRICE the least biased single-bar summary statistic, useful as a neutral input to downstream indicators when no particular price component deserves emphasis. The calculation is stateless, requires no warmup, and costs a single FMA instruction per bar.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Average Price is one of the oldest price transforms in technical analysis, predating computer-based charting by decades. Its inclusion in the TA-Lib function set (`TA_AVGPRICE`) standardized it as a canonical operation alongside MEDPRICE, TYPPRICE, and WCLPRICE. The four-price average gained popularity because it distributes weight across the full intra-bar range: Open captures the session's starting sentiment, High and Low bound the extremes where supply and demand exhausted themselves, and Close reflects the final consensus.
|
||||
|
||||
In practice, AVGPRICE and OHLC4 are identical. QuanTAlib exposes both: `TBar.OHLC4` as a zero-cost computed property for inline use, and `Avgprice` as a streaming indicator class supporting bar correction, event chaining, and batch processing. The indicator form exists because downstream consumers (Quantower adapters, chained indicator pipelines) require the `ITValuePublisher` interface and `isNew` rollback semantics that a bare struct property cannot provide.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Core Formula
|
||||
|
||||
$$\text{AvgPrice}_t = \frac{O_t + H_t + L_t + C_t}{4}$$
|
||||
|
||||
Implemented as FMA to avoid division on the hot path:
|
||||
|
||||
$$\text{AvgPrice}_t = \text{FMA}(O_t + H_t,\; 0.25,\; (L_t + C_t) \times 0.25)$$
|
||||
|
||||
### 2. State Management
|
||||
|
||||
No rolling window, no lookback buffer. The indicator is stateless per bar. State exists only for:
|
||||
|
||||
- **Last-valid substitution**: If any OHLC component is `NaN`/`Infinity`, the last finite value for that component is used.
|
||||
- **Bar correction**: `isNew=false` rolls back to previous state, enabling same-timestamp rewrites.
|
||||
|
||||
### 3. Complexity
|
||||
|
||||
$O(1)$ per bar. Two additions, one FMA. No memory allocation. Always hot after the first bar.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| (none) | No user-configurable parameters | | |
|
||||
|
||||
### Relationship to TBar Properties
|
||||
|
||||
| Transform | Formula | TBar Property | Indicator Class |
|
||||
|-----------|---------|---------------|-----------------|
|
||||
| Average Price | $(O+H+L+C) \times 0.25$ | `OHLC4` | `Avgprice` |
|
||||
| Median Price | $(H+L) \times 0.5$ | `HL2` | `Medprice` |
|
||||
| Typical Price | $(H+L+C) \times \frac{1}{3}$ | `HLC3` | `Typprice` |
|
||||
| Weighted Close | $(H+L+2C) \times 0.25$ | `HLCC4` | `Wclprice` |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function AVGPRICE(bar):
|
||||
o, h, l, c ← bar.Open, bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(o): o ← lastValidOpen
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
result ← FMA(o + h, 0.25, (l + c) × 0.25)
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|---------|---------|
|
||||
| AVGPRICE > Close | Intra-bar action skewed higher than settlement |
|
||||
| AVGPRICE < Close | Close settled above the bar's center of mass |
|
||||
| AVGPRICE $\approx$ Close | Symmetric bar (doji-like) |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|-----------|:-----:|:-------------:|:--------:|
|
||||
| ADD (O+H) | 1 | 1 | 1 |
|
||||
| ADD (L+C) | 1 | 1 | 1 |
|
||||
| MUL ((L+C) × 0.25) | 1 | 3 | 3 |
|
||||
| FMA ((O+H) × 0.25 + prev) | 1 | 4 | 4 |
|
||||
| **Total (hot)** | **4** | | **~9 cycles** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| SIMD vectorizable | Yes: element-wise arithmetic, no inter-bar dependency |
|
||||
| Optimal strategy | `Vector<double>` over OHLC spans; 4-wide on AVX2, 8-wide on AVX-512 |
|
||||
| Memory | $O(1)$ streaming; $O(n)$ batch output span |
|
||||
| Throughput | Near memory-bandwidth bound for large series |
|
||||
|
||||
## Resources
|
||||
|
||||
- **TA-Lib** `TA_AVGPRICE` function reference.
|
||||
- **Murphy, J.J.** *Technical Analysis of the Financial Markets*. New York Institute of Finance, 1999.
|
||||
@@ -0,0 +1,13 @@
|
||||
// AVGPRICE: Average Price
|
||||
// (Open + High + Low + Close) / 4
|
||||
// TA-Lib compatible — equivalent to TBar.OHLC4
|
||||
|
||||
//@version=6
|
||||
indicator("AVGPRICE: Average Price", overlay=true)
|
||||
|
||||
avgprice(float o, float h, float l, float c) =>
|
||||
(o + h + l + c) * 0.25
|
||||
|
||||
result = avgprice(open, high, low, close)
|
||||
|
||||
plot(result, "AvgPrice", color.new(color.blue, 0), 2)
|
||||
@@ -0,0 +1,173 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void HaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("HA - Heikin-Ashi", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ShortName_IsHa()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
Assert.Equal("HA", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
|
||||
Assert.Equal(1, HaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_Initialize_CreatesFourLineSeries()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ProcessUpdate_HistoricalBar_ComputesValues()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// All 4 series should have finite values
|
||||
for (int s = 0; s < 4; s++)
|
||||
{
|
||||
double val = indicator.LinesSeries[s].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"LineSeries[{s}] should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ProcessUpdate_NewBar_ComputesValues()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 115, 105, 112, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[1].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[2].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[3].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Ha.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ComputesCorrectValues()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: O=100, H=110, L=90, C=105
|
||||
// HA_Close = (100+110+90+105)/4 = 101.25
|
||||
// HA_Open = (100+105)/2 = 102.5 (seed)
|
||||
// HA_High = max(110, 102.5, 101.25) = 110
|
||||
// HA_Low = min(90, 102.5, 101.25) = 90
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double haOpen = indicator.LinesSeries[0].GetValue(0);
|
||||
double haHigh = indicator.LinesSeries[1].GetValue(0);
|
||||
double haLow = indicator.LinesSeries[2].GetValue(0);
|
||||
double haClose = indicator.LinesSeries[3].GetValue(0);
|
||||
|
||||
Assert.Equal(102.5, haOpen, 10);
|
||||
Assert.Equal(110.0, haHigh, 10);
|
||||
Assert.Equal(90.0, haLow, 10);
|
||||
Assert.Equal(101.25, haClose, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// All 4 series should have finite values (IsHot after first bar)
|
||||
for (int s = 0; s < 4; s++)
|
||||
{
|
||||
double val = indicator.LinesSeries[s].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"LineSeries[{s}] should be finite after one bar");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_HighAlwaysAboveOrEqualLow()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + (i * 2);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double haHigh = indicator.LinesSeries[1].GetValue(0);
|
||||
double haLow = indicator.LinesSeries[2].GetValue(0);
|
||||
|
||||
Assert.True(haHigh >= haLow, "HA High must be >= HA Low");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class HaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Ha _ha = null!;
|
||||
private readonly LineSeries _openSeries;
|
||||
private readonly LineSeries _highSeries;
|
||||
private readonly LineSeries _lowSeries;
|
||||
private readonly LineSeries _closeSeries;
|
||||
|
||||
public static int MinHistoryDepths => 1;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "HA";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/ha/Ha.Quantower.cs";
|
||||
|
||||
public HaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "HA - Heikin-Ashi";
|
||||
Description = "Transforms standard OHLC bars into smoothed Heikin-Ashi candles that filter noise and clarify trend direction.";
|
||||
|
||||
_openSeries = new LineSeries(name: "HA Open", color: Color.FromArgb(0, 200, 0), width: 2, style: LineStyle.Solid);
|
||||
_highSeries = new LineSeries(name: "HA High", color: IndicatorExtensions.Averages, width: 1, style: LineStyle.Solid);
|
||||
_lowSeries = new LineSeries(name: "HA Low", color: IndicatorExtensions.Averages, width: 1, style: LineStyle.Solid);
|
||||
_closeSeries = new LineSeries(name: "HA Close", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_openSeries);
|
||||
AddLineSeries(_highSeries);
|
||||
AddLineSeries(_lowSeries);
|
||||
AddLineSeries(_closeSeries);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ha = new Ha();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
_ = _ha.UpdateBar(bar, isNew: args.IsNewBar());
|
||||
|
||||
TBar haBar = _ha.LastBar;
|
||||
_openSeries.SetValue(haBar.Open, _ha.IsHot, ShowColdValues);
|
||||
_highSeries.SetValue(haBar.High, _ha.IsHot, ShowColdValues);
|
||||
_lowSeries.SetValue(haBar.Low, _ha.IsHot, ShowColdValues);
|
||||
_closeSeries.SetValue(haBar.Close, _ha.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
// Ha Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HaTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public HaTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
Assert.Equal("Ha", indicator.Name);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Ha(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_HaCloseIsOHLC4()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.UpdateBar(bar);
|
||||
// HA Close = (100 + 110 + 90 + 105) / 4 = 101.25
|
||||
Assert.Equal(101.25, result.Close, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_HaOpenIsMidpointOC()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.UpdateBar(bar);
|
||||
// HA Open on first bar = (O + C) / 2 = (100 + 105) / 2 = 102.5
|
||||
Assert.Equal(102.5, result.Open, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_HaHighIsMaxOfHOC()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.UpdateBar(bar);
|
||||
// HA High = max(110, 102.5, 101.25) = 110
|
||||
Assert.Equal(110, result.High, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_HaLowIsMinOfLOC()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.UpdateBar(bar);
|
||||
// HA Low = min(90, 102.5, 101.25) = 90
|
||||
Assert.Equal(90, result.Low, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SecondBar_HaOpenIsRecursive()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: O=100, H=110, L=90, C=105
|
||||
// HA_Open1 = (100+105)/2 = 102.5, HA_Close1 = 101.25
|
||||
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000));
|
||||
|
||||
// Second bar: O=105, H=115, L=95, C=110
|
||||
// HA_Open2 = (prevHaOpen + prevHaClose) / 2 = (102.5 + 101.25) / 2 = 101.875
|
||||
var result = indicator.UpdateBar(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000));
|
||||
Assert.Equal(101.875, result.Open, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SecondBar_HaCloseIsOHLC4()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000));
|
||||
|
||||
var result = indicator.UpdateBar(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000));
|
||||
// HA Close = (105 + 115 + 95 + 110) / 4 = 106.25
|
||||
Assert.Equal(106.25, result.Close, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_VolumePassthrough()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1234.5);
|
||||
var result = indicator.UpdateBar(bar);
|
||||
Assert.Equal(1234.5, result.Volume, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TimePassthrough()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.UpdateBar(bar);
|
||||
Assert.Equal(time.Ticks, result.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HaHighAlwaysGEHaOpenAndHaClose()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
||||
Assert.True(ha.High >= ha.Open, $"Bar {i}: High {ha.High} < Open {ha.Open}");
|
||||
Assert.True(ha.High >= ha.Close, $"Bar {i}: High {ha.High} < Close {ha.Close}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HaLowAlwaysLEHaOpenAndHaClose()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
||||
Assert.True(ha.Low <= ha.Open, $"Bar {i}: Low {ha.Low} > Open {ha.Open}");
|
||||
Assert.True(ha.Low <= ha.Close, $"Bar {i}: Low {ha.Low} > Close {ha.Close}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LastProperty_ReturnsHaClose()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
indicator.UpdateBar(bar);
|
||||
// Last.Value should equal HA Close
|
||||
Assert.Equal(101.25, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LastBarProperty_ReturnsFullHaBar()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.UpdateBar(bar);
|
||||
Assert.Equal(result, indicator.LastBar);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterFirstBar_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
Assert.False(indicator.IsHot);
|
||||
indicator.UpdateBar(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RestoresPreviousState()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar
|
||||
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
|
||||
// Second bar (new)
|
||||
indicator.UpdateBar(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000), isNew: true);
|
||||
|
||||
// Correction on second bar
|
||||
var corrected = indicator.UpdateBar(new TBar(time.AddMinutes(1), 106, 116, 96, 111, 1000), isNew: false);
|
||||
|
||||
// Verify the HA Open is computed from first bar's HA values, not second bar's
|
||||
// After first bar: prevHaOpen=102.5, prevHaClose=101.25
|
||||
// Corrected HA_Open = (102.5 + 101.25)/2 = 101.875
|
||||
Assert.Equal(101.875, corrected.Open, Tolerance);
|
||||
// Corrected HA_Close = (106+116+96+111)/4 = 107.25
|
||||
Assert.Equal(107.25, corrected.Close, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
|
||||
var bar = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000);
|
||||
var result1 = indicator.UpdateBar(bar, isNew: false);
|
||||
var result2 = indicator.UpdateBar(bar, isNew: false);
|
||||
var result3 = indicator.UpdateBar(bar, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Open, result2.Open, Tolerance);
|
||||
Assert.Equal(result1.Close, result2.Close, Tolerance);
|
||||
Assert.Equal(result1.High, result2.High, Tolerance);
|
||||
Assert.Equal(result1.Low, result2.Low, Tolerance);
|
||||
Assert.Equal(result2, result3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
indicator.UpdateBar(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
Assert.Equal(default, indicator.LastBar);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Valid bar first
|
||||
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
_ = indicator.LastBar;
|
||||
|
||||
// NaN bar — should substitute last valid values
|
||||
var nanBar = new TBar(time.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, 1000);
|
||||
var result = indicator.UpdateBar(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Open));
|
||||
Assert.True(double.IsFinite(result.High));
|
||||
Assert.True(double.IsFinite(result.Low));
|
||||
Assert.True(double.IsFinite(result.Close));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
|
||||
var infBar = new TBar(time.AddMinutes(1), double.PositiveInfinity, double.NegativeInfinity, double.NaN, double.PositiveInfinity, 1000);
|
||||
var result = indicator.UpdateBar(infBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Open));
|
||||
Assert.True(double.IsFinite(result.High));
|
||||
Assert.True(double.IsFinite(result.Low));
|
||||
Assert.True(double.IsFinite(result.Close));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests (All Modes)
|
||||
|
||||
[Fact]
|
||||
public void AllModes_StreamingAndBatch_ProduceConsistentResults()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Ha();
|
||||
TBar[] streamingResults = new TBar[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streaming.UpdateBar(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TBarSeries)
|
||||
var batchResult = Ha.Batch(bars);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] haOpenOut = new double[bars.Count];
|
||||
double[] haHighOut = new double[bars.Count];
|
||||
double[] haLowOut = new double[bars.Count];
|
||||
double[] haCloseOut = new double[bars.Count];
|
||||
Ha.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
haOpenOut, haHighOut, haLowOut, haCloseOut);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i].Open, batchResult[i].Open, Tolerance);
|
||||
Assert.Equal(streamingResults[i].High, batchResult[i].High, Tolerance);
|
||||
Assert.Equal(streamingResults[i].Low, batchResult[i].Low, Tolerance);
|
||||
Assert.Equal(streamingResults[i].Close, batchResult[i].Close, Tolerance);
|
||||
|
||||
Assert.Equal(streamingResults[i].Open, haOpenOut[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i].High, haHighOut[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i].Low, haLowOut[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i].Close, haCloseOut[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllBars_HaCloseMatchesOHLC4()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var indicator = new Ha();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.UpdateBar(bars[i], isNew: true);
|
||||
Assert.Equal(bars[i].OHLC4, result.Close, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] open = new double[10];
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[5]; // mismatched
|
||||
double[] close = new double[10];
|
||||
double[] ho = new double[10], hh = new double[10], hl = new double[10], hc = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ha.Batch(open, high, low, close, ho, hh, hl, hc));
|
||||
Assert.Equal("high", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
double[] open = new double[10];
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] close = new double[10];
|
||||
double[] ho = new double[5]; // too short
|
||||
double[] hh = new double[10], hl = new double[10], hc = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ha.Batch(open, high, low, close, ho, hh, hl, hc));
|
||||
Assert.Equal("haOpenOut", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOutput()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var result = Ha.Batch(bars);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBars(10_000);
|
||||
var result = Ha.Batch(bars);
|
||||
Assert.Equal(bars.Count, result.Count);
|
||||
Assert.True(double.IsFinite(result[^1].Close));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HA-Specific Property Tests
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed constant bars: O=100, H=100, L=100, C=100
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
_ = indicator.UpdateBar(new TBar(time.AddMinutes(i), 100, 100, 100, 100, 1000), isNew: true);
|
||||
}
|
||||
|
||||
var last = indicator.LastBar;
|
||||
// After many constant bars, all HA values should converge to 100
|
||||
Assert.Equal(100.0, last.Open, 1e-6);
|
||||
Assert.Equal(100.0, last.High, 1e-6);
|
||||
Assert.Equal(100.0, last.Low, 1e-6);
|
||||
Assert.Equal(100.0, last.Close, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaHighGERealHigh_WhenBodyExceedsHigh()
|
||||
{
|
||||
// This tests the clamping: HA High is at least as large as HA Open and HA Close
|
||||
var indicator = new Ha();
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
||||
// HA High should be >= real High OR >= haOpen/haClose
|
||||
Assert.True(ha.High >= ha.Open);
|
||||
Assert.True(ha.High >= ha.Close);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
bool fired = false;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
indicator.UpdateBar(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, ind) = Ha.Calculate(bars);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Ha Validation Tests
|
||||
// No external library (TA-Lib, Tulip) has a direct HA function.
|
||||
// Skender and Ooples have GetHeikinAshi but validation is self-consistency.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HaValidationTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
private const int DataSize = 5000;
|
||||
|
||||
public HaValidationTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchAndStreaming_Match()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ha();
|
||||
var streamingBars = new List<TBar>(DataSize);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingBars.Add(streaming.UpdateBar(bars[i], isNew: true));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Ha.Batch(bars);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingBars[i].Open, batchResult[i].Open, Tolerance);
|
||||
Assert.Equal(streamingBars[i].High, batchResult[i].High, Tolerance);
|
||||
Assert.Equal(streamingBars[i].Low, batchResult[i].Low, Tolerance);
|
||||
Assert.Equal(streamingBars[i].Close, batchResult[i].Close, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanAndStreaming_Match()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ha();
|
||||
double[] sOpen = new double[bars.Count];
|
||||
double[] sHigh = new double[bars.Count];
|
||||
double[] sLow = new double[bars.Count];
|
||||
double[] sClose = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = streaming.UpdateBar(bars[i], isNew: true);
|
||||
sOpen[i] = ha.Open;
|
||||
sHigh[i] = ha.High;
|
||||
sLow[i] = ha.Low;
|
||||
sClose[i] = ha.Close;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
double[] haO = new double[bars.Count];
|
||||
double[] haH = new double[bars.Count];
|
||||
double[] haL = new double[bars.Count];
|
||||
double[] haC = new double[bars.Count];
|
||||
Ha.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
haO, haH, haL, haC);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(sOpen[i], haO[i], Tolerance);
|
||||
Assert.Equal(sHigh[i], haH[i], Tolerance);
|
||||
Assert.Equal(sLow[i], haL[i], Tolerance);
|
||||
Assert.Equal(sClose[i], haC[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantBars_ConvergeToConstant()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
double price = 50.0;
|
||||
|
||||
TBar last = default;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
last = indicator.UpdateBar(new TBar(time.AddMinutes(i), price, price, price, price, 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(price, last.Open, 1e-6);
|
||||
Assert.Equal(price, last.High, 1e-6);
|
||||
Assert.Equal(price, last.Low, 1e-6);
|
||||
Assert.Equal(price, last.Close, 1e-6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaClose_AlwaysEqualsOHLC4()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
var indicator = new Ha();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
||||
double expected = bars[i].OHLC4;
|
||||
Assert.Equal(expected, ha.Close, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaHighLow_AlwaysContainBody()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
var indicator = new Ha();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
||||
Assert.True(ha.High >= ha.Open, $"Bar {i}: High {ha.High} < Open {ha.Open}");
|
||||
Assert.True(ha.High >= ha.Close, $"Bar {i}: High {ha.High} < Close {ha.Close}");
|
||||
Assert.True(ha.Low <= ha.Open, $"Bar {i}: Low {ha.Low} > Open {ha.Open}");
|
||||
Assert.True(ha.Low <= ha.Close, $"Bar {i}: Low {ha.Low} > Close {ha.Close}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_Consistency()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
var indicator1 = new Ha();
|
||||
var indicator2 = new Ha();
|
||||
|
||||
// Run indicator1 normally
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator1.UpdateBar(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Run indicator2 with corrections
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator2.UpdateBar(bars[i], isNew: true);
|
||||
// Simulate correction
|
||||
if (i > 0 && i % 5 == 0)
|
||||
{
|
||||
indicator2.UpdateBar(bars[i], isNew: false);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(indicator1.LastBar.Open, indicator2.LastBar.Open, Tolerance);
|
||||
Assert.Equal(indicator1.LastBar.Close, indicator2.LastBar.Close, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, indicator) = Ha.Calculate(bars);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// HA: Heikin-Ashi
|
||||
/// Transforms standard OHLC bars into smoothed Heikin-Ashi candles.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation:</b>
|
||||
/// <list type="number">
|
||||
/// <item>HA_Close = (O + H + L + C) / 4</item>
|
||||
/// <item>HA_Open = (prev_HA_Open + prev_HA_Close) / 2</item>
|
||||
/// <item>HA_High = max(H, HA_Open, HA_Close)</item>
|
||||
/// <item>HA_Low = min(L, HA_Open, HA_Close)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Output is TBar (smoothed OHLC), not TValue</item>
|
||||
/// <item>HA_Open is a recursive IIR filter (alpha=0.5, half-life=1 bar)</item>
|
||||
/// <item>HA_Close is stateless OHLC4 (identical to AVGPRICE)</item>
|
||||
/// <item>Always hot after first bar</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <seealso href="Ha.md">Detailed documentation</seealso>
|
||||
/// <seealso href="ha.pine">Reference Pine Script implementation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Ha : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double PrevHaOpen,
|
||||
double PrevHaClose,
|
||||
double LastValidOpen,
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastValidClose,
|
||||
int Count
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// The last computed Heikin-Ashi bar (full OHLC output).
|
||||
/// </summary>
|
||||
public TBar LastBar { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Ha class.
|
||||
/// </summary>
|
||||
public Ha()
|
||||
{
|
||||
WarmupPeriod = 1;
|
||||
Name = "Ha";
|
||||
_s = default;
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Ha class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
public Ha(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Computes HA_Close = (O+H+L+C)/4 via FMA.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeHaClose(double open, double high, double low, double close)
|
||||
{
|
||||
return Math.FusedMultiplyAdd(open + high, 0.25, (low + close) * 0.25);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TValue input, treats value as all four OHLC prices.
|
||||
/// Prefer Update(TBar) for standard OHLC data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
_ = UpdateBar(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// Returns a TBarSeries containing the Heikin-Ashi bars.
|
||||
/// </summary>
|
||||
public TBarSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return new TBarSeries();
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var result = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
TBar haBar = UpdateBar(source[i], isNew: true);
|
||||
result.Add(haBar);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
TValue result = Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
|
||||
tSpan[i] = result.Time;
|
||||
vSpan[i] = result.Value;
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (preferred method).
|
||||
/// Returns the smoothed Heikin-Ashi TBar.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar UpdateBar(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.Open, bar.High, bar.Low, bar.Close, bar.Volume, isNew);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TBar UpdateCore(long timeTicks, double open, double high, double low, double close, double volume, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values — use last valid values
|
||||
if (!double.IsFinite(open)) { open = s.LastValidOpen; } else { s.LastValidOpen = open; }
|
||||
if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; }
|
||||
if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; }
|
||||
if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; }
|
||||
|
||||
// HA Close = OHLC4
|
||||
double haClose = ComputeHaClose(open, high, low, close);
|
||||
|
||||
// HA Open = recursive IIR
|
||||
double haOpen;
|
||||
if (s.Count == 0)
|
||||
{
|
||||
// Seed: midpoint of O and C
|
||||
haOpen = (open + close) * 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
haOpen = (s.PrevHaOpen + s.PrevHaClose) * 0.5;
|
||||
}
|
||||
|
||||
// HA High = max(H, haOpen, haClose)
|
||||
double haHigh = Math.Max(high, Math.Max(haOpen, haClose));
|
||||
|
||||
// HA Low = min(L, haOpen, haClose)
|
||||
double haLow = Math.Min(low, Math.Min(haOpen, haClose));
|
||||
|
||||
// Store state for next bar
|
||||
s.PrevHaOpen = haOpen;
|
||||
s.PrevHaClose = haClose;
|
||||
|
||||
if (isNew) { s.Count++; }
|
||||
|
||||
_s = s;
|
||||
|
||||
LastBar = new TBar(timeTicks, haOpen, haHigh, haLow, haClose, volume);
|
||||
Last = new TValue(timeTicks, haClose);
|
||||
PubEvent(Last, isNew);
|
||||
return LastBar;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow.Ticks, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_s = default;
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
LastBar = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Heikin-Ashi bars for a bar series (static).
|
||||
/// </summary>
|
||||
public static TBarSeries Batch(TBarSeries source)
|
||||
{
|
||||
var indicator = new Ha();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using OHLC spans. Outputs 4 spans for HA O, H, L, C.
|
||||
/// HA_Open is sequential (IIR), so this cannot be fully vectorized.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> open,
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> haOpenOut,
|
||||
Span<double> haHighOut,
|
||||
Span<double> haLowOut,
|
||||
Span<double> haCloseOut)
|
||||
{
|
||||
int len = open.Length;
|
||||
if (high.Length != len || low.Length != len || close.Length != len)
|
||||
{
|
||||
throw new ArgumentException("All input spans must have the same length", nameof(high));
|
||||
}
|
||||
if (haOpenOut.Length < len || haHighOut.Length < len || haLowOut.Length < len || haCloseOut.Length < len)
|
||||
{
|
||||
throw new ArgumentException("All output spans must be at least as long as input spans", nameof(haOpenOut));
|
||||
}
|
||||
|
||||
if (len == 0) { return; }
|
||||
|
||||
// First bar: seed
|
||||
double hc = ComputeHaClose(open[0], high[0], low[0], close[0]);
|
||||
double ho = (open[0] + close[0]) * 0.5;
|
||||
haCloseOut[0] = hc;
|
||||
haOpenOut[0] = ho;
|
||||
haHighOut[0] = Math.Max(high[0], Math.Max(ho, hc));
|
||||
haLowOut[0] = Math.Min(low[0], Math.Min(ho, hc));
|
||||
|
||||
double prevHaOpen = ho;
|
||||
double prevHaClose = hc;
|
||||
|
||||
// Sequential pass (IIR dependency on HA_Open)
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
hc = ComputeHaClose(open[i], high[i], low[i], close[i]);
|
||||
ho = (prevHaOpen + prevHaClose) * 0.5;
|
||||
|
||||
haCloseOut[i] = hc;
|
||||
haOpenOut[i] = ho;
|
||||
haHighOut[i] = Math.Max(high[i], Math.Max(ho, hc));
|
||||
haLowOut[i] = Math.Min(low[i], Math.Min(ho, hc));
|
||||
|
||||
prevHaOpen = ho;
|
||||
prevHaClose = hc;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static Calculate returning both results and indicator state.
|
||||
/// </summary>
|
||||
public static (TBarSeries Results, Ha Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Ha();
|
||||
TBarSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
# HA: Heikin-Ashi
|
||||
|
||||
> "The trend is your friend — but only if the noise doesn't make you abandon it at the first bump." — Every trader, eventually
|
||||
|
||||
HA transforms standard OHLC bars into smoothed Heikin-Ashi candles by averaging each component with its predecessor. The Close is the bar's four-price mean $(O+H+L+C)/4$, the Open is a recursive midpoint of the prior HA Open and HA Close, and High/Low are clamped extremes that guarantee the HA body always fits inside the HA wick. Unlike most indicators that reduce a bar to a single scalar, HA outputs a complete `TBar` — four smoothed prices per bar — making it a bar-to-bar transform rather than a bar-to-value reduction. The recursive Open gives HA an IIR character: each bar carries a decaying memory of the entire price history, which is what flattens trend noise but also why HA prices do not match any actual traded price.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Heikin-Ashi (平均足, literally "average bar") is a Japanese charting technique that predates modern computing. The method gained widespread adoption in Western markets after Steve Nison introduced Japanese candlestick charting in the early 1990s, though Heikin-Ashi itself was popularized separately by Dan Valcu in a 2004 *Technical Analysis of Stocks & Commodities* article. The technique did not originate in academic quantitative finance; it emerged from the practitioner tradition of visually simplifying price action to identify trends.
|
||||
|
||||
The transformation is sometimes confused with a moving average, but the mechanics differ. A moving average produces a single smoothed value from a rolling window of N bars. Heikin-Ashi produces four smoothed values (O, H, L, C) using no window — the smoothing comes entirely from the recursive Open, which is a first-order IIR filter with $\alpha = 0.5$. This makes HA closer to an EMA(2) applied to the Open channel than to any FIR filter. The Close channel ($\text{OHLC4}$) is identical to `AVGPRICE` — it carries no memory between bars.
|
||||
|
||||
A persistent source of confusion across platforms: TradingView's `ticker.heikinashi()` function applies the transform at the data-feed level, meaning all built-in variables (`open`, `high`, `low`, `close`) become HA values. Indicators computed on HA data produce doubly-smoothed results that do not match the same indicator on standard data. QuanTAlib applies HA as an explicit indicator, keeping the standard data pipeline intact and the smoothing auditable.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. HA Close (Stateless)
|
||||
|
||||
$$\text{HA\_Close}_t = \frac{O_t + H_t + L_t + C_t}{4}$$
|
||||
|
||||
This is identical to `AVGPRICE` / `OHLC4`. No inter-bar dependency. Implemented as FMA:
|
||||
|
||||
$$\text{HA\_Close}_t = \text{FMA}(O_t + H_t,\; 0.25,\; (L_t + C_t) \times 0.25)$$
|
||||
|
||||
### 2. HA Open (Recursive IIR)
|
||||
|
||||
$$\text{HA\_Open}_t = \frac{\text{HA\_Open}_{t-1} + \text{HA\_Close}_{t-1}}{2}$$
|
||||
|
||||
Seed on the first bar:
|
||||
|
||||
$$\text{HA\_Open}_0 = \frac{O_0 + C_0}{2}$$
|
||||
|
||||
This is a first-order IIR filter with $\alpha = 0.5$ and $\beta = 0.5$, giving it an effective half-life of 1 bar and exponential memory decay. The recursive structure means HA_Open carries the entire price history with geometrically decaying weights — it never fully forgets, but contributions older than ~7 bars contribute less than 1% each.
|
||||
|
||||
### 3. HA High (Clamped Maximum)
|
||||
|
||||
$$\text{HA\_High}_t = \max(H_t,\; \text{HA\_Open}_t,\; \text{HA\_Close}_t)$$
|
||||
|
||||
Guarantees the wick extends above the body. In strong uptrends where the actual High exceeds both HA Open and HA Close, the HA High equals the real High.
|
||||
|
||||
### 4. HA Low (Clamped Minimum)
|
||||
|
||||
$$\text{HA\_Low}_t = \min(L_t,\; \text{HA\_Open}_t,\; \text{HA\_Close}_t)$$
|
||||
|
||||
Guarantees the wick extends below the body. In strong downtrends where the actual Low is below both HA Open and HA Close, the HA Low equals the real Low.
|
||||
|
||||
### 5. Output Structure
|
||||
|
||||
Unlike standard indicators that output a `TValue` (timestamp + double), HA outputs a `TBar`:
|
||||
|
||||
```
|
||||
TBar(Time, HA_Open, HA_High, HA_Low, HA_Close, Volume)
|
||||
```
|
||||
|
||||
Volume passes through untransformed.
|
||||
|
||||
### 6. Complexity
|
||||
|
||||
$O(1)$ per bar. One FMA + one multiplication + two comparisons (max/min). State: two doubles (previous HA_Open and HA_Close). No buffers, no lookback window.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| (none) | No user-configurable parameters | | |
|
||||
|
||||
### IIR Transfer Function
|
||||
|
||||
The HA Open channel is a first-order IIR filter on the midpoint of (HA_Open, HA_Close):
|
||||
|
||||
$$H(z) = \frac{0.5}{1 - 0.5z^{-1}}$$
|
||||
|
||||
This yields an exponential impulse response with decay factor $\beta = 0.5$ per bar:
|
||||
|
||||
$$h[n] = 0.5^{n+1}, \quad n \geq 0$$
|
||||
|
||||
Half-life: $t_{1/2} = \frac{-\ln 2}{\ln 0.5} = 1$ bar.
|
||||
|
||||
### Warmup Period
|
||||
|
||||
$$\text{WarmupPeriod} = 1$$
|
||||
|
||||
HA is "hot" from bar 1. The seed bar uses $(O_0 + C_0)/2$ for HA_Open and produces valid output immediately. The recursive filter converges rapidly due to the $\beta = 0.5$ decay — after 7 bars, the contribution of the seed value is less than 0.4%.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function HA(bar, prevHaOpen, prevHaClose):
|
||||
o, h, l, c ← bar.Open, bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(o): o ← lastValidOpen
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
haClose ← FMA(o + h, 0.25, (l + c) × 0.25)
|
||||
|
||||
if firstBar:
|
||||
haOpen ← (o + c) × 0.5
|
||||
else:
|
||||
haOpen ← (prevHaOpen + prevHaClose) × 0.5
|
||||
|
||||
haHigh ← max(h, haOpen, haClose)
|
||||
haLow ← min(l, haOpen, haClose)
|
||||
|
||||
return TBar(bar.Time, haOpen, haHigh, haLow, haClose, bar.Volume)
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Candle Pattern | Meaning |
|
||||
|----------------|---------|
|
||||
| Green body, no lower wick | Strong uptrend |
|
||||
| Red body, no upper wick | Strong downtrend |
|
||||
| Small body, both wicks | Indecision / potential reversal |
|
||||
| Increasing body size | Trend acceleration |
|
||||
| Decreasing body size | Trend deceleration |
|
||||
|
||||
## Interpretation and Signals
|
||||
|
||||
### Signal Patterns
|
||||
|
||||
- **Wickless candles**: An HA candle with no lower wick (uptrend) or no upper wick (downtrend) signals strong directional momentum. Three or more consecutive wickless candles in one direction is a high-confidence trend signal.
|
||||
- **Doji / spinning top**: Small HA bodies with wicks on both sides indicate weakening momentum and potential reversal. The smaller the body relative to the wicks, the stronger the indecision signal.
|
||||
- **Color change**: A transition from red to green (or vice versa) after a series of same-colored candles signals trend reversal. Confirmation from volume or a secondary indicator reduces false signals.
|
||||
- **Body size sequence**: Monotonically increasing HA body sizes indicate trend acceleration; decreasing sizes indicate exhaustion.
|
||||
|
||||
### Practical Notes
|
||||
|
||||
HA candles should never be used for precise entry/exit pricing because HA Open and HA Close are synthetic — they do not correspond to any traded price. Use HA for trend direction and standard candles for execution levels. Combining HA trend direction with a momentum oscillator (RSI, CCI) on standard data provides trend-filtered signals without the double-smoothing problem.
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
|--------|:-----:|-------|
|
||||
| **Accuracy** | 7/10 | HA Close = OHLC4 (exact); HA Open drifts from real prices due to recursion |
|
||||
| **Timeliness** | 8/10 | Only 1-bar effective lag from IIR Open; responds quickly to trend changes |
|
||||
| **Overshoot** | 10/10 | High/Low clamping guarantees HA range ⊆ real range on High/Low channels |
|
||||
| **Smoothness** | 8/10 | IIR Open provides consistent smoothing; Close is unsmoothed (bar-local) |
|
||||
|
||||
## Related Indicators
|
||||
|
||||
- **[AVGPRICE](../avgprice/Avgprice.md)**: HA_Close is identical to AVGPRICE. If you only need the average price per bar, AVGPRICE avoids the recursive state overhead.
|
||||
- **[EMA](../../trends_IIR/ema/Ema.md)**: HA_Open is effectively EMA(2) on the midpoint stream. For single-value smoothing with configurable responsiveness, EMA offers more control.
|
||||
- **[MEDPRICE](../medprice/Medprice.md)**: Uses (H+L)/2 — HA's seed value on bar 0 uses (O+C)/2 instead, weighting session boundaries over extremes.
|
||||
|
||||
## Validation
|
||||
|
||||
Validated against external libraries in `Ha.Validation.Tests.cs`. HA is widely implemented; cross-validation is straightforward since the formula has no ambiguity.
|
||||
|
||||
| Library | Batch | Streaming | Span | Notes |
|
||||
|---------|:-----:|:---------:|:----:|-------|
|
||||
| **TA-Lib** | ? | ? | ? | No direct `TA_HA` function; requires manual OHLC transform |
|
||||
| **Skender** | ? | ? | ? | `GetHeikinAshi()` returns OHLC results |
|
||||
| **Tulip** | ? | ? | ? | No Heikin-Ashi function |
|
||||
| **Ooples** | ? | ? | ? | `GetHeikinAshi()` |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Key Optimizations
|
||||
|
||||
- **FMA usage**: HA_Close uses `Math.FusedMultiplyAdd(o + h, 0.25, (l + c) * 0.25)` — single instruction for the four-price average.
|
||||
- **Multiplication over division**: `× 0.5` and `× 0.25` replace `/2` and `/4`.
|
||||
- **No buffer**: Only two doubles of state (previous HA_Open, previous HA_Close). No `RingBuffer` or history required.
|
||||
- **Aggressive inlining**: `Update` method decorated with `[MethodImpl(MethodImplOptions.AggressiveInlining)]`.
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|-----------|:-----:|:-------------:|:--------:|
|
||||
| ADD (O+H) | 1 | 1 | 1 |
|
||||
| ADD (L+C) | 1 | 1 | 1 |
|
||||
| MUL ((L+C) × 0.25) | 1 | 3 | 3 |
|
||||
| FMA (haClose) | 1 | 4 | 4 |
|
||||
| ADD (prevHaOpen + prevHaClose) | 1 | 1 | 1 |
|
||||
| MUL (× 0.5) | 1 | 3 | 3 |
|
||||
| MAX (3-way) | 2 | 1 | 2 |
|
||||
| MIN (3-way) | 2 | 1 | 2 |
|
||||
| **Total (hot)** | **10** | | **~17 cycles** |
|
||||
|
||||
### SIMD Analysis (Batch Mode)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| HA_Close | Fully vectorizable (element-wise OHLC4) |
|
||||
| HA_Open | Sequential — IIR dependency blocks vectorization |
|
||||
| HA_High/Low | Vectorizable after Open/Close are computed |
|
||||
| Strategy | Vectorize Close in pass 1, scalar Open in pass 2, vectorize High/Low in pass 3 |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Synthetic prices**: HA Open and HA Close do not correspond to any actual traded price. Using HA values for order placement or stop-loss levels produces fills at non-real prices. Always use standard OHLC for execution.
|
||||
|
||||
2. **Double smoothing**: Applying indicators (RSI, MACD, etc.) to HA data instead of standard data produces doubly-smoothed results with increased lag and reduced sensitivity. This is the single most common misuse of Heikin-Ashi.
|
||||
|
||||
3. **Backtesting on HA data**: Strategies backtested on HA candles show artificially smooth equity curves because the smoothed prices overstate trend persistence. Results do not replicate on live standard-data execution.
|
||||
|
||||
4. **Volume passthrough**: HA transforms only prices. Volume is unchanged. Interpreting HA candle patterns without checking whether volume confirms the signal leads to false trend readings.
|
||||
|
||||
5. **Seed sensitivity**: The first bar's HA_Open seed $(O_0 + C_0)/2$ affects all subsequent HA_Open values. Different start dates produce different HA series for the same instrument. The impact decays as $0.5^n$ — after 10 bars the seed contributes less than 0.1%.
|
||||
|
||||
6. **Gap handling**: Real gaps (overnight, weekend) produce HA_Open values that split the difference between the gap ends. This is by design (smoothing), but users expecting gap preservation will be surprised. The actual High and Low still reflect the real extremes via the max/min clamping.
|
||||
|
||||
7. **No parameters**: Unlike most indicators, HA has no configurable period or smoothing factor. The $\alpha = 0.5$ is fixed. Users wanting adjustable smoothing should consider applying an EMA or other moving average to standard OHLC data instead.
|
||||
|
||||
## References
|
||||
|
||||
- **Valcu, D.** (2004). "Using The Heikin-Ashi Technique." *Technical Analysis of Stocks & Commodities*, Vol. 22, No. 2.
|
||||
- **Nison, S.** (1991). *Japanese Candlestick Charting Techniques*. New York Institute of Finance.
|
||||
- **Vervoort, S.** (2008). "Smoothing Heikin-Ashi." *Technical Analysis of Stocks & Commodities*.
|
||||
- [Investopedia: Heikin-Ashi](https://www.investopedia.com/terms/h/heikinashi.asp) — accessible introduction to the technique and its trading applications.
|
||||
@@ -0,0 +1,19 @@
|
||||
// HA: Heikin-Ashi
|
||||
// Smoothed candle transformation with recursive open
|
||||
// HA_Close = (O + H + L + C) / 4
|
||||
// HA_Open = (prev_HA_Open + prev_HA_Close) / 2
|
||||
// HA_High = max(H, HA_Open, HA_Close)
|
||||
// HA_Low = min(L, HA_Open, HA_Close)
|
||||
|
||||
//@version=6
|
||||
indicator("HA: Heikin-Ashi", overlay=true)
|
||||
|
||||
var float haOpen = na
|
||||
var float haClose = na
|
||||
|
||||
haClose := (open + high + low + close) * 0.25
|
||||
haOpen := na(haOpen) ? (open + close) * 0.5 : (haOpen + haClose[1]) * 0.5
|
||||
haHigh = math.max(high, haOpen, haClose)
|
||||
haLow = math.min(low, haOpen, haClose)
|
||||
|
||||
plotcandle(haOpen, haHigh, haLow, haClose, "HA", color=haClose >= haOpen ? color.green : color.red)
|
||||
@@ -0,0 +1,131 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MedpriceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MedpriceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("MEDPRICE - Median Price", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_ShortName_IsMedprice()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
Assert.Equal("MEDPRICE", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
|
||||
Assert.Equal(1, MedpriceIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 115, 105, 112, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Medprice.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_ComputesCorrectMedian()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// H=110, L=90 → (110+90)/2 = 100.0
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(100.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MedpriceIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new MedpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class MedpriceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Medprice _medprice = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 1;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "MEDPRICE";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/medprice/Medprice.Quantower.cs";
|
||||
|
||||
public MedpriceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "MEDPRICE - Median Price";
|
||||
Description = "Midpoint of High and Low prices: (H+L)/2.";
|
||||
|
||||
_series = new LineSeries(name: "MEDPRICE", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_medprice = new Medprice();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _medprice.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _medprice.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Medprice Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MedpriceTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public MedpriceTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
Assert.Equal("Medprice", indicator.Name);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Medprice(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_ReturnsHL2()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.Update(bar);
|
||||
// (110 + 90) / 2 = 100
|
||||
Assert.Equal(100.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_MatchesTBarHL2()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 50, 60, 40, 55, 500);
|
||||
var result = indicator.Update(bar);
|
||||
Assert.Equal(bar.HL2, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ReturnsIdentity()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
Assert.Equal(42.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterFirstBar_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
Assert.False(indicator.IsHot);
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RestoresPreviousState()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
indicator.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000), isNew: true);
|
||||
|
||||
var corrected = indicator.Update(new TBar(time.AddMinutes(1), 106, 120, 80, 111, 1000), isNew: false);
|
||||
double expected = (120 + 80) * 0.5;
|
||||
Assert.Equal(expected, corrected.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
|
||||
var bar = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000);
|
||||
var result1 = indicator.Update(bar, isNew: false);
|
||||
var result2 = indicator.Update(bar, isNew: false);
|
||||
var result3 = indicator.Update(bar, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, Tolerance);
|
||||
Assert.Equal(result2.Value, result3.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
double validResult = indicator.Last.Value;
|
||||
|
||||
var nanBar = new TBar(time.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, 1000);
|
||||
var result = indicator.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(validResult, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests (All Modes)
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceConsistentResults()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Medprice();
|
||||
double[] streamingResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streaming.Update(bars[i], isNew: true).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TBarSeries)
|
||||
var batchResult = Medprice.Batch(bars);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[bars.Count];
|
||||
Medprice.Batch(bars.HighValues, bars.LowValues, spanOutput);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllBars_MatchTBarHL2()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var indicator = new Medprice();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i], isNew: true);
|
||||
Assert.Equal(bars[i].HL2, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[5]; // mismatched
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Medprice.Batch(high, low, output));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] output = new double[5]; // too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Medprice.Batch(high, low, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOutput()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var result = Medprice.Batch(bars);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBars(10_000);
|
||||
double[] output = new double[bars.Count];
|
||||
Medprice.Batch(bars.HighValues, bars.LowValues, output);
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
bool fired = false;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, ind) = Medprice.Calculate(bars);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MEDPRICE: Median Price
|
||||
/// Calculates the midpoint of High and Low prices.
|
||||
/// Equivalent to TBar.HL2 but as a proper streaming indicator with bar correction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation:</b>
|
||||
/// <list type="number">
|
||||
/// <item>MedPrice = (High + Low) / 2</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Stateless bar-by-bar calculation (no lookback period)</item>
|
||||
/// <item>TA-Lib compatible (MEDPRICE function)</item>
|
||||
/// <item>Always hot after first bar</item>
|
||||
/// <item>Common proxy for "fair value" within a bar</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Medprice : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastResult,
|
||||
int Count
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Medprice class.
|
||||
/// </summary>
|
||||
public Medprice()
|
||||
{
|
||||
WarmupPeriod = 1;
|
||||
Name = "Medprice";
|
||||
_s = new State(0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Medprice class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
public Medprice(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the median price from High and Low values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeMedianPrice(double high, double low)
|
||||
{
|
||||
return (high + low) * 0.5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TValue input, treats the value as both High and Low (result = value).
|
||||
/// Prefer Update(TBar) for standard OHLC data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(input.Time, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (preferred method).
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated Median Price value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.High, bar.Low, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the Median Price values.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, vSpan);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
var values = source.Values;
|
||||
|
||||
// TValue-only: result = value (identity)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source.Times[i];
|
||||
vSpan[i] = values[i];
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue UpdateCore(long timeTicks, double high, double low, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values — use last valid values
|
||||
if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; }
|
||||
if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; }
|
||||
|
||||
double result = ComputeMedianPrice(high, low);
|
||||
|
||||
if (!double.IsFinite(result))
|
||||
{
|
||||
result = s.LastResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastResult = result;
|
||||
}
|
||||
|
||||
if (isNew) { s.Count++; }
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(timeTicks, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Median Price for a bar series (static).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source)
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans for High/Low data.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
Span<double> output)
|
||||
{
|
||||
int len = high.Length;
|
||||
if (low.Length != len)
|
||||
{
|
||||
throw new ArgumentException("All input spans must have the same length", nameof(low));
|
||||
}
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
output[i] = ComputeMedianPrice(high[i], low[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using a TBarSeries (convenience overload).
|
||||
/// </summary>
|
||||
public static void Batch(TBarSeries source, Span<double> output)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as source", nameof(output));
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Batch(source.HighValues, source.LowValues, output);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Medprice Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Medprice();
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# MEDPRICE: Median Price
|
||||
|
||||
MEDPRICE computes the midpoint of a bar's High and Low: $(H + L) \times 0.5$. This is the simplest possible estimate of a bar's "fair value," splitting the difference between the session's extremes while ignoring both the opening gap and closing settlement. The result represents the geometric center of the bar's vertical range. Because it excludes Open and Close, MEDPRICE responds purely to the supply/demand boundaries that the market tested, making it a useful input for range-based indicators like CCI or as a detrending reference. Stateless, zero-warmup, one addition and one multiply per bar.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Median Price (also called "Mid Price" or "HL/2") is among the most elemental price transforms, used long before computers entered trading floors. The TA-Lib function `TA_MEDPRICE` standardized the computation, and most charting platforms expose it as a built-in price source. The name "Median Price" is a slight misnomer in the statistical sense: it is the midrange (arithmetic mean of extremes), not the median of a distribution. The name stuck through decades of usage.
|
||||
|
||||
The key distinction from Typical Price ($HLC/3$) is the exclusion of Close. This matters when the closing price diverges significantly from the bar's center, as happens with gap-up closes, stop runs, or end-of-session order flow. MEDPRICE treats the bar as a symmetric range and asks: where was the midpoint of price exploration?
|
||||
|
||||
In QuanTAlib, `TBar.HL2` provides the same value as a zero-cost computed property. The `Medprice` indicator class wraps this in the streaming `ITValuePublisher` interface with bar correction, NaN safety, and event chaining support.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Core Formula
|
||||
|
||||
$$\text{MedPrice}_t = (H_t + L_t) \times 0.5$$
|
||||
|
||||
No FMA benefit here: the pattern is $(a + b) \times c$, not $a \times b + c$.
|
||||
|
||||
### 2. State Management
|
||||
|
||||
Stateless per bar. State exists only for:
|
||||
|
||||
- **Last-valid substitution**: Non-finite High or Low values are replaced with the last known finite value for that component.
|
||||
- **Bar correction**: `isNew=false` rolls back to previous state for same-timestamp rewrites.
|
||||
|
||||
### 3. Complexity
|
||||
|
||||
$O(1)$ per bar. One addition, one multiply. No memory allocation. Always hot after the first bar.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| (none) | No user-configurable parameters | | |
|
||||
|
||||
### Price Transform Comparison
|
||||
|
||||
| Transform | Components | Weights | Bias |
|
||||
|-----------|:----------:|---------|------|
|
||||
| MEDPRICE | H, L | Equal | Range-centered; ignores O/C |
|
||||
| TYPPRICE | H, L, C | Equal | Close-influenced |
|
||||
| AVGPRICE | O, H, L, C | Equal | Fully balanced |
|
||||
| WCLPRICE | H, L, C | C double-weighted | Close-biased |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function MEDPRICE(bar):
|
||||
h, l ← bar.High, bar.Low
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
|
||||
result ← (h + l) × 0.5
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|---------|---------|
|
||||
| Close > MEDPRICE | Close above the range midpoint (bullish bar body) |
|
||||
| Close < MEDPRICE | Close below the range midpoint (bearish bar body) |
|
||||
| Close $\approx$ MEDPRICE | Close near center of range (indecision) |
|
||||
| MEDPRICE expanding | Increasing bar ranges (volatility expanding) |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|-----------|:-----:|:-------------:|:--------:|
|
||||
| ADD (H+L) | 1 | 1 | 1 |
|
||||
| MUL (× 0.5) | 1 | 3 | 3 |
|
||||
| **Total (hot)** | **2** | | **~4 cycles** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| SIMD vectorizable | Yes: element-wise add + multiply, no inter-bar dependency |
|
||||
| Optimal strategy | `Vector<double>` over High/Low spans |
|
||||
| Memory | $O(1)$ streaming; $O(n)$ batch output span |
|
||||
| Throughput | Memory-bandwidth bound; trivial compute |
|
||||
|
||||
## Resources
|
||||
|
||||
- **TA-Lib** `TA_MEDPRICE` function reference.
|
||||
- **Murphy, J.J.** *Technical Analysis of the Financial Markets*. New York Institute of Finance, 1999.
|
||||
@@ -0,0 +1,13 @@
|
||||
// MEDPRICE: Median Price
|
||||
// (High + Low) / 2
|
||||
// TA-Lib compatible — equivalent to TBar.HL2
|
||||
|
||||
//@version=6
|
||||
indicator("MEDPRICE: Median Price", overlay=true)
|
||||
|
||||
medprice(float h, float l) =>
|
||||
(h + l) * 0.5
|
||||
|
||||
result = medprice(high, low)
|
||||
|
||||
plot(result, "MedPrice", color.new(color.orange, 0), 2)
|
||||
@@ -286,24 +286,39 @@ public class MidpointTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Midpoint_EqualsAverageOfHighestAndLowest()
|
||||
public void Midpoint_EqualsAverageOfMaxAndMin()
|
||||
{
|
||||
// Verify Midpoint matches manually computed (Max + Min) / 2 from values in window
|
||||
int period = 5;
|
||||
var gbm = new GBM(12345);
|
||||
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
var midpoint = new Midpoint(period);
|
||||
var highest = new Highest(period);
|
||||
var lowest = new Lowest(period);
|
||||
var values = new List<double>();
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
values.Add(source[i].Value);
|
||||
midpoint.Update(source[i]);
|
||||
highest.Update(source[i]);
|
||||
lowest.Update(source[i]);
|
||||
|
||||
double expected = (highest.Last.Value + lowest.Last.Value) * 0.5;
|
||||
// Manually compute max and min over the window
|
||||
int start = Math.Max(0, values.Count - period);
|
||||
double max = double.MinValue;
|
||||
double min = double.MaxValue;
|
||||
for (int j = start; j < values.Count; j++)
|
||||
{
|
||||
if (values[j] > max)
|
||||
{
|
||||
max = values[j];
|
||||
}
|
||||
if (values[j] < min)
|
||||
{
|
||||
min = values[j];
|
||||
}
|
||||
}
|
||||
|
||||
double expected = (max + min) * 0.5;
|
||||
Assert.Equal(expected, midpoint.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
+25
-13
@@ -147,24 +147,36 @@ public sealed class MidpointValidationTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConsistencyWithHighestLowest()
|
||||
public void Validate_ConsistencyBatchStreamingSpan()
|
||||
{
|
||||
// Verify that Midpoint = (Highest + Lowest) / 2
|
||||
// Verify that Batch, Streaming, and Span all produce the same results
|
||||
int period = 14;
|
||||
var midpoint = new Midpoint(period);
|
||||
var highest = new Highest(period);
|
||||
var lowest = new Lowest(period);
|
||||
var gbm = new GBM(42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
// Streaming
|
||||
var streaming = new Midpoint(period);
|
||||
var streamResults = new List<double>();
|
||||
foreach (var item in source)
|
||||
{
|
||||
var midResult = midpoint.Update(item);
|
||||
var highResult = highest.Update(item);
|
||||
var lowResult = lowest.Update(item);
|
||||
|
||||
double expected = (highResult.Value + lowResult.Value) * 0.5;
|
||||
Assert.Equal(expected, midResult.Value, precision: 10);
|
||||
streamResults.Add(streaming.Update(item).Value);
|
||||
}
|
||||
_output.WriteLine("Midpoint consistency validated: equals (Highest + Lowest) / 2");
|
||||
|
||||
// Batch TSeries
|
||||
var batchResult = Midpoint.Batch(source, period);
|
||||
|
||||
// Span
|
||||
double[] sourceArray = source.Values.ToArray();
|
||||
double[] spanOutput = new double[sourceArray.Length];
|
||||
Midpoint.Batch(sourceArray.AsSpan(), spanOutput.AsSpan(), period);
|
||||
|
||||
for (int i = period; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResult[i].Value, precision: 10);
|
||||
Assert.Equal(streamResults[i], spanOutput[i], precision: 10);
|
||||
}
|
||||
_output.WriteLine("Midpoint consistency validated: Batch == Streaming == Span");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1,31 +1,35 @@
|
||||
// MIDPOINT: Rolling Midpoint - (Highest + Lowest) / 2 over lookback window
|
||||
// Composes Highest and Lowest indicators for efficient calculation
|
||||
// Uses RingBuffer directly for self-contained core dependency (no Highest/Lowest composition)
|
||||
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MIDPOINT: Rolling Midpoint
|
||||
/// Calculates the midpoint ((highest + lowest) / 2) over a specified lookback period.
|
||||
/// Composes Highest and Lowest indicators internally.
|
||||
/// Uses RingBuffer directly for O(N) max/min scanning per update.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key properties:
|
||||
/// - Returns the center of the price range within the lookback window
|
||||
/// - Returns the center of the value range within the lookback window
|
||||
/// - Useful for mean reversion, channel center, trend direction
|
||||
/// - Can be validated against TA-Lib MIDPOINT function
|
||||
/// - Self-contained: uses RingBuffer directly (no Highest/Lowest dependency)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Midpoint : AbstractBase
|
||||
{
|
||||
private readonly Highest _highest;
|
||||
private readonly Lowest _lowest;
|
||||
private readonly ITValuePublisher? _source;
|
||||
private readonly TValuePublishedHandler? _handler;
|
||||
private bool _disposed;
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
|
||||
public override bool IsHot => _highest.IsHot && _lowest.IsHot;
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValid);
|
||||
private State _s, _ps;
|
||||
|
||||
public override bool IsHot => _buffer.Count >= _period;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new Midpoint indicator with specified lookback period.
|
||||
@@ -38,8 +42,8 @@ public sealed class Midpoint : AbstractBase
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
}
|
||||
|
||||
_highest = new Highest(period);
|
||||
_lowest = new Lowest(period);
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Midpoint({period})";
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
@@ -51,35 +55,33 @@ public sealed class Midpoint : AbstractBase
|
||||
/// <param name="period">Lookback window size</param>
|
||||
public Midpoint(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
_source = source;
|
||||
_handler = HandleUpdate;
|
||||
_source.Pub += _handler;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing && _source != null && _handler != null)
|
||||
{
|
||||
_source.Pub -= _handler;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
TValue high = _highest.Update(input, isNew);
|
||||
TValue low = _lowest.Update(input, isNew);
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
var s = _s;
|
||||
|
||||
double result = (high.Value + low.Value) * 0.5;
|
||||
double value = double.IsFinite(input.Value) ? input.Value : s.LastValid;
|
||||
s = new State(value);
|
||||
|
||||
_buffer.Add(value, isNew);
|
||||
|
||||
double result = (_buffer.Max() + _buffer.Min()) * 0.5;
|
||||
|
||||
_s = s;
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
@@ -138,42 +140,14 @@ public sealed class Midpoint : AbstractBase
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
var buf = new RingBuffer(period);
|
||||
|
||||
// Use ArrayPool for large arrays to avoid stack overflow
|
||||
double[]? rentedHigh = null;
|
||||
double[]? rentedLow = null;
|
||||
|
||||
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||
Span<double> highBuffer = len <= 256
|
||||
? stackalloc double[len]
|
||||
: (rentedHigh = System.Buffers.ArrayPool<double>.Shared.Rent(len)).AsSpan(0, len);
|
||||
|
||||
Span<double> lowBuffer = len <= 256
|
||||
? stackalloc double[len]
|
||||
: (rentedLow = System.Buffers.ArrayPool<double>.Shared.Rent(len)).AsSpan(0, len);
|
||||
#pragma warning restore S1121
|
||||
|
||||
try
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Highest.Batch(source, highBuffer, period);
|
||||
Lowest.Batch(source, lowBuffer, period);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
output[i] = (highBuffer[i] + lowBuffer[i]) * 0.5;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedHigh != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedHigh);
|
||||
}
|
||||
|
||||
if (rentedLow != null)
|
||||
{
|
||||
System.Buffers.ArrayPool<double>.Shared.Return(rentedLow);
|
||||
}
|
||||
double fallback = i > 0 ? output[i - 1] : 0;
|
||||
double v = double.IsFinite(source[i]) ? source[i] : fallback;
|
||||
buf.Add(v, true);
|
||||
output[i] = (buf.Max() + buf.Min()) * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,8 +160,9 @@ public sealed class Midpoint : AbstractBase
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_highest.Reset();
|
||||
_lowest.Reset();
|
||||
_buffer.Clear();
|
||||
_s = default;
|
||||
_ps = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# MIDPOINT: Rolling Range Midpoint
|
||||
|
||||
> "The center holds, but only for the window you're watching." — Statistical folk wisdom
|
||||
|
||||
Single-series rolling midpoint: `(Highest(V, N) + Lowest(V, N)) * 0.5`. Returns the center of the value range within a lookback window. TA-Lib compatible (`MIDPOINT` function). Unlike MIDPRICE which operates on separate High/Low bar channels, MIDPOINT operates on a single value series.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The midpoint of a rolling range is one of the simplest channel-center calculations in technical analysis. It appears in virtually every charting platform as the baseline for range-based indicators. TA-Lib implements it as `MIDPOINT` (single series) vs `MIDPRICE` (dual H/L series). The distinction matters: MIDPOINT feeds any single-valued series through a rolling window, while MIDPRICE decomposes OHLC bars into separate high/low channels.
|
||||
|
||||
## Architecture and Physics
|
||||
|
||||
### 1. RingBuffer Pattern
|
||||
|
||||
Uses a single `RingBuffer(period)` to store the last N values. On each update, the buffer provides `Max()` and `Min()` for the rolling window. This is self-contained with no external indicator dependencies.
|
||||
|
||||
### 2. Data Flow
|
||||
|
||||
```text
|
||||
Input(value) --> NaN guard --> RingBuffer.Add(v, isNew)
|
||||
|
|
||||
(Max() + Min()) * 0.5
|
||||
|
|
||||
Output
|
||||
```
|
||||
|
||||
### 3. State Synchronization
|
||||
|
||||
Uses the standard `_s` / `_ps` state local copy pattern for bar correction (`isNew = false`). The `RingBuffer.Add(v, isNew)` call handles rollback internally when `isNew` is false.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Midpoint Definition
|
||||
|
||||
$$
|
||||
\text{MIDPOINT}(N) = \frac{\max(V_0, V_1, \ldots, V_{N-1}) + \min(V_0, V_1, \ldots, V_{N-1})}{2}
|
||||
$$
|
||||
|
||||
### Equivalent Formulation
|
||||
|
||||
$$
|
||||
\text{MIDPOINT}(N) = \min(V, N) + \frac{\text{range}(V, N)}{2}
|
||||
$$
|
||||
|
||||
where $\text{range}(V, N) = \max(V, N) - \min(V, N)$.
|
||||
|
||||
### Properties
|
||||
|
||||
- **Bounded:** Always between the minimum and maximum of the window
|
||||
- **Idempotent on constants:** If all values equal $c$, midpoint equals $c$
|
||||
- **Lag:** Responds only when the max or min of the window changes
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count |
|
||||
|-----------|-------|
|
||||
| Comparison (Max scan) | $O(N)$ per update |
|
||||
| Comparison (Min scan) | $O(N)$ per update |
|
||||
| Addition | 1 |
|
||||
| Multiplication | 1 |
|
||||
| **Total** | $O(N)$ |
|
||||
|
||||
### Batch Mode
|
||||
|
||||
The span-based `Batch` method uses a single `RingBuffer` with linear scan for max/min. For large datasets, amortized cost is $O(N \cdot P)$ where $P$ is the period.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score |
|
||||
|--------|-------|
|
||||
| Simplicity | 9/10 |
|
||||
| Responsiveness | 5/10 |
|
||||
| Smoothness | 3/10 |
|
||||
| SIMD potential | Low (sequential max/min dependency) |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Function | Match | Notes |
|
||||
|---------|----------|-------|-------|
|
||||
| TA-Lib | `MIDPOINT` | Exact (1e-10) | Batch + Streaming + Span validated |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Confusing MIDPOINT with MIDPRICE:** MIDPOINT takes a single value series; MIDPRICE takes separate High/Low channels from bars.
|
||||
2. **Window lag:** The midpoint only changes when the rolling max or min changes. It can remain flat for extended periods.
|
||||
3. **NaN propagation:** Implementation substitutes last-valid value for NaN/Infinity inputs to prevent corruption.
|
||||
4. **Period = 1:** Returns the input value unchanged (max = min = value).
|
||||
5. **Warmup:** First `period - 1` values use a partial window (fewer than N values).
|
||||
|
||||
## References
|
||||
|
||||
- TA-Lib `MIDPOINT` function documentation
|
||||
- Murphy, J. *Technical Analysis of the Financial Markets* (range-based indicators)
|
||||
@@ -0,0 +1,17 @@
|
||||
// MIDPOINT: Rolling Midpoint
|
||||
// (Highest(source, N) + Lowest(source, N)) / 2
|
||||
// TA-Lib compatible — rolling center of value range
|
||||
|
||||
//@version=6
|
||||
indicator("MIDPOINT: Rolling Midpoint", overlay=true)
|
||||
|
||||
int p = input.int(14, "Period", minval=1)
|
||||
|
||||
midpoint(series float src, int period) =>
|
||||
float hi = ta.highest(src, period)
|
||||
float lo = ta.lowest(src, period)
|
||||
(hi + lo) * 0.5
|
||||
|
||||
result = midpoint(close, p)
|
||||
|
||||
plot(result, "Midpoint", color.new(color.teal, 0), 2)
|
||||
@@ -0,0 +1,136 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MidpriceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void MidpriceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new MidpriceIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("MIDPRICE - Midpoint Price", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(14, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_ShortName_IncludesPeriod()
|
||||
{
|
||||
var indicator = new MidpriceIndicator();
|
||||
Assert.Equal("MIDPRICE(14)", indicator.ShortName);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal("MIDPRICE(20)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new MidpriceIndicator { Period = 10 };
|
||||
Assert.Equal(10, indicator.MinHistoryDepths);
|
||||
Assert.Equal(10, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
|
||||
indicator.Period = 25;
|
||||
Assert.Equal(25, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new MidpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MidpriceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new MidpriceIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new MidpriceIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new MidpriceIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Midprice.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new MidpriceIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 30;
|
||||
Assert.Equal(30, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MidpriceIndicator_IsHotAfterWarmup()
|
||||
{
|
||||
var indicator = new MidpriceIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class MidpriceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Midprice _midprice = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"MIDPRICE({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/midprice/Midprice.Quantower.cs";
|
||||
|
||||
public MidpriceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "MIDPRICE - Midpoint Price";
|
||||
Description = "Midpoint of rolling highest high and lowest low over a period: (HH+LL)/2.";
|
||||
|
||||
_series = new LineSeries(name: "MIDPRICE", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_midprice = new Midprice(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _midprice.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _midprice.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// Midprice Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class MidpriceTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public MidpriceTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidPeriod_SetsCorrectValues()
|
||||
{
|
||||
var indicator = new Midprice(14);
|
||||
Assert.Equal("Midprice(14)", indicator.Name);
|
||||
Assert.Equal(14, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Midprice(0));
|
||||
Assert.Throws<ArgumentException>(() => new Midprice(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_IsValid()
|
||||
{
|
||||
var indicator = new Midprice(1);
|
||||
Assert.Equal("Midprice(1)", indicator.Name);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Midprice(source, 5);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleBar_ReturnsMidpointOfHL()
|
||||
{
|
||||
var indicator = new Midprice(1);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.Update(bar);
|
||||
// Period=1: highest high = 110, lowest low = 90
|
||||
// (110 + 90) / 2 = 100
|
||||
Assert.Equal(100.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ThreeBars_UsesRollingWindow()
|
||||
{
|
||||
var indicator = new Midprice(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
|
||||
indicator.Update(new TBar(time.AddMinutes(1), 101, 110, 93, 108, 1000), isNew: true);
|
||||
var result = indicator.Update(new TBar(time.AddMinutes(2), 106, 108, 98, 104, 1000), isNew: true);
|
||||
|
||||
// Highest high over 3 bars: max(105, 110, 108) = 110
|
||||
// Lowest low over 3 bars: min(95, 93, 98) = 93
|
||||
// Midprice = (110 + 93) / 2 = 101.5
|
||||
Assert.Equal(101.5, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_UsesSameValueForBothChannels()
|
||||
{
|
||||
var indicator = new Midprice(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 100), isNew: true);
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 110), isNew: true);
|
||||
var result = indicator.Update(new TValue(time.AddMinutes(2), 105), isNew: true);
|
||||
|
||||
// With TValue, H=L=value, so highest = 110, lowest = 100
|
||||
// Midprice = (110 + 100) / 2 = 105
|
||||
Assert.Equal(105.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Warmup Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var indicator = new Midprice(5);
|
||||
Assert.False(indicator.IsHot);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
indicator.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 105, 1000));
|
||||
Assert.False(indicator.IsHot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AtWarmup_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Midprice(5);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 105, 1000));
|
||||
}
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RestoresPreviousState()
|
||||
{
|
||||
var indicator = new Midprice(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
|
||||
indicator.Update(new TBar(time.AddMinutes(1), 101, 110, 93, 108, 1000), isNew: true);
|
||||
|
||||
// New bar
|
||||
indicator.Update(new TBar(time.AddMinutes(2), 106, 108, 98, 104, 1000), isNew: true);
|
||||
|
||||
// Correction on third bar
|
||||
var corrected = indicator.Update(new TBar(time.AddMinutes(2), 106, 120, 80, 104, 1000), isNew: false);
|
||||
|
||||
// Highest high: max(105, 110, 120) = 120
|
||||
// Lowest low: min(95, 93, 80) = 80
|
||||
// Midprice = (120 + 80) / 2 = 100
|
||||
Assert.Equal(100.0, corrected.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
||||
{
|
||||
var indicator = new Midprice(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
|
||||
indicator.Update(new TBar(time.AddMinutes(1), 101, 110, 93, 108, 1000), isNew: true);
|
||||
|
||||
var bar = new TBar(time.AddMinutes(2), 106, 108, 98, 104, 1000);
|
||||
var result1 = indicator.Update(bar, isNew: false);
|
||||
var result2 = indicator.Update(bar, isNew: false);
|
||||
var result3 = indicator.Update(bar, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, Tolerance);
|
||||
Assert.Equal(result2.Value, result3.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Midprice(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 105, 1000));
|
||||
}
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests (All Modes)
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceConsistentResults()
|
||||
{
|
||||
int period = 14;
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Midprice(period);
|
||||
double[] streamingResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streaming.Update(bars[i], isNew: true).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TBarSeries)
|
||||
var batchResult = Midprice.Batch(bars, period);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[bars.Count];
|
||||
Midprice.Batch(bars.HighValues, bars.LowValues, spanOutput, period);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[5]; // mismatched
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Midprice.Batch(high, low, output, 5));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] output = new double[5]; // too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Midprice.Batch(high, low, output, 5));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Midprice.Batch(high, low, output, 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOutput()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var result = Midprice.Batch(bars, 5);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBars(10_000);
|
||||
double[] output = new double[bars.Count];
|
||||
Midprice.Batch(bars.HighValues, bars.LowValues, output, 14);
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var indicator = new Midprice(5);
|
||||
bool fired = false;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, ind) = Midprice.Calculate(bars, 14);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// MIDPRICE: Midpoint Price over Period
|
||||
/// Calculates the midpoint of the highest High and lowest Low over a rolling window.
|
||||
/// Unlike Midpoint (which operates on a single series), Midprice uses separate H/L channels.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation:</b>
|
||||
/// <list type="number">
|
||||
/// <item>MidPrice = (Highest(High, N) + Lowest(Low, N)) / 2</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Rolling bar-level calculation with lookback period</item>
|
||||
/// <item>TA-Lib compatible (MIDPRICE function)</item>
|
||||
/// <item>Uses RingBuffer directly for self-contained core dependency</item>
|
||||
/// <item>Represents the center of the price channel over the lookback window</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Difference from Midpoint:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Midpoint operates on a single value series: (Highest(V,N) + Lowest(V,N)) / 2</item>
|
||||
/// <item>Midprice operates on OHLC bars: (Highest(H,N) + Lowest(L,N)) / 2</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Midprice : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _highBuffer;
|
||||
private readonly RingBuffer _lowBuffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValidHigh, double LastValidLow);
|
||||
private State _s, _ps;
|
||||
|
||||
/// <summary>
|
||||
/// True if both internal buffers have enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _highBuffer.Count >= _period;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Midprice class.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback window size (must be >= 1)</param>
|
||||
public Midprice(int period)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_highBuffer = new RingBuffer(period);
|
||||
_lowBuffer = new RingBuffer(period);
|
||||
Name = $"Midprice({period})";
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Midprice class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
/// <param name="period">Lookback window size.</param>
|
||||
public Midprice(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TValue input, treats the value as both High and Low (same as Midpoint behavior).
|
||||
/// Prefer Update(TBar) for standard OHLC data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(input.Time, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (preferred method).
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated Midprice value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.High, bar.Low, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the Midprice values.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, vSpan, WarmupPeriod);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var result = new TSeries(source.Count);
|
||||
ReadOnlySpan<double> values = source.Values;
|
||||
ReadOnlySpan<long> 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;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue UpdateCore(long timeTicks, double high, double low, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
var s = _s;
|
||||
|
||||
double h = double.IsFinite(high) ? high : s.LastValidHigh;
|
||||
double l = double.IsFinite(low) ? low : s.LastValidLow;
|
||||
s = new State(h, l);
|
||||
|
||||
_highBuffer.Add(h, isNew);
|
||||
_lowBuffer.Add(l, isNew);
|
||||
|
||||
double result = (_highBuffer.Max() + _lowBuffer.Min()) * 0.5;
|
||||
|
||||
_s = s;
|
||||
Last = new TValue(timeTicks, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_highBuffer.Clear();
|
||||
_lowBuffer.Clear();
|
||||
_s = default;
|
||||
_ps = default;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Midprice for a bar series (static).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source, int period)
|
||||
{
|
||||
var indicator = new Midprice(period);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans for High/Low data with rolling window.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
Span<double> output,
|
||||
int period)
|
||||
{
|
||||
int len = high.Length;
|
||||
if (low.Length != len)
|
||||
{
|
||||
throw new ArgumentException("High and Low spans must have the same length", nameof(low));
|
||||
}
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
|
||||
}
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 1", nameof(period));
|
||||
}
|
||||
|
||||
// Use RingBuffer for rolling max/min — self-contained, no Highest/Lowest dependency
|
||||
var highBuf = new RingBuffer(period);
|
||||
var lowBuf = new RingBuffer(period);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double fallback = i > 0 ? output[i - 1] : 0;
|
||||
double h = double.IsFinite(high[i]) ? high[i] : fallback;
|
||||
double l = double.IsFinite(low[i]) ? low[i] : fallback;
|
||||
|
||||
highBuf.Add(h, true);
|
||||
lowBuf.Add(l, true);
|
||||
|
||||
output[i] = (highBuf.Max() + lowBuf.Min()) * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using a TBarSeries (convenience overload).
|
||||
/// </summary>
|
||||
public static void Batch(TBarSeries source, Span<double> output, int period)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as source", nameof(output));
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Batch(source.HighValues, source.LowValues, output, period);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Midprice Indicator) Calculate(TBarSeries source, int period)
|
||||
{
|
||||
var indicator = new Midprice(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
# MIDPRICE: Midpoint Price over Period
|
||||
|
||||
MIDPRICE computes the center of a rolling price channel by averaging the highest High and lowest Low over the past $N$ bars: $(\text{Highest}(H, N) + \text{Lowest}(L, N)) \times 0.5$. Unlike the stateless price transforms (AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE) that operate on a single bar, MIDPRICE maintains a lookback window and produces a rolling estimate of the price range's midpoint. This makes it a simplified channel center line, equivalent to the midpoint of a Donchian Channel. The calculation uses two internal RingBuffers for $O(N)$ max/min computation per bar. TA-Lib compatible via `TA_MIDPRICE`.
|
||||
|
||||
## Historical Context
|
||||
|
||||
MIDPRICE is the simplest possible channel-based price reference, conceptually dating back to Richard Donchian's channel breakout work in the 1960s. Where Donchian Channels plot the full upper/lower envelope, MIDPRICE extracts only the midline. The TA-Lib function `TA_MIDPRICE` takes separate High and Low arrays and a period parameter, which distinguishes it from `TA_MIDPOINT` (which operates on a single series).
|
||||
|
||||
The distinction between MIDPRICE and MIDPOINT matters:
|
||||
|
||||
- **MIDPOINT**: $(\text{Highest}(V, N) + \text{Lowest}(V, N)) \times 0.5$ on a single value series
|
||||
- **MIDPRICE**: $(\text{Highest}(H, N) + \text{Lowest}(L, N)) \times 0.5$ on separate High/Low channels
|
||||
|
||||
MIDPRICE always produces a wider (or equal) range because the highest High is at least as large as the highest Close, and the lowest Low is at most as small as the lowest Close. This makes MIDPRICE a more conservative channel center, reflecting the full extent of price exploration rather than just settlement levels.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Core Formula
|
||||
|
||||
$$\text{MidPrice}_t = \left(\max_{i=0}^{N-1} H_{t-i} + \min_{i=0}^{N-1} L_{t-i}\right) \times 0.5$$
|
||||
|
||||
### 2. Rolling Window Implementation
|
||||
|
||||
Two independent `RingBuffer` instances maintain the last $N$ High and Low values:
|
||||
|
||||
- `_highBuffer`: Stores High values; `Max()` returns the rolling maximum
|
||||
- `_lowBuffer`: Stores Low values; `Min()` returns the rolling minimum
|
||||
|
||||
The `RingBuffer.Max()` and `RingBuffer.Min()` operations scan the buffer linearly, making each `Update` call $O(N)$. This was a deliberate design choice to avoid the cross-project dependency that composing `Highest`/`Lowest` indicator instances from `lib/numerics/` would introduce. The core library must remain self-contained for Quantower builds.
|
||||
|
||||
### 3. State Management
|
||||
|
||||
- **RingBuffer snapshots**: `isNew=true` captures buffer state via `Snapshot()`; `isNew=false` restores via `Restore()` for bar correction.
|
||||
- **Last-valid substitution**: Non-finite High or Low values are replaced with the last known finite value.
|
||||
- **Warmup**: `IsHot` becomes true when the buffer reaches `period` elements.
|
||||
|
||||
### 4. Complexity
|
||||
|
||||
$O(N)$ per bar where $N$ is the period, due to linear scan for max/min. For typical periods (5-20), this is negligible. Always-hot after $N$ bars.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| `period` | Lookback window for rolling max/min ($N$) | (required) | $\geq 1$ |
|
||||
|
||||
### MIDPRICE vs Related Indicators
|
||||
|
||||
| Indicator | Formula | Input | State |
|
||||
|-----------|---------|-------|-------|
|
||||
| MIDPRICE | $(\max(H,N) + \min(L,N)) \times 0.5$ | TBar (H/L channels) | Rolling window |
|
||||
| MIDPOINT | $(\max(V,N) + \min(V,N)) \times 0.5$ | Single series | Rolling window |
|
||||
| MEDPRICE | $(H + L) \times 0.5$ | TBar (single bar) | Stateless |
|
||||
| Donchian Mid | Same as MIDPRICE | TBar (H/L channels) | Rolling window |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function MIDPRICE(bar, period):
|
||||
validate: period ≥ 1
|
||||
|
||||
h, l ← bar.High, bar.Low
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
|
||||
highBuffer.Add(h)
|
||||
lowBuffer.Add(l)
|
||||
|
||||
result ← (highBuffer.Max() + lowBuffer.Min()) × 0.5
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|---------|---------|
|
||||
| Price > MIDPRICE | Trading in the upper half of the $N$-bar channel |
|
||||
| Price < MIDPRICE | Trading in the lower half of the $N$-bar channel |
|
||||
| MIDPRICE rising | Channel shifting upward (uptrend) |
|
||||
| MIDPRICE flat | Range-bound market; channel stable |
|
||||
| MIDPRICE converging with price | Trend exhaustion; approaching channel center |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|-----------|:-----:|:-------------:|:--------:|
|
||||
| RingBuffer.Add (high) | 1 | ~3 | 3 |
|
||||
| RingBuffer.Add (low) | 1 | ~3 | 3 |
|
||||
| RingBuffer.Max() scan | $N$ | ~$N$ | $N$ |
|
||||
| RingBuffer.Min() scan | $N$ | ~$N$ | $N$ |
|
||||
| ADD (max+min) | 1 | 1 | 1 |
|
||||
| MUL (× 0.5) | 1 | 3 | 3 |
|
||||
| **Total (hot)** | **$2N+4$** | | **~$2N + 10$ cycles** |
|
||||
|
||||
For period=14: approximately 38 cycles per bar.
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| SIMD vectorizable | Partial: max/min scans are sequential per window; final midpoint is vectorizable |
|
||||
| Optimal strategy | Monotonic deque for $O(1)$ amortized max/min (not yet implemented) |
|
||||
| Memory | $O(N)$: two RingBuffers of size $N$ |
|
||||
| Throughput | Dominated by max/min scans; ~5x slower than stateless transforms at period=14 |
|
||||
|
||||
### Potential Optimization
|
||||
|
||||
A monotonic deque (sliding window max/min) would reduce per-bar cost from $O(N)$ to $O(1)$ amortized. This is a known optimization path stored for future implementation when profiling shows MIDPRICE as a bottleneck in production pipelines.
|
||||
|
||||
## Resources
|
||||
|
||||
- **TA-Lib** `TA_MIDPRICE` function reference.
|
||||
- **Donchian, R.** "High Finance in Copper." *Financial Analysts Journal*, 1960. (Origin of channel-based price analysis)
|
||||
- **Achelis, S.B.** *Technical Analysis from A to Z*. McGraw-Hill, 2000.
|
||||
@@ -0,0 +1,17 @@
|
||||
// MIDPRICE: Midpoint Price over Period
|
||||
// (Highest(High, N) + Lowest(Low, N)) / 2
|
||||
// TA-Lib compatible — center of the H/L price channel
|
||||
|
||||
//@version=6
|
||||
indicator("MIDPRICE: Midpoint Price over Period", overlay=true)
|
||||
|
||||
int p = input.int(14, "Period", minval=1)
|
||||
|
||||
midprice(int period) =>
|
||||
float hi = ta.highest(high, period)
|
||||
float lo = ta.lowest(low, period)
|
||||
(hi + lo) * 0.5
|
||||
|
||||
result = midprice(p)
|
||||
|
||||
plot(result, "MidPrice", color.new(color.purple, 0), 2)
|
||||
@@ -0,0 +1,131 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TyppriceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TyppriceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("TYPPRICE - Typical Price", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ShortName_IsTypprice()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
Assert.Equal("TYPPRICE", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
|
||||
Assert.Equal(1, TyppriceIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 115, 105, 112, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Typprice.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ComputesCorrectTypicalPrice()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// H=110, L=90, C=105 → (110+90+105)/3 = 101.666...
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(305.0 * (1.0 / 3.0), val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TyppriceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Typprice _typprice = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 1;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "TYPPRICE";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/typprice/Typprice.Quantower.cs";
|
||||
|
||||
public TyppriceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "TYPPRICE - Typical Price";
|
||||
Description = "Average of High, Low, and Close prices: (H+L+C)/3.";
|
||||
|
||||
_series = new LineSeries(name: "TYPPRICE", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_typprice = new Typprice();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _typprice.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _typprice.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// Typprice Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TyppriceTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public TyppriceTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
Assert.Equal("Typprice", indicator.Name);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Typprice(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_ReturnsHLC3()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.Update(bar);
|
||||
// (110 + 90 + 105) * (1/3) = 101.666...
|
||||
double expected = (110.0 + 90.0 + 105.0) * (1.0 / 3.0);
|
||||
Assert.Equal(expected, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_MatchesTBarHLC3()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 50, 60, 40, 55, 500);
|
||||
var result = indicator.Update(bar);
|
||||
Assert.Equal(bar.HLC3, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ReturnsIdentity()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
Assert.Equal(42.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterFirstBar_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
Assert.False(indicator.IsHot);
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RestoresPreviousState()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
indicator.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000), isNew: true);
|
||||
|
||||
var corrected = indicator.Update(new TBar(time.AddMinutes(1), 106, 120, 80, 111, 1000), isNew: false);
|
||||
double expected = (120.0 + 80.0 + 111.0) * (1.0 / 3.0);
|
||||
Assert.Equal(expected, corrected.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
|
||||
var bar = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000);
|
||||
var result1 = indicator.Update(bar, isNew: false);
|
||||
var result2 = indicator.Update(bar, isNew: false);
|
||||
var result3 = indicator.Update(bar, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, Tolerance);
|
||||
Assert.Equal(result2.Value, result3.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
double validResult = indicator.Last.Value;
|
||||
|
||||
var nanBar = new TBar(time.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, 1000);
|
||||
var result = indicator.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(validResult, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests (All Modes)
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceConsistentResults()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Typprice();
|
||||
double[] streamingResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streaming.Update(bars[i], isNew: true).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TBarSeries)
|
||||
var batchResult = Typprice.Batch(bars);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[bars.Count];
|
||||
Typprice.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, spanOutput);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllBars_MatchTBarHLC3()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var indicator = new Typprice();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i], isNew: true);
|
||||
Assert.Equal(bars[i].HLC3, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[5]; // mismatched
|
||||
double[] close = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Typprice.Batch(high, low, close, output));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] close = new double[10];
|
||||
double[] output = new double[5]; // too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Typprice.Batch(high, low, close, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOutput()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var result = Typprice.Batch(bars);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBars(10_000);
|
||||
double[] output = new double[bars.Count];
|
||||
Typprice.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, output);
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
bool fired = false;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, ind) = Typprice.Calculate(bars);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// TYPPRICE: Typical Price
|
||||
/// Calculates the average of High, Low, and Close prices.
|
||||
/// Equivalent to TBar.HLC3 but as a proper streaming indicator with bar correction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation:</b>
|
||||
/// <list type="number">
|
||||
/// <item>TypPrice = (High + Low + Close) / 3</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Stateless bar-by-bar calculation (no lookback period)</item>
|
||||
/// <item>TA-Lib compatible (TYPPRICE function)</item>
|
||||
/// <item>Always hot after first bar</item>
|
||||
/// <item>Widely used as the default price input for many indicators (e.g., CCI)</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Typprice : AbstractBase
|
||||
{
|
||||
private const double OneThird = 1.0 / 3.0;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastValidClose,
|
||||
double LastResult,
|
||||
int Count
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Typprice class.
|
||||
/// </summary>
|
||||
public Typprice()
|
||||
{
|
||||
WarmupPeriod = 1;
|
||||
Name = "Typprice";
|
||||
_s = new State(0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Typprice class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
public Typprice(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the typical price from HLC values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeTypicalPrice(double high, double low, double close)
|
||||
{
|
||||
return Math.FusedMultiplyAdd(high, OneThird, (low + close) * OneThird);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TValue input, treats the value as H, L, and C (result = value).
|
||||
/// Prefer Update(TBar) for standard OHLC data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(input.Time, input.Value, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (preferred method).
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated Typical Price value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.High, bar.Low, bar.Close, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the Typical Price values.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, source.CloseValues, vSpan);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
var values = source.Values;
|
||||
|
||||
// TValue-only: result = value (identity)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source.Times[i];
|
||||
vSpan[i] = values[i];
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue UpdateCore(long timeTicks, double high, double low, double close, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values — use last valid values
|
||||
if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; }
|
||||
if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; }
|
||||
if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; }
|
||||
|
||||
double result = ComputeTypicalPrice(high, low, close);
|
||||
|
||||
if (!double.IsFinite(result))
|
||||
{
|
||||
result = s.LastResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastResult = result;
|
||||
}
|
||||
|
||||
if (isNew) { s.Count++; }
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(timeTicks, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Typical Price for a bar series (static).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source)
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans for HLC data.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> output)
|
||||
{
|
||||
int len = high.Length;
|
||||
if (low.Length != len || close.Length != len)
|
||||
{
|
||||
throw new ArgumentException("All input spans must have the same length", nameof(low));
|
||||
}
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
output[i] = ComputeTypicalPrice(high[i], low[i], close[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using a TBarSeries (convenience overload).
|
||||
/// </summary>
|
||||
public static void Batch(TBarSeries source, Span<double> output)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as source", nameof(output));
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Batch(source.HighValues, source.LowValues, source.CloseValues, output);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Typprice Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# TYPPRICE: Typical Price
|
||||
|
||||
TYPPRICE computes the equal-weighted average of High, Low, and Close: $(H + L + C) \times \frac{1}{3}$. This three-component mean is the most widely used "representative price" in technical analysis, serving as the default input for CCI, MFI, and many other indicators. By including Close but excluding Open, Typical Price captures both the range extremes and the settlement point, giving slightly more weight to closing action than AVGPRICE does. The calculation is stateless and costs a single FMA instruction per bar.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Typical Price became the standard price transform through its adoption by Donald Lambert in his 1980 Commodity Channel Index (CCI), which explicitly requires $(H+L+C)/3$ as its input. Gene Quong and Avrum Soudack used it in the Money Flow Index (MFI) in 1989. The TA-Lib function `TA_TYPPRICE` codified it as a standalone operation. TradingView exposes it as the `hlc3` built-in source selector.
|
||||
|
||||
The choice of three components rather than four is not arbitrary. Excluding Open removes the overnight gap component, which reflects news-driven repositioning rather than intra-session supply and demand. For intraday analysis, this makes Typical Price a purer measure of within-session fair value than AVGPRICE. For daily bars on instruments with significant gaps (equities, futures at session boundaries), the distinction matters; for 24-hour markets (forex, crypto), it is negligible.
|
||||
|
||||
In QuanTAlib, `TBar.HLC3` provides the same value as a zero-cost computed property. The `Typprice` indicator class wraps this in the streaming `ITValuePublisher` interface with bar correction, NaN safety, and event chaining.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Core Formula
|
||||
|
||||
$$\text{TypPrice}_t = (H_t + L_t + C_t) \times \tfrac{1}{3}$$
|
||||
|
||||
Implemented as FMA with a precomputed reciprocal constant:
|
||||
|
||||
$$\text{TypPrice}_t = \text{FMA}\!\left(H_t,\; \tfrac{1}{3},\; (L_t + C_t) \times \tfrac{1}{3}\right)$$
|
||||
|
||||
The constant $\frac{1}{3}$ is stored as `private const double OneThird = 1.0 / 3.0`, evaluated at compile time. No runtime division occurs.
|
||||
|
||||
### 2. State Management
|
||||
|
||||
Stateless per bar. State exists only for:
|
||||
|
||||
- **Last-valid substitution**: Non-finite H, L, or C values are replaced with the last known finite value for that component.
|
||||
- **Bar correction**: `isNew=false` rolls back to previous state for same-timestamp rewrites.
|
||||
|
||||
### 3. Complexity
|
||||
|
||||
$O(1)$ per bar. One addition, one FMA. No memory allocation. Always hot after the first bar.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| (none) | No user-configurable parameters | | |
|
||||
|
||||
### Why Not Divide by 3?
|
||||
|
||||
Division by a non-power-of-two constant is 4-5x more expensive than multiplication on modern x86 CPUs (~15 cycles vs ~3 cycles). Precomputing $\frac{1}{3}$ as a `const double` and multiplying eliminates the division entirely. The compiler constant-folds `1.0 / 3.0` to the IEEE 754 double `0x3FD5555555555555` at compile time, so the hot path sees only multiply/FMA operations.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function TYPPRICE(bar):
|
||||
const OneThird ← 1.0 / 3.0 // compile-time constant
|
||||
|
||||
h, l, c ← bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
result ← FMA(h, OneThird, (l + c) × OneThird)
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|---------|---------|
|
||||
| Close > TYPPRICE | Close above session's HLC center (bullish settlement) |
|
||||
| Close < TYPPRICE | Close below session's HLC center (bearish settlement) |
|
||||
| TYPPRICE trending up | Both range and settlement are rising |
|
||||
| TYPPRICE as CCI input | Standard; CCI = (Price - SMA(Price)) / (0.015 × MeanDeviation) |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|-----------|:-----:|:-------------:|:--------:|
|
||||
| ADD (L+C) | 1 | 1 | 1 |
|
||||
| MUL ((L+C) × OneThird) | 1 | 3 | 3 |
|
||||
| FMA (H × OneThird + prev) | 1 | 4 | 4 |
|
||||
| **Total (hot)** | **3** | | **~8 cycles** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| SIMD vectorizable | Yes: element-wise arithmetic, no inter-bar dependency |
|
||||
| Optimal strategy | `Vector<double>` over H/L/C spans with broadcast OneThird |
|
||||
| Memory | $O(1)$ streaming; $O(n)$ batch output span |
|
||||
| Throughput | Near memory-bandwidth bound for large series |
|
||||
|
||||
## Resources
|
||||
|
||||
- **Lambert, D.R.** "Commodity Channel Index: Tools for Trading Cyclical Trends." *Technical Analysis of Stocks & Commodities*, 1980.
|
||||
- **Quong, G. & Soudack, A.** "Volume-Weighted RSI: Money Flow." *Technical Analysis of Stocks & Commodities*, 1989.
|
||||
- **TA-Lib** `TA_TYPPRICE` function reference.
|
||||
@@ -0,0 +1,13 @@
|
||||
// TYPPRICE: Typical Price
|
||||
// (High + Low + Close) / 3
|
||||
// TA-Lib compatible — equivalent to TBar.HLC3
|
||||
|
||||
//@version=6
|
||||
indicator("TYPPRICE: Typical Price", overlay=true)
|
||||
|
||||
typprice(float h, float l, float c) =>
|
||||
(h + l + c) / 3.0
|
||||
|
||||
result = typprice(high, low, close)
|
||||
|
||||
plot(result, "TypPrice", color.new(color.green, 0), 2)
|
||||
@@ -0,0 +1,131 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class WclpriceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void WclpriceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("WCLPRICE - Weighted Close Price", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_ShortName_IsWclprice()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
Assert.Equal("WCLPRICE", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
|
||||
Assert.Equal(1, WclpriceIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 115, 105, 112, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Wclprice.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_ComputesCorrectWeightedClose()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// H=110, L=90, C=105 → (110+90+2*105)/4 = (110+90+210)/4 = 410/4 = 102.5
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(102.5, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WclpriceIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new WclpriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class WclpriceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Wclprice _wclprice = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 1;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "WCLPRICE";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/wclprice/Wclprice.Quantower.cs";
|
||||
|
||||
public WclpriceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "WCLPRICE - Weighted Close Price";
|
||||
Description = "Weighted average emphasizing Close: (H+L+2*C)/4.";
|
||||
|
||||
_series = new LineSeries(name: "WCLPRICE", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_wclprice = new Wclprice();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _wclprice.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _wclprice.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Wclprice Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class WclpriceTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public WclpriceTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
Assert.Equal("Wclprice", indicator.Name);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Wclprice(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_ReturnsHLCC4()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
var result = indicator.Update(bar);
|
||||
// (110 + 90 + 2*105) / 4 = 410/4 = 102.5
|
||||
Assert.Equal(102.5, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_MatchesTBarHLCC4()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 50, 60, 40, 55, 500);
|
||||
var result = indicator.Update(bar);
|
||||
Assert.Equal(bar.HLCC4, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ReturnsIdentity()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
var result = indicator.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
Assert.Equal(42.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_UsesFMA()
|
||||
{
|
||||
// Verify FMA computation: close*0.5 + (high+low)*0.25
|
||||
var indicator = new Wclprice();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 200, 50, 150, 1000);
|
||||
var result = indicator.Update(bar);
|
||||
// FMA: 150*0.5 + (200+50)*0.25 = 75 + 62.5 = 137.5
|
||||
// Standard: (200+50+2*150)/4 = 550/4 = 137.5
|
||||
Assert.Equal(137.5, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterFirstBar_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
Assert.False(indicator.IsHot);
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RestoresPreviousState()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
indicator.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000), isNew: true);
|
||||
|
||||
var corrected = indicator.Update(new TBar(time.AddMinutes(1), 106, 120, 80, 111, 1000), isNew: false);
|
||||
// FMA: 111*0.5 + (120+80)*0.25 = 55.5 + 50 = 105.5
|
||||
double expected = Math.FusedMultiplyAdd(111.0, 0.5, (120.0 + 80.0) * 0.25);
|
||||
Assert.Equal(expected, corrected.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
|
||||
var bar = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000);
|
||||
var result1 = indicator.Update(bar, isNew: false);
|
||||
var result2 = indicator.Update(bar, isNew: false);
|
||||
var result3 = indicator.Update(bar, isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, Tolerance);
|
||||
Assert.Equal(result2.Value, result3.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(indicator.IsHot);
|
||||
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Robustness Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
||||
double validResult = indicator.Last.Value;
|
||||
|
||||
var nanBar = new TBar(time.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, 1000);
|
||||
var result = indicator.Update(nanBar, isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(validResult, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Consistency Tests (All Modes)
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceConsistentResults()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Wclprice();
|
||||
double[] streamingResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streaming.Update(bars[i], isNew: true).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TBarSeries)
|
||||
var batchResult = Wclprice.Batch(bars);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[bars.Count];
|
||||
Wclprice.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, spanOutput);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
|
||||
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllBars_MatchTBarHLCC4()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var indicator = new Wclprice();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i], isNew: true);
|
||||
Assert.Equal(bars[i].HLCC4, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[5]; // mismatched
|
||||
double[] close = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Wclprice.Batch(high, low, close, output));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[10];
|
||||
double[] close = new double[10];
|
||||
double[] output = new double[5]; // too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Wclprice.Batch(high, low, close, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOutput()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var result = Wclprice.Batch(bars);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBars(10_000);
|
||||
double[] output = new double[bars.Count];
|
||||
Wclprice.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, output);
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
bool fired = false;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, ind) = Wclprice.Calculate(bars);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// WCLPRICE: Weighted Close Price
|
||||
/// Calculates the weighted average of High, Low, and Close, giving Close double weight.
|
||||
/// Equivalent to TBar.HLCC4 but as a proper streaming indicator with bar correction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation:</b>
|
||||
/// <list type="number">
|
||||
/// <item>WclPrice = (High + Low + 2 × Close) / 4</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Stateless bar-by-bar calculation (no lookback period)</item>
|
||||
/// <item>TA-Lib compatible (WCLPRICE function)</item>
|
||||
/// <item>Always hot after first bar</item>
|
||||
/// <item>Close-weighted — emphasizes settlement price over intra-bar extremes</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Wclprice : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastValidClose,
|
||||
double LastResult,
|
||||
int Count
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Wclprice class.
|
||||
/// </summary>
|
||||
public Wclprice()
|
||||
{
|
||||
WarmupPeriod = 1;
|
||||
Name = "Wclprice";
|
||||
_s = new State(0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Wclprice class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
public Wclprice(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the weighted close price from HLC values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeWeightedClose(double high, double low, double close)
|
||||
{
|
||||
return Math.FusedMultiplyAdd(close, 0.5, (high + low) * 0.25);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TValue input, treats the value as H, L, and C (result = value).
|
||||
/// Prefer Update(TBar) for standard OHLC data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(input.Time, input.Value, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (preferred method).
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated Weighted Close Price value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.High, bar.Low, bar.Close, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the Weighted Close Price values.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, source.CloseValues, vSpan);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
var values = source.Values;
|
||||
|
||||
// TValue-only: result = value (identity)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source.Times[i];
|
||||
vSpan[i] = values[i];
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue UpdateCore(long timeTicks, double high, double low, double close, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values — use last valid values
|
||||
if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; }
|
||||
if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; }
|
||||
if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; }
|
||||
|
||||
double result = ComputeWeightedClose(high, low, close);
|
||||
|
||||
if (!double.IsFinite(result))
|
||||
{
|
||||
result = s.LastResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastResult = result;
|
||||
}
|
||||
|
||||
if (isNew) { s.Count++; }
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(timeTicks, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Weighted Close Price for a bar series (static).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source)
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans for HLC data.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> output)
|
||||
{
|
||||
int len = high.Length;
|
||||
if (low.Length != len || close.Length != len)
|
||||
{
|
||||
throw new ArgumentException("All input spans must have the same length", nameof(low));
|
||||
}
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
output[i] = ComputeWeightedClose(high[i], low[i], close[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using a TBarSeries (convenience overload).
|
||||
/// </summary>
|
||||
public static void Batch(TBarSeries source, Span<double> output)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as source", nameof(output));
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Batch(source.HighValues, source.LowValues, source.CloseValues, output);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Wclprice Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Wclprice();
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# WCLPRICE: Weighted Close Price
|
||||
|
||||
WCLPRICE computes a Close-biased average of High, Low, and Close by double-weighting the closing price: $(H + L + 2C) \times 0.25$. This gives Close 50% of the total weight versus 25% each for High and Low, reflecting the widely held belief that the closing price is the most important price of the bar because it represents the final consensus of buyers and sellers. The calculation is stateless, costs a single FMA instruction per bar, and is TA-Lib compatible (`TA_WCLPRICE`).
|
||||
|
||||
## Historical Context
|
||||
|
||||
Weighted Close Price appears in technical analysis literature from the 1970s onward, typically credited to the general tradition of market technicians rather than a single inventor. The rationale is straightforward: while High and Low show where price was rejected, Close shows where participants were willing to hold positions overnight (or into the next period). Double-weighting Close amplifies this "settlement consensus" signal.
|
||||
|
||||
The formula $(H + L + 2C) / 4$ is algebraically equivalent to $(H + L) / 4 + C / 2$, which reveals its structure: half the weight on Close, and the other half split equally between the range extremes. This makes WCLPRICE a compromise between raw Close and the range-neutral MEDPRICE. When Close is at the midpoint of the range, WCLPRICE equals MEDPRICE; when Close diverges from the midpoint, WCLPRICE follows Close more aggressively than either TYPPRICE or AVGPRICE.
|
||||
|
||||
In QuanTAlib, `TBar.HLCC4` provides the same value as a zero-cost computed property. The `Wclprice` indicator class wraps this in the streaming `ITValuePublisher` interface with bar correction, NaN safety, and event chaining.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Core Formula
|
||||
|
||||
$$\text{WclPrice}_t = (H_t + L_t + 2C_t) \times 0.25$$
|
||||
|
||||
Implemented as FMA to avoid division:
|
||||
|
||||
$$\text{WclPrice}_t = \text{FMA}(C_t,\; 0.5,\; (H_t + L_t) \times 0.25)$$
|
||||
|
||||
This form is optimal: the FMA computes $C \times 0.5 + (H+L) \times 0.25$ in a single fused operation, avoiding the intermediate rounding that separate multiply-add would produce.
|
||||
|
||||
### 2. State Management
|
||||
|
||||
Stateless per bar. State exists only for:
|
||||
|
||||
- **Last-valid substitution**: Non-finite H, L, or C values are replaced with the last known finite value for that component.
|
||||
- **Bar correction**: `isNew=false` rolls back to previous state for same-timestamp rewrites.
|
||||
|
||||
### 3. Complexity
|
||||
|
||||
$O(1)$ per bar. One addition, one FMA. No memory allocation. Always hot after the first bar.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| (none) | No user-configurable parameters | | |
|
||||
|
||||
### Weight Distribution
|
||||
|
||||
| Transform | O weight | H weight | L weight | C weight |
|
||||
|-----------|:--------:|:--------:|:--------:|:--------:|
|
||||
| AVGPRICE | 25% | 25% | 25% | 25% |
|
||||
| MEDPRICE | 0% | 50% | 50% | 0% |
|
||||
| TYPPRICE | 0% | 33.3% | 33.3% | 33.3% |
|
||||
| **WCLPRICE** | **0%** | **25%** | **25%** | **50%** |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function WCLPRICE(bar):
|
||||
h, l, c ← bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
result ← FMA(c, 0.5, (h + l) × 0.25)
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Context | Meaning |
|
||||
|---------|---------|
|
||||
| WCLPRICE > TYPPRICE | Close above the HLC midpoint (strong close) |
|
||||
| WCLPRICE < TYPPRICE | Close below the HLC midpoint (weak close) |
|
||||
| WCLPRICE $\approx$ MEDPRICE | Close at range midpoint; balanced bar |
|
||||
| WCLPRICE diverging from AVGPRICE | Open and Close on opposite sides of the range |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
|-----------|:-----:|:-------------:|:--------:|
|
||||
| ADD (H+L) | 1 | 1 | 1 |
|
||||
| MUL ((H+L) × 0.25) | 1 | 3 | 3 |
|
||||
| FMA (C × 0.5 + prev) | 1 | 4 | 4 |
|
||||
| **Total (hot)** | **3** | | **~8 cycles** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| SIMD vectorizable | Yes: element-wise FMA, no inter-bar dependency |
|
||||
| Optimal strategy | `Fma.MultiplyAdd` over H/L/C vectors on AVX2+ |
|
||||
| Memory | $O(1)$ streaming; $O(n)$ batch output span |
|
||||
| Throughput | Near memory-bandwidth bound for large series |
|
||||
|
||||
## Resources
|
||||
|
||||
- **TA-Lib** `TA_WCLPRICE` function reference.
|
||||
- **Achelis, S.B.** *Technical Analysis from A to Z*. McGraw-Hill, 2000. (Weighted Close definition)
|
||||
@@ -0,0 +1,13 @@
|
||||
// WCLPRICE: Weighted Close Price
|
||||
// (High + Low + 2 * Close) / 4
|
||||
// TA-Lib compatible — equivalent to TBar.HLCC4
|
||||
|
||||
//@version=6
|
||||
indicator("WCLPRICE: Weighted Close Price", overlay=true)
|
||||
|
||||
wclprice(float h, float l, float c) =>
|
||||
(h + l + 2.0 * c) * 0.25
|
||||
|
||||
result = wclprice(high, low, close)
|
||||
|
||||
plot(result, "WclPrice", color.new(color.red, 0), 2)
|
||||
+16
-18
@@ -6,21 +6,19 @@ Cycle analysis identifies repeating patterns in price data. John Ehlers pioneere
|
||||
|
||||
## Indicators
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [CCOR](ccor/Ccor.md) | Ehlers Correlation Cycle | Ehlers. Dual Pearson correlation (cos + -sin). Phasor angle + market state. |
|
||||
| [CCYC](ccyc/Ccyc.md) | Ehlers Cyber Cycle | Ehlers. 4-tap FIR + 2-pole high-pass IIR. Isolates dominant cycle component. |
|
||||
| [CG](cg/Cg.md) | Ehlers Center of Gravity | Ehlers. Weighted sum position. Minimal lag cycle indicator. |
|
||||
| [DSP](dsp/Dsp.md) | Ehlers Detrended Synthetic Price | Removes trend to reveal underlying cycles. |
|
||||
| [EACP](eacp/Eacp.md) | Ehlers Autocorrelation Periodogram | Ehlers. Spectral analysis via autocorrelation. Detects dominant period. |
|
||||
| [EBSW](ebsw/Ebsw.md) | Ehlers Even Better Sinewave | Ehlers. Improved sinewave extraction. Reduces false signals. |
|
||||
| [HOMOD](homod/Homod.md) | Ehlers Homodyne Discriminator | Dominant cycle detection via homodyne technique. |
|
||||
| [HT_DCPERIOD](ht_dcperiod/Ht_dcperiod.md) | Ehlers Hilbert Transform Dominant Cycle Period | Ehlers Hilbert Transform. Measures current cycle length. |
|
||||
| [HT_DCPHASE](ht_dcphase/Ht_dcphase.md) | Ehlers Hilbert Transform Dominant Cycle Phase | Ehlers Hilbert Transform. Measures current position in cycle. |
|
||||
| [HT_PHASOR](ht_phasor/HtPhasor.md) | Ehlers Hilbert Transform Phasor Components | Ehlers. In-phase and quadrature components. |
|
||||
| [HT_SINE](ht_sine/HtSine.md) | Ehlers Hilbert Transform SineWave | Ehlers Hilbert Transform. Sine and lead sine for cycle timing. |
|
||||
| [LUNAR](lunar/Lunar.md) | Lunar Phase | 29.5-day lunar cycle. Studied for market correlations. |
|
||||
| [SINE](sine/Sine.md) | Ehlers Sine Wave | Ehlers. Basic sinewave indicator for cycle mode. |
|
||||
| [SOLAR](solar/Solar.md) | Solar Activity Cycle | ~11-year sunspot cycle. Long-term research indicator. |
|
||||
| [SSFDSP](ssfdsp/Ssfdsp.md) | Ehlers SSF Detrended Synthetic Price | Super Smoother Filter based DSP. Cleaner cycle extraction. |
|
||||
| [STC](stc/Stc.md) | Schaff Trend Cycle | MACD + double Stochastic smoothing. Fast cycle oscillator (0-100). |
|
||||
| Indicator | Full Name | Description |
|
||||
| :--------------------------------------- | :----------------------------------------------------- | :--------------------------------------------------------------------------- |
|
||||
| [CCOR](ccor/Ccor.md) | Ehlers Correlation Cycle | Ehlers. Dual Pearson correlation (cos + -sin). Phasor angle + market state. |
|
||||
| [CCYC](ccyc/Ccyc.md) | Ehlers Cyber Cycle | Ehlers. 4-tap FIR + 2-pole high-pass IIR. Isolates dominant cycle component. |
|
||||
| [CG](cg/Cg.md) | Ehlers Center of Gravity | Ehlers. Weighted sum position. Minimal lag cycle indicator. |
|
||||
| [DSP](dsp/Dsp.md) | Ehlers Detrended Synthetic Price | Removes trend to reveal underlying cycles. |
|
||||
| [EACP](eacp/Eacp.md) | Ehlers Autocorrelation Periodogram | Ehlers. Spectral analysis via autocorrelation. Detects dominant period. |
|
||||
| [EBSW](ebsw/Ebsw.md) | Ehlers Even Better Sinewave | Ehlers. Improved sinewave extraction. Reduces false signals. |
|
||||
| [HOMOD](homod/Homod.md) | Ehlers Homodyne Discriminator | Dominant cycle detection via homodyne technique. |
|
||||
| [HT_DCPERIOD](ht_dcperiod/Htdcperiod.md) | Ehlers Hilbert Transform Dominant Cycle Period | Ehlers Hilbert Transform. Measures current cycle length. |
|
||||
| [HT_DCPHASE](ht_dcphase/Htdcphase.md) | Ehlers Hilbert Transform Dominant Cycle Phase | Ehlers Hilbert Transform. Measures current position in cycle. |
|
||||
| [HT_PHASOR](ht_phasor/HtPhasor.md) | Ehlers Hilbert Transform Phasor Components | Ehlers. In-phase and quadrature components. |
|
||||
| [HT_SINE](ht_sine/HtSine.md) | Ehlers Hilbert Transform SineWave (also known as SINE) | Ehlers Hilbert Transform. Sine and lead sine for cycle timing. |
|
||||
| [LUNAR](lunar/Lunar.md) | Lunar Phase | 29.5-day lunar cycle. Studied for market correlations. |
|
||||
| [SOLAR](solar/Solar.md) | Solar Activity Cycle | ~11-year sunspot cycle. Long-term research indicator. |
|
||||
| [SSFDSP](ssfdsp/Ssfdsp.md) | Ehlers SSF Detrended Synthetic Price | Super Smoother Filter based DSP. Cleaner cycle extraction. |
|
||||
|
||||
@@ -4,9 +4,9 @@ using System.Runtime.InteropServices;
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// HT_SINE: Hilbert Transform - SineWave indicator that uses the Hilbert Transform
|
||||
/// to compute the sine of the dominant cycle phase. Returns both Sine and LeadSine
|
||||
/// (45° phase lead) for cycle timing.
|
||||
/// HT_SINE: Hilbert Transform - SineWave (also known as SINE) indicator that uses
|
||||
/// the Hilbert Transform to compute the sine of the dominant cycle phase. Returns
|
||||
/// both Sine and LeadSine (45° phase lead) for cycle timing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Hilbert Transform SineWave indicator identifies the dominant market cycle
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# HT_SINE: Ehlers Hilbert Transform SineWave
|
||||
# HT_SINE: Ehlers Hilbert Transform SineWave (also known as SINE)
|
||||
|
||||
HT_SINE extracts the dominant market cycle phase and outputs both Sine and LeadSine (45° phase advance) for cycle timing. The crossover of these two waves identifies turning points in ranging markets up to one-eighth of a cycle early. Compatible with TA-Lib's `HT_SINE` function, the indicator builds on the full Hilbert Transform cascade (phasor extraction, homodyne period estimation, DFT phase accumulation) to produce dual bounded $[-1, +1]$ oscillators that track cycle position rather than price amplitude.
|
||||
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class SineIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SineIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SineIndicator();
|
||||
|
||||
Assert.Equal(40, indicator.HpPeriod);
|
||||
Assert.Equal(10, indicator.SsfPeriod);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SINE - Ehlers Sine Wave", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new SineIndicator();
|
||||
|
||||
Assert.Equal(0, SineIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
|
||||
Assert.True(indicator.ShortName.Contains("SINE", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("20", StringComparison.Ordinal));
|
||||
Assert.True(indicator.ShortName.Contains("5", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_Initialize_CreatesInternalSine()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 40, SsfPeriod = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (SINE + Zero + Upper + Lower lines)
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 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 SineIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Should not throw an exception
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 105, 103, 107, 110, 108, 112, 115, 113 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_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 SineIndicator { HpPeriod = 20, SsfPeriod = 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 SineIndicator_HpPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 40 };
|
||||
|
||||
Assert.Equal(40, indicator.HpPeriod);
|
||||
|
||||
indicator.HpPeriod = 20;
|
||||
Assert.Equal(20, indicator.HpPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_SsfPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new SineIndicator { SsfPeriod = 10 };
|
||||
|
||||
Assert.Equal(10, indicator.SsfPeriod);
|
||||
|
||||
indicator.SsfPeriod = 5;
|
||||
Assert.Equal(5, indicator.SsfPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_Source_CanBeChanged()
|
||||
{
|
||||
var indicator = new SineIndicator { Source = SourceType.Close };
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
|
||||
indicator.Source = SourceType.Open;
|
||||
Assert.Equal(SourceType.Open, indicator.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new SineIndicator { ShowColdValues = true };
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ShortName_UpdatesWhenParametersChange()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 40, SsfPeriod = 10 };
|
||||
string initialName = indicator.ShortName;
|
||||
|
||||
Assert.True(initialName.Contains("40", StringComparison.Ordinal));
|
||||
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
|
||||
|
||||
indicator.HpPeriod = 20;
|
||||
indicator.SsfPeriod = 5;
|
||||
string updatedName = indicator.ShortName;
|
||||
|
||||
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
|
||||
Assert.True(updatedName.Contains("5", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ProcessUpdate_IgnoresNonBarUpdates()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Process other update reasons - should not throw
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
// Assert that the indicator still exists (method completed without exception)
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_LineSeries_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 40, SsfPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var lineSeries = indicator.LinesSeries[0];
|
||||
|
||||
Assert.Equal("SINE", lineSeries.Name);
|
||||
Assert.Equal(2, lineSeries.Width);
|
||||
Assert.Equal(LineStyle.Solid, lineSeries.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ZeroLine_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 40, SsfPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var zeroLine = indicator.LinesSeries[1];
|
||||
|
||||
Assert.Equal("Zero", zeroLine.Name);
|
||||
Assert.Equal(1, zeroLine.Width);
|
||||
Assert.Equal(LineStyle.Dash, zeroLine.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_BoundaryLines_HasCorrectProperties()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 40, SsfPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var upperLine = indicator.LinesSeries[2];
|
||||
var lowerLine = indicator.LinesSeries[3];
|
||||
|
||||
Assert.Equal("+1", upperLine.Name);
|
||||
Assert.Equal("-1", lowerLine.Name);
|
||||
Assert.Equal(LineStyle.Dot, upperLine.Style);
|
||||
Assert.Equal(LineStyle.Dot, lowerLine.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_DifferentParameters_Work()
|
||||
{
|
||||
var paramSets = new[] { (10, 3), (20, 5), (40, 10), (80, 20) };
|
||||
|
||||
foreach (var (hpPeriod, ssfPeriod) in paramSets)
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = hpPeriod, SsfPeriod = ssfPeriod };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add enough bars to fill the buffer
|
||||
for (int i = 0; i < hpPeriod + 10; i++)
|
||||
{
|
||||
double close = 100 + (i % 10);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Last value should be finite
|
||||
double sineValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(sineValue), $"HP {hpPeriod}, SSF {ssfPeriod} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ConstantPrice_ProducesBoundedOutput()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add constant price bars
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100, 100, 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Output is normalized to [-1, +1]
|
||||
double sineValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(sineValue >= -1.0 && sineValue <= 1.0,
|
||||
$"SINE value {sineValue} should be in [-1, +1]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_OutputBounded_BetweenNegativeOneAndOne()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add varying price bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + 20 * Math.Sin(i * 0.2);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double sineValue = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(sineValue >= -1.0 && sineValue <= 1.0,
|
||||
$"SINE value {sineValue} should be in [-1, +1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_OscillatesAroundZero_ForSineWave()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 40, SsfPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var values = new List<double>();
|
||||
|
||||
// Generate sine wave price pattern
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(i * 0.1);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
values.Add(indicator.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
|
||||
// Should have both positive and negative values
|
||||
int positiveCount = values.Count(v => v > 0);
|
||||
int negativeCount = values.Count(v => v < 0);
|
||||
|
||||
Assert.True(positiveCount > 0, "Should have positive SINE values");
|
||||
Assert.True(negativeCount > 0, "Should have negative SINE values");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineIndicator_ZeroCrossings_IndicateCyclePhase()
|
||||
{
|
||||
var indicator = new SineIndicator { HpPeriod = 20, SsfPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var values = new List<double>();
|
||||
|
||||
// Generate sine wave price pattern
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(i * 0.15);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
values.Add(indicator.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
|
||||
// Count zero crossings
|
||||
int crossings = 0;
|
||||
for (int i = 1; i < values.Count; i++)
|
||||
{
|
||||
if (values[i - 1] * values[i] < 0)
|
||||
{
|
||||
crossings++;
|
||||
}
|
||||
}
|
||||
|
||||
// Should have multiple zero crossings for oscillating price
|
||||
Assert.True(crossings >= 3, $"Should have multiple zero crossings, got {crossings}");
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class SineIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("HP Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int HpPeriod { get; set; } = 40;
|
||||
|
||||
[InputParameter("SSF Period", sortIndex: 2, 1, 500, 1, 0)]
|
||||
public int SsfPeriod { get; set; } = 10;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Sine _sine = null!;
|
||||
private readonly LineSeries _series;
|
||||
private readonly LineSeries _zeroLine;
|
||||
private readonly LineSeries _upperLine;
|
||||
private readonly LineSeries _lowerLine;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"SINE ({HpPeriod},{SsfPeriod})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/cycles/sine/Sine.Quantower.cs";
|
||||
|
||||
public SineIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "SINE - Ehlers Sine Wave";
|
||||
Description = "Ehlers' Sine Wave indicator extracts the dominant cycle from price data using High-Pass filter, Super-Smoother, and Hilbert Transform";
|
||||
|
||||
_series = new LineSeries(name: "SINE", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
|
||||
_zeroLine = new LineSeries(name: "Zero", color: Color.Gray, width: 1, style: LineStyle.Dash);
|
||||
_upperLine = new LineSeries(name: "+1", color: Color.DarkGray, width: 1, style: LineStyle.Dot);
|
||||
_lowerLine = new LineSeries(name: "-1", color: Color.DarkGray, width: 1, style: LineStyle.Dot);
|
||||
AddLineSeries(_series);
|
||||
AddLineSeries(_zeroLine);
|
||||
AddLineSeries(_upperLine);
|
||||
AddLineSeries(_lowerLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_sine = new Sine(HpPeriod, SsfPeriod);
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _priceSelector(item);
|
||||
var time = this.HistoricalData.Time();
|
||||
|
||||
var input = new TValue(time, value);
|
||||
TValue result = _sine.Update(input, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _sine.IsHot, ShowColdValues);
|
||||
_zeroLine.SetValue(0.0);
|
||||
_upperLine.SetValue(1.0);
|
||||
_lowerLine.SetValue(-1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
using Xunit;
|
||||
|
||||
public class SineTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
private readonly GBM _gbm;
|
||||
|
||||
public SineTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromDays(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_ConstructorDefaults()
|
||||
{
|
||||
var sine = new Sine();
|
||||
Assert.Equal("SINE", sine.Name);
|
||||
Assert.Equal(40, sine.HpPeriod);
|
||||
Assert.Equal(10, sine.SsfPeriod);
|
||||
Assert.Equal(48, sine.WarmupPeriod); // max(40, 10) + 8
|
||||
Assert.False(sine.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_ConstructorCustomParameters()
|
||||
{
|
||||
var sine = new Sine(hpPeriod: 20, ssfPeriod: 5);
|
||||
Assert.Equal(20, sine.HpPeriod);
|
||||
Assert.Equal(5, sine.SsfPeriod);
|
||||
Assert.Equal(28, sine.WarmupPeriod); // max(20, 5) + 8
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_ConstructorValidation_ThrowsOnInvalidHpPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(hpPeriod: 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(hpPeriod: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_ConstructorValidation_ThrowsOnInvalidSsfPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(ssfPeriod: 0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Sine(ssfPeriod: -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_Update_ReturnsValidRange()
|
||||
{
|
||||
var sine = new Sine();
|
||||
var bars = GenerateBars(200);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = sine.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(result.Value >= -1.0 && result.Value <= 1.0,
|
||||
$"Sine value {result.Value} out of range [-1, 1]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_IsHot_AfterWarmup()
|
||||
{
|
||||
var sine = new Sine(hpPeriod: 20, ssfPeriod: 5);
|
||||
var bars = GenerateBars(50);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
sine.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
|
||||
if (i + 1 < sine.WarmupPeriod)
|
||||
{
|
||||
Assert.False(sine.IsHot, $"Should not be hot at index {i}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(sine.IsHot, $"Should be hot at index {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_IsNew_AdvancesState()
|
||||
{
|
||||
var sine = new Sine();
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
|
||||
var result1 = sine.Update(input, isNew: true);
|
||||
var result2 = sine.Update(new TValue(DateTime.UtcNow.AddDays(1), 101.0), isNew: true);
|
||||
|
||||
// With isNew=true, each call should advance state
|
||||
// Values might be the same early on, but state should advance
|
||||
Assert.NotEqual(result1.Time, result2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_IsNew_False_UpdatesCurrentBar()
|
||||
{
|
||||
var sine = new Sine();
|
||||
var bars = GenerateBars(60);
|
||||
|
||||
// Process first 50 bars normally
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sine.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
|
||||
}
|
||||
|
||||
// Get result at bar 50
|
||||
var newBarResult = sine.Update(new TValue(bars[50].Time, bars[50].Close), isNew: true);
|
||||
|
||||
// Reset and replay to bar 49, then update bar 50 with different value
|
||||
var sine2 = new Sine();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sine2.Update(new TValue(bars[i].Time, bars[i].Close), isNew: true);
|
||||
}
|
||||
|
||||
// First update bar 50
|
||||
sine2.Update(new TValue(bars[50].Time, bars[50].Close), isNew: true);
|
||||
|
||||
// Update same bar with different value (bar correction)
|
||||
var correctedResult = sine2.Update(new TValue(bars[50].Time, bars[50].Close * 1.1), isNew: false);
|
||||
|
||||
// Results should differ due to different input
|
||||
Assert.NotEqual(newBarResult.Value, correctedResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_Reset_ClearsState()
|
||||
{
|
||||
var sine = new Sine();
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
sine.Update(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
Assert.True(sine.IsHot);
|
||||
|
||||
sine.Reset();
|
||||
|
||||
Assert.False(sine.IsHot);
|
||||
Assert.Equal(0, sine.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_TSeries_Update()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
var series = new TSeries(100);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
series.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var sine = new Sine();
|
||||
var result = sine.Update(series);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
|
||||
// Verify all values are in range
|
||||
foreach (var val in result)
|
||||
{
|
||||
Assert.True(val.Value >= -1.0 && val.Value <= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_StaticCalculate_TSeries()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
var series = new TSeries(100);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
series.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var result = Sine.Batch(series);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_StaticCalculate_WithCustomParams()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
var series = new TSeries(100);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
series.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var result = Sine.Batch(series, hpPeriod: 20, ssfPeriod: 5);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_Chaining_Works()
|
||||
{
|
||||
var source = new Sma(10);
|
||||
var sine = new Sine(source);
|
||||
|
||||
bool eventFired = false;
|
||||
sine.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
var input = new TValue(DateTime.UtcNow, 100.0);
|
||||
source.Update(input);
|
||||
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var sine = new Sine();
|
||||
var empty = new TSeries();
|
||||
var result = sine.Update(empty);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_Streaming_MatchesBatch()
|
||||
{
|
||||
var bars = GenerateBars(200);
|
||||
var series = new TSeries(200);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
series.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Streaming calculation
|
||||
var streamingSine = new Sine();
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = streamingSine.Update(new TValue(bar.Time, bar.Close));
|
||||
streamingResults.Add(result.Value);
|
||||
}
|
||||
|
||||
// Batch calculation
|
||||
var batchResult = Sine.Batch(series);
|
||||
|
||||
// Compare last 100 values (after warmup)
|
||||
for (int i = 100; i < 200; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResult[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_NaN_HandledGracefully()
|
||||
{
|
||||
var sine = new Sine();
|
||||
var bars = GenerateBars(60);
|
||||
|
||||
// Process some bars
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
sine.Update(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
|
||||
// Feed NaN - should substitute with last valid value
|
||||
var nanResult = sine.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Result should NOT be NaN (last-valid substitution) and in valid range
|
||||
Assert.False(double.IsNaN(nanResult.Value), "NaN should not propagate");
|
||||
Assert.True(nanResult.Value >= -1.0 && nanResult.Value <= 1.0,
|
||||
$"Value {nanResult.Value} should be in [-1, 1]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_Prime_InitializesState()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
var primeData = bars.Select(b => b.Close).ToArray();
|
||||
|
||||
var sine = new Sine();
|
||||
sine.Prime(primeData);
|
||||
|
||||
Assert.True(sine.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_WithCyclingData_ProducesOscillation()
|
||||
{
|
||||
var sine = new Sine(hpPeriod: 20, ssfPeriod: 5);
|
||||
|
||||
// Generate sinusoidal price data
|
||||
var results = new List<double>();
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
// Create a price with embedded 30-bar cycle
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 30.0);
|
||||
var result = sine.Update(new TValue(baseTime.AddDays(i), price));
|
||||
results.Add(result.Value);
|
||||
}
|
||||
|
||||
// After warmup, check that we have both positive and negative values
|
||||
var afterWarmup = results.Skip(30).ToList();
|
||||
Assert.True(afterWarmup.Any(v => v > 0.5), "Should have positive cycle values");
|
||||
Assert.True(afterWarmup.Any(v => v < -0.5), "Should have negative cycle values");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_ConstantInput_ProducesValidOutput()
|
||||
{
|
||||
var sine = new Sine();
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Feed constant values
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
var result = sine.Update(new TValue(baseTime.AddDays(i), 100.0));
|
||||
// Output should always be in valid range regardless of input
|
||||
Assert.True(result.Value >= -1.0 && result.Value <= 1.0,
|
||||
$"Value {result.Value} out of range at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sine_TrendingInput_ProducesValidOutput()
|
||||
{
|
||||
var sine = new Sine(hpPeriod: 40, ssfPeriod: 10);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Feed trending data (very low frequency)
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double price = 100.0 + i * 0.1; // Slow uptrend
|
||||
var result = sine.Update(new TValue(baseTime.AddDays(i), price));
|
||||
// Output should always be in valid range regardless of input
|
||||
Assert.True(result.Value >= -1.0 && result.Value <= 1.0,
|
||||
$"Value {result.Value} out of range at index {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Ehlers Sine Wave indicator.
|
||||
/// Sine is Ehlers' proprietary cycle indicator not commonly implemented in trading libraries
|
||||
/// (TA-Lib, Skender, Tulip), so validation is done against mathematical properties
|
||||
/// and known theoretical results based on the original algorithm.
|
||||
/// </summary>
|
||||
public class SineValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validation_OutputRange_NegativeOneToOne()
|
||||
{
|
||||
// Sine wave output should be in [-1, 1]
|
||||
var sine = new Sine();
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
sine.Update(new TValue(bar.Time, bar.Close));
|
||||
if (sine.IsHot)
|
||||
{
|
||||
double val = sine.Last.Value;
|
||||
Assert.True(val >= -1.0 && val <= 1.0,
|
||||
$"Sine value {val} is outside expected range [-1, 1]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_ConstantSeries_Bounded()
|
||||
{
|
||||
// For a constant price series, there is no real cycle — output should remain bounded
|
||||
var sine = new Sine();
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
sine.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
|
||||
}
|
||||
|
||||
// Constant series may not produce exactly zero due to filter initialization artifacts
|
||||
// but output should remain within the bounded range [-1, 1]
|
||||
Assert.True(sine.Last.Value >= -1.0 && sine.Last.Value <= 1.0,
|
||||
$"Constant series should produce bounded sine output, got {sine.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_SinusoidInput_DetectsCycle()
|
||||
{
|
||||
// Feed a known sinusoidal signal and verify output oscillates
|
||||
var sine = new Sine(hpPeriod: 40, ssfPeriod: 10);
|
||||
|
||||
var values = new List<double>();
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double price = 100.0 + 5.0 * Math.Sin(2.0 * Math.PI * i / 20.0);
|
||||
sine.Update(new TValue(DateTime.UtcNow.AddSeconds(i), price));
|
||||
if (sine.IsHot)
|
||||
{
|
||||
values.Add(sine.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// The output should oscillate: check that it crosses zero at least once
|
||||
bool hasCrossedZero = false;
|
||||
for (int i = 1; i < values.Count; i++)
|
||||
{
|
||||
if ((values[i - 1] >= 0 && values[i] < 0) || (values[i - 1] < 0 && values[i] >= 0))
|
||||
{
|
||||
hasCrossedZero = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(hasCrossedZero, "Sine should oscillate (cross zero) on sinusoidal input");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_FiniteOutputs()
|
||||
{
|
||||
var sine = new Sine();
|
||||
|
||||
var gbm = new GBM(seed: 99);
|
||||
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
sine.Update(new TValue(bar.Time, bar.Close));
|
||||
Assert.True(double.IsFinite(sine.Last.Value),
|
||||
$"Sine produced non-finite value: {sine.Last.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validation_DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var sine1 = new Sine(hpPeriod: 20, ssfPeriod: 5);
|
||||
var sine2 = new Sine(hpPeriod: 80, ssfPeriod: 20);
|
||||
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
bool foundDifference = false;
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
sine1.Update(new TValue(bar.Time, bar.Close));
|
||||
sine2.Update(new TValue(bar.Time, bar.Close));
|
||||
if (sine1.IsHot && sine2.IsHot &&
|
||||
Math.Abs(sine1.Last.Value - sine2.Last.Value) > 1e-6)
|
||||
{
|
||||
foundDifference = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(foundDifference, "Different HP/SSF periods should produce different results");
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
// Ehlers Sine Wave (SINE) - Cycle extraction using Hilbert Transform
|
||||
// Uses High-Pass filter + Super-Smoother + Hilbert Transform to extract sine wave
|
||||
// Based on John Ehlers' "Cybernetic Analysis for Stocks and Futures"
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Ehlers Sine Wave indicator extracts the dominant cycle from price data.
|
||||
/// Uses a High-Pass filter for detrending, Super-Smoother for noise reduction,
|
||||
/// and Hilbert Transform FIR for quadrature component extraction.
|
||||
/// Output ranges from -1.0 to +1.0 representing the normalized sine wave.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Sine : AbstractBase
|
||||
{
|
||||
private readonly int _hpPeriod;
|
||||
private readonly int _ssfPeriod;
|
||||
private readonly RingBuffer _srcBuffer;
|
||||
private readonly RingBuffer _hpBuffer;
|
||||
private readonly RingBuffer _filtBuffer;
|
||||
|
||||
// High-Pass filter coefficient
|
||||
private readonly double _alphaHP;
|
||||
|
||||
// Super-Smoother coefficients
|
||||
private readonly double _c1, _c2, _c3;
|
||||
|
||||
// Hilbert FIR coefficients
|
||||
private const double H1 = 0.0962;
|
||||
private const double H2 = 0.5769;
|
||||
|
||||
// State tracking
|
||||
private int _count;
|
||||
|
||||
public int HpPeriod => _hpPeriod;
|
||||
public int SsfPeriod => _ssfPeriod;
|
||||
public override bool IsHot => _count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Ehlers Sine Wave indicator.
|
||||
/// </summary>
|
||||
/// <param name="hpPeriod">High-Pass filter period for detrending (default: 40)</param>
|
||||
/// <param name="ssfPeriod">Super-Smoother filter period for smoothing (default: 10)</param>
|
||||
public Sine(int hpPeriod = 40, int ssfPeriod = 10)
|
||||
{
|
||||
if (hpPeriod < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(hpPeriod), "High-Pass period must be >= 1");
|
||||
}
|
||||
if (ssfPeriod < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(ssfPeriod), "Super-Smoother period must be >= 1");
|
||||
}
|
||||
|
||||
_hpPeriod = hpPeriod;
|
||||
_ssfPeriod = ssfPeriod;
|
||||
Name = "SINE";
|
||||
WarmupPeriod = Math.Max(hpPeriod, ssfPeriod) + 8; // +8 for Hilbert lookback
|
||||
|
||||
// High-Pass filter coefficient
|
||||
double angHP = 2.0 * Math.PI / hpPeriod;
|
||||
_alphaHP = (1.0 - Math.Sin(angHP)) / Math.Cos(angHP);
|
||||
|
||||
// Super-Smoother coefficients (2-pole Butterworth)
|
||||
double angSSF = Math.Sqrt(2.0) * Math.PI / ssfPeriod;
|
||||
double aSSF = Math.Exp(-angSSF);
|
||||
double bSSF = 2.0 * aSSF * Math.Cos(angSSF);
|
||||
_c2 = bSSF;
|
||||
_c3 = -aSSF * aSSF;
|
||||
_c1 = 1.0 - _c2 - _c3;
|
||||
|
||||
// Buffers for historical values
|
||||
_srcBuffer = new RingBuffer(2); // src[0], src[1]
|
||||
_hpBuffer = new RingBuffer(2); // hp[0], hp[1]
|
||||
_filtBuffer = new RingBuffer(8); // filt[0..7] for Hilbert
|
||||
|
||||
_count = 0;
|
||||
Last = new TValue(DateTime.UtcNow, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a chained Sine indicator.
|
||||
/// </summary>
|
||||
public Sine(ITValuePublisher source, int hpPeriod = 40, int ssfPeriod = 10) : this(hpPeriod, ssfPeriod)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
source.Pub += HandleInput;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleInput(object? sender, in TValueEventArgs e)
|
||||
{
|
||||
Update(e.Value, e.IsNew);
|
||||
}
|
||||
|
||||
// Last valid value for NaN substitution
|
||||
private double _lastValidValue;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double src = input.Value;
|
||||
|
||||
// Handle NaN/Infinity: substitute with last valid value
|
||||
if (!double.IsFinite(src))
|
||||
{
|
||||
src = _lastValidValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastValidValue = src;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_srcBuffer.Add(src);
|
||||
_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_srcBuffer.UpdateNewest(src);
|
||||
}
|
||||
|
||||
// High-Pass filter: hp = 0.5 * (1 + α) * (src - src[1]) + α * hp[1]
|
||||
double src1 = _srcBuffer.Count > 1 ? _srcBuffer[0] : 0;
|
||||
double hp1 = _hpBuffer.Count > 0 ? _hpBuffer[^1] : 0;
|
||||
double hp = Math.FusedMultiplyAdd(0.5 * (1.0 + _alphaHP), src - src1, _alphaHP * hp1);
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_hpBuffer.Add(hp);
|
||||
}
|
||||
else
|
||||
{
|
||||
_hpBuffer.UpdateNewest(hp);
|
||||
}
|
||||
|
||||
// Super-Smoother: filt = c1 * (hp + hp[1]) / 2 + c2 * filt[1] + c3 * filt[2]
|
||||
double hp1b = _hpBuffer.Count > 1 ? _hpBuffer[0] : hp;
|
||||
double filt1 = _filtBuffer.Count > 0 ? _filtBuffer[^1] : 0;
|
||||
double filt2 = _filtBuffer.Count > 1 ? _filtBuffer[^2] : 0;
|
||||
double filt = Math.FusedMultiplyAdd(_c1, (hp + hp1b) / 2.0,
|
||||
Math.FusedMultiplyAdd(_c2, filt1, _c3 * filt2));
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_filtBuffer.Add(filt);
|
||||
}
|
||||
else
|
||||
{
|
||||
_filtBuffer.UpdateNewest(filt);
|
||||
}
|
||||
|
||||
// Hilbert Transform for quadrature component Q
|
||||
// Q = 0.0962 * filt[3] + 0.5769 * filt[1] - 0.5769 * filt[5] - 0.0962 * filt[7]
|
||||
// Using ^N for from-end indexing: ^1 = newest, ^2 = second newest, etc.
|
||||
double filt1q = _filtBuffer.Count > 1 ? _filtBuffer[^2] : 0;
|
||||
double filt3 = _filtBuffer.Count > 3 ? _filtBuffer[^4] : 0;
|
||||
double filt5 = _filtBuffer.Count > 5 ? _filtBuffer[^6] : 0;
|
||||
double filt7 = _filtBuffer.Count > 7 ? _filtBuffer[^8] : 0;
|
||||
|
||||
double Q = Math.FusedMultiplyAdd(H1, filt3,
|
||||
Math.FusedMultiplyAdd(H2, filt1q,
|
||||
Math.FusedMultiplyAdd(-H2, filt5, -H1 * filt7)));
|
||||
|
||||
// In-phase component I = filt (current smoothed value)
|
||||
double I = filt;
|
||||
|
||||
// Power and normalization
|
||||
double pwr = (I * I) + (Q * Q);
|
||||
double sineWave = pwr < double.Epsilon ? 0.0 : I / Math.Sqrt(pwr);
|
||||
|
||||
// Clamp to [-1, 1]
|
||||
sineWave = Math.Clamp(sineWave, -1.0, 1.0);
|
||||
|
||||
Last = new TValue(input.Time, sineWave);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Sine for an entire TSeries.
|
||||
/// </summary>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
// Reset and process each value
|
||||
Reset();
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var result = Update(source[i], true);
|
||||
tSpan[i] = source.Times[i];
|
||||
vSpan[i] = result.Value;
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Sine indicator and calculates for the source series.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int hpPeriod = 40, int ssfPeriod = 10)
|
||||
{
|
||||
var sine = new Sine(hpPeriod, ssfPeriod);
|
||||
return sine.Update(source);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Sine Indicator) Calculate(TSeries source, int hpPeriod = 40, int ssfPeriod = 10)
|
||||
{
|
||||
var indicator = new Sine(hpPeriod, ssfPeriod);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_srcBuffer.Clear();
|
||||
_hpBuffer.Clear();
|
||||
_filtBuffer.Clear();
|
||||
_count = 0;
|
||||
Last = new TValue(DateTime.UtcNow, 0);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromDays(1);
|
||||
DateTime baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(baseTime + (interval * i), source[i]), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
# SINE: Ehlers Sine Wave
|
||||
|
||||
SINE extracts the dominant cycle from price data using cascaded signal processing: a high-pass filter removes the trend, a Super-Smoother filter removes noise, and a Hilbert Transform FIR decomposes the filtered signal into In-Phase and Quadrature components for power-normalized sine wave output. The result oscillates between $-1$ and $+1$, representing the normalized position within the current cycle. Unlike HT_SINE which derives phase from the full TA-Lib Hilbert cascade, this Ehlers implementation uses explicit detrending and bandpass stages for cleaner cycle isolation.
|
||||
|
||||
## Historical Context
|
||||
|
||||
John Ehlers introduced the Sine Wave indicator in *Cybernetic Analysis for Stocks and Futures* (2004) as a refined approach to cycle extraction. The design philosophy separates three signal processing concerns into distinct filter stages: (1) trend removal via high-pass filtering sets the long-wavelength cutoff, (2) aliasing prevention via Super-Smoother sets the short-wavelength cutoff, and (3) cycle extraction via Hilbert Transform generates the quadrature decomposition. This staged approach produces cleaner output than attempting all three simultaneously (as in the HT_SINE). The Sine Wave output at extremes ($\pm 1$) indicates the cyclical component is stretched and likely to revert, while zero crossings indicate phase transitions. The indicator is particularly valuable for mean-reversion strategies in ranging markets.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. High-Pass Filter (Detrending)
|
||||
|
||||
A single-pole high-pass filter removes low-frequency trends below the cutoff:
|
||||
|
||||
$$\alpha_{HP} = \frac{1 - \sin(2\pi / P_{HP})}{\cos(2\pi / P_{HP})}$$
|
||||
|
||||
$$HP_t = \frac{1 + \alpha_{HP}}{2}(P_t - P_{t-1}) + \alpha_{HP} \cdot HP_{t-1}$$
|
||||
|
||||
### 2. Super-Smoother Filter (Noise Removal)
|
||||
|
||||
A 2-pole Butterworth low-pass removes high-frequency noise:
|
||||
|
||||
$$a = e^{-\sqrt{2}\pi / P_{SSF}}$$
|
||||
|
||||
$$b = 2a \cos(\sqrt{2}\pi / P_{SSF})$$
|
||||
|
||||
$$c_1 = 1 - b + a^2, \quad c_2 = b, \quad c_3 = -a^2$$
|
||||
|
||||
$$Filt_t = \frac{c_1}{2}(HP_t + HP_{t-1}) + c_2 \cdot Filt_{t-1} + c_3 \cdot Filt_{t-2}$$
|
||||
|
||||
### 3. Hilbert Transform FIR
|
||||
|
||||
Discrete Hilbert approximation extracts quadrature component:
|
||||
|
||||
$$Q_t = 0.0962 \cdot Filt_{t-3} + 0.5769 \cdot Filt_{t-1} - 0.5769 \cdot Filt_{t-5} - 0.0962 \cdot Filt_{t-7}$$
|
||||
|
||||
$$I_t = Filt_t$$
|
||||
|
||||
### 4. Power Normalization
|
||||
|
||||
$$Power_t = I_t^2 + Q_t^2$$
|
||||
|
||||
$$Sine_t = \frac{I_t}{\sqrt{Power_t}}$$
|
||||
|
||||
When $Power \approx 0$, output is zero.
|
||||
|
||||
### 5. Complexity
|
||||
|
||||
$O(1)$ per bar. Fixed filter stages with ring buffers of 2 (source) + 2 (HP) + 8 (filtered) = 12 elements. Warmup: $\max(P_{HP}, P_{SSF}) + 8$ bars.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| `hpPeriod` | High-pass filter cutoff period | 40 | $\geq 1$ |
|
||||
| `ssfPeriod` | Super-smoother filter period | 10 | $\geq 1$ |
|
||||
|
||||
### Tuning Relationship
|
||||
|
||||
Typically $P_{SSF} \approx P_{HP} / 4$ to $P_{HP} / 2$. The high-pass defines the trend/cycle boundary; the super-smoother defines the noise/cycle boundary. Together they create a bandpass that isolates the frequency range of interest.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function SINE(source, hpPeriod, ssfPeriod):
|
||||
// Precompute HP coefficient
|
||||
α_hp ← (1 - sin(2π/hpPeriod)) / cos(2π/hpPeriod)
|
||||
|
||||
// Precompute SSF coefficients
|
||||
a ← exp(-√2·π / ssfPeriod)
|
||||
b ← 2·a·cos(√2·π / ssfPeriod)
|
||||
c₁ ← (1 - b + a²) / 2
|
||||
|
||||
hp_prev ← 0; p_prev ← 0
|
||||
filt_1 ← 0; filt_2 ← 0
|
||||
filtBuf ← CircularBuffer(8)
|
||||
|
||||
for each price in source:
|
||||
// High-pass
|
||||
hp ← 0.5·(1 + α_hp)·(price - p_prev) + α_hp·hp_prev
|
||||
|
||||
// Super-smoother
|
||||
filt ← c₁·(hp + hp_prev) + b·filt_1 - a²·filt_2
|
||||
|
||||
// Hilbert FIR quadrature
|
||||
filtBuf.Add(filt)
|
||||
Q ← 0.0962·filtBuf[3] + 0.5769·filtBuf[1]
|
||||
- 0.5769·filtBuf[5] - 0.0962·filtBuf[7]
|
||||
I ← filt
|
||||
|
||||
// Power normalization
|
||||
power ← I² + Q²
|
||||
sine ← (power > 0) ? I / √power : 0
|
||||
|
||||
// Shift state
|
||||
hp_prev ← hp; p_prev ← price
|
||||
filt_2 ← filt_1; filt_1 ← filt
|
||||
|
||||
emit sine
|
||||
```
|
||||
|
||||
### SINE vs HT_SINE
|
||||
|
||||
| Aspect | SINE | HT_SINE |
|
||||
|--------|------|---------|
|
||||
| Detrending | Explicit high-pass filter | Implicit in Hilbert cascade |
|
||||
| Noise removal | Explicit Super-Smoother | 4-bar WMA only |
|
||||
| Period tuning | User-configurable (hpPeriod, ssfPeriod) | Fixed (TA-Lib spec) |
|
||||
| Output | Single (Sine only) | Dual (Sine + LeadSine) |
|
||||
| Phase source | I/Q power normalization | DFT phase accumulation |
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| Condition | Meaning |
|
||||
|-----------|---------|
|
||||
| $Sine \approx +1$ | Cycle peak (potential short / mean-reversion) |
|
||||
| $Sine \approx -1$ | Cycle trough (potential long / mean-reversion) |
|
||||
| Zero crossing up | Bullish phase transition |
|
||||
| Zero crossing down | Bearish phase transition |
|
||||
| Erratic output | Strong trend overwhelming cycle extraction |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count per bar | Notes |
|
||||
|-----------|--------------|-------|
|
||||
| High-pass filter | ~4 | 1 SUB + 1 MUL + 1 FMA |
|
||||
| Super-Smoother (2-pole IIR) | ~5 | 1 ADD + 2 FMA + 1 MUL |
|
||||
| Hilbert FIR (quadrature) | ~7 | 4-tap FIR: 4 MUL + 3 ADD |
|
||||
| I² + Q² (power) | ~3 | 2 MUL + 1 ADD |
|
||||
| SQRT + normalization | ~4 | 1 SQRT + 1 DIV + 1 branch |
|
||||
| Buffer management | ~3 | 1 circular buffer write + index update |
|
||||
| State shift | ~4 | 4 register moves |
|
||||
| **Total** | **~30** | **O(1) fixed; single SQRT is only transcendental** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| SIMD vectorizable | No: HP and SSF are recursive IIR with sequential state dependencies |
|
||||
| Bottleneck | `Math.Sqrt` in power normalization (~15 cycles); rest is pure arithmetic |
|
||||
| Parallelism | None: each bar's HP/SSF output depends on previous bar |
|
||||
| Memory | O(1): 8-element ring buffer + 4 scalar state variables (~96 bytes) |
|
||||
| Throughput | Very fast; slightly faster than EBSW (no 3-bar averaging, no clamp) |
|
||||
|
||||
## Resources
|
||||
|
||||
- **Ehlers, J.F.** *Cybernetic Analysis for Stocks and Futures*. Wiley, 2004.
|
||||
- **Ehlers, J.F.** *Cycle Analytics for Traders*. Wiley, 2013.
|
||||
@@ -1,47 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Ehlers Sine Wave (SINE)", "SINE", overlay=false)
|
||||
|
||||
//@function Calculates Ehlers’ original Sine Wave using a two‑pole High‑Pass, a Super‑Smoother,
|
||||
// and a Hilbert‑transform FIR pair (In‑phase I / Quadrature Q).
|
||||
//@param src Series to calculate the Sine Wave from
|
||||
//@param hpLength High‑Pass filter length (detrending period)
|
||||
//@param ssfLength Super‑Smoother filter length (cycle smoothing period)
|
||||
//@returns single normalized sine‑wave value in [‑1 … +1]
|
||||
sine(series float src, simple int hpLength, simple int ssfLength) =>
|
||||
if hpLength <= 0 or ssfLength <= 0
|
||||
runtime.error("Periods must be > 0")
|
||||
float pi = 2 * math.asin(1)
|
||||
float angHP = 2 * pi / hpLength
|
||||
float aHP = (1 - math.sin(angHP)) / math.cos(angHP)
|
||||
var float hp = 0.0
|
||||
hp := 0.5 * (1 + aHP) * (src - nz(src[1])) + aHP * nz(hp[1])
|
||||
float angSSF = math.sqrt(2) * pi / ssfLength
|
||||
float aSSF = math.exp(-angSSF)
|
||||
float bSSF = 2 * aSSF * math.cos(angSSF)
|
||||
float c2 = bSSF
|
||||
float c3 = -aSSF * aSSF
|
||||
float c1 = 1 - c2 - c3
|
||||
var float filt = 0.0
|
||||
filt := c1 * (hp + nz(hp[1])) / 2 + c2 * nz(filt[1]) + c3 * nz(filt[2])
|
||||
float Q = 0.0962 * nz(filt[3]) + 0.5769 * nz(filt[1])
|
||||
- 0.5769 * nz(filt[5]) - 0.0962 * nz(filt[7])
|
||||
float I = filt
|
||||
float pwr = I*I + Q*Q
|
||||
float sineWave = pwr == 0 ? 0 : I / math.sqrt(pwr)
|
||||
math.min(1, math.max(-1, sineWave))
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_hpLength = input.int(40, "High‑Pass Filter Length", minval=1)
|
||||
i_ssfLength = input.int(10, "Super‑Smoother Filter Length", minval=1)
|
||||
|
||||
// Calculation
|
||||
sine_wave = sine(i_source, i_hpLength, i_ssfLength)
|
||||
|
||||
// Plot
|
||||
plot(sine_wave, "SINE", color=color.yellow, linewidth=2)
|
||||
hline(0, "Zero Line", color.gray, linestyle=hline.style_dashed)
|
||||
@@ -19,10 +19,14 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu
|
||||
| [DX](dx/Dx.md) | Directional Movement Index | Raw directional strength. Unsmoothed ADX component. |
|
||||
| [HT_TRENDMODE](ht_trendmode/Ht_trendmode.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. |
|
||||
| [IMI](imi/Imi.md) | Intraday Momentum Index | RSI variant using open-close range. Intraday overbought/oversold. |
|
||||
| [IMPULSE](impulse/Impulse.md) | Elder Impulse System | EMA + MACD histogram alignment. Color-coded trend/momentum filter. |
|
||||
| [QSTICK](qstick/Qstick.md) | Qstick | MA of (Close - Open). Positive = buying pressure. |
|
||||
| [SUPER](super/Super.md) | SuperTrend | ATR-based trailing stop. Flips on breakout. Color-coded direction. |
|
||||
| [TTM_TREND](ttm_trend/TtmTrend.md) | TTM Trend | Fast 6-period EMA. Color-coded trend from John Carter. |
|
||||
| [TTM_SQUEEZE](ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | BB inside KC squeeze detection with linear regression momentum. John Carter. |
|
||||
| [VORTEX](vortex/Vortex.md) | Vortex Indicator | VI+ and VI- measure positive/negative trend movement. |
|
||||
| GATOR | Williams Gator Oscillator | Histogram of Alligator line differences. |
|
||||
| GHLA | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover. |
|
||||
| PFE | Polarized Fractal Efficiency | Trend efficiency: straight-line / total path distance. |
|
||||
| RAVI | Chande Range Action Verification Index | \|SMA(short) − SMA(long)\| / SMA(long) × 100. |
|
||||
| VHF | Vertical Horizontal Filter | Max-min range / sum of absolute changes. |
|
||||
|
||||
@@ -43,8 +43,8 @@ chop(simple int length) =>
|
||||
float price_range = hhv - llv
|
||||
float chop_value = na
|
||||
if win >= 2 and price_range > 0
|
||||
float log_ratio = math.log10(sum_tr / price_range)
|
||||
float log_len = math.log10(win)
|
||||
float log_ratio = math.log(sum_tr / price_range) / math.log(10)
|
||||
float log_len = math.log(win) / math.log(10)
|
||||
chop_value := 100.0 * log_ratio / log_len
|
||||
chop_value := math.max(0.0, math.min(100.0, chop_value))
|
||||
chop_value
|
||||
|
||||
@@ -34,3 +34,4 @@ Error metrics and loss functions for model/strategy evaluation. All error indica
|
||||
| [TUKEY](tukey/Tukey.md) | Tukey Biweight Loss | Hard-rejects outliers beyond threshold. Redescending. |
|
||||
| [WMAPE](wmape/Wmape.md) | Weighted MAPE | Volume-weighted percentage error. For heterogeneous data. |
|
||||
| [WRMSE](wrmse/Wrmse.md) | Weighted RMSE | Weighted root mean squared error. Custom observation weighting. |
|
||||
| STDERR | Standard Error | StdDev / √n. Measures sampling variability. |
|
||||
|
||||
@@ -10,7 +10,7 @@ stable_logcosh(float x) =>
|
||||
float LOG2 = 0.6931471805599453
|
||||
float absX = math.abs(x)
|
||||
// For large values, use asymptotic approximation to avoid overflow
|
||||
absX > 20.0 ? absX - LOG2 : math.log(math.cosh(x))
|
||||
absX > 20.0 ? absX - LOG2 : math.log((math.exp(x) + math.exp(-x)) / 2.0)
|
||||
|
||||
//@function Calculates Log-Cosh Loss
|
||||
//@param actual Series of actual values
|
||||
|
||||
@@ -8,6 +8,7 @@ Signal processing filters adapted for financial time series. These are not indic
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [AGC](agc/Agc.md) | Ehlers Automatic Gain Control | Amplitude normalization via exponential peak tracking. Signal processing building block. |
|
||||
| [ALAGUERRE](alaguerre/ALaguerre.md) | Ehlers Adaptive Laguerre Filter | Ehlers. Variable-alpha Laguerre from tracking-error normalization. |
|
||||
| [BAXTERKING](baxterking/BaxterKing.md) | Baxter-King Band-Pass Filter | Symmetric FIR band-pass. Ideal for business cycle extraction. |
|
||||
| [CFITZ](cfitz/Cfitz.md) | Christiano-Fitzgerald Filter | Asymmetric full-sample band-pass. Optimal under random-walk assumption. |
|
||||
@@ -27,11 +28,15 @@ Signal processing filters adapted for financial time series. These are not indic
|
||||
| [KALMAN](kalman/Kalman.md) | Kalman Filter | Recursive state estimation. Optimal under Gaussian assumptions. |
|
||||
| [LAGUERRE](laguerre/Laguerre.md) | Ehlers Laguerre Filter | Ehlers. 4-element all-pass cascade. γ-controlled smoothing. |
|
||||
| [LMS](lms/Lms.md) | Least Mean Squares | Widrow-Hoff adaptive FIR. NLMS weight update. O(order) per bar. |
|
||||
| [RLS](rls/Rls.md) | Recursive Least Squares | Inverse correlation matrix. Faster convergence than LMS. O(order²) per bar. |
|
||||
| [LOESS](loess/Loess.md) | LOESS Smoothing | Local polynomial regression. Robust to outliers. |
|
||||
| [MODF](modf/Modf.md) | Modular Filter | Dual-path adaptive filter with upper/lower EMA bands and state selection. |
|
||||
| [NOTCH](notch/Notch.md) | Notch Filter | Band-stop. Removes specific frequency (e.g., 60 Hz noise). |
|
||||
| [NW](nw/Nw.md) | Nadaraya-Watson Kernel Regression | Non-parametric kernel regression smoothing. Bandwidth-adaptive. |
|
||||
| [ONEEURO](oneeuro/OneEuro.md) | One Euro Filter | Speed-adaptive low-pass. Adaptive cutoff from signal derivative. |
|
||||
| [RLS](rls/Rls.md) | Recursive Least Squares | Inverse correlation matrix. Faster convergence than LMS. O(order²) per bar. |
|
||||
| [RMED](rmed/Rmed.md) | Ehlers Recursive Median Filter | Recursive median approximation via IIR structure. Outlier-resistant. |
|
||||
| [ROOFING](roofing/Roofing.md) | Ehlers Roofing Filter | Ehlers. HP + SS cascade. Bandpass for cycle extraction. |
|
||||
| SAK | Ehlers Swiss Army Knife | Configurable multi-mode filter (LP, HP, BP, BS). Single framework. |
|
||||
| [SGF](sgf/Sgf.md) | Savitzky-Golay | Polynomial smoothing. Preserves higher moments (derivatives). |
|
||||
| [SPBF](spbf/Spbf.md) | Ehlers Super Passband Filter | Ehlers. Wide-band bandpass via differenced EMAs with RMS envelope. |
|
||||
| [SSF2](ssf2/Ssf2.md) | Ehlers 2-Pole Super Smoother Filter | Ehlers. 2-pole Butterworth variant. Standard cycle pre-filter. |
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# MEDF: Moving Median Filter
|
||||
|
||||
> "The median is the only filter that can remove a spike without flinching. SMA smears it, EMA decays it over time, but the median simply ignores it. For impulse noise in financial data — bad ticks, flash crashes, fat-finger errors — the median is the correct tool."
|
||||
|
||||
MEDF outputs the median of the most recent $N$ values in a sliding window, providing a nonlinear filter that is robust to impulse noise and outliers while preserving edges and steps better than any linear filter. Unlike SMA or EMA, which spread the effect of a single outlier across the entire window (SMA) or decay it exponentially (EMA), the median completely rejects outliers that do not constitute a majority of the window. This makes MEDF the filter of choice for cleaning price data contaminated with bad ticks or anomalous prints.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The running median was introduced by John Tukey in *Exploratory Data Analysis* (1977) as a fundamental tool for resistant smoothing. Tukey recognized that the arithmetic mean (and by extension, linear filters like SMA and EMA) is highly sensitive to outliers: a single extreme value can shift the mean arbitrarily far from the "typical" value. The median, being the 50th percentile, requires more than $N/2$ values to be corrupted before it fails.
|
||||
|
||||
In signal processing, median filters gained prominence in image processing (Huang, Yang, and Tang, 1979), where they excel at removing "salt and pepper" noise while preserving sharp edges. The same property applies to financial time series: price levels often exhibit step-like behavior (e.g., after a gap or news event), and the median preserves these steps while linear filters blur them.
|
||||
|
||||
The computational cost of a naive median filter is $O(N \log N)$ per bar (sort the window, extract the middle). More efficient algorithms exist: the rolling median via two heaps achieves $O(\log N)$ per bar, and Huang's histogram method achieves $O(1)$ amortized for integer-valued data. The Pine implementation uses a sort-based approach.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Circular Buffer
|
||||
|
||||
A ring buffer of size $N$ stores the most recent $N$ values.
|
||||
|
||||
### 2. Window Extraction and Sort
|
||||
|
||||
Each bar, the buffer contents are copied to a temporary array and sorted. This is $O(N \log N)$ via array sort.
|
||||
|
||||
### 3. Median Extraction
|
||||
|
||||
For odd $N$: the middle element is the median. For even $N$: the average of the two middle elements.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
The median of a set $\{x_1, x_2, \ldots, x_N\}$ is:
|
||||
|
||||
$$
|
||||
\text{median}(X) = \begin{cases} X_{[(N+1)/2]} & N \text{ odd} \\ \frac{X_{[N/2]} + X_{[N/2+1]}}{2} & N \text{ even} \end{cases}
|
||||
$$
|
||||
|
||||
where $X_{[k]}$ denotes the $k$-th order statistic (sorted value).
|
||||
|
||||
**Key properties:**
|
||||
|
||||
| Property | Median | SMA | EMA |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Outlier rejection | Complete (if $< N/2$ outliers) | None | Partial (decays) |
|
||||
| Edge preservation | Yes | Blurs edges | Blurs edges |
|
||||
| Linearity | Nonlinear | Linear | Linear |
|
||||
| Frequency response | No closed form | Sinc | Exponential decay |
|
||||
| Idempotent | No | No | No |
|
||||
|
||||
**Breakdown point:** The median has a 50% breakdown point, meaning up to $\lfloor N/2 \rfloor$ values can be arbitrarily corrupted without affecting the output (assuming the remaining values are within the signal range). This is the highest possible breakdown point for any estimator.
|
||||
|
||||
**Default parameters:** `period = 5`, `minPeriod = 1`.
|
||||
|
||||
**Pseudo-code (streaming):**
|
||||
|
||||
```
|
||||
buffer[head] = src
|
||||
head = (head + 1) % period
|
||||
count = min(count + 1, period)
|
||||
|
||||
sorted = sort(buffer[0..count-1])
|
||||
if count is odd:
|
||||
return sorted[count / 2]
|
||||
else:
|
||||
return (sorted[count/2 - 1] + sorted[count/2]) / 2
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Tukey, J.W. (1977). *Exploratory Data Analysis*. Addison-Wesley. Chapter 7: Resistant Smoothing.
|
||||
- Huang, T.S., Yang, G.J., & Tang, G.Y. (1979). "A Fast Two-Dimensional Median Filtering Algorithm." *IEEE Trans. Acoust., Speech, Signal Process.*, 27(1), 13-18.
|
||||
- Yin, L. et al. (1996). "Weighted Median Filters: A Tutorial." *IEEE Trans. Circuits and Systems II*, 43(3), 157-192.
|
||||
@@ -1,55 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
|
||||
//@version=6
|
||||
indicator("MEDF - Moving Median Filter", "MEDF", overlay=true)
|
||||
|
||||
// ── Functions ──────────────────────────────────────────────────────────
|
||||
// @function Calculates the Moving Median Filter over a sliding window.
|
||||
// A nonlinear filter that outputs the median of the last N values.
|
||||
// Robust to impulse noise and outliers while preserving edges/steps
|
||||
// better than any linear filter (SMA, EMA, etc.).
|
||||
// Uses circular buffer + insertion sort for O(N log N) per bar.
|
||||
// @param source Series to filter
|
||||
// @param period Window size (must be > 0)
|
||||
// @returns Median-filtered value, valid from bar 1
|
||||
export medf(series float source, simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
|
||||
float src = nz(source)
|
||||
|
||||
var array<float> buffer = array.new_float(period, 0.0)
|
||||
var int head = 0
|
||||
var int count = 0
|
||||
|
||||
array.set(buffer, head, src)
|
||||
head := (head + 1) % period
|
||||
if count < period
|
||||
count += 1
|
||||
|
||||
var array<float> sorted = array.new_float(0)
|
||||
array.clear(sorted)
|
||||
for i = 0 to count - 1
|
||||
array.push(sorted, array.get(buffer, i))
|
||||
array.sort(sorted)
|
||||
|
||||
int n = array.size(sorted)
|
||||
float result = 0.0
|
||||
if n % 2 == 1
|
||||
result := array.get(sorted, n / 2)
|
||||
else
|
||||
float mid1 = array.get(sorted, n / 2 - 1)
|
||||
float mid2 = array.get(sorted, n / 2)
|
||||
result := (mid1 + mid2) / 2.0
|
||||
result
|
||||
|
||||
// ── Inputs ─────────────────────────────────────────────────────────────
|
||||
int i_period = input.int(5, "Period", minval=1)
|
||||
string i_source = input.source(close, "Source")
|
||||
|
||||
// ── Calculation ────────────────────────────────────────────────────────
|
||||
float value = medf(i_source, i_period)
|
||||
|
||||
// ── Plot ───────────────────────────────────────────────────────────────
|
||||
plot(value, "MEDF", color.yellow, 2)
|
||||
@@ -0,0 +1,134 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class ModfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ModfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new ModfIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(0.8, indicator.Beta);
|
||||
Assert.False(indicator.Feedback);
|
||||
Assert.Equal(0.5, indicator.FbWeight);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("MODF - Modular Filter", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModfIndicator_MinHistoryDepths_EqualsExpectedValue()
|
||||
{
|
||||
var indicator = new ModfIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(14, ModfIndicator.MinHistoryDepths);
|
||||
Assert.Equal(14, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModfIndicator_ShortName_IncludesParametersAndSource()
|
||||
{
|
||||
var indicator = new ModfIndicator { Period = 20, Beta = 0.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("MODF", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("0.5", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("Close", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModfIndicator_Initialize_CreatesInternalModf()
|
||||
{
|
||||
var indicator = new ModfIndicator { Period = 10, Beta = 0.6 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ModfIndicator { Period = 3, Beta = 0.8 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ModfIndicator { Period = 3, Beta = 0.8 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
// Process first update
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// Line series should have values
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new ModfIndicator { Period = 3, Beta = 0.8 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Update with new tick (same bar data - simulates intrabar update)
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Both values should be finite
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModfIndicator_DifferentSourceTypes()
|
||||
{
|
||||
foreach (var sourceType in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new ModfIndicator { Period = 5, Source = sourceType };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public class ModfIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Beta", sortIndex: 2, 0.0, 1.0, 0.1, 1)]
|
||||
public double Beta { get; set; } = 0.8;
|
||||
|
||||
[InputParameter("Feedback", sortIndex: 3)]
|
||||
public bool Feedback { get; set; } = false;
|
||||
|
||||
[InputParameter("Feedback Weight", sortIndex: 4, 0.01, 1.0, 0.05, 2)]
|
||||
public double FbWeight { get; set; } = 0.5;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Modf _ma = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 14;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"MODF({Period},{Beta:F1}):{_sourceName}";
|
||||
|
||||
public ModfIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "MODF - Modular Filter";
|
||||
Description = "Dual-path adaptive filter with upper/lower EMA bands and state selection.";
|
||||
_series = new LineSeries(name: $"MODF {Period}", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_ma = new Modf(Period, Beta, Feedback, FbWeight);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
var input = new TValue(item.TimeLeft.Ticks, _priceSelector(item));
|
||||
double value = _ma.Update(input, isNew).Value;
|
||||
_series.SetValue(value, _ma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ModfTests
|
||||
{
|
||||
private readonly GBM _gbm = new(startPrice: 100, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
private readonly TSeries _data;
|
||||
|
||||
public ModfTests()
|
||||
{
|
||||
_data = _gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
}
|
||||
|
||||
// ── A) Constructor validation ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodLessThan2_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Modf(1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_BetaNegative_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Modf(14, beta: -0.1));
|
||||
Assert.Equal("beta", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_BetaOverOne_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Modf(14, beta: 1.1));
|
||||
Assert.Equal("beta", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_FbWeightZero_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Modf(14, fbWeight: 0.0));
|
||||
Assert.Equal("fbWeight", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidParams_SetsProperties()
|
||||
{
|
||||
var modf = new Modf(20, beta: 0.7, feedback: true, fbWeight: 0.3);
|
||||
Assert.Equal(20, modf.Period);
|
||||
Assert.Equal(0.7, modf.Beta);
|
||||
Assert.True(modf.Feedback);
|
||||
Assert.Equal(0.3, modf.FbWeight);
|
||||
Assert.Contains("Modf", modf.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsValidTValue()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
var result = modf.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_IsAccessible()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(modf.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ContainsModf()
|
||||
{
|
||||
var modf = new Modf(14, beta: 0.8);
|
||||
Assert.Contains("Modf", modf.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstBar_OutputEqualsInput()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
var result = modf.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
Assert.Equal(42.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
double v1 = modf.Update(new TValue(DateTime.UtcNow, 110.0), isNew: true).Value;
|
||||
double v2 = modf.Update(new TValue(DateTime.UtcNow, 110.0), isNew: true).Value;
|
||||
// Second bar with same value should differ from first (state progressed)
|
||||
Assert.True(double.IsFinite(v1));
|
||||
Assert.True(double.IsFinite(v2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_RollsBack()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
double baseline = modf.Update(new TValue(DateTime.UtcNow, 150.0), isNew: true).Value;
|
||||
double corrected = modf.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false).Value;
|
||||
Assert.NotEqual(baseline, corrected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_Restore()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0 + i * 0.5), isNew: true);
|
||||
}
|
||||
|
||||
double before = modf.Last.Value;
|
||||
// Correct last bar multiple times
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0 + 29 * 0.5), isNew: false);
|
||||
}
|
||||
|
||||
Assert.Equal(before, modf.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
modf.Reset();
|
||||
Assert.False(modf.IsHot);
|
||||
var result = modf.Update(new TValue(DateTime.UtcNow, 50.0));
|
||||
Assert.Equal(50.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// ── D) Warmup/convergence ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterWarmup()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
Assert.False(modf.IsHot);
|
||||
for (int i = 0; i < 14; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
Assert.True(modf.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsPeriod()
|
||||
{
|
||||
var modf = new Modf(20);
|
||||
Assert.Equal(20, modf.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── E) Robustness ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void NaN_UsesLastValid()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
modf.Update(new TValue(DateTime.UtcNow, 105.0));
|
||||
var result = modf.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_UsesLastValid()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = modf.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Handled()
|
||||
{
|
||||
double[] src = [100, 101, double.NaN, 103, 104];
|
||||
double[] output = new double[src.Length];
|
||||
Modf.Batch(src, output, 3);
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// ── F) Consistency (4-API modes match) ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllFourModes_ProduceSameResults()
|
||||
{
|
||||
int period = 14;
|
||||
double beta = 0.8;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Modf(period, beta);
|
||||
var streamResults = new double[_data.Count];
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(_data[i]).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch TSeries
|
||||
var batchTs = Modf.Batch(_data, period, beta);
|
||||
|
||||
// Mode 3: Batch Span
|
||||
double[] spanOut = new double[_data.Count];
|
||||
Modf.Batch(_data.Values, spanOut, period, beta);
|
||||
|
||||
// Mode 4: Calculate
|
||||
var (calcTs, _) = Modf.Calculate(_data, period, beta);
|
||||
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchTs[i].Value, 10);
|
||||
Assert.Equal(streamResults[i], spanOut[i], 10);
|
||||
Assert.Equal(streamResults[i], calcTs[i].Value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_LengthMismatch_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = new double[2];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Modf.Batch(src, output, 2));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = new double[3];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Modf.Batch(src, output, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreaming()
|
||||
{
|
||||
int period = 10;
|
||||
double[] spanOut = new double[_data.Count];
|
||||
Modf.Batch(_data.Values, spanOut, period);
|
||||
|
||||
var streaming = new Modf(period);
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
double sv = streaming.Update(_data[i]).Value;
|
||||
Assert.Equal(sv, spanOut[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── H) Chainability ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Pub_Fires()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
int count = 0;
|
||||
modf.Pub += (object? _, in TValueEventArgs _) => count++;
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChain_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var modf = new Modf(source, 14);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
source.Add(new TValue(DateTime.UtcNow, 105.0));
|
||||
Assert.True(double.IsFinite(modf.Last.Value));
|
||||
}
|
||||
|
||||
// ── MODF-specific tests ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Beta1_SmoothFilter_TracksPrice()
|
||||
{
|
||||
var modf = new Modf(14, beta: 1.0);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
// With beta=1.0 (smooth mode), output should track price closely
|
||||
Assert.True(modf.Last.Value > 100.0);
|
||||
Assert.True(modf.Last.Value <= 149.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Beta0_TrailingStop_BehavesLike()
|
||||
{
|
||||
var modf = new Modf(14, beta: 0.0);
|
||||
// Feed uptrend
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 100.0 + i * 2.0));
|
||||
}
|
||||
double upResult = modf.Last.Value;
|
||||
// Output should be at or below price in uptrend (lower band tracks behind)
|
||||
Assert.True(upResult <= 100.0 + 29 * 2.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Feedback_ProducesSmootherOutput()
|
||||
{
|
||||
// Feedback should produce different (smoother) results than no feedback
|
||||
var noFb = new Modf(14, feedback: false);
|
||||
var withFb = new Modf(14, feedback: true, fbWeight: 0.5);
|
||||
|
||||
double lastNoFb = 0, lastWithFb = 0;
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
lastNoFb = noFb.Update(_data[i]).Value;
|
||||
lastWithFb = withFb.Update(_data[i]).Value;
|
||||
}
|
||||
// Results should differ when feedback is enabled
|
||||
Assert.NotEqual(lastNoFb, lastWithFb);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
}
|
||||
Assert.Equal(42.0, modf.Last.Value, 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
TSeries result = modf.Update(_data);
|
||||
Assert.Equal(_data.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var (results, indicator) = Modf.Calculate(_data, 14);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(_data.Count, results.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class ModfValidationTests : IDisposable
|
||||
{
|
||||
private readonly TBarSeries _bars;
|
||||
|
||||
public ModfValidationTests(ITestOutputHelper _)
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
_bars = gbm.Fetch(5000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
public void Dispose() { Dispose(true); }
|
||||
private void Dispose(bool _) { /* nothing to release */ }
|
||||
|
||||
[Fact]
|
||||
public void BatchStreaming_Match()
|
||||
{
|
||||
int period = 14;
|
||||
var data = _bars.Close;
|
||||
var batch = Modf.Batch(data, period);
|
||||
|
||||
var streaming = new Modf(period);
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
double sv = streaming.Update(data[i]).Value;
|
||||
Assert.Equal(sv, batch[i].Value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanStreaming_Match()
|
||||
{
|
||||
int period = 14;
|
||||
var data = _bars.Close;
|
||||
double[] spanOut = new double[data.Count];
|
||||
Modf.Batch(data.Values, spanOut, period);
|
||||
|
||||
var streaming = new Modf(period);
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
double sv = streaming.Update(data[i]).Value;
|
||||
Assert.Equal(sv, spanOut[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var data = _bars.Close;
|
||||
double[] out10 = new double[data.Count];
|
||||
double[] out30 = new double[data.Count];
|
||||
Modf.Batch(data.Values, out10, 10);
|
||||
Modf.Batch(data.Values, out30, 30);
|
||||
|
||||
bool anyDifferent = false;
|
||||
for (int i = 50; i < data.Count; i++)
|
||||
{
|
||||
if (Math.Abs(out10[i] - out30[i]) > 1e-8) { anyDifferent = true; break; }
|
||||
}
|
||||
Assert.True(anyDifferent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var modf = new Modf(14);
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
modf.Update(new TValue(DateTime.UtcNow, 50.0));
|
||||
}
|
||||
Assert.Equal(50.0, modf.Last.Value, 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var data = _bars.Close;
|
||||
var (results, indicator) = Modf.Calculate(data, 14);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(data.Count, results.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_Consistency()
|
||||
{
|
||||
int period = 14;
|
||||
var data = _bars.Close;
|
||||
|
||||
var modf1 = new Modf(period);
|
||||
var modf2 = new Modf(period);
|
||||
|
||||
// modf1: stream normally
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
modf1.Update(data[i]);
|
||||
}
|
||||
|
||||
// modf2: stream with corrections on each bar
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
modf2.Update(new TValue(data[i].Time, data[i].Value * 1.01), isNew: true);
|
||||
modf2.Update(data[i], isNew: false);
|
||||
}
|
||||
|
||||
Assert.Equal(modf1.Last.Value, modf2.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentBetas_ProduceDifferentResults()
|
||||
{
|
||||
var data = _bars.Close;
|
||||
double[] outLow = new double[data.Count];
|
||||
double[] outHigh = new double[data.Count];
|
||||
Modf.Batch(data.Values, outLow, 14, beta: 0.2);
|
||||
Modf.Batch(data.Values, outHigh, 14, beta: 0.9);
|
||||
|
||||
bool anyDifferent = false;
|
||||
for (int i = 20; i < data.Count; i++)
|
||||
{
|
||||
if (Math.Abs(outLow[i] - outHigh[i]) > 1e-8) { anyDifferent = true; break; }
|
||||
}
|
||||
Assert.True(anyDifferent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Feedback_ProducesDifferentResults()
|
||||
{
|
||||
var data = _bars.Close;
|
||||
double[] outNoFb = new double[data.Count];
|
||||
double[] outFb = new double[data.Count];
|
||||
Modf.Batch(data.Values, outNoFb, 14, feedback: false);
|
||||
Modf.Batch(data.Values, outFb, 14, feedback: true, fbWeight: 0.5);
|
||||
|
||||
bool anyDifferent = false;
|
||||
for (int i = 20; i < data.Count; i++)
|
||||
{
|
||||
if (Math.Abs(outNoFb[i] - outFb[i]) > 1e-8) { anyDifferent = true; break; }
|
||||
}
|
||||
Assert.True(anyDifferent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class Modf : AbstractBase
|
||||
{
|
||||
private readonly double _alpha;
|
||||
private readonly double _oneMinusAlpha;
|
||||
private readonly double _beta;
|
||||
private readonly bool _feedback;
|
||||
private readonly double _fbWeight;
|
||||
private readonly double _oneMinusFbWeight;
|
||||
private readonly double _oneMinusBeta;
|
||||
private readonly ITValuePublisher? _publisher;
|
||||
private readonly TValuePublishedHandler? _handler;
|
||||
private int _index;
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
public double B;
|
||||
public double C;
|
||||
public double Os;
|
||||
public double Ts;
|
||||
public double LastValue;
|
||||
public bool Initialized;
|
||||
}
|
||||
|
||||
public int Period { get; }
|
||||
public double Beta => _beta;
|
||||
public bool Feedback => _feedback;
|
||||
public double FbWeight => _fbWeight;
|
||||
public override bool IsHot => _index >= WarmupPeriod;
|
||||
|
||||
public Modf(int period, double beta = 0.8, bool feedback = false, double fbWeight = 0.5)
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2.");
|
||||
}
|
||||
if (beta < 0.0 || beta > 1.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(beta), "Beta must be in [0, 1].");
|
||||
}
|
||||
if (fbWeight <= 0.0 || fbWeight > 1.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(fbWeight), "Feedback weight must be in (0, 1].");
|
||||
}
|
||||
|
||||
Period = period;
|
||||
_beta = beta;
|
||||
_feedback = feedback;
|
||||
_fbWeight = fbWeight;
|
||||
_oneMinusFbWeight = 1.0 - fbWeight;
|
||||
_alpha = 2.0 / (period + 1);
|
||||
_oneMinusAlpha = 1.0 - _alpha;
|
||||
_oneMinusBeta = 1.0 - beta;
|
||||
WarmupPeriod = period;
|
||||
Name = feedback
|
||||
? $"Modf({period},{beta:F1},fb={fbWeight:F2})"
|
||||
: $"Modf({period},{beta:F1})";
|
||||
|
||||
Init();
|
||||
}
|
||||
|
||||
public Modf(TSeries source, int period, double beta = 0.8, bool feedback = false, double fbWeight = 0.5)
|
||||
: this(period, beta, feedback, fbWeight)
|
||||
{
|
||||
_publisher = source;
|
||||
_handler = Sub;
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
private void Sub(object? source, in TValueEventArgs args)
|
||||
{
|
||||
Update(args.Value, args.IsNew);
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_index = 0;
|
||||
_s = default;
|
||||
_ps = default;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew) { _ps = _s; _index++; }
|
||||
else { _s = _ps; }
|
||||
|
||||
var s = _s;
|
||||
|
||||
double val = input.Value;
|
||||
if (double.IsNaN(val) || double.IsInfinity(val))
|
||||
{
|
||||
val = s.LastValue;
|
||||
}
|
||||
|
||||
double result;
|
||||
if (!s.Initialized)
|
||||
{
|
||||
s.B = val;
|
||||
s.C = val;
|
||||
s.Os = 0.0;
|
||||
s.Ts = val;
|
||||
s.Initialized = true;
|
||||
result = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Input: optionally blend source with previous output (feedback)
|
||||
double a = _feedback
|
||||
? Math.FusedMultiplyAdd(_oneMinusFbWeight, s.Ts, _fbWeight * val)
|
||||
: val;
|
||||
|
||||
// Upper band: EMA that snaps up to 'a' when a exceeds EMA
|
||||
double emaB = Math.FusedMultiplyAdd(_oneMinusAlpha, s.B, _alpha * a);
|
||||
s.B = a > emaB ? a : emaB;
|
||||
|
||||
// Lower band: EMA that snaps down to 'a' when a falls below EMA
|
||||
double emaC = Math.FusedMultiplyAdd(_oneMinusAlpha, s.C, _alpha * a);
|
||||
s.C = a < emaC ? a : emaC;
|
||||
|
||||
// Oscillator state: 1 = upper (bullish), 0 = lower (bearish)
|
||||
// skipcq: CS-R1085 - exact equality intentional for snap detection
|
||||
if (a == s.B)
|
||||
{
|
||||
s.Os = 1.0;
|
||||
}
|
||||
else if (a == s.C)
|
||||
{
|
||||
s.Os = 0.0;
|
||||
}
|
||||
|
||||
// Beta-weighted band combinations
|
||||
double upper = Math.FusedMultiplyAdd(_beta, s.B, _oneMinusBeta * s.C);
|
||||
double lower = Math.FusedMultiplyAdd(_beta, s.C, _oneMinusBeta * s.B);
|
||||
|
||||
// Final output: state-selected weighted band
|
||||
result = Math.FusedMultiplyAdd(s.Os, upper, (1.0 - s.Os) * lower);
|
||||
s.Ts = result;
|
||||
}
|
||||
|
||||
if (!double.IsNaN(val) && !double.IsInfinity(val))
|
||||
{
|
||||
s.LastValue = val;
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
TValue output = new(input.Time, result);
|
||||
Last = output;
|
||||
PubEvent(output, isNew);
|
||||
return output;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var tsResult = new TSeries();
|
||||
ReadOnlySpan<double> srcSpan = source.Values;
|
||||
double[] outArray = new double[srcSpan.Length];
|
||||
|
||||
Batch(srcSpan, outArray.AsSpan(), Period, _beta, _feedback, _fbWeight);
|
||||
|
||||
for (int i = 0; i < outArray.Length; i++)
|
||||
{
|
||||
tsResult.Add(new TValue(source.Times[i], outArray[i]));
|
||||
}
|
||||
|
||||
if (srcSpan.Length > 0)
|
||||
{
|
||||
int replayStart = Math.Max(0, srcSpan.Length - Math.Max(WarmupPeriod, 4));
|
||||
Reset();
|
||||
for (int i = replayStart; i < srcSpan.Length; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], srcSpan[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
return tsResult;
|
||||
}
|
||||
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output,
|
||||
int period, double beta = 0.8, bool feedback = false, double fbWeight = 0.5)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output lengths must match.", nameof(output));
|
||||
}
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 2.");
|
||||
}
|
||||
|
||||
double alpha = 2.0 / (period + 1);
|
||||
double oneMinusAlpha = 1.0 - alpha;
|
||||
double oneMinusBeta = 1.0 - beta;
|
||||
double oneMinusFbWeight = 1.0 - fbWeight;
|
||||
|
||||
double b = 0, c = 0, os = 0, ts = 0, lastVal = 0;
|
||||
bool initialized = false;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsNaN(val) || double.IsInfinity(val))
|
||||
{
|
||||
val = lastVal;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastVal = val;
|
||||
}
|
||||
|
||||
if (!initialized)
|
||||
{
|
||||
b = val;
|
||||
c = val;
|
||||
os = 0;
|
||||
ts = val;
|
||||
initialized = true;
|
||||
output[i] = val;
|
||||
continue;
|
||||
}
|
||||
|
||||
double a = feedback
|
||||
? Math.FusedMultiplyAdd(oneMinusFbWeight, ts, fbWeight * val)
|
||||
: val;
|
||||
|
||||
double emaB = Math.FusedMultiplyAdd(oneMinusAlpha, b, alpha * a);
|
||||
b = a > emaB ? a : emaB;
|
||||
|
||||
double emaC = Math.FusedMultiplyAdd(oneMinusAlpha, c, alpha * a);
|
||||
c = a < emaC ? a : emaC;
|
||||
|
||||
if (a == b)
|
||||
{
|
||||
os = 1.0;
|
||||
}
|
||||
else if (a == c)
|
||||
{
|
||||
os = 0.0;
|
||||
}
|
||||
|
||||
double upper = Math.FusedMultiplyAdd(beta, b, oneMinusBeta * c);
|
||||
double lower = Math.FusedMultiplyAdd(beta, c, oneMinusBeta * b);
|
||||
|
||||
ts = Math.FusedMultiplyAdd(os, upper, (1.0 - os) * lower);
|
||||
output[i] = ts;
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period, double beta = 0.8,
|
||||
bool feedback = false, double fbWeight = 0.5)
|
||||
{
|
||||
var result = new TSeries();
|
||||
ReadOnlySpan<double> srcSpan = source.Values;
|
||||
double[] outArray = new double[srcSpan.Length];
|
||||
|
||||
Batch(srcSpan, outArray.AsSpan(), period, beta, feedback, fbWeight);
|
||||
|
||||
for (int i = 0; i < outArray.Length; i++)
|
||||
{
|
||||
result.Add(new TValue(source.Times[i], outArray[i]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static (TSeries Results, Modf Indicator) Calculate(TSeries source,
|
||||
int period, double beta = 0.8, bool feedback = false, double fbWeight = 0.5)
|
||||
{
|
||||
var indicator = new Modf(period, beta, feedback, fbWeight);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _publisher != null && _handler != null)
|
||||
{
|
||||
_publisher.Pub -= _handler;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -7,37 +7,45 @@ indicator("Notch Filter (NOTCH)", "NOTCH", overlay=true)
|
||||
//@param src Input series
|
||||
//@param period The period of the cycle to remove (center frequency of the notch)
|
||||
//@param bandwidth The relative bandwidth of the notch (e.g., 0.1 for 10%)
|
||||
//@returns Filtered series with the specified frequency component attenuated
|
||||
//@optimized Uses 2nd order IIR notch filter with O(1) complexity per bar
|
||||
//@returns Filtered series with the specified frequency component attenuated to zero
|
||||
//@optimized Uses RBJ 2nd-order IIR notch (band-reject) design with O(1) complexity per bar
|
||||
notch(series float src, simple int period, simple float bandwidth) =>
|
||||
if period < 2
|
||||
runtime.error("Period must be >= 2")
|
||||
if bandwidth <= 0.0 or bandwidth >= 1.0
|
||||
runtime.error("Bandwidth must be > 0 and < 1")
|
||||
float omega = 2.0 * math.pi / period
|
||||
float bw_abs = bandwidth * omega
|
||||
float alpha_tan_arg = bw_abs / 2.0
|
||||
alpha_tan_arg := math.max(math.min(alpha_tan_arg, math.pi * 0.499), 0.0001)
|
||||
float tan_bw_half = math.tan(alpha_tan_arg)
|
||||
float alpha = (1.0 - tan_bw_half) / (1.0 + tan_bw_half)
|
||||
float beta = math.cos(omega)
|
||||
float a1 = -2.0 * beta * alpha
|
||||
float a2 = alpha * alpha
|
||||
float b0 = (1.0 + alpha * alpha) / 2.0
|
||||
float b1 = a1
|
||||
float b2 = b0
|
||||
|
||||
// RBJ Audio EQ Cookbook notch design
|
||||
// omega = center frequency in radians
|
||||
// Q = reciprocal of relative bandwidth
|
||||
// alpha = sin(omega) / (2*Q) — controls pole radius / bandwidth
|
||||
float omega = 2.0 * math.pi / period
|
||||
float cos_w = math.cos(omega)
|
||||
float sin_w = math.sin(omega)
|
||||
float Q = 1.0 / bandwidth
|
||||
float alpha = sin_w / (2.0 * Q)
|
||||
|
||||
// Zeros exactly on the unit circle at ±omega → H(e^jω₀) = 0 guaranteed
|
||||
// b1 = -2*cos(omega) is independent of alpha — this is the critical distinction
|
||||
float b0 = 1.0 / (1.0 + alpha)
|
||||
float b1 = -2.0 * cos_w / (1.0 + alpha)
|
||||
float b2 = 1.0 / (1.0 + alpha)
|
||||
float a1 = -2.0 * cos_w / (1.0 + alpha)
|
||||
float a2 = (1.0 - alpha) / (1.0 + alpha)
|
||||
|
||||
var float y1 = 0.0
|
||||
var float y2 = 0.0
|
||||
var float x1 = 0.0
|
||||
var float x2 = 0.0
|
||||
|
||||
float x0 = nz(src, 0.0)
|
||||
if bar_index < 1
|
||||
x1 := x0
|
||||
x2 := x0
|
||||
y1 := x0
|
||||
y2 := x0
|
||||
float y0 = 0.0
|
||||
y0 := b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2
|
||||
|
||||
float y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2
|
||||
y2 := y1
|
||||
y1 := y0
|
||||
x2 := x1
|
||||
@@ -47,12 +55,12 @@ notch(series float src, simple int period, simple float bandwidth) =>
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_period = input.int(14, "Period to Remove", minval=2)
|
||||
i_source = input.source(close, "Source")
|
||||
i_period = input.int(14, "Period to Remove", minval=2)
|
||||
i_bandwidth = input.float(0.3, "Relative Bandwidth", minval=0.01, maxval=0.99, step=0.01)
|
||||
|
||||
// Calculation
|
||||
notch_val = notch(i_source, i_period, i_bandwidth)
|
||||
|
||||
// Plot
|
||||
plot(notch_val, "Notch", color=color.yellow, linewidth=2)
|
||||
plot(notch_val, "Notch", color=color.yellow, linewidth=2)
|
||||
@@ -0,0 +1,132 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class NwIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void NwIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new NwIndicator();
|
||||
|
||||
Assert.Equal(64, indicator.Period);
|
||||
Assert.Equal(8.0, indicator.Bandwidth);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("NW - Nadaraya-Watson Estimator", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NwIndicator_MinHistoryDepths_EqualsExpectedValue()
|
||||
{
|
||||
var indicator = new NwIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(64, NwIndicator.MinHistoryDepths);
|
||||
Assert.Equal(64, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NwIndicator_ShortName_IncludesParametersAndSource()
|
||||
{
|
||||
var indicator = new NwIndicator { Period = 32, Bandwidth = 4.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("NW", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("32", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("4.0", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("Close", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NwIndicator_Initialize_CreatesInternalNw()
|
||||
{
|
||||
var indicator = new NwIndicator { Period = 10, Bandwidth = 4.0 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NwIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new NwIndicator { Period = 3, Bandwidth = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NwIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new NwIndicator { Period = 3, Bandwidth = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
// Process first update
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// Line series should have values
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NwIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new NwIndicator { Period = 3, Bandwidth = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Update with new tick (same bar data - simulates intrabar update)
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Both values should be finite
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NwIndicator_DifferentSourceTypes()
|
||||
{
|
||||
foreach (var sourceType in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new NwIndicator { Period = 5, Source = sourceType };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public class NwIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 64;
|
||||
|
||||
[InputParameter("Bandwidth", sortIndex: 2, 0.1, 100.0, 0.5, 1)]
|
||||
public double Bandwidth { get; set; } = 8.0;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Nw _ma = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 64;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"NW({Period},{Bandwidth:F1}):{_sourceName}";
|
||||
|
||||
public NwIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "NW - Nadaraya-Watson Estimator";
|
||||
Description = "Gaussian kernel-weighted FIR filter using Nadaraya-Watson regression.";
|
||||
_series = new LineSeries(name: $"NW {Period}", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_ma = new Nw(Period, Bandwidth);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
var input = new TValue(item.TimeLeft.Ticks, _priceSelector(item));
|
||||
double value = _ma.Update(input, isNew).Value;
|
||||
_series.SetValue(value, _ma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class NwTests
|
||||
{
|
||||
private readonly GBM _gbm = new(startPrice: 100, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
private readonly TSeries _data;
|
||||
|
||||
public NwTests()
|
||||
{
|
||||
_data = _gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
}
|
||||
|
||||
// ── A) Constructor validation ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodZero_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Nw(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Nw(-5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroBandwidth_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Nw(10, 0.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeBandwidth_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Nw(10, -1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsProperties()
|
||||
{
|
||||
var nw = new Nw(20, 5.0);
|
||||
Assert.Equal(20, nw.Period);
|
||||
Assert.Equal(5.0, nw.Bandwidth);
|
||||
Assert.Contains("Nw(", nw.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters()
|
||||
{
|
||||
var nw = new Nw();
|
||||
Assert.Equal(64, nw.Period);
|
||||
Assert.Equal(8.0, nw.Bandwidth);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodOne_Succeeds()
|
||||
{
|
||||
var nw = new Nw(1, 1.0);
|
||||
Assert.Equal(1, nw.Period);
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsValue()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
TValue result = nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.False(double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantInput_ReturnsConstant()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
nw.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
}
|
||||
Assert.Equal(42.0, nw.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ReturnsInput()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
TValue result = nw.Update(new TValue(DateTime.UtcNow, 50.0));
|
||||
// First bar: only one sample, weight is 1.0, so result = input
|
||||
Assert.Equal(50.0, result.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_IsAccessible()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(nw.Update(new TValue(DateTime.UtcNow, 200.0)).Value, nw.Last.Value);
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_Advances()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 200.0), isNew: true);
|
||||
double after2 = nw.Last.Value;
|
||||
// Two distinct bars processed, result should differ from single bar
|
||||
Assert.NotEqual(100.0, after2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_Rewrites()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 200.0), isNew: true);
|
||||
double before = nw.Last.Value;
|
||||
nw.Update(new TValue(DateTime.UtcNow, 150.0), isNew: false);
|
||||
double after = nw.Last.Value;
|
||||
Assert.NotEqual(before, after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0 + i), isNew: true);
|
||||
}
|
||||
double baseline = nw.Last.Value;
|
||||
|
||||
// Correct last bar multiple times, then restore original value
|
||||
nw.Update(new TValue(DateTime.UtcNow, 999.0), isNew: false);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 119.0), isNew: false); // original value
|
||||
double restored = nw.Last.Value;
|
||||
|
||||
Assert.Equal(baseline, restored, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
Assert.True(nw.IsHot);
|
||||
nw.Reset();
|
||||
Assert.False(nw.IsHot);
|
||||
}
|
||||
|
||||
// ── D) Warmup / convergence ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
int period = 10;
|
||||
var nw = new Nw(period, 3.0);
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
if (i < period - 1)
|
||||
{
|
||||
Assert.False(nw.IsHot);
|
||||
}
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
Assert.True(nw.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var nw = new Nw(20, 5.0);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
Assert.False(nw.IsHot);
|
||||
}
|
||||
|
||||
// ── E) Robustness ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValid()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
TValue result = nw.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.False(double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValid()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
TValue result = nw.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.False(double.IsInfinity(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_BatchNaN_Safe()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
TValue r = nw.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.False(double.IsNaN(r.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ── F) Consistency (4 API modes) ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
int period = 10;
|
||||
double bw = 3.0;
|
||||
var series = _data;
|
||||
|
||||
// Mode 1: Static Batch(TSeries)
|
||||
var batchResult = Nw.Batch(series, period, bw);
|
||||
|
||||
// Mode 2: Static Batch(Span)
|
||||
double[] spanResult = new double[series.Count];
|
||||
Nw.Batch(series.Values, spanResult.AsSpan(), period, bw);
|
||||
|
||||
// Mode 3: Instance Update(TSeries)
|
||||
var instance = new Nw(period, bw);
|
||||
var tseriesResult = instance.Update(series);
|
||||
|
||||
// Mode 4: Streaming Update(TValue)
|
||||
var streamingInstance = new Nw(period, bw);
|
||||
double[] streamResult = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamResult[i] = streamingInstance.Update(new TValue(series.Times[i], series.Values[i])).Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanResult[i], 10);
|
||||
Assert.Equal(batchResult[i].Value, tseriesResult[i].Value, 10);
|
||||
Assert.Equal(batchResult[i].Value, streamResult[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_LengthMismatch_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] dst = new double[5];
|
||||
Assert.Throws<ArgumentException>(() => Nw.Batch(src, dst, 5, 2.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] dst = new double[10];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Nw.Batch(src, dst, 0, 2.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidBandwidth_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] dst = new double[10];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => Nw.Batch(src, dst, 5, 0.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Empty_Succeeds()
|
||||
{
|
||||
double[] src = Array.Empty<double>();
|
||||
double[] dst = Array.Empty<double>();
|
||||
Nw.Batch(src, dst, 5, 2.0);
|
||||
Assert.Empty(dst);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesTSeries()
|
||||
{
|
||||
int period = 10;
|
||||
double bw = 3.0;
|
||||
var series = _data;
|
||||
|
||||
var tseriesResult = Nw.Batch(series, period, bw);
|
||||
|
||||
double[] spanOut = new double[series.Count];
|
||||
Nw.Batch(series.Values, spanOut.AsSpan(), period, bw);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, spanOut[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NaN_Safe()
|
||||
{
|
||||
double[] src = { 1.0, 2.0, double.NaN, 4.0, 5.0 };
|
||||
double[] dst = new double[5];
|
||||
Nw.Batch(src, dst, 3, 1.0);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Assert.False(double.IsNaN(dst[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeData_NoStackOverflow()
|
||||
{
|
||||
int len = 10000;
|
||||
double[] src = new double[len];
|
||||
double[] dst = new double[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
src[i] = 100.0 + i * 0.01;
|
||||
}
|
||||
Nw.Batch(src, dst, 500, 50.0); // period > StackallocThreshold
|
||||
Assert.False(double.IsNaN(dst[len - 1]));
|
||||
}
|
||||
|
||||
// ── H) Chainability ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Pub_Fires()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
int count = 0;
|
||||
nw.Pub += (object? _, in TValueEventArgs _) => count++;
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPublisher_Subscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var nw = new Nw(source, 10, 3.0);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.False(double.IsNaN(nw.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithPublisher_Unsubscribes()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var nw = new Nw(source, 10, 3.0);
|
||||
double lastBefore = nw.Last.Value;
|
||||
nw.Dispose();
|
||||
source.Add(new TValue(DateTime.UtcNow, 200.0));
|
||||
Assert.Equal(lastBefore, nw.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_WithoutPublisher_DoesNotThrow()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
double lastBefore = nw.Last.Value;
|
||||
nw.Dispose();
|
||||
Assert.Equal(lastBefore, nw.Last.Value);
|
||||
}
|
||||
|
||||
// ── Additional: Filter behavior ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SmallBandwidth_TracksPrice()
|
||||
{
|
||||
// Small h → tight kernel → output close to most recent value
|
||||
var nw = new Nw(20, 0.5);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
nw.Update(new TValue(DateTime.UtcNow, 200.0));
|
||||
// With h=0.5 and period=20, weights decay fast — output dominated by newest bar
|
||||
Assert.True(nw.Last.Value > 150.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeBandwidth_Smooths()
|
||||
{
|
||||
// Large h → wide kernel → output close to average
|
||||
var nw = new Nw(20, 100.0);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
nw.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
nw.Update(new TValue(DateTime.UtcNow, 200.0), isNew: true);
|
||||
// With h=100 and period=20, all weights nearly equal → nearly SMA
|
||||
double expected = (100.0 * 19 + 200.0) / 20.0; // ~105
|
||||
Assert.True(Math.Abs(nw.Last.Value - expected) < 5.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentBandwidths_ProduceDifferentOutputs()
|
||||
{
|
||||
var narrow = new Nw(20, 1.0);
|
||||
var wide = new Nw(20, 50.0);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double v = 100.0 + i;
|
||||
narrow.Update(new TValue(DateTime.UtcNow, v));
|
||||
wide.Update(new TValue(DateTime.UtcNow, v));
|
||||
}
|
||||
|
||||
Assert.NotEqual(narrow.Last.Value, wide.Last.Value, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoInstances_SameInput_SameOutput()
|
||||
{
|
||||
var n1 = new Nw(10, 3.0);
|
||||
var n2 = new Nw(10, 3.0);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double v = 100.0 + Math.Sin(i * 0.3) * 10.0;
|
||||
n1.Update(new TValue(DateTime.UtcNow, v));
|
||||
n2.Update(new TValue(DateTime.UtcNow, v));
|
||||
}
|
||||
Assert.Equal(n1.Last.Value, n2.Last.Value, 12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var (results, indicator) = Nw.Calculate(_data, 10, 3.0);
|
||||
Assert.Equal(_data.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EmptyTSeries_ReturnsEmpty()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
var empty = new TSeries();
|
||||
var result = nw.Update(empty);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_WarmsUpIndicator()
|
||||
{
|
||||
var nw = new Nw(10, 3.0);
|
||||
double[] data = new double[15];
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
data[i] = 100.0 + i;
|
||||
}
|
||||
nw.Prime(data);
|
||||
Assert.True(nw.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class NwValidationTests : IDisposable
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly TBarSeries _bars;
|
||||
private bool _disposed;
|
||||
|
||||
public NwValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
_bars = gbm.Fetch(5000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_MatchesSpan()
|
||||
{
|
||||
int period = 20;
|
||||
double bw = 5.0;
|
||||
var data = _bars.Close;
|
||||
var batchTs = Nw.Batch(data, period, bw);
|
||||
|
||||
double[] spanOut = new double[data.Count];
|
||||
Nw.Batch(data.Values, spanOut.AsSpan(), period, bw);
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchTs[i].Value, spanOut[i], 12);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Batch TSeries vs Span: {data.Count} bars matched at 12-digit precision.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsCorrectCount()
|
||||
{
|
||||
int period = 20;
|
||||
double bw = 5.0;
|
||||
var data = _bars.Close;
|
||||
var (results, indicator) = Nw.Calculate(data, period, bw);
|
||||
|
||||
Assert.Equal(data.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
_output.WriteLine($"Calculate: {results.Count} results, indicator is hot.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ReturnsConstant()
|
||||
{
|
||||
int period = 20;
|
||||
double bw = 5.0;
|
||||
double constVal = 42.0;
|
||||
double[] src = new double[100];
|
||||
double[] dst = new double[100];
|
||||
Array.Fill(src, constVal);
|
||||
Nw.Batch(src, dst, period, bw);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(constVal, dst[i], 10);
|
||||
}
|
||||
_output.WriteLine("Constant input: all outputs match constant value.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeBandwidth_ApproachesSMA()
|
||||
{
|
||||
// With very large h, all weights are nearly equal → approaches SMA
|
||||
int period = 10;
|
||||
double bw = 1000.0;
|
||||
var data = _bars.Close;
|
||||
double[] nwOut = new double[data.Count];
|
||||
Nw.Batch(data.Values, nwOut.AsSpan(), period, bw);
|
||||
|
||||
// Compare with manual SMA for bars ≥ period
|
||||
int startBar = period;
|
||||
for (int t = startBar; t < Math.Min(data.Count, startBar + 100); t++)
|
||||
{
|
||||
double smaSum = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
smaSum += data.Values[t - i];
|
||||
}
|
||||
double sma = smaSum / period;
|
||||
Assert.Equal(sma, nwOut[t], 2); // Within 2 decimal places
|
||||
}
|
||||
_output.WriteLine($"Large bandwidth (h={bw}): NW ≈ SMA within 2 decimals.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch()
|
||||
{
|
||||
int period = 20;
|
||||
double bw = 5.0;
|
||||
var data = _bars.Close;
|
||||
|
||||
// Batch
|
||||
double[] batchOut = new double[data.Count];
|
||||
Nw.Batch(data.Values, batchOut.AsSpan(), period, bw);
|
||||
|
||||
// Streaming
|
||||
var nw = new Nw(period, bw);
|
||||
double[] streamOut = new double[data.Count];
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
streamOut[i] = nw.Update(new TValue(data.Times[i], data.Values[i])).Value;
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchOut[i], streamOut[i], 10);
|
||||
}
|
||||
_output.WriteLine($"Streaming vs Batch: {data.Count} bars match at 10-digit precision.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentBandwidths_OrderedSmoothing()
|
||||
{
|
||||
var data = _bars.Close;
|
||||
double[] out2 = new double[data.Count];
|
||||
double[] out20 = new double[data.Count];
|
||||
Nw.Batch(data.Values, out2.AsSpan(), 30, 2.0);
|
||||
Nw.Batch(data.Values, out20.AsSpan(), 30, 20.0);
|
||||
|
||||
// Wider bandwidth should produce smoother output (lower variance in diffs)
|
||||
double var2 = 0, var20 = 0;
|
||||
int n = data.Count - 1;
|
||||
for (int i = 1; i < data.Count; i++)
|
||||
{
|
||||
double d2 = out2[i] - out2[i - 1];
|
||||
double d20 = out20[i] - out20[i - 1];
|
||||
var2 += d2 * d2;
|
||||
var20 += d20 * d20;
|
||||
}
|
||||
var2 /= n;
|
||||
var20 /= n;
|
||||
|
||||
Assert.True(var20 < var2, $"Wider bandwidth should be smoother: var(h=20)={var20:E4} < var(h=2)={var2:E4}");
|
||||
_output.WriteLine($"Bandwidth ordering: var(h=2)={var2:E4} > var(h=20)={var20:E4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_Manual_Calc()
|
||||
{
|
||||
// Manual NW calculation for small dataset
|
||||
double[] src = { 10.0, 20.0, 30.0, 40.0, 50.0 };
|
||||
double[] dst = new double[5];
|
||||
int period = 3;
|
||||
double h = 1.0;
|
||||
Nw.Batch(src, dst, period, h);
|
||||
|
||||
// Bar 0: only src[0], w0=1.0 → result = 10.0
|
||||
Assert.Equal(10.0, dst[0], 10);
|
||||
|
||||
// Bar 1: src[1] with w0=1.0, src[0] with w1=exp(-1/(2*1))=exp(-0.5)
|
||||
double w0 = 1.0;
|
||||
double w1 = Math.Exp(-0.5);
|
||||
double expected1 = (w0 * 20.0 + w1 * 10.0) / (w0 + w1);
|
||||
Assert.Equal(expected1, dst[1], 10);
|
||||
|
||||
// Bar 2: src[2] w0=1, src[1] w1=exp(-0.5), src[0] w2=exp(-4/2)=exp(-2)
|
||||
double w2 = Math.Exp(-2.0);
|
||||
double expected2 = (w0 * 30.0 + w1 * 20.0 + w2 * 10.0) / (w0 + w1 + w2);
|
||||
Assert.Equal(expected2, dst[2], 10);
|
||||
|
||||
_output.WriteLine($"Manual calc: bar0={dst[0]:F6}, bar1={dst[1]:F6} (expect {expected1:F6}), bar2={dst[2]:F6} (expect {expected2:F6})");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class Nw : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _bandwidth;
|
||||
private readonly double[] _weights;
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly ITValuePublisher? _publisher;
|
||||
private readonly TValuePublishedHandler? _handler;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValid, int Count);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
public int Period => _period;
|
||||
public double Bandwidth => _bandwidth;
|
||||
public override bool IsHot => _s.Count >= _period;
|
||||
|
||||
public Nw(int period = 64, double bandwidth = 8.0)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
|
||||
}
|
||||
if (bandwidth <= 0.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bandwidth), "Bandwidth must be > 0.");
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_bandwidth = bandwidth;
|
||||
_buffer = new RingBuffer(period);
|
||||
WarmupPeriod = period;
|
||||
Name = $"Nw({period},{bandwidth:F1})";
|
||||
|
||||
// Precompute Gaussian kernel weights: w[i] = exp(-i^2 / (2*h^2))
|
||||
_weights = new double[period];
|
||||
double h2x2 = 2.0 * bandwidth * bandwidth;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
_weights[i] = Math.Exp(-((double)i * i) / h2x2);
|
||||
}
|
||||
|
||||
Init();
|
||||
}
|
||||
|
||||
public Nw(ITValuePublisher source, int period = 64, double bandwidth = 8.0)
|
||||
: this(period, bandwidth)
|
||||
{
|
||||
_publisher = source;
|
||||
_handler = Sub;
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
private void Sub(object? source, in TValueEventArgs args)
|
||||
{
|
||||
Update(args.Value, args.IsNew);
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_s = default;
|
||||
_ps = default;
|
||||
_buffer.Clear();
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// skipcq: CS-R1140 - NW reads individual buffer positions; cannot use Snapshot/Restore
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
double val = input.Value;
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
val = double.IsFinite(s.LastValid) ? s.LastValid : 0.0;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_buffer.Add(val);
|
||||
s.Count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer.UpdateNewest(val);
|
||||
}
|
||||
|
||||
// Nadaraya-Watson: weighted average with Gaussian kernel
|
||||
// _buffer[0] = oldest, _buffer[Count-1] = newest
|
||||
// weights[0] = newest weight (1.0), weights[i] = i bars ago
|
||||
int bufCount = _buffer.Count;
|
||||
int bars = Math.Min(bufCount, _period);
|
||||
int newestIdx = bufCount - 1;
|
||||
double num = 0.0;
|
||||
double den = 0.0;
|
||||
|
||||
for (int i = 0; i < bars; i++)
|
||||
{
|
||||
double w = _weights[i];
|
||||
double sample = _buffer[newestIdx - i];
|
||||
num = Math.FusedMultiplyAdd(w, sample, num);
|
||||
den += w;
|
||||
}
|
||||
|
||||
double result = den > 0.0 ? num / den : val;
|
||||
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
s.LastValid = val;
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
TValue output = new(input.Time, result);
|
||||
Last = output;
|
||||
PubEvent(output, isNew);
|
||||
return output;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var tsResult = new TSeries();
|
||||
ReadOnlySpan<double> srcSpan = source.Values;
|
||||
double[] outArray = new double[srcSpan.Length];
|
||||
|
||||
Batch(srcSpan, outArray.AsSpan(), _period, _bandwidth);
|
||||
|
||||
for (int i = 0; i < outArray.Length; i++)
|
||||
{
|
||||
tsResult.Add(new TValue(source.Times[i], outArray[i]));
|
||||
}
|
||||
|
||||
if (srcSpan.Length > 0)
|
||||
{
|
||||
int replayStart = Math.Max(0, srcSpan.Length - Math.Max(WarmupPeriod, 4));
|
||||
Reset();
|
||||
for (int i = replayStart; i < srcSpan.Length; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], srcSpan[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
return tsResult;
|
||||
}
|
||||
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output,
|
||||
int period = 64, double bandwidth = 8.0)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output lengths must match.", nameof(output));
|
||||
}
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
|
||||
}
|
||||
if (bandwidth <= 0.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bandwidth), "Bandwidth must be > 0.");
|
||||
}
|
||||
|
||||
// Precompute Gaussian weights
|
||||
const int StackallocThreshold = 256;
|
||||
double[]? rentedW = null;
|
||||
scoped Span<double> weights;
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
weights = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedW = ArrayPool<double>.Shared.Rent(period);
|
||||
weights = rentedW.AsSpan(0, period);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double h2x2 = 2.0 * bandwidth * bandwidth;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
weights[i] = Math.Exp(-((double)i * i) / h2x2);
|
||||
}
|
||||
|
||||
// Pre-clean source: replace NaN/Infinity with last-valid
|
||||
// (matches streaming behavior where buffer stores cleaned values)
|
||||
const int CleanThreshold = 256;
|
||||
double[]? rentedClean = null;
|
||||
scoped Span<double> clean;
|
||||
if (source.Length <= CleanThreshold)
|
||||
{
|
||||
clean = stackalloc double[source.Length];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedClean = ArrayPool<double>.Shared.Rent(source.Length);
|
||||
clean = rentedClean.AsSpan(0, source.Length);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double lastVal = 0.0;
|
||||
for (int t = 0; t < source.Length; t++)
|
||||
{
|
||||
double val = source[t];
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
val = lastVal;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastVal = val;
|
||||
}
|
||||
clean[t] = val;
|
||||
}
|
||||
|
||||
for (int t = 0; t < source.Length; t++)
|
||||
{
|
||||
int bars = Math.Min(t + 1, period);
|
||||
double num = 0.0;
|
||||
double den = 0.0;
|
||||
|
||||
for (int i = 0; i < bars; i++)
|
||||
{
|
||||
double w = weights[i];
|
||||
double sample = clean[t - i];
|
||||
num = Math.FusedMultiplyAdd(w, sample, num);
|
||||
den += w;
|
||||
}
|
||||
|
||||
output[t] = den > 0.0 ? num / den : clean[t];
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedClean != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedClean);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedW != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period = 64, double bandwidth = 8.0)
|
||||
{
|
||||
var result = new TSeries();
|
||||
ReadOnlySpan<double> srcSpan = source.Values;
|
||||
double[] outArray = new double[srcSpan.Length];
|
||||
|
||||
Batch(srcSpan, outArray.AsSpan(), period, bandwidth);
|
||||
|
||||
for (int i = 0; i < outArray.Length; i++)
|
||||
{
|
||||
result.Add(new TValue(source.Times[i], outArray[i]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static (TSeries Results, Nw Indicator) Calculate(TSeries source,
|
||||
int period = 64, double bandwidth = 8.0)
|
||||
{
|
||||
var indicator = new Nw(period, bandwidth);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _publisher != null && _handler != null)
|
||||
{
|
||||
_publisher.Pub -= _handler;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ indicator("NW - Nadaraya-Watson Kernel Regression", "NW", overlay=true)
|
||||
// @param period Lookback window (must be > 0)
|
||||
// @param bandwidth Gaussian kernel bandwidth h (must be > 0)
|
||||
// @returns NW kernel regression estimate
|
||||
export nw(series float source, simple int period, simple float bandwidth) =>
|
||||
nw(series float source, simple int period, simple float bandwidth) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
if bandwidth <= 0
|
||||
@@ -48,7 +48,7 @@ export nw(series float source, simple int period, simple float bandwidth) =>
|
||||
// ── Inputs ─────────────────────────────────────────────────────────────
|
||||
int i_period = input.int(64, "Period", minval=1)
|
||||
float i_bandwidth = input.float(8.0, "Bandwidth (h)", minval=0.1, step=0.5)
|
||||
string i_source = input.source(close, "Source")
|
||||
float i_source = input.source(close, "Source")
|
||||
|
||||
// ── Calculation ────────────────────────────────────────────────────────
|
||||
float value = nw(i_source, i_period, i_bandwidth)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//@version=6
|
||||
indicator("One Euro Filter (ONEEURO)", "ONEEURO", overlay=true)
|
||||
|
||||
//@function One Euro adaptive low-pass filter
|
||||
//@function One Euro adaptive low-pass filter — trades jitter suppression
|
||||
// against responsiveness. At low signal speed, uses a low cutoff frequency
|
||||
// for heavy smoothing (reduces jitter). At high speed, raises the cutoff
|
||||
// to reduce lag. Only two meaningful parameters to tune.
|
||||
@@ -13,22 +13,22 @@ indicator("One Euro Filter (ONEEURO)", "ONEEURO", overlay=true)
|
||||
// α = r / (r + 1)
|
||||
//
|
||||
// Per-bar algorithm (T_e = 1.0 for uniform bar spacing):
|
||||
// 1. dx = x - x̂_prev (raw derivative)
|
||||
// 2. d̂x = α_d · dx + (1 - α_d) · d̂x_prev (smoothed derivative)
|
||||
// 3. f_c = minCutoff + β · |d̂x| (adaptive cutoff)
|
||||
// 4. α = smoothingFactor(f_c)
|
||||
// 5. x̂ = α · x + (1 - α) · x̂_prev (filtered output)
|
||||
// 1. dx = x - x_prev (raw derivative)
|
||||
// 2. d̂x = α_d · dx + (1 - α_d) · d̂x_prev (smoothed derivative)
|
||||
// 3. f_c = minCutoff + β · |d̂x| (adaptive cutoff)
|
||||
// 4. α = smoothingFactor(f_c)
|
||||
// 5. x̂ = α · x + (1 - α) · x̂_prev (filtered output)
|
||||
//
|
||||
// Reference: Casiez, Roussel & Vogel (2012), "1€ Filter: A Simple
|
||||
// Speed-Based Low-Pass Filter for Noisy Input in Interactive Systems,"
|
||||
// CHI '12, pp. 2527-2530. DOI: 10.1145/2207676.2208639
|
||||
// CHI '12, pp. 2527–2530. DOI: 10.1145/2207676.2208639
|
||||
//
|
||||
//@param src Input series
|
||||
//@param src Input series
|
||||
//@param minCutoff Minimum cutoff frequency. Lower = smoother at low speed.
|
||||
//@param beta Speed coefficient. Higher = faster response to rapid moves.
|
||||
//@param dCutoff Cutoff frequency for the derivative estimator.
|
||||
//@param beta Speed coefficient. Higher = faster response to rapid moves.
|
||||
//@param dCutoff Cutoff frequency for the derivative estimator.
|
||||
//@returns Filtered series (overlay, tracks price)
|
||||
//@optimized O(1) per bar, O(1) memory — 3 state variables (x̂, d̂x, prev).
|
||||
//@optimized O(1) per bar, O(1) memory — 4 state variables (x̂, d̂x, x_prev, init flag)
|
||||
oneeuro(series float src, simple float minCutoff, simple float beta, simple float dCutoff) =>
|
||||
if minCutoff <= 0.0
|
||||
runtime.error("minCutoff must be > 0")
|
||||
@@ -39,25 +39,28 @@ oneeuro(series float src, simple float minCutoff, simple float beta, simple floa
|
||||
|
||||
var float xHat = na
|
||||
var float dxHat = 0.0
|
||||
var float xPrev = na // ← previous RAW input, not filtered output
|
||||
|
||||
if bar_index == 0 or na(xHat)
|
||||
xHat := nz(src, 0.0)
|
||||
xPrev := nz(src, 0.0) // ← initialise alongside xHat
|
||||
dxHat := 0.0
|
||||
else
|
||||
// Smoothing factor: α(f_c) = r / (r + 1), r = 2π · f_c · T_e
|
||||
// T_e = 1.0 for uniform bars
|
||||
|
||||
// Step 1-3: smoothed derivative
|
||||
float rD = 2.0 * math.pi * dCutoff
|
||||
// Derivative smoothing factor — constant, depends only on dCutoff
|
||||
float rD = 2.0 * math.pi * dCutoff
|
||||
float alphaD = rD / (rD + 1.0)
|
||||
float dx = src - xHat
|
||||
dxHat := alphaD * dx + (1.0 - alphaD) * dxHat
|
||||
|
||||
// Step 4-5: adaptive cutoff and filter
|
||||
// Step 1-2: raw derivative from consecutive RAW inputs, then smooth it
|
||||
float dx = src - xPrev // ← fix: raw-to-raw, no filter feedback
|
||||
dxHat := alphaD * dx + (1.0 - alphaD) * dxHat
|
||||
|
||||
// Step 3-5: adaptive cutoff driven by smoothed speed, then filter
|
||||
float fc = minCutoff + beta * math.abs(dxHat)
|
||||
float rC = 2.0 * math.pi * fc
|
||||
float alpha = rC / (rC + 1.0)
|
||||
xHat := alpha * src + (1.0 - alpha) * xHat
|
||||
xHat := alpha * src + (1.0 - alpha) * xHat
|
||||
|
||||
xPrev := src // ← store raw input for next bar's derivative
|
||||
|
||||
xHat
|
||||
|
||||
@@ -75,4 +78,4 @@ i_dCutoff = input.float(1.0, "Derivative Cutoff", minval=0.001, step=0.1,
|
||||
float filtered = oneeuro(i_source, i_minCutoff, i_beta, i_dCutoff)
|
||||
|
||||
// ─── Plot ───
|
||||
plot(filtered, "OneEuro", color.new(color.teal, 0), 2)
|
||||
plot(filtered, "OneEuro", color.new(color.yellow, 0), 2)
|
||||
@@ -0,0 +1,118 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class RmedIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RmedIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new RmedIndicator();
|
||||
|
||||
Assert.Equal(12, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RMED - Ehlers Recursive Median Filter", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmedIndicator_MinHistoryDepths_EqualsExpectedValue()
|
||||
{
|
||||
var indicator = new RmedIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(12, RmedIndicator.MinHistoryDepths);
|
||||
Assert.Equal(12, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmedIndicator_ShortName_IncludesParametersAndSource()
|
||||
{
|
||||
var indicator = new RmedIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("RMED", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("Close", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmedIndicator_Initialize_CreatesInternalRmed()
|
||||
{
|
||||
var indicator = new RmedIndicator { Period = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmedIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RmedIndicator { 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 RmedIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new RmedIndicator { 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 RmedIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new RmedIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RmedIndicator_DifferentSourceTypes()
|
||||
{
|
||||
foreach (var sourceType in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new RmedIndicator { Period = 5, Source = sourceType };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public class RmedIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 12;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Rmed _ma = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 12;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"RMED({Period}):{_sourceName}";
|
||||
|
||||
public RmedIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "RMED - Ehlers Recursive Median Filter";
|
||||
Description = "Nonlinear IIR filter applying EMA smoothing to a 5-bar running median.";
|
||||
_series = new LineSeries(name: $"RMED {Period}", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_ma = new Rmed(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
var input = new TValue(item.TimeLeft.Ticks, _priceSelector(item));
|
||||
double value = _ma.Update(input, isNew).Value;
|
||||
_series.SetValue(value, _ma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RmedTests
|
||||
{
|
||||
private readonly GBM _gbm = new(startPrice: 100, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
|
||||
// ── A) Constructor validation ──
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodLessThanOne_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Rmed(0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_PeriodOne_Succeeds()
|
||||
{
|
||||
var rmed = new Rmed(1);
|
||||
Assert.Equal(1, rmed.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_Is12()
|
||||
{
|
||||
var rmed = new Rmed();
|
||||
Assert.Equal(12, rmed.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Name_ContainsPeriod()
|
||||
{
|
||||
var rmed = new Rmed(20);
|
||||
Assert.Contains("20", rmed.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("Rmed", rmed.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Alpha_IsInValidRange()
|
||||
{
|
||||
var rmed = new Rmed(12);
|
||||
Assert.InRange(rmed.Alpha, 0.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_Alpha_Is1()
|
||||
{
|
||||
// When period=1, alpha should be clamped to 1.0 (cos(2π) + sin(2π) - 1) / cos(2π) = 1
|
||||
var rmed = new Rmed(1);
|
||||
Assert.InRange(rmed.Alpha, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ──
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
var result = rmed.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ReturnsInputValue()
|
||||
{
|
||||
var rmed = new Rmed(12);
|
||||
var result = rmed.Update(new TValue(DateTime.UtcNow, 42.0));
|
||||
Assert.Equal(42.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_IsAccessible_AfterUpdate()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
rmed.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(double.IsFinite(rmed.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_IsAccessible()
|
||||
{
|
||||
var rmed = new Rmed(10);
|
||||
Assert.False(string.IsNullOrEmpty(rmed.Name));
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
TSeries src = _gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
double prev = 0;
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
var result = rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true);
|
||||
if (i > 0)
|
||||
{
|
||||
// Values should generally differ as price changes
|
||||
_ = result.Value;
|
||||
}
|
||||
prev = result.Value;
|
||||
}
|
||||
Assert.True(double.IsFinite(prev));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_RollsBackState()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
TSeries src = _gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
// Feed warmup
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Capture state before correction
|
||||
var beforeCorrection = rmed.Update(new TValue(src.Times[7], src.Values[7]), isNew: true);
|
||||
|
||||
// Correct the bar (isNew=false with different value)
|
||||
var corrected = rmed.Update(new TValue(src.Times[7], src.Values[7] + 50.0), isNew: false);
|
||||
|
||||
// They should differ since different input
|
||||
Assert.NotEqual(beforeCorrection.Value, corrected.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreState()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
TSeries src = _gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
// Feed all bars
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true);
|
||||
}
|
||||
double finalValue = rmed.Last.Value;
|
||||
|
||||
// Correct last bar multiple times, then restore original
|
||||
rmed.Update(new TValue(src.Times[^1], 999.0), isNew: false);
|
||||
rmed.Update(new TValue(src.Times[^1], 888.0), isNew: false);
|
||||
var restored = rmed.Update(new TValue(src.Times[^1], src.Values[^1]), isNew: false);
|
||||
|
||||
Assert.Equal(finalValue, restored.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
TSeries src = _gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
rmed.Reset();
|
||||
Assert.False(rmed.IsHot);
|
||||
|
||||
// After reset, first bar should return input
|
||||
var result = rmed.Update(new TValue(DateTime.UtcNow, 50.0));
|
||||
Assert.Equal(50.0, result.Value);
|
||||
}
|
||||
|
||||
// ── D) Warmup/convergence ──
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterWarmup()
|
||||
{
|
||||
var rmed = new Rmed(12);
|
||||
Assert.False(rmed.IsHot);
|
||||
|
||||
TSeries src = _gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true);
|
||||
if (i < 4)
|
||||
{
|
||||
Assert.False(rmed.IsHot);
|
||||
}
|
||||
}
|
||||
// After 5+ bars (MedianWindow), should be hot
|
||||
Assert.True(rmed.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_Equals_5()
|
||||
{
|
||||
var rmed = new Rmed(20);
|
||||
Assert.Equal(5, rmed.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── E) Robustness ──
|
||||
|
||||
[Fact]
|
||||
public void NaN_UsesLastValidValue()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
rmed.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 102.0));
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 104.0));
|
||||
|
||||
var nanResult = rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(3), double.NaN));
|
||||
Assert.True(double.IsFinite(nanResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_UsesLastValidValue()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
rmed.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 102.0));
|
||||
|
||||
var infResult = rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(2), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(infResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_DoesNotCorruptStream()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
TSeries src = _gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
// Inject NaN at various positions
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
double val = (i == 5 || i == 10 || i == 15) ? double.NaN : src.Values[i];
|
||||
var result = rmed.Update(new TValue(src.Times[i], val), isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// ── F) Consistency (4 modes must match) ──
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
const int period = 10;
|
||||
TSeries src = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var rmedStreaming = new Rmed(period);
|
||||
double[] streaming = new double[src.Count];
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
streaming[i] = rmedStreaming.Update(new TValue(src.Times[i], src.Values[i]), isNew: true).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch TSeries
|
||||
TSeries batchTs = Rmed.Batch(src, period);
|
||||
|
||||
// Mode 3: Batch Span
|
||||
double[] spanOut = new double[src.Count];
|
||||
Rmed.Batch(src.Values, spanOut.AsSpan(), period);
|
||||
|
||||
// Mode 4: Event-driven
|
||||
var rmedEvent = new Rmed(period);
|
||||
double[] evented = new double[src.Count];
|
||||
int eventCount = 0;
|
||||
rmedEvent.Pub += (object? _, in TValueEventArgs args) =>
|
||||
{
|
||||
if (args.IsNew && eventCount < evented.Length)
|
||||
{
|
||||
evented[eventCount++] = args.Value.Value;
|
||||
}
|
||||
};
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
rmedEvent.Update(new TValue(src.Times[i], src.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Compare all 4 modes
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
Assert.Equal(streaming[i], batchTs.Values[i], 10);
|
||||
Assert.Equal(streaming[i], spanOut[i], 10);
|
||||
Assert.Equal(streaming[i], evented[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_MatchesBatch()
|
||||
{
|
||||
const int period = 12;
|
||||
TSeries src = _gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
// Streaming
|
||||
var rmed = new Rmed(period);
|
||||
double[] streaming = new double[src.Count];
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
streaming[i] = rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] batch = new double[src.Count];
|
||||
Rmed.Batch(src.Values, batch.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
Assert.Equal(streaming[i], batch[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = [0, 0];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Rmed.Batch(src.AsSpan(), output.AsSpan(), 5));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_InvalidPeriod_Throws()
|
||||
{
|
||||
double[] src = [1, 2, 3];
|
||||
double[] output = [0, 0, 0];
|
||||
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => Rmed.Batch(src.AsSpan(), output.AsSpan(), 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NaN_DoesNotCorrupt()
|
||||
{
|
||||
double[] src = [100, 101, double.NaN, 103, 104, 105, 106, 107];
|
||||
double[] output = new double[src.Length];
|
||||
|
||||
Rmed.Batch(src.AsSpan(), output.AsSpan(), 5);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeData_DoesNotStackOverflow()
|
||||
{
|
||||
const int size = 10_000;
|
||||
TSeries src = _gbm.Fetch(size, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
double[] output = new double[size];
|
||||
|
||||
Rmed.Batch(src.Values, output.AsSpan(), 20);
|
||||
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
// ── H) Chainability ──
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
int count = 0;
|
||||
rmed.Pub += (object? _, in TValueEventArgs _) => count++;
|
||||
|
||||
rmed.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
|
||||
Assert.Equal(2, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var src = new TSeries();
|
||||
var rmed = new Rmed(src, 5);
|
||||
|
||||
TSeries data = _gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
src.Add(new TValue(data.Times[i], data.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(rmed.IsHot);
|
||||
Assert.True(double.IsFinite(rmed.Last.Value));
|
||||
}
|
||||
|
||||
// ── Additional: Spike rejection ──
|
||||
|
||||
[Fact]
|
||||
public void SingleSpike_IsRejected()
|
||||
{
|
||||
// Feed 10 bars at ~100, inject one massive spike, verify output barely moves
|
||||
var rmed = new Rmed(5);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0), isNew: true);
|
||||
}
|
||||
_ = rmed.Last.Value;
|
||||
|
||||
// Inject spike
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(5), 10000.0), isNew: true);
|
||||
double afterSpike = rmed.Last.Value;
|
||||
|
||||
// The median rejects the spike, so the EMA shouldn't jump to 10000
|
||||
// With 5-bar median of [100,100,100,100,10000], median = 100
|
||||
// So afterSpike ≈ alpha*100 + (1-alpha)*beforeSpike, roughly still near 100
|
||||
Assert.True(afterSpike < 500.0, $"Spike should be rejected, got {afterSpike}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoConsecutiveSpikes_AreRejected()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0), isNew: true);
|
||||
}
|
||||
|
||||
// Two consecutive spikes — still within the breakdown point of 5-bar median
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(5), 10000.0), isNew: true);
|
||||
rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(6), 10000.0), isNew: true);
|
||||
double afterSpikes = rmed.Last.Value;
|
||||
|
||||
// Median of [100,100,100,10000,10000] = 100 → spike rejected
|
||||
Assert.True(afterSpikes < 500.0, $"Two spikes should be rejected, got {afterSpikes}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var rmed = new Rmed(10);
|
||||
TValue result = default;
|
||||
// Feed 50 bars of constant 50.0 — buffer zeros pollute median for first 4 bars,
|
||||
// then IIR converges. After enough bars the output must match the input.
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
result = rmed.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 50.0), isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
// After 50 bars of constant input, EMA must have converged to 50.0
|
||||
Assert.Equal(50.0, result.Value, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_Works()
|
||||
{
|
||||
TSeries src = _gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
var rmed = new Rmed(10);
|
||||
TSeries result = rmed.Update(src);
|
||||
|
||||
Assert.Equal(src.Count, result.Count);
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Values[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsTupleWithIndicator()
|
||||
{
|
||||
TSeries src = _gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
var (results, indicator) = Rmed.Calculate(src, 10);
|
||||
|
||||
Assert.Equal(src.Count, results.Count);
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_PopulatesState()
|
||||
{
|
||||
var rmed = new Rmed(5);
|
||||
TSeries src = _gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
rmed.Prime(src.Values);
|
||||
Assert.True(rmed.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_UnsubscribesFromSource()
|
||||
{
|
||||
TSeries src = _gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
var rmed = new Rmed(src, 5);
|
||||
double lastBeforeDispose = rmed.Last.Value;
|
||||
|
||||
rmed.Dispose();
|
||||
|
||||
// Adding more data to source should not affect the disposed indicator
|
||||
src.Add(new TValue(DateTime.UtcNow.AddMinutes(100), 999.0));
|
||||
Assert.Equal(lastBeforeDispose, rmed.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class RmedValidationTests : IDisposable
|
||||
{
|
||||
private readonly GBM _gbm = new(startPrice: 100, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public RmedValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rmed_SelfConsistency_StreamingMatchesBatch()
|
||||
{
|
||||
const int period = 12;
|
||||
TSeries src = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
// Streaming
|
||||
var rmed = new Rmed(period);
|
||||
double[] streaming = new double[src.Count];
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
streaming[i] = rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] batch = new double[src.Count];
|
||||
Rmed.Batch(src.Values, batch.AsSpan(), period);
|
||||
|
||||
double maxDiff = 0;
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
double diff = Math.Abs(streaming[i] - batch[i]);
|
||||
maxDiff = Math.Max(maxDiff, diff);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Max streaming vs batch diff: {maxDiff:E3}");
|
||||
Assert.True(maxDiff < 1e-10, $"Max diff {maxDiff} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rmed_SelfConsistency_TSeriesBatchMatchesSpanBatch()
|
||||
{
|
||||
const int period = 10;
|
||||
TSeries src = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
TSeries tsBatch = Rmed.Batch(src, period);
|
||||
|
||||
double[] spanBatch = new double[src.Count];
|
||||
Rmed.Batch(src.Values, spanBatch.AsSpan(), period);
|
||||
|
||||
double maxDiff = 0;
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
double diff = Math.Abs(tsBatch.Values[i] - spanBatch[i]);
|
||||
maxDiff = Math.Max(maxDiff, diff);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Max TSeries vs Span batch diff: {maxDiff:E3}");
|
||||
Assert.True(maxDiff < 1e-10, $"Max diff {maxDiff} exceeds tolerance");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rmed_SelfConsistency_CalculateMatchesBatch()
|
||||
{
|
||||
const int period = 12;
|
||||
TSeries src = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
var (calcResult, indicator) = Rmed.Calculate(src, period);
|
||||
TSeries batchResult = Rmed.Batch(src, period);
|
||||
|
||||
double maxDiff = 0;
|
||||
for (int i = 0; i < src.Count; i++)
|
||||
{
|
||||
double diff = Math.Abs(calcResult.Values[i] - batchResult.Values[i]);
|
||||
maxDiff = Math.Max(maxDiff, diff);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Max Calculate vs Batch diff: {maxDiff:E3}");
|
||||
Assert.True(maxDiff < 1e-10, $"Max diff {maxDiff} exceeds tolerance");
|
||||
Assert.Equal(period, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rmed_SpikeRejection_Property()
|
||||
{
|
||||
// Verify that the median stage truly rejects a single spike
|
||||
const int period = 12;
|
||||
TSeries src = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
// Clean run
|
||||
double[] clean = new double[src.Count];
|
||||
Rmed.Batch(src.Values, clean.AsSpan(), period);
|
||||
|
||||
// Run with single spike at bar 25
|
||||
double[] spiked = new double[src.Count];
|
||||
src.Values.CopyTo(spiked);
|
||||
spiked[25] = src.Values[25] + 100000.0; // massive spike
|
||||
|
||||
double[] spikedOut = new double[src.Count];
|
||||
Rmed.Batch(spiked.AsSpan(), spikedOut.AsSpan(), period);
|
||||
|
||||
// The spike at bar 25 should barely affect the output at bar 25+1
|
||||
// because median of 5 rejects a single outlier
|
||||
double cleanVal = clean[26];
|
||||
double spikedVal = spikedOut[26];
|
||||
double impact = Math.Abs(spikedVal - cleanVal);
|
||||
double relativeImpact = impact / Math.Abs(cleanVal);
|
||||
|
||||
_output.WriteLine($"Clean[26]={cleanVal:F6}, Spiked[26]={spikedVal:F6}, Impact={impact:F6}, Relative={relativeImpact:P2}");
|
||||
|
||||
// The spike should have minimal impact (< 50% relative change)
|
||||
Assert.True(relativeImpact < 0.50, $"Spike impact {relativeImpact:P2} exceeds 50% — median rejection failed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rmed_EhlersAlpha_MatchesKnownValues()
|
||||
{
|
||||
// Verify alpha = (cos θ + sin θ - 1) / cos θ where θ = 2π/P
|
||||
// P=5 → α≈0.8416, P=10 → α≈0.4905, P=20 → α≈0.2735, P=40 → α≈0.1459
|
||||
var rmed5 = new Rmed(5);
|
||||
var rmed10 = new Rmed(10);
|
||||
var rmed20 = new Rmed(20);
|
||||
var rmed40 = new Rmed(40);
|
||||
|
||||
_output.WriteLine($"P=5: α={rmed5.Alpha:F4}");
|
||||
_output.WriteLine($"P=10: α={rmed10.Alpha:F4}");
|
||||
_output.WriteLine($"P=20: α={rmed20.Alpha:F4}");
|
||||
_output.WriteLine($"P=40: α={rmed40.Alpha:F4}");
|
||||
|
||||
Assert.InRange(rmed5.Alpha, 0.80, 0.90);
|
||||
Assert.InRange(rmed10.Alpha, 0.44, 0.55);
|
||||
Assert.InRange(rmed20.Alpha, 0.24, 0.32);
|
||||
Assert.InRange(rmed40.Alpha, 0.12, 0.18);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rmed_DifferentPeriods_HaveDifferentSmoothness()
|
||||
{
|
||||
TSeries src = _gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
double[] out5 = new double[src.Count];
|
||||
double[] out40 = new double[src.Count];
|
||||
Rmed.Batch(src.Values, out5.AsSpan(), 5);
|
||||
Rmed.Batch(src.Values, out40.AsSpan(), 40);
|
||||
|
||||
// Compute roughness (sum of absolute first differences)
|
||||
double roughness5 = 0, roughness40 = 0;
|
||||
for (int i = 1; i < src.Count; i++)
|
||||
{
|
||||
roughness5 += Math.Abs(out5[i] - out5[i - 1]);
|
||||
roughness40 += Math.Abs(out40[i] - out40[i - 1]);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Roughness P=5: {roughness5:F2}, P=40: {roughness40:F2}");
|
||||
|
||||
// Longer period → smoother → less roughness
|
||||
Assert.True(roughness40 < roughness5, "Longer period should produce smoother output");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rmed_BarCorrection_IsReversible()
|
||||
{
|
||||
const int period = 12;
|
||||
TSeries src = _gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
|
||||
|
||||
var rmed = new Rmed(period);
|
||||
for (int i = 0; i < src.Count - 1; i++)
|
||||
{
|
||||
rmed.Update(new TValue(src.Times[i], src.Values[i]), isNew: true);
|
||||
}
|
||||
|
||||
// Process last bar
|
||||
var originalLast = rmed.Update(new TValue(src.Times[^1], src.Values[^1]), isNew: true);
|
||||
|
||||
// Correct it with different value
|
||||
rmed.Update(new TValue(src.Times[^1], src.Values[^1] + 50.0), isNew: false);
|
||||
|
||||
// Correct it back to original
|
||||
var restored = rmed.Update(new TValue(src.Times[^1], src.Values[^1]), isNew: false);
|
||||
|
||||
_output.WriteLine($"Original: {originalLast.Value:F10}, Restored: {restored.Value:F10}");
|
||||
Assert.Equal(originalLast.Value, restored.Value, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rmed : AbstractBase
|
||||
{
|
||||
private const int MedianWindow = 5;
|
||||
|
||||
private readonly double _alpha;
|
||||
private readonly double _decay; // 1 - alpha
|
||||
private readonly ITValuePublisher? _publisher;
|
||||
private readonly TValuePublishedHandler? _handler;
|
||||
private int _index;
|
||||
|
||||
// 5-bar circular buffer for median computation
|
||||
private readonly double[] _buf;
|
||||
private readonly double[] _pBuf;
|
||||
private int _head;
|
||||
private int _pHead;
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
public double Rm;
|
||||
public double LastValue;
|
||||
public bool Initialized;
|
||||
}
|
||||
|
||||
public int Period { get; }
|
||||
public double Alpha => _alpha;
|
||||
public override bool IsHot => _index >= WarmupPeriod;
|
||||
|
||||
public Rmed(int period = 12)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
|
||||
}
|
||||
|
||||
Period = period;
|
||||
|
||||
// Ehlers alpha from cycle period: α = (cos θ + sin θ - 1) / cos θ, θ = 2π/P
|
||||
double angle = 2.0 * Math.PI / period;
|
||||
double cosA = Math.Cos(angle);
|
||||
double sinA = Math.Sin(angle);
|
||||
_alpha = Math.Clamp((cosA + sinA - 1.0) / cosA, 0.0, 1.0);
|
||||
_decay = 1.0 - _alpha;
|
||||
|
||||
WarmupPeriod = MedianWindow;
|
||||
Name = $"Rmed({period})";
|
||||
|
||||
_buf = new double[MedianWindow];
|
||||
_pBuf = new double[MedianWindow];
|
||||
|
||||
Init();
|
||||
}
|
||||
|
||||
public Rmed(TSeries source, int period = 12)
|
||||
: this(period)
|
||||
{
|
||||
_publisher = source;
|
||||
_handler = Sub;
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
private void Sub(object? source, in TValueEventArgs args)
|
||||
{
|
||||
Update(args.Value, args.IsNew);
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_index = 0;
|
||||
_head = 0;
|
||||
_pHead = 0;
|
||||
_s = default;
|
||||
_ps = default;
|
||||
Array.Clear(_buf);
|
||||
Array.Clear(_pBuf);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
_pHead = _head;
|
||||
Array.Copy(_buf, _pBuf, MedianWindow);
|
||||
_index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_head = _pHead;
|
||||
Array.Copy(_pBuf, _buf, MedianWindow);
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
double val = input.Value;
|
||||
if (double.IsNaN(val) || double.IsInfinity(val))
|
||||
{
|
||||
val = s.LastValue;
|
||||
}
|
||||
|
||||
// Write into 5-bar circular buffer
|
||||
_buf[_head] = val;
|
||||
_head = (_head + 1) % MedianWindow;
|
||||
|
||||
// Compute 5-bar median via sorting network (constant-time for 5 elements)
|
||||
double med5 = Median5(_buf);
|
||||
|
||||
double result;
|
||||
if (!s.Initialized)
|
||||
{
|
||||
s.Rm = val;
|
||||
s.Initialized = true;
|
||||
result = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
// RM = α·Med5 + (1-α)·RM_prev → FMA: decay·RM_prev + α·Med5
|
||||
s.Rm = Math.FusedMultiplyAdd(_decay, s.Rm, _alpha * med5);
|
||||
result = s.Rm;
|
||||
}
|
||||
|
||||
if (!double.IsNaN(val) && !double.IsInfinity(val))
|
||||
{
|
||||
s.LastValue = val;
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
TValue output = new(input.Time, result);
|
||||
Last = output;
|
||||
PubEvent(output, isNew);
|
||||
return output;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var tsResult = new TSeries();
|
||||
ReadOnlySpan<double> srcSpan = source.Values;
|
||||
double[] outArray = new double[srcSpan.Length];
|
||||
|
||||
Batch(srcSpan, outArray.AsSpan(), Period);
|
||||
|
||||
for (int i = 0; i < outArray.Length; i++)
|
||||
{
|
||||
tsResult.Add(new TValue(source.Times[i], outArray[i]));
|
||||
}
|
||||
|
||||
if (srcSpan.Length > 0)
|
||||
{
|
||||
int replayStart = Math.Max(0, srcSpan.Length - Math.Max(WarmupPeriod, MedianWindow));
|
||||
Reset();
|
||||
for (int i = replayStart; i < srcSpan.Length; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], srcSpan[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
return tsResult;
|
||||
}
|
||||
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = 12)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output lengths must match.", nameof(output));
|
||||
}
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
|
||||
}
|
||||
|
||||
// Ehlers alpha
|
||||
double angle = 2.0 * Math.PI / period;
|
||||
double cosA = Math.Cos(angle);
|
||||
double sinA = Math.Sin(angle);
|
||||
double alpha = Math.Clamp((cosA + sinA - 1.0) / cosA, 0.0, 1.0);
|
||||
double decay = 1.0 - alpha;
|
||||
|
||||
// Pre-clean source for NaN/Infinity (so median lookback matches streaming)
|
||||
int len = source.Length;
|
||||
double[] rented = ArrayPool<double>.Shared.Rent(len);
|
||||
Span<double> clean = rented.AsSpan(0, len);
|
||||
|
||||
try
|
||||
{
|
||||
double lastClean = 0;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double v = source[i];
|
||||
if (double.IsNaN(v) || double.IsInfinity(v))
|
||||
{
|
||||
clean[i] = lastClean;
|
||||
}
|
||||
else
|
||||
{
|
||||
clean[i] = v;
|
||||
lastClean = v;
|
||||
}
|
||||
}
|
||||
|
||||
// 5-bar circular buffer
|
||||
Span<double> buf = stackalloc double[MedianWindow];
|
||||
buf.Clear();
|
||||
int head = 0;
|
||||
double rm = 0;
|
||||
bool initialized = false;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = clean[i];
|
||||
|
||||
buf[head] = val;
|
||||
head = (head + 1) % MedianWindow;
|
||||
|
||||
double med5 = Median5Span(buf);
|
||||
|
||||
if (!initialized)
|
||||
{
|
||||
rm = val;
|
||||
initialized = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
rm = Math.FusedMultiplyAdd(decay, rm, alpha * med5);
|
||||
}
|
||||
|
||||
output[i] = rm;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period = 12)
|
||||
{
|
||||
var result = new TSeries();
|
||||
ReadOnlySpan<double> srcSpan = source.Values;
|
||||
double[] outArray = new double[srcSpan.Length];
|
||||
|
||||
Batch(srcSpan, outArray.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < outArray.Length; i++)
|
||||
{
|
||||
result.Add(new TValue(source.Times[i], outArray[i]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static (TSeries Results, Rmed Indicator) Calculate(TSeries source, int period = 12)
|
||||
{
|
||||
var indicator = new Rmed(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Median of exactly 5 elements using a sorting network (9 compare-swaps).
|
||||
/// Constant-time, branch-based, zero-allocation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double Median5(double[] a)
|
||||
{
|
||||
double a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4];
|
||||
return Median5Core(a0, a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double Median5Span(Span<double> a)
|
||||
{
|
||||
double a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4];
|
||||
return Median5Core(a0, a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimal 6-comparison median-of-5 algorithm.
|
||||
/// Based on the known optimal comparison network for median selection.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double Median5Core(double a0, double a1, double a2, double a3, double a4)
|
||||
{
|
||||
// Sort pairs to guarantee a0<=a1, a3<=a4
|
||||
if (a0 > a1) { (a0, a1) = (a1, a0); }
|
||||
if (a3 > a4) { (a3, a4) = (a4, a3); }
|
||||
|
||||
// Ensure the pair with smaller minimum comes first: a0<=a3
|
||||
if (a0 > a3) { (a0, a3) = (a3, a0); (a1, a4) = (a4, a1); }
|
||||
|
||||
// a0 is now the global minimum → discard it
|
||||
// Median is among {a1, a2, a3, a4}, find median of middle two
|
||||
|
||||
// Compare a2 with a3
|
||||
if (a2 > a3) { (a2, a3) = (a3, a2); }
|
||||
|
||||
// Median = min(a1, a3) means: the answer is max(min-of-pairs)
|
||||
// But more precisely: median = min(max(a1,a2), a3)
|
||||
double maxA1A2 = a1 > a2 ? a1 : a2;
|
||||
return maxA1A2 < a3 ? maxA1A2 : a3;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _publisher != null && _handler != null)
|
||||
{
|
||||
_publisher.Pub -= _handler;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ indicator("Ehlers Recursive Median Filter (RMED)", "RMED", overlay = true)
|
||||
//@reference Ehlers, J.F. (2018). "Recursive Median Filters."
|
||||
// Technical Analysis of Stocks & Commodities, Mar 2018.
|
||||
//@optimized O(1) per bar — 5-element sort network + EMA update
|
||||
export rmed(series float source, simple int period) =>
|
||||
rmed(series float source, simple int period) =>
|
||||
if period < 1
|
||||
runtime.error("Period must be at least 1")
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ indicator("SAK: Swiss Army Knife Indicator", shorttitle="SAK", overlay=false)
|
||||
// @param n SMA lookback length (used only for SMA mode). Must be >= 1.
|
||||
// @param delta Bandwidth parameter for BandPass/BandStop modes.
|
||||
// @returns The filtered value.
|
||||
export sak(series float src, simple string filterType, simple int period, simple int n, simple float delta) =>
|
||||
sak(series float src, simple string filterType, simple int period, simple int n, simple float delta) =>
|
||||
// ── Compute alpha, beta, gamma from Period and filterType ──
|
||||
float alpha = 0.0
|
||||
float beta1 = 0.0
|
||||
|
||||
@@ -111,5 +111,5 @@ i_source = input.source(close, "Source")
|
||||
wav_val = wavelet(i_source, i_levels, i_threshold)
|
||||
|
||||
// Plot
|
||||
plot(wav_val, "Wavelet", color=color.new(color.purple, 0), linewidth=2)
|
||||
plot(i_source, "Source", color=color.new(color.gray, 60), linewidth=1)
|
||||
plot(wav_val, "Wavelet", color=color.new(color.yellow, 0), linewidth=2)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ Momentum indicators measure the velocity and acceleration of price changes. Unli
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| ASI | Accumulation Swing Index | Wilder's cumulative swing index. Breakout confirmation. |
|
||||
| [BIAS](bias/Bias.md) | Bias (also known as Disparity Index) | Percentage deviation from moving average. Measures overextension. |
|
||||
| [BOP](bop/Bop.md) | Balance of Power | Measures buyer/seller strength by comparing close to open relative to range. |
|
||||
| [CCI](cci/Cci.md) | Commodity Channel Index | Measures price deviation from statistical mean, identifies cyclical turns. |
|
||||
| [CFB](cfb/Cfb.md) | Composite Fractal Behavior | Measures trend duration and quality via fractal efficiency across 96 time scales. |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user